Compare commits
15 Commits
files-in-d
...
web/add-co
Author | SHA1 | Date | |
---|---|---|---|
00c1e17b52 | |||
3c2ce40afd | |||
2aceed285e | |||
cba8e84bbe | |||
d313fd7fb4 | |||
102811508f | |||
16b3ca3715 | |||
8b4e0361c4 | |||
22cb5b7379 | |||
2d0117d096 | |||
035bda4eac | |||
50906214e5 | |||
e505f274b6 | |||
fe52f44dca | |||
3146e5a50f |
@ -1,32 +0,0 @@
|
||||
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",
|
||||
)
|
@ -1,44 +0,0 @@
|
||||
# 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"
|
||||
),
|
||||
),
|
||||
]
|
@ -1,32 +0,0 @@
|
||||
# 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,7 +29,6 @@ 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
|
||||
@ -534,13 +533,12 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
)
|
||||
|
||||
# For template applications, this can be set to /static/authentik/applications/*
|
||||
meta_old_icon = models.FileField(
|
||||
meta_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)
|
||||
|
||||
@ -1102,44 +1100,3 @@ 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,7 +8,6 @@ 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
|
||||
@ -79,7 +78,6 @@ api_urlpatterns = [
|
||||
TransactionalApplicationView.as_view(),
|
||||
name="core-transactional-application",
|
||||
),
|
||||
("core/files", FileViewSet),
|
||||
("core/groups", GroupViewSet),
|
||||
("core/users", UserViewSet),
|
||||
("core/tokens", TokenViewSet),
|
||||
|
@ -20,6 +20,9 @@ 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):
|
||||
@ -109,10 +112,15 @@ 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=self.executor.plan.context.get("connection_settings", {}),
|
||||
settings=settings or {},
|
||||
session=self.request.session["authenticatedsession"],
|
||||
expires=now() + timedelta_from_string(self.provider.connection_expiry),
|
||||
expiring=True,
|
||||
|
14
go.mod
14
go.mod
@ -62,12 +62,6 @@ 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
|
||||
@ -83,11 +77,9 @@ 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.39.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.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
|
||||
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,14 +191,6 @@ 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=
|
||||
@ -213,10 +205,6 @@ 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=
|
||||
@ -320,8 +308,6 @@ 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=
|
||||
@ -429,8 +415,6 @@ 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=
|
||||
@ -438,8 +422,6 @@ 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=
|
||||
@ -573,10 +555,6 @@ 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,7 +5,6 @@ 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__"`
|
||||
|
||||
@ -26,16 +25,6 @@ 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"`
|
||||
|
@ -1,62 +0,0 @@
|
||||
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,8 +17,6 @@ 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"
|
||||
@ -51,7 +49,6 @@ type WebServer struct {
|
||||
mainRouter *mux.Router
|
||||
loggingRouter *mux.Router
|
||||
log *log.Entry
|
||||
postgresClient *gorm.DB
|
||||
upstreamClient *http.Client
|
||||
upstreamURL *url.URL
|
||||
|
||||
@ -67,21 +64,6 @@ 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)
|
||||
|
||||
@ -106,7 +88,6 @@ func NewWebServer() *WebServer {
|
||||
mainRouter: mainHandler,
|
||||
loggingRouter: loggingHandler,
|
||||
log: l,
|
||||
postgresClient: db,
|
||||
gunicornReady: false,
|
||||
upstreamClient: upstreamClient,
|
||||
upstreamURL: u,
|
||||
|
109
web/package-lock.json
generated
109
web/package-lock.json
generated
@ -51,6 +51,7 @@
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.6.0",
|
||||
"ninja-keys": "^1.2.2",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@ -2587,6 +2588,57 @@
|
||||
"@lit/reactive-element": "^1.0.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@material/mwc-icon": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@material/mwc-icon/-/mwc-icon-0.25.3.tgz",
|
||||
"integrity": "sha512-36076AWZIRSr8qYOLjuDDkxej/HA0XAosrj7TS1ZeLlUBnLUtbDtvc1S7KSa0hqez7ouzOqGaWK24yoNnTa2OA==",
|
||||
"deprecated": "MWC beta is longer supported. Please upgrade to @material/web",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lit": "^2.0.0",
|
||||
"tslib": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@material/mwc-icon/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@material/mwc-icon/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@material/mwc-icon/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@material/mwc-icon/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdx-js/mdx": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz",
|
||||
@ -16940,6 +16992,12 @@
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "3.8.7",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
|
||||
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@ -21632,6 +21690,57 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ninja-keys": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ninja-keys/-/ninja-keys-1.2.2.tgz",
|
||||
"integrity": "sha512-ylo8jzKowi3XBHkgHRjBJaKQkl32WRLr7kRiA0ajiku11vHRDJ2xANtTScR5C7XlDwKEOYvUPesCKacUeeLAYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@material/mwc-icon": "0.25.3",
|
||||
"hotkeys-js": "3.8.7",
|
||||
"lit": "2.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ninja-keys/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ninja-keys/node_modules/lit": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.2.6.tgz",
|
||||
"integrity": "sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-element": "^3.2.0",
|
||||
"lit-html": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ninja-keys/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ninja-keys/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
|
@ -122,6 +122,7 @@
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.6.0",
|
||||
"ninja-keys": "^1.2.2",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
172
web/src/admin/AdminInterface/AdminCommands.ts
Normal file
172
web/src/admin/AdminInterface/AdminCommands.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { navigate } from "@goauthentik/elements/router/RouterOutlet";
|
||||
import { INinjaAction } from "ninja-keys/dist/interfaces/ininja-action.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
export const adminCommands: INinjaAction[] = [
|
||||
{
|
||||
id: msg("Overview"),
|
||||
title: msg("Dashboard"),
|
||||
handler: () => navigate("/administration/overview"),
|
||||
section: msg("Dashboards"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/administration/dashboard/users"),
|
||||
id: msg("User Statistics"),
|
||||
title: msg("User Statistics"),
|
||||
icon: '<i class="pf-icon pf-icon-user"></i>',
|
||||
section: msg("Dashboards"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/administration/system-tasks"),
|
||||
id: msg("System Tasks"),
|
||||
title: msg("System Tasks"),
|
||||
section: msg("Dashboards"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/core/applications"),
|
||||
id: msg("Applications"),
|
||||
title: msg("Applications"),
|
||||
section: msg("Applications"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/core/providers"),
|
||||
id: msg("Providers"),
|
||||
title: msg("Providers"),
|
||||
section: msg("Applications"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/outpost/outposts"),
|
||||
id: msg("Outposts"),
|
||||
title: msg("Outposts"),
|
||||
section: msg("Applications"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/events/log"),
|
||||
id: msg("Logs"),
|
||||
title: msg("Logs"),
|
||||
section: msg("Events"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/events/rules"),
|
||||
id: msg("Notification Rules"),
|
||||
title: msg("Notification Rules"),
|
||||
section: msg("Events"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/events/transports"),
|
||||
id: msg("Notification Transports"),
|
||||
title: msg("Notification Transports"),
|
||||
section: msg("Events"),
|
||||
},
|
||||
|
||||
{
|
||||
handler: () => navigate("/policy/policies"),
|
||||
id: msg("Policies"),
|
||||
title: msg("Policies"),
|
||||
section: msg("Customization"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/core/property-mappings"),
|
||||
id: msg("Property Mappings"),
|
||||
title: msg("Property Mappings"),
|
||||
section: msg("Customization"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/blueprints/instances"),
|
||||
id: msg("Blueprints"),
|
||||
title: msg("Blueprints"),
|
||||
section: msg("Customization"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/policy/reputation"),
|
||||
id: msg("Reputation scores"),
|
||||
title: msg("Reputation scores"),
|
||||
section: msg("Customization"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/flow/flows"),
|
||||
id: msg("Flows"),
|
||||
title: msg("Flows"),
|
||||
section: msg("Flows"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/flow/stages"),
|
||||
id: msg("Stages"),
|
||||
title: msg("Stages"),
|
||||
section: msg("Flows"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/flow/stages/prompts"),
|
||||
id: msg("Prompts"),
|
||||
title: msg("Prompts"),
|
||||
section: msg("Flows"),
|
||||
},
|
||||
|
||||
{
|
||||
handler: () => navigate("/identity/users"),
|
||||
id: msg("Users"),
|
||||
title: msg("Users"),
|
||||
section: msg("Directory"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/identity/groups"),
|
||||
id: msg("Groups"),
|
||||
title: msg("Groups"),
|
||||
section: msg("Directory"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/identity/roles"),
|
||||
id: msg("Roles"),
|
||||
title: msg("Roles"),
|
||||
section: msg("Directory"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/core/sources"),
|
||||
id: msg("Federation and Social login"),
|
||||
title: msg("Federation and Social login"),
|
||||
section: msg("Directory"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/core/tokens"),
|
||||
id: msg("Tokens and App passwords"),
|
||||
title: msg("Tokens and App passwords"),
|
||||
section: msg("Directory"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/flow/stages/invitations"),
|
||||
id: msg("Invitations"),
|
||||
title: msg("Invitations"),
|
||||
section: msg("Directory"),
|
||||
},
|
||||
|
||||
{
|
||||
handler: () => navigate("/core/brands"),
|
||||
id: msg("Brands"),
|
||||
title: msg("Brands"),
|
||||
section: msg("System"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/crypto/certificates"),
|
||||
id: msg("Certificates"),
|
||||
title: msg("Certificates"),
|
||||
section: msg("System"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/outpost/integrations"),
|
||||
id: msg("Outpost Integrations"),
|
||||
title: msg("Outpost Integrations"),
|
||||
section: msg("System"),
|
||||
},
|
||||
{
|
||||
handler: () => navigate("/admin/settings"),
|
||||
id: msg("Settings"),
|
||||
title: msg("Settings"),
|
||||
section: msg("System"),
|
||||
},
|
||||
{
|
||||
handler: () => window.location.assign("/if/user/"),
|
||||
id: msg("User interface"),
|
||||
title: msg("Go to my User page"),
|
||||
},
|
||||
];
|
@ -1,5 +1,6 @@
|
||||
import "#admin/AdminInterface/AboutModal";
|
||||
import type { AboutModal } from "#admin/AdminInterface/AboutModal";
|
||||
import { adminCommands } from "#admin/AdminInterface/AdminCommands";
|
||||
import { ROUTES } from "#admin/Routes";
|
||||
import { EVENT_API_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE } from "#common/constants";
|
||||
import { configureSentry } from "#common/sentry/index";
|
||||
@ -21,6 +22,7 @@ import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
|
||||
import "#elements/router/RouterOutlet";
|
||||
import "#elements/sidebar/Sidebar";
|
||||
import "#elements/sidebar/SidebarItem";
|
||||
import "ninja-keys";
|
||||
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, eventOptions, property, query } from "lit/decorators.js";
|
||||
@ -119,6 +121,10 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
|
||||
.pf-c-drawer__panel {
|
||||
z-index: var(--pf-global--ZIndex--xl);
|
||||
}
|
||||
ninja-keys {
|
||||
--ninja-z-index: 99999;
|
||||
--ninja-accent-color: var(--ak-accent);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@ -190,6 +196,11 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
|
||||
};
|
||||
|
||||
return html` <ak-locale-context>
|
||||
<ninja-keys
|
||||
.data=${adminCommands}
|
||||
noAutoLoadMdicons
|
||||
class="${this.activeTheme === UiThemeEnum.Dark ? "dark" : ""}"
|
||||
></ninja-keys>
|
||||
<div class="pf-c-page">
|
||||
<ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}>
|
||||
<ak-version-banner></ak-version-banner>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-textarea-input
|
||||
<ak-secret-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-private-textarea-input>
|
||||
<ak-private-textarea-input
|
||||
></ak-secret-textarea-input>
|
||||
<ak-secret-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-private-textarea-input>`;
|
||||
></ak-secret-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-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
name="key"
|
||||
?revealed=${this.instance === undefined}
|
||||
label=${msg("License key")}
|
||||
input-hint="code"
|
||||
>
|
||||
</ak-private-textarea-input>`;
|
||||
</ak-secret-textarea-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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-private-text-input.js";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-text-input
|
||||
<ak-secret-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-private-text-input>
|
||||
<ak-private-textarea-input
|
||||
></ak-secret-text-input>
|
||||
<ak-secret-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-private-textarea-input>
|
||||
></ak-secret-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-private-textarea-input
|
||||
<ak-secret-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-private-textarea-input>
|
||||
></ak-secret-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-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-text-input
|
||||
<ak-secret-text-input
|
||||
label=${msg("Bind Password")}
|
||||
name="bindPassword"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-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-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Consumer secret")}
|
||||
name="consumerSecret"
|
||||
input-hint="code"
|
||||
help=${msg("Also known as Client Secret.")}
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -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-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Secret key")}
|
||||
input-hint="code"
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-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-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="adminSecretKey"
|
||||
label=${msg("Secret key")}
|
||||
input-hint="code"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-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-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="password"
|
||||
label=${msg("SMTP Password")}
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-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-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-text-input
|
||||
<ak-secret-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-private-text-input>
|
||||
></ak-secret-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-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-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-private-text-input
|
||||
<ak-secret-text-input
|
||||
label=${msg("SMTP Password")}
|
||||
name="password"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
<ak-form-element-horizontal name="useTls">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AKElement, type AKElementProps } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement.js";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
@ -6,6 +6,19 @@ 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
|
||||
@ -18,37 +31,81 @@ 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 = "";
|
||||
|
||||
renderControl() {
|
||||
protected renderControl() {
|
||||
throw new Error("Must be implemented in a subclass");
|
||||
}
|
||||
|
||||
|
159
web/src/components/ak-hidden-text-input.ts
Normal file
159
web/src/components/ak-hidden-text-input.ts
Normal file
@ -0,0 +1,159 @@
|
||||
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;
|
||||
}
|
||||
}
|
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
@ -0,0 +1,128 @@
|
||||
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-private-text-input")
|
||||
export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
@customElement("ak-secret-text-input")
|
||||
export class AkSecretTextInput extends HorizontalLightComponent<string> {
|
||||
@property({ type: String, reflect: true })
|
||||
public value = "";
|
||||
|
||||
@ -23,7 +23,7 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
this.revealed = true;
|
||||
}
|
||||
|
||||
#renderPrivateInput() {
|
||||
#renderSecretInput() {
|
||||
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
@ -60,14 +60,14 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
}
|
||||
|
||||
public override renderControl() {
|
||||
return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput();
|
||||
return this.revealed ? this.renderVisibleInput() : this.#renderSecretInput();
|
||||
}
|
||||
}
|
||||
|
||||
export default AkPrivateTextInput;
|
||||
export default AkSecretTextInput;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-private-text-input": AkPrivateTextInput;
|
||||
"ak-secret-text-input": AkSecretTextInput;
|
||||
}
|
||||
}
|
@ -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 { AkPrivateTextInput } from "./ak-private-text-input.js";
|
||||
import { AkSecretTextInput } from "./ak-secret-text-input.js";
|
||||
|
||||
@customElement("ak-private-textarea-input")
|
||||
export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||
@customElement("ak-secret-textarea-input")
|
||||
export class AkSecretTextAreaInput extends AkSecretTextInput {
|
||||
protected override renderVisibleInput() {
|
||||
const code = this.inputHint === "code";
|
||||
const setValue = (ev: InputEvent) => {
|
||||
@ -34,10 +34,10 @@ export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||
}
|
||||
}
|
||||
|
||||
export default AkPrivateTextAreaInput;
|
||||
export default AkSecretTextAreaInput;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-private-textarea-input": AkPrivateTextAreaInput;
|
||||
"ak-secret-textarea-input": AkSecretTextAreaInput;
|
||||
}
|
||||
}
|
89
web/src/components/ak-visibility-toggle.ts
Normal file
89
web/src/components/ak-visibility-toggle.ts
Normal file
@ -0,0 +1,89 @@
|
||||
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;
|
||||
}
|
||||
}
|
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
@ -0,0 +1,93 @@
|
||||
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,
|
||||
},
|
||||
};
|
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
@ -0,0 +1,140 @@
|
||||
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",
|
||||
},
|
||||
};
|
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
@ -0,0 +1,121 @@
|
||||
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,8 +16,12 @@ import { property } from "lit/decorators.js";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
export interface AKElementProps {
|
||||
activeTheme: ResolvedUITheme;
|
||||
}
|
||||
|
||||
@localized()
|
||||
export class AKElement extends LitElement {
|
||||
export class AKElement extends LitElement implements AKElementProps {
|
||||
//#region Static Properties
|
||||
|
||||
public static styles?: Array<CSSResult | CSSModule>;
|
||||
|
Reference in New Issue
Block a user