Compare commits
	
		
			6 Commits
		
	
	
		
			providers/
			...
			files-in-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d410083cfc | |||
| 6045f96a05 | |||
| c50df0f843 | |||
| c8ebd9f74b | |||
| b3f441f2cd | |||
| 647f054be3 | 
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @ -94,7 +94,7 @@ gen-build:  ## Extract the schema from the database | ||||
| 	AUTHENTIK_DEBUG=true \ | ||||
| 		AUTHENTIK_TENANTS__ENABLED=true \ | ||||
| 		AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ | ||||
| 		uv run ak make_blueprint_schema --file blueprints/schema.json | ||||
| 		uv run ak make_blueprint_schema > blueprints/schema.json | ||||
| 	AUTHENTIK_DEBUG=true \ | ||||
| 		AUTHENTIK_TENANTS__ENABLED=true \ | ||||
| 		AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \ | ||||
|  | ||||
| @ -72,33 +72,20 @@ class Command(BaseCommand): | ||||
|                     "additionalProperties": True, | ||||
|                 }, | ||||
|                 "entries": { | ||||
|                     "anyOf": [ | ||||
|                         { | ||||
|                             "type": "array", | ||||
|                             "items": {"$ref": "#/$defs/blueprint_entry"}, | ||||
|                         }, | ||||
|                         { | ||||
|                             "type": "object", | ||||
|                             "additionalProperties": { | ||||
|                                 "type": "array", | ||||
|                                 "items": {"$ref": "#/$defs/blueprint_entry"}, | ||||
|                             }, | ||||
|                         }, | ||||
|                     ], | ||||
|                     "type": "array", | ||||
|                     "items": { | ||||
|                         "oneOf": [], | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|             "$defs": {"blueprint_entry": {"oneOf": []}}, | ||||
|             "$defs": {}, | ||||
|         } | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument("--file", type=str) | ||||
|  | ||||
|     @no_translations | ||||
|     def handle(self, *args, file: str, **options): | ||||
|     def handle(self, *args, **options): | ||||
|         """Generate JSON Schema for blueprints""" | ||||
|         self.build() | ||||
|         with open(file, "w") as _schema: | ||||
|             _schema.write(dumps(self.schema, indent=4, default=Command.json_default)) | ||||
|         self.stdout.write(dumps(self.schema, indent=4, default=Command.json_default)) | ||||
|  | ||||
|     @staticmethod | ||||
|     def json_default(value: Any) -> Any: | ||||
| @ -125,7 +112,7 @@ class Command(BaseCommand): | ||||
|                 } | ||||
|             ) | ||||
|             model_path = f"{model._meta.app_label}.{model._meta.model_name}" | ||||
|             self.schema["$defs"]["blueprint_entry"]["oneOf"].append( | ||||
|             self.schema["properties"]["entries"]["items"]["oneOf"].append( | ||||
|                 self.template_entry(model_path, model, serializer) | ||||
|             ) | ||||
|  | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| version: 1 | ||||
| entries: | ||||
|   foo: | ||||
|       - identifiers: | ||||
|             name: "%(id)s" | ||||
|             slug: "%(id)s" | ||||
|         model: authentik_flows.flow | ||||
|         state: present | ||||
|         attrs: | ||||
|             designation: stage_configuration | ||||
|             title: foo | ||||
|     - identifiers: | ||||
|           name: "%(id)s" | ||||
|           slug: "%(id)s" | ||||
|       model: authentik_flows.flow | ||||
|       state: present | ||||
|       attrs: | ||||
|           designation: stage_configuration | ||||
|           title: foo | ||||
|  | ||||
| @ -191,18 +191,11 @@ class Blueprint: | ||||
|     """Dataclass used for a full export""" | ||||
|  | ||||
|     version: int = field(default=1) | ||||
|     entries: list[BlueprintEntry] | dict[str, list[BlueprintEntry]] = field(default_factory=list) | ||||
|     entries: list[BlueprintEntry] = field(default_factory=list) | ||||
|     context: dict = field(default_factory=dict) | ||||
|  | ||||
|     metadata: BlueprintMetadata | None = field(default=None) | ||||
|  | ||||
|     def iter_entries(self) -> Iterable[BlueprintEntry]: | ||||
|         if isinstance(self.entries, dict): | ||||
|             for _section, entries in self.entries.items(): | ||||
|                 yield from entries | ||||
|         else: | ||||
|             yield from self.entries | ||||
|  | ||||
|  | ||||
| class YAMLTag: | ||||
|     """Base class for all YAML Tags""" | ||||
| @ -233,7 +226,7 @@ class KeyOf(YAMLTag): | ||||
|         self.id_from = node.value | ||||
|  | ||||
|     def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: | ||||
|         for _entry in blueprint.iter_entries(): | ||||
|         for _entry in blueprint.entries: | ||||
|             if _entry.id == self.id_from and _entry._state.instance: | ||||
|                 # Special handling for PolicyBindingModels, as they'll have a different PK | ||||
|                 # which is used when creating policy bindings | ||||
|  | ||||
| @ -384,7 +384,7 @@ class Importer: | ||||
|     def _apply_models(self, raise_errors=False) -> bool: | ||||
|         """Apply (create/update) models yaml""" | ||||
|         self.__pk_map = {} | ||||
|         for entry in self._import.iter_entries(): | ||||
|         for entry in self._import.entries: | ||||
|             model_app_label, model_name = entry.get_model(self._import).split(".") | ||||
|             try: | ||||
|                 model: type[SerializerModel] = registry.get_model(model_app_label, model_name) | ||||
|  | ||||
							
								
								
									
										32
									
								
								authentik/core/api/files.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								authentik/core/api/files.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import ModelSerializer | ||||
| from authentik.core.models import File | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class FileSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = File | ||||
|         fields = ( | ||||
|             "pk", | ||||
|             "name", | ||||
|             "content", | ||||
|             "location", | ||||
|             "private", | ||||
|             "url", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class FileViewSet(UsedByMixin, ModelViewSet): | ||||
|     queryset = File.objects.all() | ||||
|     serializer_class = FileSerializer | ||||
|     filterset_fields = ("private",) | ||||
|     ordering = ("name",) | ||||
|     search_fields = ( | ||||
|         "name", | ||||
|         "location", | ||||
|     ) | ||||
| @ -0,0 +1,44 @@ | ||||
| # Generated by Django 5.1.11 on 2025-06-13 15:12 | ||||
|  | ||||
| import uuid | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_core", "0048_delete_oldauthenticatedsession_content_type"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="File", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.UUIDField( | ||||
|                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.TextField()), | ||||
|                 ("content", models.BinaryField()), | ||||
|                 ("public", models.BooleanField(default=False)), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Files", | ||||
|             }, | ||||
|         ), | ||||
|         migrations.RenameField( | ||||
|             model_name="application", | ||||
|             old_name="meta_icon", | ||||
|             new_name="meta_old_icon", | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="application", | ||||
|             name="meta_icon", | ||||
|             field=models.ForeignKey( | ||||
|                 null=True, on_delete=django.db.models.deletion.SET_NULL, to="authentik_core.file" | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,32 @@ | ||||
| # Generated by Django 5.1.11 on 2025-06-13 15:29 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_core", "0049_file_rename_meta_icon_application_meta_old_icon"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="file", | ||||
|             name="location", | ||||
|             field=models.TextField(null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="file", | ||||
|             name="content", | ||||
|             field=models.BinaryField(null=True), | ||||
|         ), | ||||
|         migrations.AddConstraint( | ||||
|             model_name="file", | ||||
|             constraint=models.CheckConstraint( | ||||
|                 condition=models.Q( | ||||
|                     ("content__isnull", False), ("location__isnull", False), _connector="OR" | ||||
|                 ), | ||||
|                 name="one_of_content_location_is_defined", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -29,6 +29,7 @@ from authentik.blueprints.models import ManagedModel | ||||
| from authentik.core.expression.exceptions import PropertyMappingExpressionException | ||||
| from authentik.core.types import UILoginButton, UserSettingSerializer | ||||
| from authentik.lib.avatars import get_avatar | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.expression.exceptions import ControlFlowException | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.lib.merge import MERGE_LIST_UNIQUE | ||||
| @ -533,12 +534,13 @@ class Application(SerializerModel, PolicyBindingModel): | ||||
|     ) | ||||
|  | ||||
|     # For template applications, this can be set to /static/authentik/applications/* | ||||
|     meta_icon = models.FileField( | ||||
|     meta_old_icon = models.FileField( | ||||
|         upload_to="application-icons/", | ||||
|         default=None, | ||||
|         null=True, | ||||
|         max_length=500, | ||||
|     ) | ||||
|     meta_icon = models.ForeignKey("File", null=True, on_delete=models.SET_NULL) | ||||
|     meta_description = models.TextField(default="", blank=True) | ||||
|     meta_publisher = models.TextField(default="", blank=True) | ||||
|  | ||||
| @ -1100,3 +1102,44 @@ class AuthenticatedSession(SerializerModel): | ||||
|             session=Session.objects.filter(session_key=request.session.session_key).first(), | ||||
|             user=user, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class File(SerializerModel): | ||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|  | ||||
|     name = models.TextField() | ||||
|     content = models.BinaryField(null=True) | ||||
|     location = models.TextField(null=True) | ||||
|     public = models.BooleanField(default=False) | ||||
|     delete_on_delete = models.BooleanField(default=False) | ||||
|     expiry = models.DateTimeField() | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("File") | ||||
|         verbose_name = _("Files") | ||||
|         constraints = ( | ||||
|             models.CheckConstraint( | ||||
|                 condition=Q(content__isnull=False) | Q(location__isnull=False), | ||||
|                 name="one_of_content_location_is_defined", | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.name | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.core.api.files import FileSerializer | ||||
|  | ||||
|         return FileSerializer | ||||
|  | ||||
|     @property | ||||
|     def url(self) -> str: | ||||
|         if self.content: | ||||
|             return ( | ||||
|                 CONFIG.get("web.path", "/")[:-1] | ||||
|                 + f"/files/{'public' if self.public else 'private'}/{self.pk}" | ||||
|             ) | ||||
|         elif self.location.startswith("/static"): | ||||
|             return CONFIG.get("web.path", "/")[:-1] + self.location | ||||
|         return self.location | ||||
|  | ||||
| @ -8,6 +8,7 @@ from authentik.core.api.application_entitlements import ApplicationEntitlementVi | ||||
| from authentik.core.api.applications import ApplicationViewSet | ||||
| from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet | ||||
| from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet | ||||
| from authentik.core.api.files import FileViewSet | ||||
| from authentik.core.api.groups import GroupViewSet | ||||
| from authentik.core.api.property_mappings import PropertyMappingViewSet | ||||
| from authentik.core.api.providers import ProviderViewSet | ||||
| @ -78,6 +79,7 @@ api_urlpatterns = [ | ||||
|         TransactionalApplicationView.as_view(), | ||||
|         name="core-transactional-application", | ||||
|     ), | ||||
|     ("core/files", FileViewSet), | ||||
|     ("core/groups", GroupViewSet), | ||||
|     ("core/users", UserViewSet), | ||||
|     ("core/tokens", TokenViewSet), | ||||
|  | ||||
| @ -15,7 +15,6 @@ class OAuth2Error(SentryIgnoredException): | ||||
|  | ||||
|     error: str | ||||
|     description: str | ||||
|     cause: str | None = None | ||||
|  | ||||
|     def create_dict(self): | ||||
|         """Return error as dict for JSON Rendering""" | ||||
| @ -35,10 +34,6 @@ class OAuth2Error(SentryIgnoredException): | ||||
|             **kwargs, | ||||
|         ) | ||||
|  | ||||
|     def with_cause(self, cause: str): | ||||
|         self.cause = cause | ||||
|         return self | ||||
|  | ||||
|  | ||||
| class RedirectUriError(OAuth2Error): | ||||
|     """The request fails due to a missing, invalid, or mismatching | ||||
|  | ||||
| @ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.providers.oauth2.constants import SCOPE_OFFLINE_ACCESS, SCOPE_OPENID, TOKEN_TYPE | ||||
| from authentik.providers.oauth2.constants import TOKEN_TYPE | ||||
| from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError | ||||
| from authentik.providers.oauth2.models import ( | ||||
|     AccessToken, | ||||
| @ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase): | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], | ||||
|         ) | ||||
|         with self.assertRaises(AuthorizeError) as cm: | ||||
|         with self.assertRaises(AuthorizeError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -53,7 +53,6 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.error, "unsupported_response_type") | ||||
|  | ||||
|     def test_invalid_client_id(self): | ||||
|         """Test invalid client ID""" | ||||
| @ -69,7 +68,7 @@ class TestAuthorize(OAuthTestCase): | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], | ||||
|         ) | ||||
|         with self.assertRaises(AuthorizeError) as cm: | ||||
|         with self.assertRaises(AuthorizeError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -80,30 +79,19 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.error, "request_not_supported") | ||||
|  | ||||
|     def test_invalid_redirect_uri_missing(self): | ||||
|         """test missing redirect URI""" | ||||
|         OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError) as cm: | ||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.cause, "redirect_uri_missing") | ||||
|  | ||||
|     def test_invalid_redirect_uri(self): | ||||
|         """test invalid redirect URI""" | ||||
|         """test missing/invalid redirect URI""" | ||||
|         OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError) as cm: | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -113,7 +101,6 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.cause, "redirect_uri_no_match") | ||||
|  | ||||
|     def test_blocked_redirect_uri(self): | ||||
|         """test missing/invalid redirect URI""" | ||||
| @ -121,9 +108,9 @@ class TestAuthorize(OAuthTestCase): | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:localhost")], | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError) as cm: | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -133,7 +120,6 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.cause, "redirect_uri_forbidden_scheme") | ||||
|  | ||||
|     def test_invalid_redirect_uri_empty(self): | ||||
|         """test missing/invalid redirect URI""" | ||||
| @ -143,6 +129,9 @@ class TestAuthorize(OAuthTestCase): | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         request = self.factory.get( | ||||
|             "/", | ||||
|             data={ | ||||
| @ -161,9 +150,12 @@ class TestAuthorize(OAuthTestCase): | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, "http://local.invalid?")], | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError) as cm: | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -173,7 +165,6 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.cause, "redirect_uri_no_match") | ||||
|  | ||||
|     def test_redirect_uri_invalid_regex(self): | ||||
|         """test missing/invalid redirect URI (invalid regex)""" | ||||
| @ -181,9 +172,12 @@ class TestAuthorize(OAuthTestCase): | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, "+")], | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError) as cm: | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -193,22 +187,23 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.cause, "redirect_uri_no_match") | ||||
|  | ||||
|     def test_redirect_uri_regex(self): | ||||
|         """test valid redirect URI (regex)""" | ||||
|     def test_empty_redirect_uri(self): | ||||
|         """test empty redirect URI (configure in provider)""" | ||||
|         OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, ".+")], | ||||
|         ) | ||||
|         with self.assertRaises(RedirectUriError): | ||||
|             request = self.factory.get("/", data={"response_type": "code", "client_id": "test"}) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         request = self.factory.get( | ||||
|             "/", | ||||
|             data={ | ||||
|                 "response_type": "code", | ||||
|                 "client_id": "test", | ||||
|                 "redirect_uri": "http://foo.bar.baz", | ||||
|                 "redirect_uri": "http://localhost", | ||||
|             }, | ||||
|         ) | ||||
|         OAuthAuthorizationParams.from_request(request) | ||||
| @ -263,7 +258,7 @@ class TestAuthorize(OAuthTestCase): | ||||
|             GrantTypes.IMPLICIT, | ||||
|         ) | ||||
|         # Implicit without openid scope | ||||
|         with self.assertRaises(AuthorizeError) as cm: | ||||
|         with self.assertRaises(AuthorizeError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -290,7 +285,7 @@ class TestAuthorize(OAuthTestCase): | ||||
|         self.assertEqual( | ||||
|             OAuthAuthorizationParams.from_request(request).grant_type, GrantTypes.HYBRID | ||||
|         ) | ||||
|         with self.assertRaises(AuthorizeError) as cm: | ||||
|         with self.assertRaises(AuthorizeError): | ||||
|             request = self.factory.get( | ||||
|                 "/", | ||||
|                 data={ | ||||
| @ -300,7 +295,6 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             ) | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.error, "unsupported_response_type") | ||||
|  | ||||
|     def test_full_code(self): | ||||
|         """Test full authorization""" | ||||
| @ -621,54 +615,3 @@ class TestAuthorize(OAuthTestCase): | ||||
|                 }, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_openid_missing_invalid(self): | ||||
|         """test request requiring an OpenID scope to be set""" | ||||
|         OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], | ||||
|         ) | ||||
|         request = self.factory.get( | ||||
|             "/", | ||||
|             data={ | ||||
|                 "response_type": "id_token", | ||||
|                 "client_id": "test", | ||||
|                 "redirect_uri": "http://localhost", | ||||
|                 "scope": "", | ||||
|             }, | ||||
|         ) | ||||
|         with self.assertRaises(AuthorizeError) as cm: | ||||
|             OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertEqual(cm.exception.cause, "scope_openid_missing") | ||||
|  | ||||
|     @apply_blueprint("system/providers-oauth2.yaml") | ||||
|     def test_offline_access_invalid(self): | ||||
|         """test request for offline_access with invalid response type""" | ||||
|         provider = OAuth2Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|             client_id="test", | ||||
|             authorization_flow=create_test_flow(), | ||||
|             redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], | ||||
|         ) | ||||
|         provider.property_mappings.set( | ||||
|             ScopeMapping.objects.filter( | ||||
|                 managed__in=[ | ||||
|                     "goauthentik.io/providers/oauth2/scope-openid", | ||||
|                     "goauthentik.io/providers/oauth2/scope-offline_access", | ||||
|                 ] | ||||
|             ) | ||||
|         ) | ||||
|         request = self.factory.get( | ||||
|             "/", | ||||
|             data={ | ||||
|                 "response_type": "id_token", | ||||
|                 "client_id": "test", | ||||
|                 "redirect_uri": "http://localhost", | ||||
|                 "scope": f"{SCOPE_OPENID} {SCOPE_OFFLINE_ACCESS}", | ||||
|                 "nonce": generate_id(), | ||||
|             }, | ||||
|         ) | ||||
|         parsed = OAuthAuthorizationParams.from_request(request) | ||||
|         self.assertNotIn(SCOPE_OFFLINE_ACCESS, parsed.scope) | ||||
|  | ||||
| @ -190,7 +190,7 @@ class OAuthAuthorizationParams: | ||||
|         allowed_redirect_urls = self.provider.redirect_uris | ||||
|         if not self.redirect_uri: | ||||
|             LOGGER.warning("Missing redirect uri.") | ||||
|             raise RedirectUriError("", allowed_redirect_urls).with_cause("redirect_uri_missing") | ||||
|             raise RedirectUriError("", allowed_redirect_urls) | ||||
|  | ||||
|         if len(allowed_redirect_urls) < 1: | ||||
|             LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri) | ||||
| @ -219,14 +219,10 @@ class OAuthAuthorizationParams: | ||||
|                         provider=self.provider, | ||||
|                     ) | ||||
|         if not match_found: | ||||
|             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls).with_cause( | ||||
|                 "redirect_uri_no_match" | ||||
|             ) | ||||
|             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) | ||||
|         # Check against forbidden schemes | ||||
|         if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES: | ||||
|             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls).with_cause( | ||||
|                 "redirect_uri_forbidden_scheme" | ||||
|             ) | ||||
|             raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) | ||||
|  | ||||
|     def check_scope(self, github_compat=False): | ||||
|         """Ensure openid scope is set in Hybrid flows, or when requesting an id_token""" | ||||
| @ -255,9 +251,7 @@ class OAuthAuthorizationParams: | ||||
|             or self.response_type in [ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN] | ||||
|         ): | ||||
|             LOGGER.warning("Missing 'openid' scope.") | ||||
|             raise AuthorizeError( | ||||
|                 self.redirect_uri, "invalid_scope", self.grant_type, self.state | ||||
|             ).with_cause("scope_openid_missing") | ||||
|             raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state) | ||||
|         if SCOPE_OFFLINE_ACCESS in self.scope: | ||||
|             # https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess | ||||
|             # Don't explicitly request consent with offline_access, as the spec allows for | ||||
| @ -292,9 +286,7 @@ class OAuthAuthorizationParams: | ||||
|             return | ||||
|         if not self.nonce: | ||||
|             LOGGER.warning("Missing nonce for OpenID Request") | ||||
|             raise AuthorizeError( | ||||
|                 self.redirect_uri, "invalid_request", self.grant_type, self.state | ||||
|             ).with_cause("none_missing") | ||||
|             raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state) | ||||
|  | ||||
|     def check_code_challenge(self): | ||||
|         """PKCE validation of the transformation method.""" | ||||
| @ -353,10 +345,10 @@ class AuthorizationFlowInitView(PolicyAccessView): | ||||
|                 self.request, github_compat=self.github_compat | ||||
|             ) | ||||
|         except AuthorizeError as error: | ||||
|             LOGGER.warning(error.description, redirect_uri=error.redirect_uri, cause=error.cause) | ||||
|             LOGGER.warning(error.description, redirect_uri=error.redirect_uri) | ||||
|             raise RequestValidationError(error.get_response(self.request)) from None | ||||
|         except OAuth2Error as error: | ||||
|             LOGGER.warning(error.description, cause=error.cause) | ||||
|             LOGGER.warning(error.description) | ||||
|             raise RequestValidationError( | ||||
|                 bad_request_message(self.request, error.description, title=error.error) | ||||
|             ) from None | ||||
|  | ||||
| @ -20,9 +20,6 @@ from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | ||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||
|  | ||||
| PLAN_CONNECTION_SETTINGS = "connection_settings" | ||||
|  | ||||
|  | ||||
| class RACStartView(PolicyAccessView): | ||||
| @ -112,15 +109,10 @@ class RACFinalStage(RedirectStage): | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|     def get_challenge(self, *args, **kwargs) -> RedirectChallenge: | ||||
|         settings = self.executor.plan.context.get(PLAN_CONNECTION_SETTINGS) | ||||
|         if not settings: | ||||
|             settings = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).get( | ||||
|                 PLAN_CONNECTION_SETTINGS | ||||
|             ) | ||||
|         token = ConnectionToken.objects.create( | ||||
|             provider=self.provider, | ||||
|             endpoint=self.endpoint, | ||||
|             settings=settings or {}, | ||||
|             settings=self.executor.plan.context.get("connection_settings", {}), | ||||
|             session=self.request.session["authenticatedsession"], | ||||
|             expires=now() + timedelta_from_string(self.provider.connection_expiry), | ||||
|             expiring=True, | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										14
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								go.mod
									
									
									
									
									
								
							| @ -62,6 +62,12 @@ require ( | ||||
| 	github.com/go-openapi/validate v0.24.0 // indirect | ||||
| 	github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect | ||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||
| 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||||
| 	github.com/jackc/pgx/v5 v5.7.5 // indirect | ||||
| 	github.com/jackc/puddle/v2 v2.2.2 // indirect | ||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| 	github.com/jinzhu/now v1.1.5 // indirect | ||||
| 	github.com/josharian/intern v1.0.0 // indirect | ||||
| 	github.com/klauspost/compress v1.18.0 // indirect | ||||
| 	github.com/mailru/easyjson v0.7.7 // indirect | ||||
| @ -77,9 +83,11 @@ require ( | ||||
| 	go.opentelemetry.io/otel v1.24.0 // indirect | ||||
| 	go.opentelemetry.io/otel/metric v1.24.0 // indirect | ||||
| 	go.opentelemetry.io/otel/trace v1.24.0 // indirect | ||||
| 	golang.org/x/crypto v0.36.0 // indirect | ||||
| 	golang.org/x/sys v0.31.0 // indirect | ||||
| 	golang.org/x/text v0.24.0 // indirect | ||||
| 	golang.org/x/crypto v0.39.0 // indirect | ||||
| 	golang.org/x/sys v0.33.0 // indirect | ||||
| 	golang.org/x/text v0.26.0 // indirect | ||||
| 	google.golang.org/protobuf v1.36.5 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	gorm.io/driver/postgres v1.6.0 // indirect | ||||
| 	gorm.io/gorm v1.30.0 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										22
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								go.sum
									
									
									
									
									
								
							| @ -191,6 +191,14 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ | ||||
| github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= | ||||
| github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||||
| github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | ||||
| github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | ||||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= | ||||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= | ||||
| github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= | ||||
| github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= | ||||
| github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= | ||||
| github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= | ||||
| github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= | ||||
| github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= | ||||
| github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= | ||||
| @ -205,6 +213,10 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ | ||||
| github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= | ||||
| github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= | ||||
| github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= | ||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= | ||||
| github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= | ||||
| github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||
| @ -308,6 +320,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh | ||||
| golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= | ||||
| golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= | ||||
| golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= | ||||
| golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= | ||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
| golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
| golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= | ||||
| @ -415,6 +429,8 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= | ||||
| golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | ||||
| golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| @ -422,6 +438,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= | ||||
| golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= | ||||
| golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= | ||||
| golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= | ||||
| golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| @ -555,6 +573,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= | ||||
| gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= | ||||
| gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= | ||||
| gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= | ||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
|  | ||||
| @ -5,6 +5,7 @@ type Config struct { | ||||
| 	Storage        StorageConfig        `yaml:"storage"` | ||||
| 	LogLevel       string               `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL, overwrite"` | ||||
| 	ErrorReporting ErrorReportingConfig `yaml:"error_reporting" env:", prefix=AUTHENTIK_ERROR_REPORTING__"` | ||||
| 	Postgresql     PostgresqlConfig     `yaml:"postgresql" env:", prefix=AUTHENTIK_POSTGRESQL__"` | ||||
| 	Redis          RedisConfig          `yaml:"redis" env:", prefix=AUTHENTIK_REDIS__"` | ||||
| 	Outposts       OutpostConfig        `yaml:"outposts" env:", prefix=AUTHENTIK_OUTPOSTS__"` | ||||
|  | ||||
| @ -25,6 +26,16 @@ type Config struct { | ||||
| 	AuthentikInsecure    bool   `env:"AUTHENTIK_INSECURE"` | ||||
| } | ||||
|  | ||||
| // TODO: SSL | ||||
| type PostgresqlConfig struct { | ||||
| 	Host          string `yaml:"host" env:"HOST, overwrite"` | ||||
| 	Port          string `yaml:"port" env:"PORT, overwrite"` | ||||
| 	User          string `yaml:"user" env:"USER, overwrite"` | ||||
| 	Password      string `yaml:"password" env:"PASSWORD, overwrite"` | ||||
| 	Name          string `yaml:"name" env:"NAME, overwrite"` | ||||
| 	DefaultSchema string `yaml:"default_schema" env:"DEFAULT_SCHEMA, overwrite"` | ||||
| } | ||||
|  | ||||
| type RedisConfig struct { | ||||
| 	Host      string `yaml:"host" env:"HOST, overwrite"` | ||||
| 	Port      int    `yaml:"port" env:"PORT, overwrite"` | ||||
|  | ||||
							
								
								
									
										62
									
								
								internal/web/files.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								internal/web/files.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| package web | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/go-http-utils/etag" | ||||
| 	"github.com/gorilla/mux" | ||||
|  | ||||
| 	"goauthentik.io/internal/config" | ||||
| 	"goauthentik.io/internal/constants" | ||||
| ) | ||||
|  | ||||
| type File struct { | ||||
| 	ID string `gorm:"primaryKey"` | ||||
|  | ||||
| 	Name     string | ||||
| 	Content  []byte | ||||
| 	Location string | ||||
| 	Public   bool | ||||
| } | ||||
|  | ||||
| func (ws *WebServer) configureFiles() { | ||||
| 	// Setup routers | ||||
| 	filesRouter := ws.loggingRouter.NewRoute().Subrouter() | ||||
| 	filesRouter.Use(ws.filesHeaderMiddleware) | ||||
|  | ||||
| 	filesRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/files/public/{pk}").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		vars := mux.Vars(r) | ||||
|  | ||||
| 		pk := vars["pk"] | ||||
|  | ||||
| 		var file File | ||||
| 		ws.postgresClient.First(&file, "id = ? AND public = true AND content <> NULL", pk) | ||||
|  | ||||
| 		// TODO: get from DB | ||||
| 		rw.Write(file.Content) | ||||
| 	}) | ||||
|  | ||||
| 	filesRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/files/private/{pk}").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		vars := mux.Vars(r) | ||||
|  | ||||
| 		// TODO: check session | ||||
|  | ||||
| 		pk := vars["pk"] | ||||
|  | ||||
| 		var file File | ||||
| 		ws.postgresClient.First(&file, "id = ? AND content <> NULL", pk) | ||||
|  | ||||
| 		rw.Write([]byte(file.Content)) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // TODO: anything else? | ||||
| func (ws *WebServer) filesHeaderMiddleware(h http.Handler) http.Handler { | ||||
| 	etagHandler := etag.Handler(h, false) | ||||
| 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.Header().Set("Cache-Control", "public, no-transform") | ||||
| 		w.Header().Set("X-authentik-version", constants.VERSION) | ||||
| 		w.Header().Set("Vary", "X-authentik-version, Etag") | ||||
| 		etagHandler.ServeHTTP(w, r) | ||||
| 	}) | ||||
| } | ||||
| @ -17,6 +17,8 @@ import ( | ||||
| 	"github.com/gorilla/securecookie" | ||||
| 	"github.com/pires/go-proxyproto" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"gorm.io/driver/postgres" | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"goauthentik.io/api/v3" | ||||
| 	"goauthentik.io/internal/config" | ||||
| @ -49,6 +51,7 @@ type WebServer struct { | ||||
| 	mainRouter     *mux.Router | ||||
| 	loggingRouter  *mux.Router | ||||
| 	log            *log.Entry | ||||
| 	postgresClient *gorm.DB | ||||
| 	upstreamClient *http.Client | ||||
| 	upstreamURL    *url.URL | ||||
|  | ||||
| @ -64,6 +67,21 @@ func NewWebServer() *WebServer { | ||||
| 	loggingHandler := mainHandler.NewRoute().Subrouter() | ||||
| 	loggingHandler.Use(web.NewLoggingHandler(l, nil)) | ||||
|  | ||||
| 	// TODO: ssl | ||||
| 	postgresDsn := fmt.Sprintf( | ||||
| 		"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", | ||||
| 		config.Get().Postgresql.Host, | ||||
| 		config.Get().Postgresql.Port, | ||||
| 		config.Get().Postgresql.User, | ||||
| 		config.Get().Postgresql.Password, | ||||
| 		config.Get().Postgresql.Name, | ||||
| 	) | ||||
|  | ||||
| 	db, err := gorm.Open(postgres.Open(postgresDsn), &gorm.Config{}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	tmp := os.TempDir() | ||||
| 	socketPath := path.Join(tmp, UnixSocketName) | ||||
|  | ||||
| @ -88,6 +106,7 @@ func NewWebServer() *WebServer { | ||||
| 		mainRouter:     mainHandler, | ||||
| 		loggingRouter:  loggingHandler, | ||||
| 		log:            l, | ||||
| 		postgresClient: db, | ||||
| 		gunicornReady:  false, | ||||
| 		upstreamClient: upstreamClient, | ||||
| 		upstreamURL:    u, | ||||
|  | ||||
| @ -6,18 +6,18 @@ | ||||
| # Translators: | ||||
| # jcamat, 2022 | ||||
| # Angel, 2024 | ||||
| # Iamanaws, 2024 | ||||
| # Marcelo Elizeche Landó, 2025 | ||||
| # Jens L. <jens@goauthentik.io>, 2025 | ||||
| # Iamanaws, 2025 | ||||
| #  | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-06-04 00:12+0000\n" | ||||
| "POT-Creation-Date: 2025-05-28 11:25+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Iamanaws, 2025\n" | ||||
| "Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n" | ||||
| "Language-Team: Spanish (https://app.transifex.com/authentik/teams/119923/es/)\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| @ -111,7 +111,7 @@ msgstr "Certificado Web usado por el servidor web Core de authentik" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Certificates used for client authentication." | ||||
| msgstr "Certificados utilizados para la autenticación del cliente." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/brands/models.py | ||||
| msgid "Brand" | ||||
| @ -131,7 +131,7 @@ msgstr "Descripción adicional no disponible." | ||||
|  | ||||
| #: authentik/core/api/groups.py | ||||
| msgid "Cannot set group as parent of itself." | ||||
| msgstr "No se puede establecer un grupo como su propio padre." | ||||
| msgstr "No se puede establecer el grupo como padre de sí mismo." | ||||
|  | ||||
| #: authentik/core/api/providers.py | ||||
| msgid "" | ||||
| @ -183,11 +183,11 @@ msgstr "Remueve usuario del grupo" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Enable superuser status" | ||||
| msgstr "Habilitar el estado de superusuario" | ||||
| msgstr "Habiliar estado de \"superusuario\"" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Disable superuser status" | ||||
| msgstr "Deshabilitar el estado de superusuario" | ||||
| msgstr "Deshabiliar estado de \"superusuario\"" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "User's display name." | ||||
| @ -241,7 +241,7 @@ msgstr "Flujo utilizado al autorizar a este proveedor." | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Flow used ending the session from a provider." | ||||
| msgstr "Flujo utilizado para finalizar la sesión desde un proveedor." | ||||
| msgstr "Flujo usado para terminar la sesión de un proveedor." | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "" | ||||
| @ -273,11 +273,11 @@ msgstr "Aplicaciones" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Application Entitlement" | ||||
| msgstr "Derecho de Aplicación" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Application Entitlements" | ||||
| msgstr "Derechos de Aplicación" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Use the source-specific identifier" | ||||
| @ -288,9 +288,9 @@ msgid "" | ||||
| "Link to a user with identical email address. Can have security implications " | ||||
| "when a source doesn't validate email addresses." | ||||
| msgstr "" | ||||
| "Enlace a un usuario con la misma dirección de correo electrónico. Puede " | ||||
| "tener implicaciones de seguridad cuando una fuente no valida las direcciones" | ||||
| " de correo electrónico." | ||||
| "Apunta a un usuario con una dirección de correo electrónico idéntica. Puede " | ||||
| "tener implicaciones de seguridad cuando una fuente no valida la dirección de" | ||||
| " correo electrónico." | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "" | ||||
| @ -305,8 +305,8 @@ msgid "" | ||||
| "Link to a user with identical username. Can have security implications when " | ||||
| "a username is used with another source." | ||||
| msgstr "" | ||||
| "Enlace a un usuario con el mismo nombre de usuario. Puede tener " | ||||
| "implicaciones de seguridad cuando un nombre de usuario se utiliza con otra " | ||||
| "Enlace a un usuario con un nombre de usuario idéntico. Puede tener " | ||||
| "implicaciones de seguridad cuando se usa un nombre de usuario con otra " | ||||
| "fuente." | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| @ -322,8 +322,8 @@ msgid "" | ||||
| "Link to a group with identical name. Can have security implications when a " | ||||
| "group name is used with another source." | ||||
| msgstr "" | ||||
| "Enlace a un grupo con el mismo nombre. Puede tener implicaciones de " | ||||
| "seguridad cuando un nombre de grupo se utiliza con otra fuente." | ||||
| "Enlace a un grupo con un nombre idéntico. Puede tener implicaciones de " | ||||
| "seguridad cuando se utiliza un nombre de grupo con otra fuente." | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Use the group name, but deny enrollment when the name already exists." | ||||
| @ -385,7 +385,7 @@ msgstr "Asignaciones de Propiedades" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "session data" | ||||
| msgstr "datos de sesión" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py | ||||
| msgid "Session" | ||||
| @ -424,7 +424,7 @@ msgstr "¡Autenticado exitosamente con {source}!" | ||||
| #: authentik/core/sources/flow_manager.py | ||||
| #, python-brace-format | ||||
| msgid "Successfully linked {source}!" | ||||
| msgstr "¡{source} enlazado correctamente!" | ||||
| msgstr "¡{source} vinculado exitosamente!" | ||||
|  | ||||
| #: authentik/core/sources/flow_manager.py | ||||
| msgid "Source is not configured for enrollment." | ||||
| @ -476,11 +476,11 @@ msgstr "" | ||||
|  | ||||
| #: authentik/crypto/models.py | ||||
| msgid "Certificate-Key Pair" | ||||
| msgstr "Par Certificado-Clave" | ||||
| msgstr "Par de claves de certificado" | ||||
|  | ||||
| #: authentik/crypto/models.py | ||||
| msgid "Certificate-Key Pairs" | ||||
| msgstr "Pares Certificado-Clave" | ||||
| msgstr "Pares de claves de certificado" | ||||
|  | ||||
| #: authentik/enterprise/api.py | ||||
| msgid "Enterprise is required to create/update this object." | ||||
| @ -511,7 +511,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Number of passwords to check against." | ||||
| msgstr "Número de contraseñas contra las que verificar." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| #: authentik/policies/password/models.py | ||||
| @ -521,20 +521,18 @@ msgstr "La contraseña no se ha establecido en contexto" | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "This password has been used previously. Please choose a different one." | ||||
| msgstr "" | ||||
| "Esta contraseña se ha utilizado anteriormente. Por favor, elija una " | ||||
| "diferente." | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policy" | ||||
| msgstr "Política de Unicidad de Contraseñas" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "Password Uniqueness Policies" | ||||
| msgstr "Políticas de Unicidad de Contraseñas" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policies/unique_password/models.py | ||||
| msgid "User Password History" | ||||
| msgstr "Historial de Contraseñas del Usuario" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/policy.py | ||||
| msgid "Enterprise required to access this feature." | ||||
| @ -619,39 +617,39 @@ msgstr "Clave de firma" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Key used to sign the SSF Events." | ||||
| msgstr "Clave utilizada para firmar los eventos SSF." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Provider" | ||||
| msgstr "Proveedor del Marco de Señales Compartidas" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Shared Signals Framework Providers" | ||||
| msgstr "Proveedores del Marco de Señales Compartidas" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "Add stream to SSF provider" | ||||
| msgstr "Agregar flujo de datos al proveedor SSF" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream" | ||||
| msgstr "Flujo de Datos SSF" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Streams" | ||||
| msgstr "Flujos de Datos SSF" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Event" | ||||
| msgstr "Evento de Flujo de Datos SSF" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/models.py | ||||
| msgid "SSF Stream Events" | ||||
| msgstr "Eventos de Flujos de Datos SSF" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/providers/ssf/tasks.py | ||||
| msgid "Failed to send request" | ||||
| msgstr "Error al enviar la solicitud" | ||||
| msgstr "Falló envio de petición" | ||||
|  | ||||
| #: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py | ||||
| msgid "Endpoint Authenticator Google Device Trust Connector Stage" | ||||
| @ -683,29 +681,26 @@ msgid "" | ||||
| "option has a higher priority than the `client_certificate` option on " | ||||
| "`Brand`." | ||||
| msgstr "" | ||||
| "Configura las autoridades certificadoras para validar el certificado. Esta " | ||||
| "opción tiene una prioridad mayor que la opción `client_certificate` en " | ||||
| "`Brand`." | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stage" | ||||
| msgstr "Etapa de TLS mutuo" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Mutual TLS Stages" | ||||
| msgstr "Etapas de TLS mutuo" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/models.py | ||||
| msgid "Permissions to pass Certificates for outposts." | ||||
| msgstr "Permisos para pasar Certificados a los puestos avanzados." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "Certificate required but no certificate was given." | ||||
| msgstr "Se requiere certificado, pero no se proporcionó ninguno." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/mtls/stage.py | ||||
| msgid "No user found for certificate." | ||||
| msgstr "No se encontró usuario para el certificado." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/enterprise/stages/source/models.py | ||||
| msgid "" | ||||
| @ -758,16 +753,12 @@ msgid "" | ||||
| "Customize the body of the request. Mapping should return data that is JSON-" | ||||
| "serializable." | ||||
| msgstr "" | ||||
| "Personaliza el cuerpo de la solicitud. El mapeo debe devolver datos que sean" | ||||
| " serializables en JSON." | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | ||||
| "of key-value pairs" | ||||
| msgstr "" | ||||
| "Configura encabezados adicionales para enviar. El mapeo debe devolver un " | ||||
| "diccionario de pares clave-valor" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| @ -795,7 +786,7 @@ msgstr "Transporte de notificaciones" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "Notification Transports" | ||||
| msgstr "Medios de Notificación" | ||||
| msgstr "Transportes de notificación" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "Notice" | ||||
| @ -822,9 +813,9 @@ msgid "" | ||||
| "Select which transports should be used to notify the user. If none are " | ||||
| "selected, the notification will only be shown in the authentik UI." | ||||
| msgstr "" | ||||
| "Selecciona qué medios se deben usar para notificar al usuario. Si no se " | ||||
| "selecciona ninguno, la notificación solo se mostrará en la interfaz de " | ||||
| "authentik." | ||||
| "Seleccione qué transportes se deben usar para notificar al usuario. Si no se" | ||||
| " selecciona ninguno, la notificación solo se mostrará en la interfaz de " | ||||
| "usuario de authentik." | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "Controls which severity level the created notifications will have." | ||||
| @ -996,7 +987,7 @@ msgstr "Evalúa políticas durante el proceso de planeación del Flujo." | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "Evaluate policies when the Stage is presented to the user." | ||||
| msgstr "Evaluar las políticas cuando la Etapa se presenta al usuario." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/flows/models.py | ||||
| msgid "" | ||||
| @ -1043,8 +1034,6 @@ msgid "" | ||||
| "When enabled, provider will not modify or create objects in the remote " | ||||
| "system." | ||||
| msgstr "" | ||||
| "Cuando está habilitado, el proveedor no modificará ni creará objetos en el " | ||||
| "sistema remoto." | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Starting full provider sync" | ||||
| @ -1052,21 +1041,20 @@ msgstr "Iniciando sincronización completa de proveedor" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing users" | ||||
| msgstr "Sincronizando usuarios" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Syncing groups" | ||||
| msgstr "Sincronizando grupos" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| msgid "Syncing page {page} of {object_type}" | ||||
| msgstr "Sincronizando página {page} de {object_type}" | ||||
| msgid "Syncing page {page} of groups" | ||||
| msgstr "Sincronizando página {page} de grupos" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| msgid "Dropping mutating request due to dry run" | ||||
| msgstr "" | ||||
| "Descartando solicitud de mutación debido a ejecución en modo de simulación" | ||||
|  | ||||
| #: authentik/lib/sync/outgoing/tasks.py | ||||
| #, python-brace-format | ||||
| @ -1245,7 +1233,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/policies/expiry/models.py | ||||
| msgid "Password has expired." | ||||
| msgstr "La contraseña ha expirado." | ||||
| msgstr "La contraseña ha caducado." | ||||
|  | ||||
| #: authentik/policies/expiry/models.py | ||||
| msgid "Password Expiry Policy" | ||||
| @ -1283,7 +1271,7 @@ msgstr "La IP del cliente no está en un país permitido." | ||||
|  | ||||
| #: authentik/policies/geoip/models.py | ||||
| msgid "Distance from previous authentication is larger than threshold." | ||||
| msgstr "La distancia desde la autenticación anterior es mayor que el umbral." | ||||
| msgstr "La distancia desde la autenticación previa es mayor que el límite." | ||||
|  | ||||
| #: authentik/policies/geoip/models.py | ||||
| msgid "Distance is further than possible." | ||||
| @ -1332,7 +1320,7 @@ msgstr "Vinculación de Políticas" | ||||
|  | ||||
| #: authentik/policies/models.py | ||||
| msgid "Policy Bindings" | ||||
| msgstr "Vinculaciones de Políticas" | ||||
| msgstr "Vinculaciones de políticas" | ||||
|  | ||||
| #: authentik/policies/models.py | ||||
| msgid "" | ||||
| @ -1606,11 +1594,11 @@ msgstr "ES256 (Encriptación Asimétrica)" | ||||
|  | ||||
| #: authentik/providers/oauth2/models.py | ||||
| msgid "ES384 (Asymmetric Encryption)" | ||||
| msgstr "ES384 (Encriptación Asimétrica)" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/oauth2/models.py | ||||
| msgid "ES512 (Asymmetric Encryption)" | ||||
| msgstr "ES512 (Encriptación Asimétrica)" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/oauth2/models.py | ||||
| msgid "Scope used by the client" | ||||
| @ -1825,7 +1813,7 @@ msgstr "Valida Certificados SSL de servidores de origen" | ||||
|  | ||||
| #: authentik/providers/proxy/models.py | ||||
| msgid "Internal host SSL Validation" | ||||
| msgstr "Validación SSL del host interno" | ||||
| msgstr "Validación SSL de host interno" | ||||
|  | ||||
| #: authentik/providers/proxy/models.py | ||||
| msgid "" | ||||
| @ -2039,7 +2027,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "AuthnContextClassRef Property Mapping" | ||||
| msgstr "Asignación de Propiedades de AuthnContextClassRef" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| @ -2047,9 +2035,6 @@ msgid "" | ||||
| "empty, the AuthnContextClassRef will be set based on which authentication " | ||||
| "methods the user used to authenticate." | ||||
| msgstr "" | ||||
| "Configura cómo se creará el valor de AuthnContextClassRef. Si se deja vacío," | ||||
| " el AuthnContextClassRef se establecerá según los métodos de autenticación " | ||||
| "que el usuario haya utilizado para autenticarse." | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| @ -2199,11 +2184,11 @@ msgstr "Predeterminado" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "AWS" | ||||
| msgstr "AWS" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "Slack" | ||||
| msgstr "Slack" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "Base URL to SCIM requests, usually ends in /v2" | ||||
| @ -2215,13 +2200,11 @@ msgstr "Token de Autenticación" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "SCIM Compatibility Mode" | ||||
| msgstr "Modo de Compatibilidad SCIM" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "Alter authentik behavior for vendor-specific SCIM implementations." | ||||
| msgstr "" | ||||
| "Modificar el comportamiento de authentik para implementaciones SCIM " | ||||
| "específicas de proveedores." | ||||
|  | ||||
| #: authentik/providers/scim/models.py | ||||
| msgid "SCIM Provider" | ||||
| @ -2249,7 +2232,7 @@ msgstr "Roles" | ||||
|  | ||||
| #: authentik/rbac/models.py | ||||
| msgid "Initial Permissions" | ||||
| msgstr "Permisos Iniciales" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/rbac/models.py | ||||
| msgid "System permission" | ||||
| @ -2287,7 +2270,7 @@ msgstr "" | ||||
|  | ||||
| #: authentik/recovery/views.py | ||||
| msgid "Used recovery-link to authenticate." | ||||
| msgstr "Se utilizó un enlace de recuperación para autenticarse." | ||||
| msgstr "Se usó el enlace de recuperación para autenticarse." | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "Kerberos realm" | ||||
| @ -2299,7 +2282,7 @@ msgstr "krb5.conf personalizado a usar. Usa el del sistema por defecto." | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "KAdmin server type" | ||||
| msgstr "Tipo de servidor KAdmin" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "Sync users from Kerberos into authentik" | ||||
| @ -2307,24 +2290,23 @@ msgstr "Sincronizar usuarios desde Kerberos hacia Authentik" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "When a user changes their password, sync it back to Kerberos" | ||||
| msgstr "" | ||||
| "Cuando un usuario cambie su contraseña, sincronizarla de vuelta a Kerberos." | ||||
| msgstr "Cuando un usuario cambia su contraseña, sincronizarlo hacia Kerberos" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "Principal to authenticate to kadmin for sync." | ||||
| msgstr "Principal para autenticarse en kadmin para la sincronización." | ||||
| msgstr "Principal para autenticarse como kadmin para la sincronización." | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "Password to authenticate to kadmin for sync" | ||||
| msgstr "Contraseña para autenticarse en kadmin para la sincronización" | ||||
| msgstr "Contraseña para autenticarse como kadmin para la sincronización" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "" | ||||
| "Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the " | ||||
| "form TYPE:residual" | ||||
| msgstr "" | ||||
| "Keytab para autenticarse en kadmin para la sincronización. Debe estar " | ||||
| "codificado en base64 o en el formato TIPO:residuo" | ||||
| "Keytab para autenticarse como kadmin para la sincronización. Debe estar " | ||||
| "codificado en base64 o en el formato TIPO:residual" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "" | ||||
| @ -2340,7 +2322,7 @@ msgid "" | ||||
| "HTTP@hostname" | ||||
| msgstr "" | ||||
| "Forzar el uso de un nombre de servidor específico para SPNEGO. Debe estar en" | ||||
| " el formato HTTP@nombre_de_host" | ||||
| " el formato HTTP@nombredelservidor" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path" | ||||
| @ -2357,8 +2339,8 @@ msgid "" | ||||
| "If enabled, the authentik-stored password will be updated upon login with " | ||||
| "the Kerberos password backend" | ||||
| msgstr "" | ||||
| "Si está habilitado, la contraseña almacenada en authentik se actualizará al " | ||||
| "iniciar sesión con el backend de contraseñas de Kerberos." | ||||
| "Si está habilitado, la contraseña almacenada por authentik será actualizada " | ||||
| "al iniciar sesión con el backend de contraseñas Kerberos" | ||||
|  | ||||
| #: authentik/sources/kerberos/models.py | ||||
| msgid "Kerberos Source" | ||||
| @ -2406,7 +2388,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "\n" | ||||
| "                    Asegúrate de que tienes entradas válidas\n" | ||||
| "                    (obtenibles mediante kinit) \n" | ||||
| "                    (se obtienen a través de kinit) \n" | ||||
| "                    y de haber configurado correctamente el navegador.\n" | ||||
| "                    Por favor, contacta a tu administrador.\n" | ||||
| "                " | ||||
| @ -2471,10 +2453,6 @@ msgstr "DN de grupo de adición" | ||||
| msgid "Consider Objects matching this filter to be Users." | ||||
| msgstr "Considere que los objetos que coinciden con este filtro son usuarios." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Attribute which matches the value of `group_membership_field`." | ||||
| msgstr "Atributo que coincide con el valor de `group_membership_field`." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Field which contains members of a group." | ||||
| msgstr "Campo que contiene los miembros de un grupo." | ||||
| @ -2507,17 +2485,12 @@ msgid "" | ||||
| "attribute. This allows nested group resolution on systems like FreeIPA and " | ||||
| "Active Directory" | ||||
| msgstr "" | ||||
| "Buscar la pertenencia a grupos basándose en un atributo del usuario en lugar" | ||||
| " de un atributo del grupo. Esto permite la resolución de grupos anidados en " | ||||
| "sistemas como FreeIPA y Active Directory" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "" | ||||
| "Delete authentik users and groups which were previously supplied by this " | ||||
| "source, but are now missing from it." | ||||
| msgstr "" | ||||
| "Eliminar usuarios y grupos de authentik que fueron proporcionados " | ||||
| "previamente por esta fuente, pero que ahora están ausentes." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "LDAP Source" | ||||
| @ -2539,24 +2512,22 @@ msgstr "Asignaciones de Propiedades de Fuente de LDAP" | ||||
| msgid "" | ||||
| "Unique ID used while checking if this object still exists in the directory." | ||||
| msgstr "" | ||||
| "ID único utilizado para verificar si este objeto aún existe en el " | ||||
| "directorio." | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connection" | ||||
| msgstr "Conexión de Fuente LDAP de Usuario" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "User LDAP Source Connections" | ||||
| msgstr "Conexiones de Fuente LDAP de Usuario" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Group LDAP Source Connection" | ||||
| msgstr "Conexión de Fuente LDAP de Grupo" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/models.py | ||||
| msgid "Group LDAP Source Connections" | ||||
| msgstr "Conexiones de Fuente LDAP de Grupo" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/ldap/signals.py | ||||
| msgid "Password does not match Active Directory Complexity." | ||||
| @ -2568,11 +2539,11 @@ msgstr "No se recibió ningún token." | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "HTTP Basic Authentication" | ||||
| msgstr "Autenticación Básica HTTP" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "Include the client ID and secret as request parameters" | ||||
| msgstr "Incluir el ID de cliente y el secreto como parámetros de la solicitud" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "Request Token URL" | ||||
| @ -2619,8 +2590,6 @@ msgid "" | ||||
| "How to perform authentication during an authorization_code token request " | ||||
| "flow" | ||||
| msgstr "" | ||||
| "Cómo realizar la autenticación durante un flujo de solicitud de token con " | ||||
| "authorization_code" | ||||
|  | ||||
| #: authentik/sources/oauth/models.py | ||||
| msgid "OAuth Source" | ||||
| @ -2938,7 +2907,7 @@ msgstr "Conexiones de Fuente de SAML de Grupo" | ||||
| #: authentik/sources/saml/views.py | ||||
| #, python-brace-format | ||||
| msgid "Continue to {source_name}" | ||||
| msgstr "Continuar a {source_name}" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/sources/scim/models.py | ||||
| msgid "SCIM Source" | ||||
| @ -2974,7 +2943,7 @@ msgstr "Dispositivos Duo" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email OTP" | ||||
| msgstr "OTP por Correo Electrónico" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| #: authentik/stages/email/models.py | ||||
| @ -2995,11 +2964,11 @@ msgstr "" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email Authenticator Setup Stage" | ||||
| msgstr "Etapa de Configuración del Autenticador de Correo Electrónico" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email Authenticator Setup Stages" | ||||
| msgstr "Etapas de Configuración del Autenticador de Correo Electrónico" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| #: authentik/stages/authenticator_email/stage.py | ||||
| @ -3010,11 +2979,11 @@ msgstr "" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email Device" | ||||
| msgstr "Dispositivo de correo electrónico" | ||||
| msgstr "Dispositivo de Email" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/models.py | ||||
| msgid "Email Devices" | ||||
| msgstr "Dispositivos de correo electrónico" | ||||
| msgstr "Dispositivos de Email" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/stage.py | ||||
| #: authentik/stages/authenticator_sms/stage.py | ||||
| @ -3024,7 +2993,7 @@ msgstr "El código no coincide" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/stage.py | ||||
| msgid "Invalid email" | ||||
| msgstr "Correo electrónico inválido" | ||||
| msgstr "Email Inválido" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html | ||||
| #: authentik/stages/email/templates/email/password_reset.html | ||||
| @ -3044,9 +3013,6 @@ msgid "" | ||||
| "          Email MFA code.\n" | ||||
| "          " | ||||
| msgstr "" | ||||
| "\n" | ||||
| "          Código MFA por correo electrónico.\n" | ||||
| "          " | ||||
|  | ||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.html | ||||
| #, python-format | ||||
| @ -3056,8 +3022,7 @@ msgid "" | ||||
| "    " | ||||
| msgstr "" | ||||
| "\n" | ||||
| "    Si no solicitaste este código, por favor ignora este correo. El código anterior es válido por %(expires)s.\n" | ||||
| "    " | ||||
| "Si no solicitaste este código, por favor ignora este correo. El código anterior es válido por %(expires)s." | ||||
|  | ||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.txt | ||||
| #: authentik/stages/email/templates/email/password_reset.txt | ||||
| @ -3070,8 +3035,6 @@ msgid "" | ||||
| "\n" | ||||
| "Email MFA code\n" | ||||
| msgstr "" | ||||
| "\n" | ||||
| "Código MFA por correo electrónico\n" | ||||
|  | ||||
| #: authentik/stages/authenticator_email/templates/email/email_otp.txt | ||||
| #, python-format | ||||
| @ -3313,8 +3276,8 @@ msgstr "No se pudo validar el token" | ||||
| msgid "" | ||||
| "Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3)." | ||||
| msgstr "" | ||||
| "Desfase después del cual expira el consentimiento. (Formato: " | ||||
| "hours=1;minutes=2;seconds=3)." | ||||
| "Compensación después de la cual caduca el consentimiento. (Formato: horas = " | ||||
| "1; minutos = 2; segundos = 3)." | ||||
|  | ||||
| #: authentik/stages/consent/models.py | ||||
| msgid "Consent Stage" | ||||
| @ -3334,7 +3297,7 @@ msgstr "Consentimientos del usuario" | ||||
|  | ||||
| #: authentik/stages/consent/stage.py | ||||
| msgid "Invalid consent token, re-showing prompt" | ||||
| msgstr "Token de consentimiento inválido, mostrando el aviso nuevamente" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/deny/models.py | ||||
| msgid "Deny Stage" | ||||
| @ -3354,11 +3317,11 @@ msgstr "Etapas ficticias" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Continue to confirm this email address." | ||||
| msgstr "Continúa para confirmar esta dirección de correo electrónico." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/flow.py | ||||
| msgid "Link was already used, please request a new link." | ||||
| msgstr "El enlace ya fue utilizado, por favor, solícita uno nuevo." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/email/models.py | ||||
| msgid "Password Reset" | ||||
| @ -3482,8 +3445,7 @@ msgid "" | ||||
| "    " | ||||
| msgstr "" | ||||
| "\n" | ||||
| "    Si no solicitaste un cambio de contraseña, por favor ignora este correo. El enlace anterior es válido por %(expires)s.\n" | ||||
| "    " | ||||
| "Si no solicitaste un cambio de contraseña, por favor ignora este correo. El enlace anterior es válido por %(expires)s." | ||||
|  | ||||
| #: authentik/stages/email/templates/email/password_reset.txt | ||||
| msgid "" | ||||
| @ -3567,26 +3529,24 @@ msgid "" | ||||
| "Show the user the 'Remember me on this device' toggle, allowing repeat users" | ||||
| " to skip straight to entering their password." | ||||
| msgstr "" | ||||
| "Mostrar al usuario la opción \"Recordarme en este dispositivo\", permitiendo" | ||||
| " que los usuarios recurrentes pasen directamente a ingresar su contraseña." | ||||
|  | ||||
| #: authentik/stages/identification/models.py | ||||
| msgid "Optional enrollment flow, which is linked at the bottom of the page." | ||||
| msgstr "" | ||||
| "Flujo de inscripción opcional, que se enlaza en la parte inferior de la " | ||||
| "página." | ||||
| "Flujo de inscripción opcional, que está vinculado en la parte inferior de la" | ||||
| " página." | ||||
|  | ||||
| #: authentik/stages/identification/models.py | ||||
| msgid "Optional recovery flow, which is linked at the bottom of the page." | ||||
| msgstr "" | ||||
| "Flujo de recuperación opcional, que se enlaza en la parte inferior de la " | ||||
| "página." | ||||
| "Flujo de recuperación opcional, que está vinculado en la parte inferior de " | ||||
| "la página." | ||||
|  | ||||
| #: authentik/stages/identification/models.py | ||||
| msgid "Optional passwordless flow, which is linked at the bottom of the page." | ||||
| msgstr "" | ||||
| "Flujo opcional sin contraseña, que se enlaza en la parte inferior de la " | ||||
| "página." | ||||
| "Flujo sin contraseña opcional, el cual está vinculado en la parte inferior " | ||||
| "de la página." | ||||
|  | ||||
| #: authentik/stages/identification/models.py | ||||
| msgid "Specify which sources should be shown." | ||||
| @ -3820,11 +3780,11 @@ msgstr "Las contraseñas no coinciden." | ||||
|  | ||||
| #: authentik/stages/redirect/api.py | ||||
| msgid "Target URL should be present when mode is Static." | ||||
| msgstr "La URL de destino debe estar presente cuando el modo es Estático." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/redirect/api.py | ||||
| msgid "Target Flow should be present when mode is Flow." | ||||
| msgstr "El Flujo de Destino debe estar presente cuando el modo es Flujo." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/stages/redirect/models.py | ||||
| msgid "Redirect Stage" | ||||
| @ -3881,6 +3841,10 @@ msgstr "Etapas de inicio de" | ||||
| msgid "No Pending user to login." | ||||
| msgstr "Ningún usuario pendiente para iniciar sesión." | ||||
|  | ||||
| #: authentik/stages/user_login/stage.py | ||||
| msgid "Successfully logged in!" | ||||
| msgstr "¡Se ha iniciado sesión correctamente!" | ||||
|  | ||||
| #: authentik/stages/user_logout/models.py | ||||
| msgid "User Logout Stage" | ||||
| msgstr "Etapa de cierre de sesión del usuario" | ||||
| @ -3956,12 +3920,10 @@ msgstr "" | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Reputation cannot decrease lower than this value. Zero or negative." | ||||
| msgstr "" | ||||
| "La reputación no puede disminuir por debajo de este valor. Cero o negativo." | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Reputation cannot increase higher than this value. Zero or positive." | ||||
| msgstr "" | ||||
| "La reputación no puede aumentar por encima de este valor. Cero o positivo." | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "The option configures the footer links on the flow executor pages." | ||||
| @ -3984,8 +3946,8 @@ msgstr "Personificación habilitada/deshabilitada globalmente." | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Require administrators to provide a reason for impersonating a user." | ||||
| msgstr "" | ||||
| "Requerir que los administradores proporcionen una razón para personificar a " | ||||
| "un usuario." | ||||
| "Requerir a los administradores proporcionar una razón para suplantar un " | ||||
| "usuario." | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Default token duration" | ||||
| @ -3997,7 +3959,7 @@ msgstr "Longitud predeterminada del token" | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Tenant" | ||||
| msgstr "Inquilino" | ||||
| msgstr "inquilino" | ||||
|  | ||||
| #: authentik/tenants/models.py | ||||
| msgid "Tenants" | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-secret-textarea-input.js"; | ||||
| import "@goauthentik/components/ak-private-textarea-input.js"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| @ -46,7 +46,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string | ||||
|                     required | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-secret-textarea-input | ||||
|             <ak-private-textarea-input | ||||
|                 label=${msg("Certificate")} | ||||
|                 name="certificateData" | ||||
|                 input-hint="code" | ||||
| @ -54,8 +54,8 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string | ||||
|                 required | ||||
|                 ?revealed=${this.instance === undefined} | ||||
|                 help=${msg("PEM-encoded Certificate data.")} | ||||
|             ></ak-secret-textarea-input> | ||||
|             <ak-secret-textarea-input | ||||
|             ></ak-private-textarea-input> | ||||
|             <ak-private-textarea-input | ||||
|                 label=${msg("Private Key")} | ||||
|                 name="keyData" | ||||
|                 input-hint="code" | ||||
| @ -63,7 +63,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string | ||||
|                 help=${msg( | ||||
|                     "Optional Private Key. If this is set, you can use this keypair for encryption.", | ||||
|                 )} | ||||
|             ></ak-secret-textarea-input>`; | ||||
|             ></ak-private-textarea-input>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants"; | ||||
| import "@goauthentik/components/ak-secret-textarea-input.js"; | ||||
| import "@goauthentik/components/ak-private-textarea-input.js"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| @ -62,13 +62,13 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> { | ||||
|                     value="${ifDefined(this.installID)}" | ||||
|                 /> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-secret-textarea-input | ||||
|             <ak-private-textarea-input | ||||
|                 name="key" | ||||
|                 ?revealed=${this.instance === undefined} | ||||
|                 label=${msg("License key")} | ||||
|                 input-hint="code" | ||||
|             > | ||||
|             </ak-secret-textarea-input>`; | ||||
|             </ak-private-textarea-input>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -4,7 +4,6 @@ import { | ||||
|     propertyMappingsSelector, | ||||
| } from "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderFormHelpers.js"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-hidden-text-input"; | ||||
| import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; | ||||
| import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| @ -69,15 +68,21 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn | ||||
|                             ${msg("Client ID for the app registration.")} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-hidden-text-input | ||||
|                         name="clientSecret" | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${msg("Client Secret")} | ||||
|                         value="${this.instance?.clientSecret ?? ""}" | ||||
|                         input-hint="code" | ||||
|                         required | ||||
|                         .help=${msg("Client secret for the app registration.")} | ||||
|                         name="clientSecret" | ||||
|                     > | ||||
|                     </ak-hidden-text-input> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             value="${this.instance?.clientSecret ?? ""}" | ||||
|                             class="pf-c-form-control pf-m-monospace" | ||||
|                             required | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${msg("Client secret for the app registration.")} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${msg("Tenant ID")} required name="tenantId"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|  | ||||
| @ -5,7 +5,6 @@ import { | ||||
|     akOAuthRedirectURIInput, | ||||
| } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI"; | ||||
| import { ascii_letters, digits, randomString } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/components/ak-hidden-text-input"; | ||||
| import "@goauthentik/components/ak-radio-input"; | ||||
| import "@goauthentik/components/ak-text-input"; | ||||
| import "@goauthentik/components/ak-textarea-input"; | ||||
| @ -167,16 +166,17 @@ export function renderForm( | ||||
|                     input-hint="code" | ||||
|                 > | ||||
|                 </ak-text-input> | ||||
|                 <ak-hidden-text-input | ||||
|                 <ak-text-input | ||||
|                     name="clientSecret" | ||||
|                     label=${msg("Client Secret")} | ||||
|                     value="${provider?.clientSecret ?? randomString(128, ascii_letters + digits)}" | ||||
|                     input-hint="code" | ||||
|                     ?hidden=${!showClientSecret} | ||||
|                 > | ||||
|                 </ak-hidden-text-input> | ||||
|                 </ak-text-input> | ||||
|                 <ak-form-element-horizontal | ||||
|                     label=${msg("Redirect URIs/Origins (RegEx)")} | ||||
|                     required | ||||
|                     name="redirectUris" | ||||
|                 > | ||||
|                     <ak-array-input | ||||
|  | ||||
| @ -1,8 +1,6 @@ | ||||
| import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; | ||||
| import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; | ||||
| import { ascii_letters, digits, randomString } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/components/ak-hidden-text-input"; | ||||
| import "@goauthentik/components/ak-text-input"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import "@goauthentik/elements/forms/SearchSelect"; | ||||
| @ -76,14 +74,14 @@ export function renderForm( | ||||
|         <ak-form-group expanded> | ||||
|             <span slot="header"> ${msg("Protocol settings")} </span> | ||||
|             <div slot="body" class="pf-c-form"> | ||||
|                 <ak-hidden-text-input | ||||
|                 <ak-text-input | ||||
|                     name="sharedSecret" | ||||
|                     label=${msg("Shared secret")} | ||||
|                     .errorMessages=${errors?.sharedSecret ?? []} | ||||
|                     value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)} | ||||
|                     required | ||||
|                     input-hint="code" | ||||
|                 ></ak-hidden-text-input> | ||||
|                 ></ak-text-input> | ||||
|                 <ak-text-input | ||||
|                     name="clientNetworks" | ||||
|                     label=${msg("Client Networks")} | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-hidden-text-input"; | ||||
| import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -51,7 +50,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE | ||||
|                 > | ||||
|                 </ak-switch-input> | ||||
|  | ||||
|                 <ak-hidden-text-input | ||||
|                 <ak-text-input | ||||
|                     name="token" | ||||
|                     label=${msg("Token")} | ||||
|                     value="${provider?.token ?? ""}" | ||||
| @ -61,7 +60,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE | ||||
|                         "Token to authenticate with. Currently only bearer authentication is supported.", | ||||
|                     )} | ||||
|                     input-hint="code" | ||||
|                 ></ak-hidden-text-input> | ||||
|                 ></ak-text-input> | ||||
|                 <ak-radio-input | ||||
|                     name="compatibilityMode" | ||||
|                     label=${msg("Compatibility Mode")} | ||||
|  | ||||
| @ -7,8 +7,8 @@ import { | ||||
|     UserMatchingModeToLabel, | ||||
| } from "@goauthentik/admin/sources/oauth/utils"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-secret-text-input.js"; | ||||
| import "@goauthentik/components/ak-secret-textarea-input.js"; | ||||
| import "@goauthentik/components/ak-private-text-input.js"; | ||||
| import "@goauthentik/components/ak-private-textarea-input.js"; | ||||
| import "@goauthentik/components/ak-switch-input"; | ||||
| import "@goauthentik/components/ak-text-input"; | ||||
| import "@goauthentik/components/ak-textarea-input"; | ||||
| @ -248,22 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke | ||||
|                         value=${ifDefined(this.instance?.syncPrincipal)} | ||||
|                         help=${msg("Principal used to authenticate to the KDC for syncing.")} | ||||
|                     ></ak-text-input> | ||||
|                     <ak-secret-text-input | ||||
|                     <ak-private-text-input | ||||
|                         name="syncPassword" | ||||
|                         label=${msg("Sync password")} | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                         help=${msg( | ||||
|                             "Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.", | ||||
|                         )} | ||||
|                     ></ak-secret-text-input> | ||||
|                     <ak-secret-textarea-input | ||||
|                     ></ak-private-text-input> | ||||
|                     <ak-private-textarea-input | ||||
|                         name="syncKeytab" | ||||
|                         label=${msg("Sync keytab")} | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                         help=${msg( | ||||
|                             "Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.", | ||||
|                         )} | ||||
|                     ></ak-secret-textarea-input> | ||||
|                     ></ak-private-textarea-input> | ||||
|                     <ak-text-input | ||||
|                         name="syncCcache" | ||||
|                         label=${msg("Sync credentials cache")} | ||||
| @ -285,14 +285,14 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke | ||||
|                             "Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain", | ||||
|                         )} | ||||
|                     ></ak-text-input> | ||||
|                     <ak-secret-textarea-input | ||||
|                     <ak-private-textarea-input | ||||
|                         name="spnegoKeytab" | ||||
|                         label=${msg("SPNEGO keytab")} | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                         help=${msg( | ||||
|                             "Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.", | ||||
|                         )} | ||||
|                     ></ak-secret-textarea-input> | ||||
|                     ></ak-private-textarea-input> | ||||
|                     <ak-text-input | ||||
|                         name="spnegoCcache" | ||||
|                         label=${msg("SPNEGO credentials cache")} | ||||
|  | ||||
| @ -2,7 +2,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search"; | ||||
| import { placeholderHelperText } from "@goauthentik/admin/helperText"; | ||||
| import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-secret-text-input.js"; | ||||
| import "@goauthentik/components/ak-private-text-input.js"; | ||||
| import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -260,11 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> { | ||||
|                             class="pf-c-form-control" | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-secret-text-input | ||||
|                     <ak-private-text-input | ||||
|                         label=${msg("Bind Password")} | ||||
|                         name="bindPassword" | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                     ></ak-secret-text-input> | ||||
|                     ></ak-private-text-input> | ||||
|                     <ak-form-element-horizontal label=${msg("Base DN")} required name="baseDn"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|  | ||||
| @ -8,8 +8,8 @@ import { | ||||
|     UserMatchingModeToLabel, | ||||
| } from "@goauthentik/admin/sources/oauth/utils"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-private-textarea-input.js"; | ||||
| import "@goauthentik/components/ak-radio-input"; | ||||
| import "@goauthentik/components/ak-secret-textarea-input.js"; | ||||
| import "@goauthentik/elements/CodeMirror"; | ||||
| import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; | ||||
| import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; | ||||
| @ -441,14 +441,14 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth | ||||
|                         /> | ||||
|                         <p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-secret-textarea-input | ||||
|                     <ak-private-textarea-input | ||||
|                         label=${msg("Consumer secret")} | ||||
|                         name="consumerSecret" | ||||
|                         input-hint="code" | ||||
|                         help=${msg("Also known as Client Secret.")} | ||||
|                         required | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                     ></ak-secret-textarea-input> | ||||
|                     ></ak-private-textarea-input> | ||||
|                     <ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|  | ||||
| @ -128,7 +128,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo | ||||
|                     this.doAuth(); | ||||
|                 }} | ||||
|             > | ||||
|                 ${msg("Re-authenticate with Plex")} | ||||
|                 ${msg("Re-authenticate with plex")} | ||||
|             </button> | ||||
|             <ak-form-element-horizontal name="allowFriends"> | ||||
|                 <label class="pf-c-switch"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-secret-text-input.js"; | ||||
| import "@goauthentik/components/ak-private-text-input.js"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import "@goauthentik/elements/forms/SearchSelect"; | ||||
| @ -95,13 +95,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-secret-text-input | ||||
|                     <ak-private-text-input | ||||
|                         name="clientSecret" | ||||
|                         label=${msg("Secret key")} | ||||
|                         input-hint="code" | ||||
|                         required | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                     ></ak-secret-text-input> | ||||
|                     ></ak-private-text-input> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|             <ak-form-group> | ||||
| @ -125,12 +125,12 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta | ||||
|                             spellcheck="false" | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-secret-text-input | ||||
|                     <ak-private-text-input | ||||
|                         name="adminSecretKey" | ||||
|                         label=${msg("Secret key")} | ||||
|                         input-hint="code" | ||||
|                         ?revealed=${this.instance === undefined} | ||||
|                     ></ak-secret-text-input> | ||||
|                     ></ak-private-text-input> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|             <ak-form-group expanded> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; | ||||
| import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-secret-text-input.js"; | ||||
| import "@goauthentik/components/ak-private-text-input.js"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import "@goauthentik/elements/forms/Radio"; | ||||
| @ -77,11 +77,11 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai | ||||
|                     /> | ||||
|                 </ak-form-element-horizontal> | ||||
|  | ||||
|                 <ak-secret-text-input | ||||
|                 <ak-private-text-input | ||||
|                     name="password" | ||||
|                     label=${msg("SMTP Password")} | ||||
|                     ?revealed=${this.instance === undefined} | ||||
|                 ></ak-secret-text-input> | ||||
|                 ></ak-private-text-input> | ||||
|  | ||||
|                 <ak-form-element-horizontal name="useTls"> | ||||
|                     <label class="pf-c-switch"> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-number-input"; | ||||
| import "@goauthentik/components/ak-secret-text-input.js"; | ||||
| import "@goauthentik/components/ak-private-text-input.js"; | ||||
| import "@goauthentik/components/ak-switch-input"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -70,7 +70,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> { | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|  | ||||
|                     <ak-secret-text-input | ||||
|                     <ak-private-text-input | ||||
|                         name="privateKey" | ||||
|                         label=${msg("Private Key")} | ||||
|                         input-hint="code" | ||||
| @ -79,7 +79,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> { | ||||
|                         help=${msg( | ||||
|                             "Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.", | ||||
|                         )} | ||||
|                     ></ak-secret-text-input> | ||||
|                     ></ak-private-text-input> | ||||
|  | ||||
|                     <ak-switch-input | ||||
|                         name="interactive" | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/components/ak-secret-text-input.js"; | ||||
| import "@goauthentik/components/ak-private-text-input.js"; | ||||
| import "@goauthentik/elements/forms/FormGroup"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import "@goauthentik/elements/utils/TimeDeltaHelp"; | ||||
| @ -73,11 +73,11 @@ export class EmailStageForm extends BaseStageForm<EmailStage> { | ||||
|                         class="pf-c-form-control" | ||||
|                     /> | ||||
|                 </ak-form-element-horizontal> | ||||
|                 <ak-secret-text-input | ||||
|                 <ak-private-text-input | ||||
|                     label=${msg("SMTP Password")} | ||||
|                     name="password" | ||||
|                     ?revealed=${this.instance === undefined} | ||||
|                 ></ak-secret-text-input> | ||||
|                 ></ak-private-text-input> | ||||
|                 <ak-form-element-horizontal name="useTls"> | ||||
|                     <label class="pf-c-switch"> | ||||
|                         <input | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { dateTimeLocal } from "@goauthentik/common/temporal"; | ||||
| import "@goauthentik/components/ak-hidden-text-input"; | ||||
| import { Form } from "@goauthentik/elements/forms/Form"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModalForm } from "@goauthentik/elements/forms/ModalForm"; | ||||
| @ -125,14 +124,19 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> { | ||||
|                         class="pf-c-form-control" | ||||
|                     /> | ||||
|                 </ak-form-element-horizontal> | ||||
|                 <ak-hidden-text-input | ||||
|                     label=${msg("Password")} | ||||
|                     value="${this.result?.token ?? ""}" | ||||
|                     .help=${msg( | ||||
|                         "Valid for 360 days, after which the password will automatically rotate. You can copy the password from the Token List.", | ||||
|                     )} | ||||
|                 > | ||||
|                 </ak-hidden-text-input> | ||||
|                 <ak-form-element-horizontal label=${msg("Password")}> | ||||
|                     <input | ||||
|                         type="text" | ||||
|                         readonly | ||||
|                         value=${ifDefined(this.result?.token)} | ||||
|                         class="pf-c-form-control" | ||||
|                     /> | ||||
|                     <p class="pf-c-form__helper-text"> | ||||
|                         ${msg( | ||||
|                             "Valid for 360 days, after which the password will automatically rotate. You can copy the password from the Token List.", | ||||
|                         )} | ||||
|                     </p> | ||||
|                 </ak-form-element-horizontal> | ||||
|             </form>`; | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { AKElement, type AKElementProps } from "@goauthentik/elements/Base"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement.js"; | ||||
|  | ||||
| import { TemplateResult, html, nothing } from "lit"; | ||||
| @ -6,19 +6,6 @@ import { property } from "lit/decorators.js"; | ||||
|  | ||||
| type HelpType = TemplateResult | typeof nothing; | ||||
|  | ||||
| export interface HorizontalLightComponentProps<T> extends AKElementProps { | ||||
|     name: string; | ||||
|     label?: string; | ||||
|     required?: boolean; | ||||
|     help?: string; | ||||
|     bighelp?: TemplateResult | TemplateResult[]; | ||||
|     hidden?: boolean; | ||||
|     invalid?: boolean; | ||||
|     errorMessages?: string[]; | ||||
|     value?: T; | ||||
|     inputHint?: string; | ||||
| } | ||||
|  | ||||
| export class HorizontalLightComponent<T> extends AKElement { | ||||
|     // Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but | ||||
|     // we're not actually using that and, for the meantime, we need the form handlers to be able to | ||||
| @ -31,81 +18,37 @@ export class HorizontalLightComponent<T> extends AKElement { | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The name attribute for the form element | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, reflect: true }) | ||||
|     name!: string; | ||||
|  | ||||
|     /** | ||||
|      * The label for the input control | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, reflect: true }) | ||||
|     label = ""; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     required = false; | ||||
|  | ||||
|     /** | ||||
|      * Help text to display below the form element. Optional | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, reflect: true }) | ||||
|     help = ""; | ||||
|  | ||||
|     /** | ||||
|      * Extended help content. Optional. Expects to be a TemplateResult | ||||
|      * @property | ||||
|      */ | ||||
|     @property({ type: Object }) | ||||
|     bighelp?: TemplateResult | TemplateResult[]; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     hidden = false; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     invalid = false; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      */ | ||||
|     @property({ attribute: false }) | ||||
|     errorMessages: string[] = []; | ||||
|  | ||||
|     /** | ||||
|      * @attribute | ||||
|      * @property | ||||
|      */ | ||||
|     @property({ attribute: false }) | ||||
|     value?: T; | ||||
|  | ||||
|     /** | ||||
|      * Input hint. | ||||
|      *   - `code`: uses a monospace font and disables spellcheck & autocomplete | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, attribute: "input-hint" }) | ||||
|     inputHint = ""; | ||||
|  | ||||
|     protected renderControl() { | ||||
|     renderControl() { | ||||
|         throw new Error("Must be implemented in a subclass"); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,159 +0,0 @@ | ||||
| import { bound } from "#elements/decorators/bound"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators.js"; | ||||
| import { classMap } from "lit/directives/class-map.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { | ||||
|     HorizontalLightComponent, | ||||
|     HorizontalLightComponentProps, | ||||
| } from "./HorizontalLightComponent"; | ||||
| import "./ak-visibility-toggle.js"; | ||||
| import type { VisibilityToggleProps } from "./ak-visibility-toggle.js"; | ||||
|  | ||||
| type BaseProps = HorizontalLightComponentProps<string> & | ||||
|     Pick<VisibilityToggleProps, "showMessage" | "hideMessage">; | ||||
|  | ||||
| export interface AkHiddenTextInputProps extends BaseProps { | ||||
|     revealed: boolean; | ||||
|     placeholder?: string; | ||||
| } | ||||
|  | ||||
| export type InputLike = HTMLTextAreaElement | HTMLInputElement; | ||||
|  | ||||
| /** | ||||
|  * @element ak-hidden-text-input | ||||
|  * @class AkHiddenTextInput | ||||
|  * | ||||
|  * A text-input field with a visibility control, so you can show/hide sensitive fields. | ||||
|  * | ||||
|  * ## CSS Parts | ||||
|  * @csspart container - The main container div | ||||
|  * @csspart input - The input element | ||||
|  * @csspart toggle - The visibility toggle button | ||||
|  * | ||||
|  */ | ||||
| @customElement("ak-hidden-text-input") | ||||
| export class AkHiddenTextInput<T extends InputLike = HTMLInputElement> | ||||
|     extends HorizontalLightComponent<string> | ||||
|     implements AkHiddenTextInputProps | ||||
| { | ||||
|     public static get styles() { | ||||
|         return [ | ||||
|             css` | ||||
|                 main { | ||||
|                     display: flex; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, reflect: true }) | ||||
|     public value = ""; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     public revealed = false; | ||||
|  | ||||
|     /** | ||||
|      * Text for when the input has no set value | ||||
|      * | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String }) | ||||
|     public placeholder?: string; | ||||
|  | ||||
|     /** | ||||
|      * Specify kind of help the browser should try to provide | ||||
|      * | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String }) | ||||
|     public autocomplete?: "none" | AutoFill; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, attribute: "show-message" }) | ||||
|     public showMessage = msg("Show field content"); | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, attribute: "hide-message" }) | ||||
|     public hideMessage = msg("Hide field content"); | ||||
|  | ||||
|     @query("#main > input") | ||||
|     protected inputField!: T; | ||||
|  | ||||
|     @bound | ||||
|     private handleToggleVisibility() { | ||||
|         this.revealed = !this.revealed; | ||||
|  | ||||
|         // Maintain focus on input after toggle | ||||
|         this.updateComplete.then(() => { | ||||
|             if (this.inputField && document.activeElement === this) { | ||||
|                 this.inputField.focus(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content | ||||
|     // in the LightDom so the inner components actually inherit styling, the normal `css` options | ||||
|     // aren't available. Embedding styles is bad styling, and we'll fix it in the next style | ||||
|     // refresh. | ||||
|     protected renderInputField(setValue: (ev: InputEvent) => void, code: boolean) { | ||||
|         return html` <input | ||||
|             style="flex: 1 1 auto; min-width: 0;" | ||||
|             part="input" | ||||
|             type=${this.revealed ? "text" : "password"} | ||||
|             @input=${setValue} | ||||
|             value=${ifDefined(this.value)} | ||||
|             placeholder=${ifDefined(this.placeholder)} | ||||
|             class="${classMap({ | ||||
|                 "pf-c-form-control": true, | ||||
|                 "pf-m-monospace": code, | ||||
|             })}" | ||||
|             spellcheck=${code ? "false" : "true"} | ||||
|             ?required=${this.required} | ||||
|         />`; | ||||
|     } | ||||
|  | ||||
|     protected override renderControl() { | ||||
|         const code = this.inputHint === "code"; | ||||
|         const setValue = (ev: InputEvent) => { | ||||
|             this.value = (ev.target as T).value; | ||||
|         }; | ||||
|         return html` <div style="display: flex; gap: 0.25rem"> | ||||
|             ${this.renderInputField(setValue, code)} | ||||
|             <!-- --> | ||||
|             <ak-visibility-toggle | ||||
|                 part="toggle" | ||||
|                 style="flex: 0 0 auto; align-self: flex-start" | ||||
|                 ?open=${this.revealed} | ||||
|                 show-message=${this.showMessage} | ||||
|                 hide-message=${this.hideMessage} | ||||
|                 @click=${() => (this.revealed = !this.revealed)} | ||||
|             ></ak-visibility-toggle> | ||||
|         </div>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|     interface HTMLElementTagNameMap { | ||||
|         "ak-hidden-text-input": AkHiddenTextInput; | ||||
|     } | ||||
| } | ||||
| @ -1,128 +0,0 @@ | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators.js"; | ||||
| import { classMap } from "lit/directives/class-map.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { AkHiddenTextInput, type AkHiddenTextInputProps } from "./ak-hidden-text-input.js"; | ||||
|  | ||||
| export interface AkHiddenTextAreaInputProps extends AkHiddenTextInputProps { | ||||
|     /** | ||||
|      * Number of visible text lines (rows) | ||||
|      */ | ||||
|     rows?: number; | ||||
|  | ||||
|     /** | ||||
|      * Number of visible character width (cols) | ||||
|      */ | ||||
|     cols?: number; | ||||
|  | ||||
|     /** | ||||
|      * How the textarea can be resized | ||||
|      */ | ||||
|     resize?: "none" | "both" | "horizontal" | "vertical"; | ||||
|  | ||||
|     /** | ||||
|      * Whether text should wrap | ||||
|      */ | ||||
|     wrap?: "soft" | "hard" | "off"; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @element ak-hidden-text-input | ||||
|  * @class AkHiddenTextInput | ||||
|  * | ||||
|  * A text-input field with a visibility control, so you can show/hide sensitive fields. | ||||
|  * | ||||
|  * ## CSS Parts | ||||
|  * @csspart container - The main container div | ||||
|  * @csspart input - The input element | ||||
|  * @csspart toggle - The visibility toggle button | ||||
|  * | ||||
|  */ | ||||
| @customElement("ak-hidden-textarea-input") | ||||
| export class AkHiddenTextAreaInput | ||||
|     extends AkHiddenTextInput<HTMLTextAreaElement> | ||||
|     implements AkHiddenTextAreaInputProps | ||||
| { | ||||
|     /* These are mostly just forwarded to the textarea component. */ | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Number }) | ||||
|     rows?: number = 4; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Number }) | ||||
|     cols?: number; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      * | ||||
|      * You want `resize=true` so that the resize value is visible in the component tag, activating | ||||
|      * the CSS associated with these values. | ||||
|      */ | ||||
|     @property({ type: String, reflect: true }) | ||||
|     resize?: "none" | "both" | "horizontal" | "vertical" = "vertical"; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String }) | ||||
|     wrap?: "soft" | "hard" | "off" = "soft"; | ||||
|  | ||||
|     @query("#main > textarea") | ||||
|     protected inputField!: HTMLTextAreaElement; | ||||
|  | ||||
|     get displayValue() { | ||||
|         const value = this.value ?? ""; | ||||
|         if (this.revealed) { | ||||
|             return value; | ||||
|         } | ||||
|  | ||||
|         return value | ||||
|             .split("\n") | ||||
|             .reduce((acc: string[], line: string) => [...acc, "*".repeat(line.length)], []) | ||||
|             .join("\n"); | ||||
|     } | ||||
|  | ||||
|     // TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content | ||||
|     // in the LightDom so the inner components actually inherit styling, the normal `css` options | ||||
|     // aren't available. Embedding styles is bad styling, and we'll fix it in the next style | ||||
|     // refresh. | ||||
|     protected override renderInputField(setValue: (ev: InputEvent) => void, code: boolean) { | ||||
|         const wrap = this.revealed ? this.wrap : "soft"; | ||||
|  | ||||
|         return html` | ||||
|             <textarea | ||||
|                 style="flex: 1 1 auto; min-width: 0;" | ||||
|                 part="textarea" | ||||
|                 @input=${setValue} | ||||
|                 placeholder=${ifDefined(this.placeholder)} | ||||
|                 rows=${ifDefined(this.rows)} | ||||
|                 cols=${ifDefined(this.cols)} | ||||
|                 wrap=${ifDefined(wrap)} | ||||
|                 class=${classMap({ | ||||
|                     "pf-c-form-control": true, | ||||
|                     "pf-m-monospace": code, | ||||
|                 })} | ||||
|                 spellcheck=${code ? "false" : "true"} | ||||
|                 ?required=${this.required} | ||||
|             > | ||||
| ${this.displayValue}</textarea | ||||
|             > | ||||
|         `; | ||||
|     } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|     interface HTMLElementTagNameMap { | ||||
|         "ak-hidden-textarea-input": AkHiddenTextAreaInput; | ||||
|     } | ||||
| } | ||||
| @ -8,8 +8,8 @@ import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| 
 | ||||
| import { HorizontalLightComponent } from "./HorizontalLightComponent"; | ||||
| 
 | ||||
| @customElement("ak-secret-text-input") | ||||
| export class AkSecretTextInput extends HorizontalLightComponent<string> { | ||||
| @customElement("ak-private-text-input") | ||||
| export class AkPrivateTextInput extends HorizontalLightComponent<string> { | ||||
|     @property({ type: String, reflect: true }) | ||||
|     public value = ""; | ||||
| 
 | ||||
| @ -23,7 +23,7 @@ export class AkSecretTextInput extends HorizontalLightComponent<string> { | ||||
|         this.revealed = true; | ||||
|     } | ||||
| 
 | ||||
|     #renderSecretInput() { | ||||
|     #renderPrivateInput() { | ||||
|         return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
 | ||||
|             <input | ||||
|                 class="pf-c-form-control" | ||||
| @ -60,14 +60,14 @@ export class AkSecretTextInput extends HorizontalLightComponent<string> { | ||||
|     } | ||||
| 
 | ||||
|     public override renderControl() { | ||||
|         return this.revealed ? this.renderVisibleInput() : this.#renderSecretInput(); | ||||
|         return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default AkSecretTextInput; | ||||
| export default AkPrivateTextInput; | ||||
| 
 | ||||
| declare global { | ||||
|     interface HTMLElementTagNameMap { | ||||
|         "ak-secret-text-input": AkSecretTextInput; | ||||
|         "ak-private-text-input": AkPrivateTextInput; | ||||
|     } | ||||
| } | ||||
| @ -5,10 +5,10 @@ import { customElement, property } from "lit/decorators.js"; | ||||
| import { classMap } from "lit/directives/class-map.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| 
 | ||||
| import { AkSecretTextInput } from "./ak-secret-text-input.js"; | ||||
| import { AkPrivateTextInput } from "./ak-private-text-input.js"; | ||||
| 
 | ||||
| @customElement("ak-secret-textarea-input") | ||||
| export class AkSecretTextAreaInput extends AkSecretTextInput { | ||||
| @customElement("ak-private-textarea-input") | ||||
| export class AkPrivateTextAreaInput extends AkPrivateTextInput { | ||||
|     protected override renderVisibleInput() { | ||||
|         const code = this.inputHint === "code"; | ||||
|         const setValue = (ev: InputEvent) => { | ||||
| @ -34,10 +34,10 @@ export class AkSecretTextAreaInput extends AkSecretTextInput { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default AkSecretTextAreaInput; | ||||
| export default AkPrivateTextAreaInput; | ||||
| 
 | ||||
| declare global { | ||||
|     interface HTMLElementTagNameMap { | ||||
|         "ak-secret-textarea-input": AkSecretTextAreaInput; | ||||
|         "ak-private-textarea-input": AkPrivateTextAreaInput; | ||||
|     } | ||||
| } | ||||
| @ -1,89 +0,0 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base.js"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| export interface VisibilityToggleProps { | ||||
|     open: boolean; | ||||
|     disabled: boolean; | ||||
|     showMessage: string; | ||||
|     hideMessage: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @component ak-visibility-toggle | ||||
|  * @class VisibilityToggle | ||||
|  * | ||||
|  * A straightforward two-state iconic button we use in a few places as way of telling users to hide | ||||
|  * or show something secret, such as a password or private key. Expects the client to manage its | ||||
|  * state. | ||||
|  * | ||||
|  * @events | ||||
|  * - click: when the toggle is clicked. | ||||
|  */ | ||||
| @customElement("ak-visibility-toggle") | ||||
| export class VisibilityToggle extends AKElement implements VisibilityToggleProps { | ||||
|     static get styles() { | ||||
|         return [PFBase, PFButton]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     open = false; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: Boolean, reflect: true }) | ||||
|     disabled = false; | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, attribute: "show-message" }) | ||||
|     showMessage = msg("Show field content"); | ||||
|  | ||||
|     /** | ||||
|      * @property | ||||
|      * @attribute | ||||
|      */ | ||||
|     @property({ type: String, attribute: "hide-message" }) | ||||
|     hideMessage = msg("Hide field content"); | ||||
|  | ||||
|     render() { | ||||
|         const [label, icon] = this.open | ||||
|             ? [this.hideMessage, "fa-eye"] | ||||
|             : [this.showMessage, "fa-eye-slash"]; | ||||
|  | ||||
|         const onClick = (ev: PointerEvent) => { | ||||
|             ev.stopPropagation(); | ||||
|             this.dispatchEvent(new PointerEvent(ev.type, ev)); | ||||
|         }; | ||||
|  | ||||
|         return html`<button | ||||
|             aria-label=${label} | ||||
|             title=${label} | ||||
|             @click=${onClick} | ||||
|             ?disabled=${this.disabled} | ||||
|             class="pf-c-button pf-m-control" | ||||
|             type="button" | ||||
|         > | ||||
|             <i class="fas ${icon}" aria-hidden="true"></i> | ||||
|         </button>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|     interface HTMLElementTagNameMap { | ||||
|         "ak-visibility-toggle": VisibilityToggle; | ||||
|     } | ||||
| } | ||||
| @ -1,93 +0,0 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { html, nothing } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import "../ak-hidden-text-input"; | ||||
| import { type AkHiddenTextInput, type AkHiddenTextInputProps } from "../ak-hidden-text-input.js"; | ||||
|  | ||||
| const metadata: Meta<AkHiddenTextInputProps> = { | ||||
|     title: "Components / <ak-hidden-text-input>", | ||||
|     component: "ak-hidden-text-input", | ||||
|     tags: ["autodocs"], | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: { | ||||
|                 component: ` | ||||
| # Hidden Text Input Component | ||||
|  | ||||
| A text-input field with a visibility control, so you can show/hide sensitive fields. | ||||
| `, | ||||
|             }, | ||||
|         }, | ||||
|         layout: "padded", | ||||
|     }, | ||||
|     argTypes: { | ||||
|         label: { | ||||
|             control: "text", | ||||
|             description: "Label text for the input field", | ||||
|         }, | ||||
|         value: { | ||||
|             control: "text", | ||||
|             description: "Current value of the input", | ||||
|         }, | ||||
|         revealed: { | ||||
|             control: "boolean", | ||||
|             description: "Whether the text is currently visible", | ||||
|         }, | ||||
|         placeholder: { | ||||
|             control: "text", | ||||
|             description: "Placeholder text for the input", | ||||
|         }, | ||||
|         required: { | ||||
|             control: "boolean", | ||||
|             description: "Whether the input is required", | ||||
|         }, | ||||
|         inputHint: { | ||||
|             control: "select", | ||||
|             options: ["text", "code"], | ||||
|             description: "Input type hint for styling and behavior", | ||||
|         }, | ||||
|         showMessage: { | ||||
|             control: "text", | ||||
|             description: "Custom message for show action", | ||||
|         }, | ||||
|         hideMessage: { | ||||
|             control: "text", | ||||
|             description: "Custom message for hide action", | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| type Story = StoryObj<AkHiddenTextInput>; | ||||
|  | ||||
| const Template: Story = { | ||||
|     args: { | ||||
|         label: "Hidden Text Input", | ||||
|         value: "", | ||||
|         revealed: false, | ||||
|     }, | ||||
|     render: (args) => html` | ||||
|         <ak-hidden-text-input | ||||
|             label=${ifDefined(args.label)} | ||||
|             value=${ifDefined(args.value)} | ||||
|             ?revealed=${args.revealed} | ||||
|             placeholder=${ifDefined(args.placeholder)} | ||||
|             ?required=${args.required} | ||||
|             input-hint=${ifDefined(args.inputHint)} | ||||
|             show-message=${ifDefined(args.showMessage)} | ||||
|             hide-message=${ifDefined(args.hideMessage)} | ||||
|         ></ak-hidden-text-input> | ||||
|     `, | ||||
| }; | ||||
|  | ||||
| export const Password: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         label: "Password", | ||||
|         placeholder: "Enter your password", | ||||
|         required: true, | ||||
|     }, | ||||
| }; | ||||
| @ -1,140 +0,0 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { html, nothing } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import "../ak-hidden-textarea-input"; | ||||
| import { | ||||
|     type AkHiddenTextAreaInput, | ||||
|     type AkHiddenTextAreaInputProps, | ||||
| } from "../ak-hidden-textarea-input.js"; | ||||
|  | ||||
| const metadata: Meta<AkHiddenTextAreaInputProps> = { | ||||
|     title: "Components / <ak-hidden-textarea-input>", | ||||
|     component: "ak-hidden-textarea-input", | ||||
|     tags: ["autodocs"], | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: { | ||||
|                 component: ` | ||||
| # Hidden Textarea Input Component | ||||
|  | ||||
| A textarea input field with a visibility control, so you can show/hide sensitive fields. | ||||
| `, | ||||
|             }, | ||||
|         }, | ||||
|         layout: "padded", | ||||
|     }, | ||||
|     argTypes: { | ||||
|         label: { | ||||
|             control: "text", | ||||
|             description: "Label text for the input field", | ||||
|         }, | ||||
|         value: { | ||||
|             control: "text", | ||||
|             description: "Current value of the input", | ||||
|         }, | ||||
|         revealed: { | ||||
|             control: "boolean", | ||||
|             description: "Whether the text is currently visible", | ||||
|         }, | ||||
|         placeholder: { | ||||
|             control: "text", | ||||
|             description: "Placeholder text for the input", | ||||
|         }, | ||||
|         required: { | ||||
|             control: "boolean", | ||||
|             description: "Whether the input is required", | ||||
|         }, | ||||
|         inputHint: { | ||||
|             control: "select", | ||||
|             options: ["text", "code"], | ||||
|             description: "Input type hint for styling and behavior", | ||||
|         }, | ||||
|         showMessage: { | ||||
|             control: "text", | ||||
|             description: "Custom message for show action", | ||||
|         }, | ||||
|         hideMessage: { | ||||
|             control: "text", | ||||
|             description: "Custom message for hide action", | ||||
|         }, | ||||
|         rows: { | ||||
|             control: { type: "number", min: 1, max: 50 }, | ||||
|             description: "Number of visible text lines", | ||||
|         }, | ||||
|         cols: { | ||||
|             control: { type: "number", min: 10, max: 200 }, | ||||
|             description: "Number of visible character width", | ||||
|         }, | ||||
|         resize: { | ||||
|             control: "select", | ||||
|             options: ["none", "both", "horizontal", "vertical"], | ||||
|             description: "How the textarea can be resized", | ||||
|         }, | ||||
|         wrap: { | ||||
|             control: "select", | ||||
|             options: ["soft", "hard", "off"], | ||||
|             description: "Text wrapping behavior", | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| type Story = StoryObj<AkHiddenTextAreaInput>; | ||||
|  | ||||
| const Template: Story = { | ||||
|     args: { | ||||
|         label: "Hidden Textarea Input", | ||||
|         value: "", | ||||
|         revealed: false, | ||||
|         rows: 4, | ||||
|     }, | ||||
|     render: (args) => html` | ||||
|         <ak-hidden-textarea-input | ||||
|             label=${ifDefined(args.label)} | ||||
|             value=${ifDefined(args.value)} | ||||
|             ?revealed=${args.revealed} | ||||
|             placeholder=${ifDefined(args.placeholder)} | ||||
|             rows=${ifDefined(args.rows)} | ||||
|             cols=${ifDefined(args.cols)} | ||||
|             resize=${ifDefined(args.resize)} | ||||
|             wrap=${ifDefined(args.wrap)} | ||||
|             ?required=${args.required} | ||||
|             input-hint=${ifDefined(args.inputHint)} | ||||
|             show-message=${ifDefined(args.showMessage)} | ||||
|             hide-message=${ifDefined(args.hideMessage)} | ||||
|         ></ak-hidden-textarea-input> | ||||
|     `, | ||||
| }; | ||||
|  | ||||
| export const SslCertificate: Story = { | ||||
|     ...Template, | ||||
|     args: { | ||||
|         label: "SSL Certificate", | ||||
|         value: `-----BEGIN CERTIFICATE----- | ||||
| MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV | ||||
| BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX | ||||
| aWRnaXRzIFB0eSBMdGQwHhcNMTcwNTEwMTk0MDA2WhcNMTgwNTEwMTk0MDA2WjBF | ||||
| MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 | ||||
| ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE3MDUxMDE5NDAwNloXDTE4MDUxMDE5 | ||||
| NDAwNlowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV | ||||
| BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD | ||||
| ggEPADCCAQoCggEBALdUlNS31SzxwoFShahGfjHj6GgpcVbzL1Siq0Pqnf82T6M2 | ||||
| EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggE | ||||
| BAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqn | ||||
| f82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgM | ||||
| BAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeM | ||||
| Hyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDu | ||||
| neMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJ | ||||
| kPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAE= | ||||
| -----END CERTIFICATE-----`, | ||||
|         inputHint: "code", | ||||
|         rows: 15, | ||||
|         resize: "vertical", | ||||
|         showMessage: "Show certificate content", | ||||
|         hideMessage: "Hide certificate content", | ||||
|         autocomplete: "off", | ||||
|     }, | ||||
| }; | ||||
| @ -1,121 +0,0 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { html, nothing } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import "../ak-visibility-toggle"; | ||||
| import { type VisibilityToggle, type VisibilityToggleProps } from "../ak-visibility-toggle.js"; | ||||
|  | ||||
| const metadata: Meta<VisibilityToggleProps> = { | ||||
|     title: "Elements/<ak-visibility-toggle>", | ||||
|     component: "ak-visibility-toggle", | ||||
|     tags: ["autodocs"], | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: { | ||||
|                 component: ` | ||||
| # Visibility Toggle Component | ||||
|  | ||||
| A straightforward two-state iconic button for toggling the visibility of sensitive content such as passwords, private keys, or other secret information. | ||||
|                  | ||||
| - Use for sensitive content that users might want to temporarily reveal | ||||
| - There are default hide/show messages for screen readers, but they can be overridden | ||||
| - Clients always handle the state | ||||
| - The \`open\` state is false by default; we assume you want sensitive content hidden at start | ||||
| `, | ||||
|             }, | ||||
|         }, | ||||
|         layout: "padded", | ||||
|     }, | ||||
|     argTypes: { | ||||
|         open: { | ||||
|             control: "boolean", | ||||
|             description: "Whether the toggle is in the 'show' state (true) or 'hide' state (false)", | ||||
|         }, | ||||
|         showMessage: { | ||||
|             control: "text", | ||||
|             description: | ||||
|                 'Message for screen readers when in hide state (default: "Show field content")', | ||||
|         }, | ||||
|         hideMessage: { | ||||
|             control: "text", | ||||
|             description: | ||||
|                 'Message for screen readers when in show state (default: "Hide field content")', | ||||
|         }, | ||||
|         disabled: { | ||||
|             control: "boolean", | ||||
|             description: "Whether the button should be disabled (for demo purposes)", | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| type Story = StoryObj<VisibilityToggle>; | ||||
|  | ||||
| const Template: Story = { | ||||
|     args: { | ||||
|         open: false, | ||||
|         showMessage: "Show field content", | ||||
|         hideMessage: "Hide field content", | ||||
|     }, | ||||
|     render: (args) => html` | ||||
|         <ak-visibility-toggle | ||||
|             ?open=${args.open} | ||||
|             show-message=${ifDefined(args.showMessage)} | ||||
|             hide-message=${ifDefined(args.hideMessage)} | ||||
|             @click=${(e: Event) => { | ||||
|                 const target = e.target as VisibilityToggle; | ||||
|                 target.open = !target.open; | ||||
|                 // In a real application, you would also toggle the visibility | ||||
|                 // of the associated content here | ||||
|             }} | ||||
|         ></ak-visibility-toggle> | ||||
|     `, | ||||
| }; | ||||
|  | ||||
| // Password field integration example | ||||
| export const PasswordFieldExample: Story = { | ||||
|     args: { | ||||
|         showMessage: "Reveal password", | ||||
|         hideMessage: "Conceal password", | ||||
|     }, | ||||
|     render: () => { | ||||
|         let isVisible = false; | ||||
|  | ||||
|         const toggleVisibility = (e: Event) => { | ||||
|             isVisible = !isVisible; | ||||
|             const toggle = e.target as VisibilityToggle; | ||||
|             const passwordField = document.querySelector("#demo-password") as HTMLInputElement; | ||||
|  | ||||
|             toggle.open = isVisible; | ||||
|             if (passwordField) { | ||||
|                 passwordField.type = isVisible ? "text" : "password"; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return html` | ||||
|             <div style="display: flex; flex-direction: column; gap: 1rem; max-width: 300px;"> | ||||
|                 <label for="demo-password" style="font-weight: bold;">Password:</label> | ||||
|                 <div style="display: flex; align-items: center; gap: 0.5rem;"> | ||||
|                     <input | ||||
|                         id="demo-password" | ||||
|                         type="password" | ||||
|                         value="supersecretpassword123" | ||||
|                         style="flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;" | ||||
|                         readonly | ||||
|                     /> | ||||
|                     <ak-visibility-toggle | ||||
|                         ?open=${isVisible} | ||||
|                         show-message="Show password" | ||||
|                         hide-message="Hide password" | ||||
|                         @click=${toggleVisibility} | ||||
|                     ></ak-visibility-toggle> | ||||
|                 </div> | ||||
|                 <p style="font-size: 0.875rem; color: #666;"> | ||||
|                     Click the eye icon to toggle password visibility | ||||
|                 </p> | ||||
|             </div> | ||||
|         `; | ||||
|     }, | ||||
| }; | ||||
| @ -16,12 +16,8 @@ import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import { UiThemeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| export interface AKElementProps { | ||||
|     activeTheme: ResolvedUITheme; | ||||
| } | ||||
|  | ||||
| @localized() | ||||
| export class AKElement extends LitElement implements AKElementProps { | ||||
| export class AKElement extends LitElement { | ||||
|     //#region Static Properties | ||||
|  | ||||
|     public static styles?: Array<CSSResult | CSSModule>; | ||||
|  | ||||
| @ -33,7 +33,7 @@ import { | ||||
|  | ||||
| function localeComparator(a: DualSelectPair, b: DualSelectPair) { | ||||
|     const aSortBy = a[2] || a[0]; | ||||
|     const bSortBy = b[2] || b[0]; | ||||
|     const bSortBy = b[2] || a[0]; | ||||
|  | ||||
|     return aSortBy.localeCompare(bSortBy); | ||||
| } | ||||
|  | ||||
| @ -3001,6 +3001,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>Server laden</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Mit Plex erneut authentifizieren</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9236,15 +9241,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2414,6 +2414,10 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>Load servers</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Re-authenticate with plex</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
|         <target>Allow friends to authenticate via Plex, even if you don't share any servers</target> | ||||
| @ -7744,15 +7748,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2982,6 +2982,11 @@ no se aprueba cuando una o ambas de las opciones seleccionadas son iguales o sup | ||||
|         <source>Load servers</source> | ||||
|         <target>Servidores de carga</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Vuelva a autenticarse con plex</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9296,15 +9301,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -3010,6 +3010,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>Charger les serveurs</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Se ré-authentifier avec Plex</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9865,15 +9870,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -3011,6 +3011,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>Carico server</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Riautenticarsi con plex</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9848,15 +9853,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2973,6 +2973,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>서버 로드</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Plex로 다시 인증하기</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9204,15 +9209,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2987,6 +2987,11 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d | ||||
|         <source>Load servers</source> | ||||
|         <target>Servers laden</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Opnieuw authenticeren met Plex</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9108,15 +9113,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -3012,6 +3012,11 @@ nie przechodzi, gdy jedna lub obie wybrane opcje są równe lub wyższe od progu | ||||
|         <source>Load servers</source> | ||||
|         <target>Załaduj serwery</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Ponowne uwierzytelnienie za pomocą plex</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9531,15 +9536,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2990,6 +2990,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|   <target>Ĺōàď śēŕvēŕś</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|   <target>Ŕē-àũţĥēńţĩćàţē ŵĩţĥ ƥĺēx</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9540,13 +9545,4 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
| </body></file></xliff> | ||||
|  | ||||
| @ -3011,6 +3011,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>Загрузить серверы</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Повторная аутентификация с помощью plex</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9623,15 +9628,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2990,6 +2990,11 @@ Belirlenen seçeneklerden biri veya her ikisi de eşiğe eşit veya eşiğin üz | ||||
|         <source>Load servers</source> | ||||
|         <target>Sunucuları yükle</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>Plex ile yeniden kimlik doğrulama</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9595,15 +9600,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2128,6 +2128,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
| <trans-unit id="s91f389c796720a81"> | ||||
|   <source>Load servers</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s24f405197ede5ebb"> | ||||
|   <source>Re-authenticate with plex</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sc297b2e13c28ecf9"> | ||||
|   <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| </trans-unit> | ||||
| @ -6359,15 +6362,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
| </body> | ||||
| </file> | ||||
| </xliff> | ||||
|  | ||||
| @ -3011,6 +3011,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>加载服务器</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>使用 Plex 重新验证身份</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9875,15 +9880,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
|   <target>生成新的证书密钥对</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2290,6 +2290,10 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>加载服务器</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>使用 plex 重新进行身份验证</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
|         <target>允许好友通过Plex进行身份验证,即使您不共享任何服务器</target> | ||||
| @ -7444,15 +7448,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -2972,6 +2972,11 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <source>Load servers</source> | ||||
|         <target>載入伺服器</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s24f405197ede5ebb"> | ||||
|         <source>Re-authenticate with plex</source> | ||||
|         <target>使用 plex 重新身分認證</target> | ||||
|          | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc297b2e13c28ecf9"> | ||||
|         <source>Allow friends to authenticate via Plex, even if you don't share any servers</source> | ||||
| @ -9183,15 +9188,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb3d5c0a0501669df"> | ||||
|   <source>Generate New Certificate-Key Pair</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sf9686d31d28fcf7d"> | ||||
|   <source>Show field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sb1b05a7573ab618c"> | ||||
|   <source>Hide field content</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4f820625804ed29b"> | ||||
|   <source>Re-authenticate with Plex</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	