Compare commits
2 Commits
sfe-packag
...
router-tid
Author | SHA1 | Date | |
---|---|---|---|
e3f2ed0436 | |||
a5bb22a66a |
5
.github/workflows/api-ts-publish.yml
vendored
5
.github/workflows/api-ts-publish.yml
vendored
@ -36,6 +36,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
npm i @goauthentik/api@$VERSION
|
npm i @goauthentik/api@$VERSION
|
||||||
|
- name: Upgrade /web/packages/sfe
|
||||||
|
working-directory: web/packages/sfe
|
||||||
|
run: |
|
||||||
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
|
npm i @goauthentik/api@$VERSION
|
||||||
- uses: peter-evans/create-pull-request@v7
|
- uses: peter-evans/create-pull-request@v7
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
|
@ -30,6 +30,7 @@ WORKDIR /work/web
|
|||||||
|
|
||||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||||
|
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
|
||||||
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
||||||
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
||||||
npm ci --include=dev
|
npm ci --include=dev
|
||||||
@ -93,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 5: Download uv
|
# Stage 5: Download uv
|
||||||
FROM ghcr.io/astral-sh/uv:0.6.12 AS uv
|
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv
|
||||||
# Stage 6: Base python image
|
# Stage 6: Base python image
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
||||||
|
|
||||||
|
@ -179,13 +179,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
|||||||
"user",
|
"user",
|
||||||
"source",
|
"source",
|
||||||
"source_obj",
|
"source_obj",
|
||||||
"identifier",
|
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
"last_updated": {"read_only": True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -202,7 +199,7 @@ class UserSourceConnectionViewSet(
|
|||||||
queryset = UserSourceConnection.objects.all()
|
queryset = UserSourceConnection.objects.all()
|
||||||
serializer_class = UserSourceConnectionSerializer
|
serializer_class = UserSourceConnectionSerializer
|
||||||
filterset_fields = ["user", "source__slug"]
|
filterset_fields = ["user", "source__slug"]
|
||||||
search_fields = ["user__username", "source__slug", "identifier"]
|
search_fields = ["source__slug"]
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
owner_field = "user"
|
owner_field = "user"
|
||||||
|
|
||||||
@ -221,11 +218,9 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
|||||||
"source_obj",
|
"source_obj",
|
||||||
"identifier",
|
"identifier",
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
"last_updated": {"read_only": True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -242,5 +237,6 @@ class GroupSourceConnectionViewSet(
|
|||||||
queryset = GroupSourceConnection.objects.all()
|
queryset = GroupSourceConnection.objects.all()
|
||||||
serializer_class = GroupSourceConnectionSerializer
|
serializer_class = GroupSourceConnectionSerializer
|
||||||
filterset_fields = ["group", "source__slug"]
|
filterset_fields = ["group", "source__slug"]
|
||||||
search_fields = ["group__name", "source__slug", "identifier"]
|
search_fields = ["source__slug"]
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
|
owner_field = "user"
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-04-07 14:04
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0043_alter_group_options"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
name="new_identifier",
|
|
||||||
field=models.TextField(default=""),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,30 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
|
||||||
("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
|
|
||||||
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
|
|
||||||
("authentik_sources_plex", "0005_migrate_userplexsourceconnection_identifier"),
|
|
||||||
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
old_name="new_identifier",
|
|
||||||
new_name="identifier",
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
index=models.Index(fields=["identifier"], name="authentik_c_identif_59226f_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["source", "identifier"], name="authentik_c_source__649e04_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -824,7 +824,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||||
identifier = models.TextField()
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
@ -838,10 +837,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("user", "source"),)
|
unique_together = (("user", "source"),)
|
||||||
indexes = (
|
|
||||||
models.Index(fields=("identifier",)),
|
|
||||||
models.Index(fields=("source", "identifier")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||||
|
@ -13,11 +13,7 @@ from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
|||||||
from authentik.core.api.groups import GroupViewSet
|
from authentik.core.api.groups import GroupViewSet
|
||||||
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
||||||
from authentik.core.api.providers import ProviderViewSet
|
from authentik.core.api.providers import ProviderViewSet
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
GroupSourceConnectionViewSet,
|
|
||||||
SourceViewSet,
|
|
||||||
UserSourceConnectionViewSet,
|
|
||||||
)
|
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
@ -85,7 +81,6 @@ api_urlpatterns = [
|
|||||||
("core/tokens", TokenViewSet),
|
("core/tokens", TokenViewSet),
|
||||||
("sources/all", SourceViewSet),
|
("sources/all", SourceViewSet),
|
||||||
("sources/user_connections/all", UserSourceConnectionViewSet),
|
("sources/user_connections/all", UserSourceConnectionViewSet),
|
||||||
("sources/group_connections/all", GroupSourceConnectionViewSet),
|
|
||||||
("providers/all", ProviderViewSet),
|
("providers/all", ProviderViewSet),
|
||||||
("propertymappings/all", PropertyMappingViewSet),
|
("propertymappings/all", PropertyMappingViewSet),
|
||||||
("authenticators/all", DeviceViewSet, "device"),
|
("authenticators/all", DeviceViewSet, "device"),
|
||||||
|
@ -49,6 +49,6 @@
|
|||||||
</main>
|
</main>
|
||||||
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
||||||
</div>
|
</div>
|
||||||
<script src="{% static 'dist/sfe/main.js' %}"></script>
|
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
"""Kerberos Source Serializer"""
|
||||||
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import (
|
||||||
GroupSourceConnectionSerializer,
|
GroupSourceConnectionSerializer,
|
||||||
GroupSourceConnectionViewSet,
|
GroupSourceConnectionViewSet,
|
||||||
UserSourceConnectionSerializer,
|
UserSourceConnectionSerializer,
|
||||||
UserSourceConnectionViewSet,
|
|
||||||
)
|
)
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.sources.kerberos.models import (
|
from authentik.sources.kerberos.models import (
|
||||||
GroupKerberosSourceConnection,
|
GroupKerberosSourceConnection,
|
||||||
UserKerberosSourceConnection,
|
UserKerberosSourceConnection,
|
||||||
@ -13,20 +15,33 @@ from authentik.sources.kerberos.models import (
|
|||||||
|
|
||||||
|
|
||||||
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
|
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||||
class Meta(UserSourceConnectionSerializer.Meta):
|
"""Kerberos Source Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
model = UserKerberosSourceConnection
|
model = UserKerberosSourceConnection
|
||||||
|
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||||
|
|
||||||
|
|
||||||
class UserKerberosSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = UserKerberosSourceConnection.objects.all()
|
queryset = UserKerberosSourceConnection.objects.all()
|
||||||
serializer_class = UserKerberosSourceConnectionSerializer
|
serializer_class = UserKerberosSourceConnectionSerializer
|
||||||
|
filterset_fields = ["source__slug"]
|
||||||
|
search_fields = ["source__slug"]
|
||||||
|
ordering = ["source__slug"]
|
||||||
|
owner_field = "user"
|
||||||
|
|
||||||
|
|
||||||
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||||
|
"""OAuth Group-Source connection Serializer"""
|
||||||
|
|
||||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||||
model = GroupKerberosSourceConnection
|
model = GroupKerberosSourceConnection
|
||||||
|
|
||||||
|
|
||||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||||
|
"""Group-source connection Viewset"""
|
||||||
|
|
||||||
queryset = GroupKerberosSourceConnection.objects.all()
|
queryset = GroupKerberosSourceConnection.objects.all()
|
||||||
serializer_class = GroupKerberosSourceConnectionSerializer
|
serializer_class = GroupKerberosSourceConnectionSerializer
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_identifier(apps, schema_editor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
UserKerberosSourceConnection = apps.get_model(
|
|
||||||
"authentik_sources_kerberos", "UserKerberosSourceConnection"
|
|
||||||
)
|
|
||||||
|
|
||||||
for connection in UserKerberosSourceConnection.objects.using(db_alias).all():
|
|
||||||
connection.new_identifier = connection.identifier
|
|
||||||
connection.save(using=db_alias)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_sources_kerberos", "0002_kerberossource_kadmin_type"),
|
|
||||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="userkerberossourceconnection",
|
|
||||||
name="identifier",
|
|
||||||
),
|
|
||||||
]
|
|
@ -372,6 +372,8 @@ class KerberosSourcePropertyMapping(PropertyMapping):
|
|||||||
class UserKerberosSourceConnection(UserSourceConnection):
|
class UserKerberosSourceConnection(UserSourceConnection):
|
||||||
"""Connection to configured Kerberos Sources."""
|
"""Connection to configured Kerberos Sources."""
|
||||||
|
|
||||||
|
identifier = models.TextField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.sources.kerberos.api.source_connection import (
|
from authentik.sources.kerberos.api.source_connection import (
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""OAuth Source Serializer"""
|
||||||
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import (
|
||||||
@ -10,9 +12,11 @@ from authentik.sources.oauth.models import GroupOAuthSourceConnection, UserOAuth
|
|||||||
|
|
||||||
|
|
||||||
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||||
|
"""OAuth Source Serializer"""
|
||||||
|
|
||||||
class Meta(UserSourceConnectionSerializer.Meta):
|
class Meta(UserSourceConnectionSerializer.Meta):
|
||||||
model = UserOAuthSourceConnection
|
model = UserOAuthSourceConnection
|
||||||
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token"]
|
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier", "access_token"]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||||
"access_token": {"write_only": True},
|
"access_token": {"write_only": True},
|
||||||
@ -20,15 +24,21 @@ class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class UserOAuthSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
class UserOAuthSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||||
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = UserOAuthSourceConnection.objects.all()
|
queryset = UserOAuthSourceConnection.objects.all()
|
||||||
serializer_class = UserOAuthSourceConnectionSerializer
|
serializer_class = UserOAuthSourceConnectionSerializer
|
||||||
|
|
||||||
|
|
||||||
class GroupOAuthSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
class GroupOAuthSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||||
|
"""OAuth Group-Source connection Serializer"""
|
||||||
|
|
||||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||||
model = GroupOAuthSourceConnection
|
model = GroupOAuthSourceConnection
|
||||||
|
|
||||||
|
|
||||||
class GroupOAuthSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
class GroupOAuthSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||||
|
"""Group-source connection Viewset"""
|
||||||
|
|
||||||
queryset = GroupOAuthSourceConnection.objects.all()
|
queryset = GroupOAuthSourceConnection.objects.all()
|
||||||
serializer_class = GroupOAuthSourceConnectionSerializer
|
serializer_class = GroupOAuthSourceConnectionSerializer
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_identifier(apps, schema_editor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
UserOAuthSourceConnection = apps.get_model(
|
|
||||||
"authentik_sources_oauth", "UserOAuthSourceConnection"
|
|
||||||
)
|
|
||||||
|
|
||||||
for connection in UserOAuthSourceConnection.objects.using(db_alias).all():
|
|
||||||
connection.new_identifier = connection.identifier
|
|
||||||
connection.save(using=db_alias)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_sources_oauth", "0008_groupoauthsourceconnection_and_more"),
|
|
||||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="useroauthsourceconnection",
|
|
||||||
name="identifier",
|
|
||||||
),
|
|
||||||
]
|
|
@ -286,6 +286,7 @@ class OAuthSourcePropertyMapping(PropertyMapping):
|
|||||||
class UserOAuthSourceConnection(UserSourceConnection):
|
class UserOAuthSourceConnection(UserSourceConnection):
|
||||||
"""Authorized remote OAuth provider."""
|
"""Authorized remote OAuth provider."""
|
||||||
|
|
||||||
|
identifier = models.CharField(max_length=255)
|
||||||
access_token = models.TextField(blank=True, null=True, default=None)
|
access_token = models.TextField(blank=True, null=True, default=None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""Plex Source connection Serializer"""
|
||||||
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import (
|
||||||
@ -10,9 +12,14 @@ from authentik.sources.plex.models import GroupPlexSourceConnection, UserPlexSou
|
|||||||
|
|
||||||
|
|
||||||
class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||||
|
"""Plex Source connection Serializer"""
|
||||||
|
|
||||||
class Meta(UserSourceConnectionSerializer.Meta):
|
class Meta(UserSourceConnectionSerializer.Meta):
|
||||||
model = UserPlexSourceConnection
|
model = UserPlexSourceConnection
|
||||||
fields = UserSourceConnectionSerializer.Meta.fields + ["plex_token"]
|
fields = UserSourceConnectionSerializer.Meta.fields + [
|
||||||
|
"identifier",
|
||||||
|
"plex_token",
|
||||||
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||||
"plex_token": {"write_only": True},
|
"plex_token": {"write_only": True},
|
||||||
@ -20,15 +27,21 @@ class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class UserPlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
class UserPlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||||
|
"""Plex Source connection Serializer"""
|
||||||
|
|
||||||
queryset = UserPlexSourceConnection.objects.all()
|
queryset = UserPlexSourceConnection.objects.all()
|
||||||
serializer_class = UserPlexSourceConnectionSerializer
|
serializer_class = UserPlexSourceConnectionSerializer
|
||||||
|
|
||||||
|
|
||||||
class GroupPlexSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
class GroupPlexSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||||
|
"""Plex Group-Source connection Serializer"""
|
||||||
|
|
||||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||||
model = GroupPlexSourceConnection
|
model = GroupPlexSourceConnection
|
||||||
|
|
||||||
|
|
||||||
class GroupPlexSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
class GroupPlexSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||||
|
"""Group-source connection Viewset"""
|
||||||
|
|
||||||
queryset = GroupPlexSourceConnection.objects.all()
|
queryset = GroupPlexSourceConnection.objects.all()
|
||||||
serializer_class = GroupPlexSourceConnectionSerializer
|
serializer_class = GroupPlexSourceConnectionSerializer
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_identifier(apps, schema_editor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
UserPlexSourceConnection = apps.get_model("authentik_sources_plex", "UserPlexSourceConnection")
|
|
||||||
|
|
||||||
for connection in UserPlexSourceConnection.objects.using(db_alias).all():
|
|
||||||
connection.new_identifier = connection.identifier
|
|
||||||
connection.save(using=db_alias)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
(
|
|
||||||
"authentik_sources_plex",
|
|
||||||
"0004_groupplexsourceconnection_plexsourcepropertymapping_and_more",
|
|
||||||
),
|
|
||||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="userplexsourceconnection",
|
|
||||||
name="identifier",
|
|
||||||
),
|
|
||||||
]
|
|
@ -141,6 +141,7 @@ class UserPlexSourceConnection(UserSourceConnection):
|
|||||||
"""Connect user and plex source"""
|
"""Connect user and plex source"""
|
||||||
|
|
||||||
plex_token = models.TextField()
|
plex_token = models.TextField()
|
||||||
|
identifier = models.TextField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""SAML Source Serializer"""
|
||||||
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import (
|
||||||
@ -10,20 +12,29 @@ from authentik.sources.saml.models import GroupSAMLSourceConnection, UserSAMLSou
|
|||||||
|
|
||||||
|
|
||||||
class UserSAMLSourceConnectionSerializer(UserSourceConnectionSerializer):
|
class UserSAMLSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||||
|
"""SAML Source Serializer"""
|
||||||
|
|
||||||
class Meta(UserSourceConnectionSerializer.Meta):
|
class Meta(UserSourceConnectionSerializer.Meta):
|
||||||
model = UserSAMLSourceConnection
|
model = UserSAMLSourceConnection
|
||||||
|
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||||
|
|
||||||
|
|
||||||
class UserSAMLSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
class UserSAMLSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||||
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = UserSAMLSourceConnection.objects.all()
|
queryset = UserSAMLSourceConnection.objects.all()
|
||||||
serializer_class = UserSAMLSourceConnectionSerializer
|
serializer_class = UserSAMLSourceConnectionSerializer
|
||||||
|
|
||||||
|
|
||||||
class GroupSAMLSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
class GroupSAMLSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||||
|
"""OAuth Group-Source connection Serializer"""
|
||||||
|
|
||||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||||
model = GroupSAMLSourceConnection
|
model = GroupSAMLSourceConnection
|
||||||
|
|
||||||
|
|
||||||
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||||
|
"""Group-source connection Viewset"""
|
||||||
|
|
||||||
queryset = GroupSAMLSourceConnection.objects.all()
|
queryset = GroupSAMLSourceConnection.objects.all()
|
||||||
serializer_class = GroupSAMLSourceConnectionSerializer
|
serializer_class = GroupSAMLSourceConnectionSerializer
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_identifier(apps, schema_editor):
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
UserSAMLSourceConnection = apps.get_model("authentik_sources_saml", "UserSAMLSourceConnection")
|
|
||||||
|
|
||||||
for connection in UserSAMLSourceConnection.objects.using(db_alias).all():
|
|
||||||
connection.new_identifier = connection.identifier
|
|
||||||
connection.save(using=db_alias)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_sources_saml", "0018_alter_samlsource_slo_url_alter_samlsource_sso_url"),
|
|
||||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="usersamlsourceconnection",
|
|
||||||
name="identifier",
|
|
||||||
),
|
|
||||||
]
|
|
@ -318,6 +318,8 @@ class SAMLSourcePropertyMapping(PropertyMapping):
|
|||||||
class UserSAMLSourceConnection(UserSourceConnection):
|
class UserSAMLSourceConnection(UserSourceConnection):
|
||||||
"""Connection to configured SAML Sources."""
|
"""Connection to configured SAML Sources."""
|
||||||
|
|
||||||
|
identifier = models.TextField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionSerializer
|
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionSerializer
|
||||||
|
@ -104,13 +104,6 @@ def send_mail(
|
|||||||
# can't be converted to json)
|
# can't be converted to json)
|
||||||
message_object.attach(logo_data())
|
message_object.attach(logo_data())
|
||||||
|
|
||||||
if (
|
|
||||||
message_object.to
|
|
||||||
and isinstance(message_object.to[0], str)
|
|
||||||
and "=?utf-8?" in message_object.to[0]
|
|
||||||
):
|
|
||||||
message_object.to = [message_object.to[0].split("<")[-1].replace(">", "")]
|
|
||||||
|
|
||||||
LOGGER.debug("Sending mail", to=message_object.to)
|
LOGGER.debug("Sending mail", to=message_object.to)
|
||||||
backend.send_messages([message_object])
|
backend.send_messages([message_object])
|
||||||
Event.new(
|
Event.new(
|
||||||
|
@ -97,37 +97,6 @@ class TestEmailStageSending(FlowTestCase):
|
|||||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||||
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
||||||
|
|
||||||
def test_utf8_name(self):
|
|
||||||
"""Test with pending user"""
|
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
|
||||||
utf8_user = create_test_user()
|
|
||||||
utf8_user.name = "Cirilo ЉМНЊ el cirilico И̂ӢЙӤ "
|
|
||||||
utf8_user.email = "cyrillic@authentik.local"
|
|
||||||
utf8_user.save()
|
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = utf8_user
|
|
||||||
session = self.client.session
|
|
||||||
session[SESSION_KEY_PLAN] = plan
|
|
||||||
session.save()
|
|
||||||
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
|
|
||||||
|
|
||||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
|
||||||
with patch(
|
|
||||||
"authentik.stages.email.models.EmailStage.backend_class",
|
|
||||||
PropertyMock(return_value=EmailBackend),
|
|
||||||
):
|
|
||||||
response = self.client.post(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertStageResponse(
|
|
||||||
response,
|
|
||||||
self.flow,
|
|
||||||
response_errors={
|
|
||||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
|
||||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
|
||||||
self.assertEqual(mail.outbox[0].to, [f"{utf8_user.email}"])
|
|
||||||
|
|
||||||
def test_pending_fake_user(self):
|
def test_pending_fake_user(self):
|
||||||
"""Test with pending (fake) user"""
|
"""Test with pending (fake) user"""
|
||||||
self.flow.designation = FlowDesignation.RECOVERY
|
self.flow.designation = FlowDesignation.RECOVERY
|
||||||
|
@ -8231,6 +8231,7 @@
|
|||||||
},
|
},
|
||||||
"identifier": {
|
"identifier": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"maxLength": 255,
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Identifier"
|
"title": "Identifier"
|
||||||
},
|
},
|
||||||
|
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1007.0",
|
"aws-cdk": "^2.1006.0",
|
||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -17,9 +17,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aws-cdk": {
|
"node_modules/aws-cdk": {
|
||||||
"version": "2.1007.0",
|
"version": "2.1006.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1007.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz",
|
||||||
"integrity": "sha512-/UOYOTGWUm+pP9qxg03tID5tL6euC+pb+xo0RBue+xhnUWwj/Bbsw6DbqbpOPMrNzTUxmM723/uMEQmM6S26dw==",
|
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"aws-cdk": "^2.1007.0",
|
"aws-cdk": "^2.1006.0",
|
||||||
"cross-env": "^7.0.3"
|
"cross-env": "^7.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2025.2.3",
|
"version": "2025.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2025.2.3"
|
"version": "2025.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ dependencies = [
|
|||||||
"pydantic-scim",
|
"pydantic-scim",
|
||||||
"pyjwt",
|
"pyjwt",
|
||||||
"pyrad",
|
"pyrad",
|
||||||
"python-kadmin-rs ==0.6.0",
|
"python-kadmin-rs ==0.5.3",
|
||||||
"pyyaml",
|
"pyyaml",
|
||||||
"requests-oauthlib",
|
"requests-oauthlib",
|
||||||
"scim2-filter-parser",
|
"scim2-filter-parser",
|
||||||
|
545
schema.yml
545
schema.yml
@ -25938,243 +25938,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
/sources/group_connections/all/:
|
|
||||||
get:
|
|
||||||
operationId: sources_group_connections_all_list
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
parameters:
|
|
||||||
- in: query
|
|
||||||
name: group
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
- name: ordering
|
|
||||||
required: false
|
|
||||||
in: query
|
|
||||||
description: Which field to use when ordering the results.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: page
|
|
||||||
required: false
|
|
||||||
in: query
|
|
||||||
description: A page number within the paginated result set.
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
- name: page_size
|
|
||||||
required: false
|
|
||||||
in: query
|
|
||||||
description: Number of results to return per page.
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
- name: search
|
|
||||||
required: false
|
|
||||||
in: query
|
|
||||||
description: A search term.
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: source__slug
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/PaginatedGroupSourceConnectionList'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
/sources/group_connections/all/{id}/:
|
|
||||||
get:
|
|
||||||
operationId: sources_group_connections_all_retrieve
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: A unique integer value identifying this group source connection.
|
|
||||||
required: true
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupSourceConnection'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
put:
|
|
||||||
operationId: sources_group_connections_all_update
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: A unique integer value identifying this group source connection.
|
|
||||||
required: true
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupSourceConnectionRequest'
|
|
||||||
required: true
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupSourceConnection'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
patch:
|
|
||||||
operationId: sources_group_connections_all_partial_update
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: A unique integer value identifying this group source connection.
|
|
||||||
required: true
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/PatchedGroupSourceConnectionRequest'
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupSourceConnection'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
delete:
|
|
||||||
operationId: sources_group_connections_all_destroy
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: A unique integer value identifying this group source connection.
|
|
||||||
required: true
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'204':
|
|
||||||
description: No response body
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
/sources/group_connections/all/{id}/used_by/:
|
|
||||||
get:
|
|
||||||
operationId: sources_group_connections_all_used_by_list
|
|
||||||
description: Get a list of all objects that use this object
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
description: A unique integer value identifying this group source connection.
|
|
||||||
required: true
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/UsedBy'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
/sources/group_connections/kerberos/:
|
/sources/group_connections/kerberos/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_group_connections_kerberos_list
|
operationId: sources_group_connections_kerberos_list
|
||||||
@ -26236,38 +25999,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
post:
|
|
||||||
operationId: sources_group_connections_kerberos_create
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupKerberosSourceConnectionRequest'
|
|
||||||
required: true
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupKerberosSourceConnection'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
/sources/group_connections/kerberos/{id}/:
|
/sources/group_connections/kerberos/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_group_connections_kerberos_retrieve
|
operationId: sources_group_connections_kerberos_retrieve
|
||||||
@ -27048,38 +26779,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
post:
|
|
||||||
operationId: sources_group_connections_saml_create
|
|
||||||
description: Group-source connection Viewset
|
|
||||||
tags:
|
|
||||||
- sources
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupSAMLSourceConnectionRequest'
|
|
||||||
required: true
|
|
||||||
security:
|
|
||||||
- authentik: []
|
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GroupSAMLSourceConnection'
|
|
||||||
description: ''
|
|
||||||
'400':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ValidationError'
|
|
||||||
description: ''
|
|
||||||
'403':
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GenericError'
|
|
||||||
description: ''
|
|
||||||
/sources/group_connections/saml/{id}/:
|
/sources/group_connections/saml/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_group_connections_saml_retrieve
|
operationId: sources_group_connections_saml_retrieve
|
||||||
@ -30293,7 +29992,7 @@ paths:
|
|||||||
/sources/user_connections/kerberos/:
|
/sources/user_connections/kerberos/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_kerberos_list
|
operationId: sources_user_connections_kerberos_list
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- name: ordering
|
- name: ordering
|
||||||
required: false
|
required: false
|
||||||
@ -30323,10 +30022,6 @@ paths:
|
|||||||
name: source__slug
|
name: source__slug
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- in: query
|
|
||||||
name: user
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
security:
|
security:
|
||||||
@ -30352,7 +30047,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
post:
|
post:
|
||||||
operationId: sources_user_connections_kerberos_create
|
operationId: sources_user_connections_kerberos_create
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -30385,7 +30080,7 @@ paths:
|
|||||||
/sources/user_connections/kerberos/{id}/:
|
/sources/user_connections/kerberos/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_kerberos_retrieve
|
operationId: sources_user_connections_kerberos_retrieve
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30419,7 +30114,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
put:
|
put:
|
||||||
operationId: sources_user_connections_kerberos_update
|
operationId: sources_user_connections_kerberos_update
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30459,7 +30154,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
patch:
|
patch:
|
||||||
operationId: sources_user_connections_kerberos_partial_update
|
operationId: sources_user_connections_kerberos_partial_update
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30498,7 +30193,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
delete:
|
delete:
|
||||||
operationId: sources_user_connections_kerberos_destroy
|
operationId: sources_user_connections_kerberos_destroy
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30566,7 +30261,7 @@ paths:
|
|||||||
/sources/user_connections/oauth/:
|
/sources/user_connections/oauth/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_oauth_list
|
operationId: sources_user_connections_oauth_list
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- name: ordering
|
- name: ordering
|
||||||
required: false
|
required: false
|
||||||
@ -30625,7 +30320,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
post:
|
post:
|
||||||
operationId: sources_user_connections_oauth_create
|
operationId: sources_user_connections_oauth_create
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -30658,7 +30353,7 @@ paths:
|
|||||||
/sources/user_connections/oauth/{id}/:
|
/sources/user_connections/oauth/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_oauth_retrieve
|
operationId: sources_user_connections_oauth_retrieve
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30691,7 +30386,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
put:
|
put:
|
||||||
operationId: sources_user_connections_oauth_update
|
operationId: sources_user_connections_oauth_update
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30730,7 +30425,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
patch:
|
patch:
|
||||||
operationId: sources_user_connections_oauth_partial_update
|
operationId: sources_user_connections_oauth_partial_update
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30768,7 +30463,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
delete:
|
delete:
|
||||||
operationId: sources_user_connections_oauth_destroy
|
operationId: sources_user_connections_oauth_destroy
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30834,7 +30529,7 @@ paths:
|
|||||||
/sources/user_connections/plex/:
|
/sources/user_connections/plex/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_plex_list
|
operationId: sources_user_connections_plex_list
|
||||||
description: User-source connection Viewset
|
description: Plex Source connection Serializer
|
||||||
parameters:
|
parameters:
|
||||||
- name: ordering
|
- name: ordering
|
||||||
required: false
|
required: false
|
||||||
@ -30893,7 +30588,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
post:
|
post:
|
||||||
operationId: sources_user_connections_plex_create
|
operationId: sources_user_connections_plex_create
|
||||||
description: User-source connection Viewset
|
description: Plex Source connection Serializer
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -30926,7 +30621,7 @@ paths:
|
|||||||
/sources/user_connections/plex/{id}/:
|
/sources/user_connections/plex/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_plex_retrieve
|
operationId: sources_user_connections_plex_retrieve
|
||||||
description: User-source connection Viewset
|
description: Plex Source connection Serializer
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30959,7 +30654,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
put:
|
put:
|
||||||
operationId: sources_user_connections_plex_update
|
operationId: sources_user_connections_plex_update
|
||||||
description: User-source connection Viewset
|
description: Plex Source connection Serializer
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -30998,7 +30693,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
patch:
|
patch:
|
||||||
operationId: sources_user_connections_plex_partial_update
|
operationId: sources_user_connections_plex_partial_update
|
||||||
description: User-source connection Viewset
|
description: Plex Source connection Serializer
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -31036,7 +30731,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
delete:
|
delete:
|
||||||
operationId: sources_user_connections_plex_destroy
|
operationId: sources_user_connections_plex_destroy
|
||||||
description: User-source connection Viewset
|
description: Plex Source connection Serializer
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -31102,7 +30797,7 @@ paths:
|
|||||||
/sources/user_connections/saml/:
|
/sources/user_connections/saml/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_saml_list
|
operationId: sources_user_connections_saml_list
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- name: ordering
|
- name: ordering
|
||||||
required: false
|
required: false
|
||||||
@ -31161,7 +30856,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
post:
|
post:
|
||||||
operationId: sources_user_connections_saml_create
|
operationId: sources_user_connections_saml_create
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -31194,7 +30889,7 @@ paths:
|
|||||||
/sources/user_connections/saml/{id}/:
|
/sources/user_connections/saml/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_user_connections_saml_retrieve
|
operationId: sources_user_connections_saml_retrieve
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -31227,7 +30922,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
put:
|
put:
|
||||||
operationId: sources_user_connections_saml_update
|
operationId: sources_user_connections_saml_update
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -31266,7 +30961,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
patch:
|
patch:
|
||||||
operationId: sources_user_connections_saml_partial_update
|
operationId: sources_user_connections_saml_partial_update
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -31304,7 +30999,7 @@ paths:
|
|||||||
description: ''
|
description: ''
|
||||||
delete:
|
delete:
|
||||||
operationId: sources_user_connections_saml_destroy
|
operationId: sources_user_connections_saml_destroy
|
||||||
description: User-source connection Viewset
|
description: Source Viewset
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
@ -44758,7 +44453,7 @@ components:
|
|||||||
- users_obj
|
- users_obj
|
||||||
GroupKerberosSourceConnection:
|
GroupKerberosSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -44780,21 +44475,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- group
|
- group
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
GroupKerberosSourceConnectionRequest:
|
GroupKerberosSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -44894,7 +44584,7 @@ components:
|
|||||||
- username
|
- username
|
||||||
GroupOAuthSourceConnection:
|
GroupOAuthSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -44916,21 +44606,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- group
|
- group
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
GroupOAuthSourceConnectionRequest:
|
GroupOAuthSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -44947,7 +44632,7 @@ components:
|
|||||||
- source
|
- source
|
||||||
GroupPlexSourceConnection:
|
GroupPlexSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: Plex Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -44969,21 +44654,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- group
|
- group
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
GroupPlexSourceConnectionRequest:
|
GroupPlexSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: Plex Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -45028,7 +44708,7 @@ components:
|
|||||||
- name
|
- name
|
||||||
GroupSAMLSourceConnection:
|
GroupSAMLSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -45050,74 +44730,16 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- group
|
- group
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
GroupSAMLSourceConnectionRequest:
|
GroupSAMLSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
|
||||||
group:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
source:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
|
||||||
- group
|
|
||||||
- identifier
|
|
||||||
- source
|
|
||||||
GroupSourceConnection:
|
|
||||||
type: object
|
|
||||||
description: Group Source Connection
|
|
||||||
properties:
|
|
||||||
pk:
|
|
||||||
type: integer
|
|
||||||
readOnly: true
|
|
||||||
title: ID
|
|
||||||
group:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
source:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
source_obj:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/components/schemas/Source'
|
|
||||||
readOnly: true
|
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
created:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
last_updated:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
|
||||||
- created
|
|
||||||
- group
|
|
||||||
- identifier
|
|
||||||
- last_updated
|
|
||||||
- pk
|
|
||||||
- source
|
|
||||||
- source_obj
|
|
||||||
GroupSourceConnectionRequest:
|
|
||||||
type: object
|
|
||||||
description: Group Source Connection
|
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -48743,18 +48365,6 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pagination
|
- pagination
|
||||||
- results
|
- results
|
||||||
PaginatedGroupSourceConnectionList:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
pagination:
|
|
||||||
$ref: '#/components/schemas/Pagination'
|
|
||||||
results:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/GroupSourceConnection'
|
|
||||||
required:
|
|
||||||
- pagination
|
|
||||||
- results
|
|
||||||
PaginatedIdentificationStageList:
|
PaginatedIdentificationStageList:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -51135,7 +50745,7 @@ components:
|
|||||||
the remote system.
|
the remote system.
|
||||||
PatchedGroupKerberosSourceConnectionRequest:
|
PatchedGroupKerberosSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -51148,7 +50758,7 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
PatchedGroupOAuthSourceConnectionRequest:
|
PatchedGroupOAuthSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -51161,7 +50771,7 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
PatchedGroupPlexSourceConnectionRequest:
|
PatchedGroupPlexSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: Plex Group-Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -51200,20 +50810,7 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
PatchedGroupSAMLSourceConnectionRequest:
|
PatchedGroupSAMLSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Source Connection
|
description: OAuth Group-Source connection Serializer
|
||||||
properties:
|
|
||||||
group:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
source:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
PatchedGroupSourceConnectionRequest:
|
|
||||||
type: object
|
|
||||||
description: Group Source Connection
|
|
||||||
properties:
|
properties:
|
||||||
group:
|
group:
|
||||||
type: string
|
type: string
|
||||||
@ -53186,7 +52783,7 @@ components:
|
|||||||
$ref: '#/components/schemas/FlowSetRequest'
|
$ref: '#/components/schemas/FlowSetRequest'
|
||||||
PatchedUserKerberosSourceConnectionRequest:
|
PatchedUserKerberosSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: Kerberos Source Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -53243,7 +52840,7 @@ components:
|
|||||||
$ref: '#/components/schemas/FlowSetRequest'
|
$ref: '#/components/schemas/FlowSetRequest'
|
||||||
PatchedUserOAuthSourceConnectionRequest:
|
PatchedUserOAuthSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: OAuth Source Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -53253,13 +52850,14 @@ components:
|
|||||||
identifier:
|
identifier:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
maxLength: 255
|
||||||
access_token:
|
access_token:
|
||||||
type: string
|
type: string
|
||||||
writeOnly: true
|
writeOnly: true
|
||||||
nullable: true
|
nullable: true
|
||||||
PatchedUserPlexSourceConnectionRequest:
|
PatchedUserPlexSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: Plex Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -53313,7 +52911,7 @@ components:
|
|||||||
$ref: '#/components/schemas/UserTypeEnum'
|
$ref: '#/components/schemas/UserTypeEnum'
|
||||||
PatchedUserSAMLSourceConnectionRequest:
|
PatchedUserSAMLSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: SAML Source Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -53332,9 +52930,6 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
PatchedUserWriteStageRequest:
|
PatchedUserWriteStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: UserWriteStage Serializer
|
description: UserWriteStage Serializer
|
||||||
@ -58422,7 +58017,7 @@ components:
|
|||||||
- name
|
- name
|
||||||
UserKerberosSourceConnection:
|
UserKerberosSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: Kerberos Source Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58437,27 +58032,22 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Source'
|
- $ref: '#/components/schemas/Source'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
created:
|
created:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
identifier:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
- user
|
- user
|
||||||
UserKerberosSourceConnectionRequest:
|
UserKerberosSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: Kerberos Source Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58684,7 +58274,7 @@ components:
|
|||||||
- logins_failed
|
- logins_failed
|
||||||
UserOAuthSourceConnection:
|
UserOAuthSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: OAuth Source Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58699,27 +58289,23 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Source'
|
- $ref: '#/components/schemas/Source'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
created:
|
created:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
identifier:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
maxLength: 255
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
- user
|
- user
|
||||||
UserOAuthSourceConnectionRequest:
|
UserOAuthSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: OAuth Source Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58729,6 +58315,7 @@ components:
|
|||||||
identifier:
|
identifier:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
maxLength: 255
|
||||||
access_token:
|
access_token:
|
||||||
type: string
|
type: string
|
||||||
writeOnly: true
|
writeOnly: true
|
||||||
@ -58786,7 +58373,7 @@ components:
|
|||||||
- paths
|
- paths
|
||||||
UserPlexSourceConnection:
|
UserPlexSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: Plex Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58801,27 +58388,22 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Source'
|
- $ref: '#/components/schemas/Source'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
created:
|
created:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
identifier:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
- user
|
- user
|
||||||
UserPlexSourceConnectionRequest:
|
UserPlexSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: Plex Source connection Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58883,7 +58465,7 @@ components:
|
|||||||
- username
|
- username
|
||||||
UserSAMLSourceConnection:
|
UserSAMLSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: SAML Source Serializer
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
@ -58898,27 +58480,22 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Source'
|
- $ref: '#/components/schemas/Source'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
created:
|
created:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
identifier:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- identifier
|
- identifier
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
- user
|
- user
|
||||||
UserSAMLSourceConnectionRequest:
|
UserSAMLSourceConnectionRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User source connection
|
description: SAML Source Serializer
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
@ -59082,20 +58659,12 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Source'
|
- $ref: '#/components/schemas/Source'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
created:
|
created:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
last_updated:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
readOnly: true
|
|
||||||
required:
|
required:
|
||||||
- created
|
- created
|
||||||
- identifier
|
|
||||||
- last_updated
|
|
||||||
- pk
|
- pk
|
||||||
- source
|
- source
|
||||||
- source_obj
|
- source_obj
|
||||||
@ -59109,11 +58678,7 @@ components:
|
|||||||
source:
|
source:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
identifier:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
required:
|
||||||
- identifier
|
|
||||||
- source
|
- source
|
||||||
- user
|
- user
|
||||||
UserTypeEnum:
|
UserTypeEnum:
|
||||||
|
18
uv.lock
generated
18
uv.lock
generated
@ -310,7 +310,7 @@ requires-dist = [
|
|||||||
{ name = "pydantic-scim" },
|
{ name = "pydantic-scim" },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
{ name = "pyrad" },
|
{ name = "pyrad" },
|
||||||
{ name = "python-kadmin-rs", specifier = "==0.6.0" },
|
{ name = "python-kadmin-rs", specifier = "==0.5.3" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "requests-oauthlib" },
|
{ name = "requests-oauthlib" },
|
||||||
{ name = "scim2-filter-parser" },
|
{ name = "scim2-filter-parser" },
|
||||||
@ -2599,16 +2599,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-kadmin-rs"
|
name = "python-kadmin-rs"
|
||||||
version = "0.6.0"
|
version = "0.5.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/ac/df3a093b1e186cd68a6f38778fac025450e5c5e9859c4790e00c2ed0ff62/python_kadmin_rs-0.6.0.tar.gz", hash = "sha256:dadd3d4ef542b829c1dcde97360a6b6a10700a4b5686f12f24b10f6cf5ca6e6c", size = 89318 }
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/95/07b708623f13874ad86dc603f2fe36e980a5f5890edea87286d13f2b0b81/python_kadmin_rs-0.5.3.tar.gz", hash = "sha256:4f46fd854af622896136c3ac4fc5e6a37d37bfffb5b2023e438001ffa62ab7e3", size = 89865 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/6d/59fefe1c4c11177c4feb8ad65dd6a265e9cc5fc83682a928acdccb170000/python_kadmin_rs-0.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0069fbd656096b98853f8cdc6d5e24f754829fa9cb4a716dac33777f0305d37a", size = 1418187 },
|
{ url = "https://files.pythonhosted.org/packages/96/46/1bbfd7d6819851c300b991d7340452fba8edc3d2fe68b33271279eb74887/python_kadmin_rs-0.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:54b5e1c2e22da0d16c1418eb2b46da8baa11699a5db8db2afc52dbfd02d14958", size = 1416637 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/12/c00a71c0fc17f5d208b4bb5e570002d74f0bc414e35194537d46ea32080f/python_kadmin_rs-0.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cfcfe9982e969705dee62f2b97c8d7c249b55b2a97e2bc981408061ea7182b96", size = 1501759 },
|
{ url = "https://files.pythonhosted.org/packages/be/34/fd7f5c324aaf1b9ad3dd5050ac2059230618c29adc452d676d2af4d5ae79/python_kadmin_rs-0.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:d1dc7ad1f07bbfd09baeb1fb0dfc45c87776ed717052081e63d3bdba340a250e", size = 1503018 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/b5/06cf809cfaaeded84e6634bf07116264ab4f8fd5eccca7523114e197f424/python_kadmin_rs-0.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:920df382e7a554d2f6fd160436a64adf1251f3262ec16bccd6d3b9f7e039d5fa", size = 3262691 },
|
{ url = "https://files.pythonhosted.org/packages/e5/29/3931502534e07806cf7c70631374452cfcbafa44e75c5403416372b701c7/python_kadmin_rs-0.5.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86404a1060ece916088ae4a0d188e9309fd46e0b3003779ee7a8dc7493176779", size = 3268475 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/72/99884dbc1856440a548ea8bf2ff1232c7f2823b6cb1a62bbb4d902a34609/python_kadmin_rs-0.6.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:94509b7470b18105c27fcaf5e6af894644614a687af74a43499735c405217e01", size = 3382996 },
|
{ url = "https://files.pythonhosted.org/packages/ba/5d/f18ca5df97a4241711555987eb308c6e6c5505883514ac7f18d7aebd52f2/python_kadmin_rs-0.5.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7aa62a618af2b2112f708fd44f9cc3cf25e28f1562ea66a2036fb3cd1a47e649", size = 3371699 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/4f/5d7e5be27cd466affc00fcab71fb94ea0420aee95306188988faf270b129/python_kadmin_rs-0.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f89e7fbcb7220a42c143a1b008685f98ca0a72ecc55c30f85b72c9d1ba9c3b9", size = 1572007 },
|
{ url = "https://files.pythonhosted.org/packages/91/d3/42c4d57414cfdf4e4ff528dd8e72428908ee67aeeae6a63fe2f5dbcd04bc/python_kadmin_rs-0.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80813af82dfbcc6a90505183c822eab11de77b6703e5691e37ed77d292224dd9", size = 1584049 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/1e/fdd7d6cd2ebc4cc654112329311380d1c03c681511973e32ae6ab90f261c/python_kadmin_rs-0.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:775ce07ffd47a50ba27c8d74c20baacb56acfc7a8c56a8b02f2207ed9829156e", size = 1618897 },
|
{ url = "https://files.pythonhosted.org/packages/9a/65/705f179cf4bf4d16fc1daeac0810def57da2f4514a5b79ca60f24d7efb90/python_kadmin_rs-0.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6799a0faddb4ccf200acfa87da38e5fa2af54970d066b2c876e752bbf794b204", size = 1590360 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1,206 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file WebAuthn utilities.
|
|
||||||
*/
|
|
||||||
import { fromByteArray } from "base64-js";
|
|
||||||
|
|
||||||
//@ts-check
|
|
||||||
|
|
||||||
//#region Type Definitions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} Assertion
|
|
||||||
* @property {string} id
|
|
||||||
* @property {string} rawId
|
|
||||||
* @property {string} type
|
|
||||||
* @property {string} registrationClientExtensions
|
|
||||||
* @property {object} response
|
|
||||||
* @property {string} response.clientDataJSON
|
|
||||||
* @property {string} response.attestationObject
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} AuthAssertion
|
|
||||||
* @property {string} id
|
|
||||||
* @property {string} rawId
|
|
||||||
* @property {string} type
|
|
||||||
* @property {string} assertionClientExtensions
|
|
||||||
* @property {object} response
|
|
||||||
* @property {string} response.clientDataJSON
|
|
||||||
* @property {string} response.authenticatorData
|
|
||||||
* @property {string} response.signature
|
|
||||||
* @property {string | null} response.userHandle
|
|
||||||
*/
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Encoding/Decoding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into a URL-safe base64 string.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} buffer
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function encodeBase64(buffer) {
|
|
||||||
return fromByteArray(buffer).replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte array into a base64 string without URL-safe encoding, i.e., with padding.
|
|
||||||
* @param {Uint8Array} buffer
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function encodeBase64Raw(buffer) {
|
|
||||||
return fromByteArray(buffer).replace(/\+/g, "-").replace(/\//g, "_");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a base64 string into a byte array.
|
|
||||||
*
|
|
||||||
* @param {string} input
|
|
||||||
* @returns {Uint8Array}
|
|
||||||
*/
|
|
||||||
export function decodeBase64(input) {
|
|
||||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
|
||||||
c.charCodeAt(0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Utility Functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the browser supports WebAuthn.
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export function isWebAuthnSupported() {
|
|
||||||
if ("credentials" in navigator) return true;
|
|
||||||
|
|
||||||
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
|
||||||
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn("WebAuthn not supported by browser.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts that the browser supports WebAuthn and that we're in a secure context.
|
|
||||||
*
|
|
||||||
* @throws {Error} If WebAuthn is not supported.
|
|
||||||
*/
|
|
||||||
export function assertWebAuthnSupport() {
|
|
||||||
// Is the navigator exposing the credentials API?
|
|
||||||
if ("credentials" in navigator) return;
|
|
||||||
|
|
||||||
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
|
||||||
throw new Error("WebAuthn requires this page to be accessed via HTTPS.");
|
|
||||||
}
|
|
||||||
throw new Error("WebAuthn not supported by browser.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms items in the credentialCreateOptions generated on the server
|
|
||||||
* into byte arrays expected by the navigator.credentials.create() call
|
|
||||||
* @param {PublicKeyCredentialCreationOptions} credentialCreateOptions
|
|
||||||
* @param {string} userID
|
|
||||||
* @returns {PublicKeyCredentialCreationOptions}
|
|
||||||
*/
|
|
||||||
export function transformCredentialCreateOptions(credentialCreateOptions, userID) {
|
|
||||||
const user = credentialCreateOptions.user;
|
|
||||||
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
|
||||||
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
|
||||||
// string, then a byte array, re-encode it and wrap that in an array.
|
|
||||||
const stringId = decodeURIComponent(window.atob(userID));
|
|
||||||
|
|
||||||
user.id = decodeBase64(encodeBase64(decodeBase64(stringId)));
|
|
||||||
const challenge = decodeBase64(credentialCreateOptions.challenge.toString());
|
|
||||||
|
|
||||||
return {
|
|
||||||
...credentialCreateOptions,
|
|
||||||
challenge,
|
|
||||||
user,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms the binary data in the credential into base64 strings
|
|
||||||
* for posting to the server.
|
|
||||||
*
|
|
||||||
* @param {PublicKeyCredential} newAssertion
|
|
||||||
* @returns {Assertion}
|
|
||||||
*/
|
|
||||||
export function transformNewAssertionForServer(newAssertion) {
|
|
||||||
const response = /** @type {AuthenticatorAttestationResponse} */ (newAssertion.response);
|
|
||||||
|
|
||||||
const attObj = new Uint8Array(response.attestationObject);
|
|
||||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
|
||||||
const rawId = new Uint8Array(newAssertion.rawId);
|
|
||||||
|
|
||||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: newAssertion.id,
|
|
||||||
rawId: encodeBase64(rawId),
|
|
||||||
type: newAssertion.type,
|
|
||||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
|
||||||
response: {
|
|
||||||
clientDataJSON: encodeBase64(clientDataJSON),
|
|
||||||
attestationObject: encodeBase64(attObj),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms the items in the credentialRequestOptions generated on the server
|
|
||||||
*
|
|
||||||
* @param {PublicKeyCredentialRequestOptions} credentialRequestOptions
|
|
||||||
* @returns {PublicKeyCredentialRequestOptions}
|
|
||||||
*/
|
|
||||||
export function transformCredentialRequestOptions(credentialRequestOptions) {
|
|
||||||
const challenge = decodeBase64(credentialRequestOptions.challenge.toString());
|
|
||||||
|
|
||||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
|
||||||
(credentialDescriptor) => {
|
|
||||||
const id = decodeBase64(credentialDescriptor.id.toString());
|
|
||||||
return Object.assign({}, credentialDescriptor, { id });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.assign({}, credentialRequestOptions, {
|
|
||||||
challenge,
|
|
||||||
allowCredentials,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
|
||||||
* @param {PublicKeyCredential} newAssertion
|
|
||||||
* @returns {AuthAssertion}
|
|
||||||
*/
|
|
||||||
export function transformAssertionForServer(newAssertion) {
|
|
||||||
const response = /** @type {AuthenticatorAssertionResponse} */ (newAssertion.response);
|
|
||||||
|
|
||||||
const authData = new Uint8Array(response.authenticatorData);
|
|
||||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
|
||||||
const rawId = new Uint8Array(newAssertion.rawId);
|
|
||||||
const sig = new Uint8Array(response.signature);
|
|
||||||
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: newAssertion.id,
|
|
||||||
rawId: encodeBase64(rawId),
|
|
||||||
type: newAssertion.type,
|
|
||||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
|
||||||
|
|
||||||
response: {
|
|
||||||
clientDataJSON: encodeBase64Raw(clientDataJSON),
|
|
||||||
signature: encodeBase64Raw(sig),
|
|
||||||
authenticatorData: encodeBase64Raw(authData),
|
|
||||||
userHandle: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
@ -48,9 +48,6 @@ export default [
|
|||||||
"lit/no-template-bind": "error",
|
"lit/no-template-bind": "error",
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
|
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
|
||||||
// TODO: TypeScript already handles this.
|
|
||||||
// Remove after project-wide ESLint config is properly set up.
|
|
||||||
"no-undef": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"error",
|
"error",
|
||||||
@ -74,18 +71,8 @@ export default [
|
|||||||
...globals.node,
|
...globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
files: [
|
files: ["scripts/**/*.mjs", "*.ts", "*.mjs"],
|
||||||
// TODO:Remove after project-wide ESLint config is properly set up.
|
|
||||||
"scripts/**/*.mjs",
|
|
||||||
"authentication/**/*.js",
|
|
||||||
"sfe/**/*.js",
|
|
||||||
"*.ts",
|
|
||||||
"*.mjs",
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-undef": "off",
|
|
||||||
// TODO: TypeScript already handles this.
|
|
||||||
// Remove after project-wide ESLint config is properly set up.
|
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
// We WANT our scripts to output to the console!
|
// We WANT our scripts to output to the console!
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
|
1914
web/package-lock.json
generated
1914
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -57,14 +57,9 @@
|
|||||||
"ts-pattern": "^5.4.0",
|
"ts-pattern": "^5.4.0",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"webcomponent-qr-code": "^1.2.0",
|
"webcomponent-qr-code": "^1.2.0",
|
||||||
"yaml": "^2.5.1",
|
"yaml": "^2.5.1"
|
||||||
"bootstrap": "^4.6.1",
|
|
||||||
"formdata-polyfill": "^4.0.10",
|
|
||||||
"jquery": "^3.7.1",
|
|
||||||
"weakmap-polyfill": "^2.0.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jquery": "^3.5.31",
|
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
"@hcaptcha/types": "^1.0.4",
|
"@hcaptcha/types": "^1.0.4",
|
||||||
"@lit/localize-tools": "^0.8.0",
|
"@lit/localize-tools": "^0.8.0",
|
||||||
@ -95,8 +90,6 @@
|
|||||||
"@wdio/spec-reporter": "^9.1.2",
|
"@wdio/spec-reporter": "^9.1.2",
|
||||||
"chromedriver": "^131.0.1",
|
"chromedriver": "^131.0.1",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"esbuild-plugin-copy": "^2.1.1",
|
|
||||||
"esbuild-plugin-es5": "^2.1.1",
|
|
||||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||||
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
|
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
@ -168,12 +161,6 @@
|
|||||||
"watch": "run-s build-locales esbuild:watch"
|
"watch": "run-s build-locales esbuild:watch"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
|
||||||
"./package.json": "./package.json",
|
|
||||||
"./paths": "./paths.js",
|
|
||||||
"./authentication": "./authentication/index.js",
|
|
||||||
"./scripts/*": "./scripts/*.mjs"
|
|
||||||
},
|
|
||||||
"wireit": {
|
"wireit": {
|
||||||
"build": {
|
"build": {
|
||||||
"#comment": [
|
"#comment": [
|
||||||
@ -206,7 +193,8 @@
|
|||||||
"./dist/patternfly.min.css"
|
"./dist/patternfly.min.css"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"build-locales"
|
"build-locales",
|
||||||
|
"./packages/sfe:build"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"NODE_RUNNER": {
|
"NODE_RUNNER": {
|
||||||
@ -216,7 +204,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"build:sfe": {
|
"build:sfe": {
|
||||||
"command": "node scripts/build-sfe.mjs"
|
"dependencies": [
|
||||||
|
"./packages/sfe:build"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"./packages/sfe/**/*.ts"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"build-proxy": {
|
"build-proxy": {
|
||||||
"command": "node scripts/build-web.mjs --proxy",
|
"command": "node scripts/build-web.mjs --proxy",
|
||||||
@ -249,6 +242,11 @@
|
|||||||
"lint:package"
|
"lint:package"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"format:packages": {
|
||||||
|
"dependencies": [
|
||||||
|
"./packages/sfe:prettier"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"command": "eslint --max-warnings 0 --fix",
|
"command": "eslint --max-warnings 0 --fix",
|
||||||
"env": {
|
"env": {
|
||||||
@ -276,6 +274,11 @@
|
|||||||
"shell": true,
|
"shell": true,
|
||||||
"command": "sh ./scripts/lint-lockfile.sh package-lock.json"
|
"command": "sh ./scripts/lint-lockfile.sh package-lock.json"
|
||||||
},
|
},
|
||||||
|
"lint:lockfiles": {
|
||||||
|
"dependencies": [
|
||||||
|
"./packages/sfe:lint:lockfile"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lint:package": {
|
"lint:package": {
|
||||||
"command": "syncpack format -i ' '"
|
"command": "syncpack format -i ' '"
|
||||||
},
|
},
|
||||||
@ -311,7 +314,9 @@
|
|||||||
"lint:spelling",
|
"lint:spelling",
|
||||||
"lint:package",
|
"lint:package",
|
||||||
"lint:lockfile",
|
"lint:lockfile",
|
||||||
"lint:precommit"
|
"lint:lockfiles",
|
||||||
|
"lint:precommit",
|
||||||
|
"format:packages"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
23
web/packages/sfe/.prettierrc.json
Normal file
23
web/packages/sfe/.prettierrc.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false,
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||||
|
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true,
|
||||||
|
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
|
||||||
|
}
|
18
web/packages/sfe/LICENSE.txt
Normal file
18
web/packages/sfe/LICENSE.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2024 Authentik Security, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||||
|
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||||
|
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
68
web/packages/sfe/package.json
Normal file
68
web/packages/sfe/package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "@goauthentik/web-sfe",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
|
"bootstrap": "^4.6.1",
|
||||||
|
"formdata-polyfill": "^4.0.10",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"weakmap-polyfill": "^2.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||||
|
"@rollup/plugin-swc": "^0.4.0",
|
||||||
|
"@swc/cli": "^0.4.0",
|
||||||
|
"@swc/core": "^1.7.28",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/jquery": "^3.5.31",
|
||||||
|
"lockfile-lint": "^4.14.0",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"rollup": "^4.23.0",
|
||||||
|
"rollup-plugin-copy": "^3.5.0",
|
||||||
|
"wireit": "^0.14.9"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@swc/core": "^1.7.28",
|
||||||
|
"@swc/core-darwin-arm64": "^1.6.13",
|
||||||
|
"@swc/core-darwin-x64": "^1.6.13",
|
||||||
|
"@swc/core-linux-arm-gnueabihf": "^1.6.13",
|
||||||
|
"@swc/core-linux-arm64-gnu": "^1.6.13",
|
||||||
|
"@swc/core-linux-arm64-musl": "^1.6.13",
|
||||||
|
"@swc/core-linux-x64-gnu": "^1.6.13",
|
||||||
|
"@swc/core-linux-x64-musl": "^1.6.13",
|
||||||
|
"@swc/core-win32-arm64-msvc": "^1.6.13",
|
||||||
|
"@swc/core-win32-ia32-msvc": "^1.6.13",
|
||||||
|
"@swc/core-win32-x64-msvc": "^1.6.13"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "wireit",
|
||||||
|
"lint:lockfile": "wireit",
|
||||||
|
"prettier": "prettier --write ./src ./tsconfig.json ./rollup.config.js ./package.json",
|
||||||
|
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
|
||||||
|
},
|
||||||
|
"wireit": {
|
||||||
|
"build:sfe": {
|
||||||
|
"command": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||||
|
"src/index.ts"
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
"./dist/sfe/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"command": "mkdir -p ../../dist/sfe && cp -r dist/sfe/* ../../dist/sfe",
|
||||||
|
"dependencies": [
|
||||||
|
"build:sfe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lint:lockfile": {
|
||||||
|
"command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
web/packages/sfe/rollup.config.js
Normal file
43
web/packages/sfe/rollup.config.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import swc from "@rollup/plugin-swc";
|
||||||
|
import copy from "rollup-plugin-copy";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "src/index.ts",
|
||||||
|
output: {
|
||||||
|
dir: "./dist/sfe",
|
||||||
|
format: "cjs",
|
||||||
|
},
|
||||||
|
context: "window",
|
||||||
|
plugins: [
|
||||||
|
copy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: "../../node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||||
|
dest: "./dist/sfe",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
resolve({ browser: true }),
|
||||||
|
commonjs(),
|
||||||
|
swc({
|
||||||
|
swc: {
|
||||||
|
jsc: {
|
||||||
|
loose: false,
|
||||||
|
externalHelpers: false,
|
||||||
|
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
|
||||||
|
keepClassNames: false,
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
env: {
|
||||||
|
targets: {
|
||||||
|
edge: "17",
|
||||||
|
ie: "11",
|
||||||
|
},
|
||||||
|
mode: "entry",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
527
web/packages/sfe/src/index.ts
Normal file
527
web/packages/sfe/src/index.ts
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
import { fromByteArray } from "base64-js";
|
||||||
|
import "formdata-polyfill";
|
||||||
|
import $ from "jquery";
|
||||||
|
import "weakmap-polyfill";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type AuthenticatorValidationChallenge,
|
||||||
|
type AutosubmitChallenge,
|
||||||
|
type ChallengeTypes,
|
||||||
|
ChallengeTypesFromJSON,
|
||||||
|
type ContextualFlowInfo,
|
||||||
|
type DeviceChallenge,
|
||||||
|
type ErrorDetail,
|
||||||
|
type IdentificationChallenge,
|
||||||
|
type PasswordChallenge,
|
||||||
|
type RedirectChallenge,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
interface GlobalAuthentik {
|
||||||
|
brand: {
|
||||||
|
branding_logo: string;
|
||||||
|
};
|
||||||
|
api: {
|
||||||
|
base: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ak(): GlobalAuthentik {
|
||||||
|
return (
|
||||||
|
window as unknown as {
|
||||||
|
authentik: GlobalAuthentik;
|
||||||
|
}
|
||||||
|
).authentik;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleFlowExecutor {
|
||||||
|
challenge?: ChallengeTypes;
|
||||||
|
flowSlug: string;
|
||||||
|
container: HTMLDivElement;
|
||||||
|
|
||||||
|
constructor(container: HTMLDivElement) {
|
||||||
|
this.flowSlug = window.location.pathname.split("/")[3];
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
get apiURL() {
|
||||||
|
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: this.apiURL,
|
||||||
|
success: (data) => {
|
||||||
|
this.challenge = ChallengeTypesFromJSON(data);
|
||||||
|
this.renderChallenge();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(data: { [key: string]: unknown } | FormData) {
|
||||||
|
$("button[type=submit]").addClass("disabled")
|
||||||
|
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||||
|
<span role="status">Loading...</span>`);
|
||||||
|
let finalData: { [key: string]: unknown } = {};
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
finalData = {};
|
||||||
|
data.forEach((value, key) => {
|
||||||
|
finalData[key] = value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
finalData = data;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: this.apiURL,
|
||||||
|
data: JSON.stringify(finalData),
|
||||||
|
success: (data) => {
|
||||||
|
this.challenge = ChallengeTypesFromJSON(data);
|
||||||
|
this.renderChallenge();
|
||||||
|
},
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChallenge() {
|
||||||
|
switch (this.challenge?.component) {
|
||||||
|
case "ak-stage-identification":
|
||||||
|
new IdentificationStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "ak-stage-password":
|
||||||
|
new PasswordStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "xak-flow-redirect":
|
||||||
|
new RedirectStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "ak-stage-autosubmit":
|
||||||
|
new AutosubmitStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "ak-stage-authenticator-validate":
|
||||||
|
new AuthenticatorValidateStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowInfoChallenge {
|
||||||
|
flowInfo?: ContextualFlowInfo;
|
||||||
|
responseErrors?: {
|
||||||
|
[key: string]: Array<ErrorDetail>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Stage<T extends FlowInfoChallenge> {
|
||||||
|
constructor(
|
||||||
|
public executor: SimpleFlowExecutor,
|
||||||
|
public challenge: T,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
error(fieldName: string) {
|
||||||
|
if (!this.challenge.responseErrors) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.challenge.responseErrors[fieldName] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInputError(fieldName: string) {
|
||||||
|
return `${this.error(fieldName)
|
||||||
|
.map((error) => {
|
||||||
|
return `<div class="invalid-feedback">
|
||||||
|
${error.string}
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNonFieldErrors() {
|
||||||
|
return `${this.error("non_field_errors")
|
||||||
|
.map((error) => {
|
||||||
|
return `<div class="alert alert-danger" role="alert">
|
||||||
|
${error.string}
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html(html: string) {
|
||||||
|
this.executor.container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
throw new Error("Abstract method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const IS_INVALID = "is-invalid";
|
||||||
|
|
||||||
|
class IdentificationStage extends Stage<IdentificationChallenge> {
|
||||||
|
render() {
|
||||||
|
this.html(`
|
||||||
|
<form id="ident-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
${
|
||||||
|
this.challenge.applicationPre
|
||||||
|
? `<p>
|
||||||
|
Log in to continue to ${this.challenge.applicationPre}.
|
||||||
|
</p>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
|
||||||
|
</div>
|
||||||
|
${
|
||||||
|
this.challenge.passwordFields
|
||||||
|
? `<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="password" class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
|
||||||
|
${this.renderInputError("password")}
|
||||||
|
</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${this.renderNonFieldErrors()}
|
||||||
|
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
|
||||||
|
</form>`);
|
||||||
|
$("#ident-form input[name=uid_field]").trigger("focus");
|
||||||
|
$("#ident-form").on("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const data = new FormData(ev.target as HTMLFormElement);
|
||||||
|
this.executor.submit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordStage extends Stage<PasswordChallenge> {
|
||||||
|
render() {
|
||||||
|
this.html(`
|
||||||
|
<form id="password-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
|
||||||
|
${this.renderInputError("password")}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||||
|
</form>`);
|
||||||
|
$("#password-form input").trigger("focus");
|
||||||
|
$("#password-form").on("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const data = new FormData(ev.target as HTMLFormElement);
|
||||||
|
this.executor.submit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RedirectStage extends Stage<RedirectChallenge> {
|
||||||
|
render() {
|
||||||
|
window.location.assign(this.challenge.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutosubmitStage extends Stage<AutosubmitChallenge> {
|
||||||
|
render() {
|
||||||
|
this.html(`
|
||||||
|
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
||||||
|
return `<input
|
||||||
|
type="hidden"
|
||||||
|
name="${key}"
|
||||||
|
value="${value}"
|
||||||
|
/>`;
|
||||||
|
})}
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>`);
|
||||||
|
$("#autosubmit-form").submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Assertion {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
type: string;
|
||||||
|
registrationClientExtensions: string;
|
||||||
|
response: {
|
||||||
|
clientDataJSON: string;
|
||||||
|
attestationObject: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthAssertion {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
type: string;
|
||||||
|
assertionClientExtensions: string;
|
||||||
|
response: {
|
||||||
|
clientDataJSON: string;
|
||||||
|
authenticatorData: string;
|
||||||
|
signature: string;
|
||||||
|
userHandle: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
|
||||||
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
|
b64enc(buf: Uint8Array): string {
|
||||||
|
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
b64RawEnc(buf: Uint8Array): string {
|
||||||
|
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
u8arr(input: string): Uint8Array {
|
||||||
|
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWebAuthnSupport(): boolean {
|
||||||
|
if ("credentials" in navigator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
||||||
|
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.warn("WebAuthn not supported by browser.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms items in the credentialCreateOptions generated on the server
|
||||||
|
* into byte arrays expected by the navigator.credentials.create() call
|
||||||
|
*/
|
||||||
|
transformCredentialCreateOptions(
|
||||||
|
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
userId: string,
|
||||||
|
): PublicKeyCredentialCreationOptions {
|
||||||
|
const user = credentialCreateOptions.user;
|
||||||
|
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||||
|
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||||
|
// string, then a byte array, re-encode it and wrap that in an array.
|
||||||
|
const stringId = decodeURIComponent(window.atob(userId));
|
||||||
|
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
|
||||||
|
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
|
||||||
|
|
||||||
|
return Object.assign({}, credentialCreateOptions, {
|
||||||
|
challenge,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the binary data in the credential into base64 strings
|
||||||
|
* for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||||
|
const attObj = new Uint8Array(
|
||||||
|
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
|
||||||
|
);
|
||||||
|
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
|
||||||
|
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: this.b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||||
|
response: {
|
||||||
|
clientDataJSON: this.b64enc(clientDataJSON),
|
||||||
|
attestationObject: this.b64enc(attObj),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCredentialRequestOptions(
|
||||||
|
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
): PublicKeyCredentialRequestOptions {
|
||||||
|
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
|
||||||
|
|
||||||
|
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||||
|
(credentialDescriptor) => {
|
||||||
|
const id = this.u8arr(credentialDescriptor.id.toString());
|
||||||
|
return Object.assign({}, credentialDescriptor, { id });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.assign({}, credentialRequestOptions, {
|
||||||
|
challenge,
|
||||||
|
allowCredentials,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||||
|
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
||||||
|
const authData = new Uint8Array(response.authenticatorData);
|
||||||
|
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
const sig = new Uint8Array(response.signature);
|
||||||
|
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: this.b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
clientDataJSON: this.b64RawEnc(clientDataJSON),
|
||||||
|
signature: this.b64RawEnc(sig),
|
||||||
|
authenticatorData: this.b64RawEnc(authData),
|
||||||
|
userHandle: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.deviceChallenge) {
|
||||||
|
return this.renderChallengePicker();
|
||||||
|
}
|
||||||
|
switch (this.deviceChallenge.deviceClass) {
|
||||||
|
case "static":
|
||||||
|
case "totp":
|
||||||
|
this.renderCodeInput();
|
||||||
|
break;
|
||||||
|
case "webauthn":
|
||||||
|
this.renderWebauthn();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChallengePicker() {
|
||||||
|
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
|
||||||
|
challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
|
||||||
|
? undefined
|
||||||
|
: challenge,
|
||||||
|
);
|
||||||
|
this.html(`<form id="picker-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
${
|
||||||
|
challenges.length > 0
|
||||||
|
? "<p>Select an authentication method.</p>"
|
||||||
|
: `
|
||||||
|
<p>No compatible authentication method available</p>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
${challenges
|
||||||
|
.map((challenge) => {
|
||||||
|
let label = undefined;
|
||||||
|
switch (challenge.deviceClass) {
|
||||||
|
case "static":
|
||||||
|
label = "Recovery keys";
|
||||||
|
break;
|
||||||
|
case "totp":
|
||||||
|
label = "Traditional authenticator";
|
||||||
|
break;
|
||||||
|
case "webauthn":
|
||||||
|
label = "Security key";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!label) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `<div class="form-label-group my-3 has-validation">
|
||||||
|
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
|
||||||
|
${label}
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
</form>`);
|
||||||
|
this.challenge.deviceChallenges.forEach((challenge) => {
|
||||||
|
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
this.deviceChallenge = challenge;
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCodeInput() {
|
||||||
|
this.html(`
|
||||||
|
<form id="totp-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? IS_INVALID : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
|
||||||
|
${this.renderInputError("code")}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||||
|
</form>`);
|
||||||
|
$("#totp-form input").trigger("focus");
|
||||||
|
$("#totp-form").on("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const data = new FormData(ev.target as HTMLFormElement);
|
||||||
|
this.executor.submit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWebauthn() {
|
||||||
|
this.html(`
|
||||||
|
<form id="totp-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
navigator.credentials
|
||||||
|
.get({
|
||||||
|
publicKey: this.transformCredentialRequestOptions(
|
||||||
|
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.then((assertion) => {
|
||||||
|
if (!assertion) {
|
||||||
|
throw new Error("No assertion");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// we now have an authentication assertion! encode the byte arrays contained
|
||||||
|
// in the assertion data as strings for posting to the server
|
||||||
|
const transformedAssertionForServer = this.transformAssertionForServer(
|
||||||
|
assertion as PublicKeyCredential,
|
||||||
|
);
|
||||||
|
|
||||||
|
// post the assertion to the server for verification.
|
||||||
|
this.executor.submit({
|
||||||
|
webauthn: transformedAssertionForServer,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error when validating assertion on server: ${err}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
|
this.deviceChallenge = undefined;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
|
||||||
|
sfe.start();
|
7
web/packages/sfe/tsconfig.json
Normal file
7
web/packages/sfe/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["jquery"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["DOM", "ES2015", "ES2017"]
|
||||||
|
}
|
||||||
|
}
|
25
web/paths.js
25
web/paths.js
@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Path constants for the web package.
|
|
||||||
*/
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {'@goauthentik/web'} WebPackageIdentifier
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The root of the web package.
|
|
||||||
*/
|
|
||||||
export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(__dirname));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path to the web package's distribution directory.
|
|
||||||
*
|
|
||||||
* This is where the built files are located after running the build process.
|
|
||||||
*/
|
|
||||||
export const DistDirectory = /** @type {`${WebPackageIdentifier}/dist`} */ (
|
|
||||||
resolve(__dirname, "dist")
|
|
||||||
);
|
|
@ -1,90 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Build script for the simplified flow executor (SFE).
|
|
||||||
*/
|
|
||||||
import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
|
|
||||||
import esbuild from "esbuild";
|
|
||||||
import copy from "esbuild-plugin-copy";
|
|
||||||
import { es5Plugin } from "esbuild-plugin-es5";
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import * as path from "node:path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the Simplified Flow Executor bundle.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* The output directory and file names are referenced by the backend.
|
|
||||||
* @see {@link ../../authentik/flows/templates/if/flow-sfe.html}
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function buildSFE() {
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
const sourceDirectory = path.join(PackageRoot, "sfe");
|
|
||||||
|
|
||||||
const entryPoint = path.join(sourceDirectory, "main.js");
|
|
||||||
const outDirectory = path.join(DistDirectory, "sfe");
|
|
||||||
|
|
||||||
const bootstrapCSSPath = require.resolve(
|
|
||||||
path.join("bootstrap", "dist", "css", "bootstrap.min.css"),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {esbuild.BuildOptions}
|
|
||||||
*/
|
|
||||||
const config = {
|
|
||||||
tsconfig: path.join(sourceDirectory, "tsconfig.json"),
|
|
||||||
entryPoints: [entryPoint],
|
|
||||||
minify: false,
|
|
||||||
bundle: true,
|
|
||||||
sourcemap: true,
|
|
||||||
treeShaking: true,
|
|
||||||
legalComments: "external",
|
|
||||||
platform: "browser",
|
|
||||||
format: "iife",
|
|
||||||
alias: {
|
|
||||||
"@swc/helpers": path.dirname(require.resolve("@swc/helpers/package.json")),
|
|
||||||
},
|
|
||||||
banner: {
|
|
||||||
js: [
|
|
||||||
// ---
|
|
||||||
"// Simplified Flow Executor (SFE)",
|
|
||||||
`// Bundled on ${new Date().toISOString()}`,
|
|
||||||
"// @ts-nocheck",
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
copy({
|
|
||||||
assets: [
|
|
||||||
{
|
|
||||||
from: bootstrapCSSPath,
|
|
||||||
to: outDirectory,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
es5Plugin({
|
|
||||||
swc: {
|
|
||||||
jsc: {
|
|
||||||
loose: false,
|
|
||||||
externalHelpers: false,
|
|
||||||
keepClassNames: false,
|
|
||||||
},
|
|
||||||
minify: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
target: ["es5"],
|
|
||||||
outdir: outDirectory,
|
|
||||||
};
|
|
||||||
|
|
||||||
esbuild.build(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSFE()
|
|
||||||
.then(() => {
|
|
||||||
console.log("Build complete");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Build failed", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
@ -1,4 +1,3 @@
|
|||||||
import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
|
|
||||||
import { execFileSync } from "child_process";
|
import { execFileSync } from "child_process";
|
||||||
import { deepmerge } from "deepmerge-ts";
|
import { deepmerge } from "deepmerge-ts";
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
@ -171,7 +170,7 @@ function composeVersionID() {
|
|||||||
* @throws {Error} on build failure
|
* @throws {Error} on build failure
|
||||||
*/
|
*/
|
||||||
function createEntryPointOptions([source, dest], overrides = {}) {
|
function createEntryPointOptions([source, dest], overrides = {}) {
|
||||||
const outdir = path.join(DistDirectory, dest);
|
const outdir = path.join(__dirname, "..", "dist", dest);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -234,7 +233,7 @@ async function doWatch() {
|
|||||||
buildObserverPlugin({
|
buildObserverPlugin({
|
||||||
serverURL,
|
serverURL,
|
||||||
logPrefix: entryPoint[1],
|
logPrefix: entryPoint[1],
|
||||||
relativeRoot: PackageRoot,
|
relativeRoot: path.join(__dirname, ".."),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
|
@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { AuthenticatorValidationChallenge, DeviceChallenge } from "@goauthentik/api";
|
|
||||||
* @import { FlowExecutor } from './Stage.js';
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
isWebAuthnSupported,
|
|
||||||
transformAssertionForServer,
|
|
||||||
transformCredentialRequestOptions,
|
|
||||||
} from "@goauthentik/web/authentication";
|
|
||||||
import $ from "jquery";
|
|
||||||
|
|
||||||
import { Stage } from "./Stage.js";
|
|
||||||
import { ak } from "./utils.js";
|
|
||||||
|
|
||||||
//@ts-check
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {AuthenticatorValidationChallenge} T
|
|
||||||
* @extends {Stage<T>}
|
|
||||||
*/
|
|
||||||
export class AuthenticatorValidateStage extends Stage {
|
|
||||||
/**
|
|
||||||
* @param {FlowExecutor} executor - The executor for this stage
|
|
||||||
* @param {T} challenge - The challenge for this stage
|
|
||||||
*/
|
|
||||||
constructor(executor, challenge) {
|
|
||||||
super(executor, challenge);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {DeviceChallenge | null}
|
|
||||||
*/
|
|
||||||
this.deviceChallenge = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.deviceChallenge) {
|
|
||||||
this.renderChallengePicker();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (this.deviceChallenge.deviceClass) {
|
|
||||||
case "static":
|
|
||||||
case "totp":
|
|
||||||
this.renderCodeInput();
|
|
||||||
break;
|
|
||||||
case "webauthn":
|
|
||||||
this.renderWebauthn();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
renderChallengePicker() {
|
|
||||||
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
|
|
||||||
challenge.deviceClass === "webauthn" && !isWebAuthnSupported() ? undefined : challenge,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.html(/* html */ `<form id="picker-form">
|
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
|
||||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
|
||||||
${
|
|
||||||
challenges.length > 0
|
|
||||||
? /* html */ `<p>Select an authentication method.</p>`
|
|
||||||
: /* html */ `<p>No compatible authentication method available</p>`
|
|
||||||
}
|
|
||||||
${challenges
|
|
||||||
.map((challenge) => {
|
|
||||||
let label = undefined;
|
|
||||||
|
|
||||||
switch (challenge.deviceClass) {
|
|
||||||
case "static":
|
|
||||||
label = "Recovery keys";
|
|
||||||
break;
|
|
||||||
case "totp":
|
|
||||||
label = "Traditional authenticator";
|
|
||||||
break;
|
|
||||||
case "webauthn":
|
|
||||||
label = "Security key";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!label) return "";
|
|
||||||
|
|
||||||
return /* html */ `<div class="form-label-group my-3 has-validation">
|
|
||||||
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
|
|
||||||
${label}
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
})
|
|
||||||
.join("")}
|
|
||||||
</form>`);
|
|
||||||
|
|
||||||
this.challenge.deviceChallenges.forEach((challenge) => {
|
|
||||||
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
|
|
||||||
"click",
|
|
||||||
() => {
|
|
||||||
this.deviceChallenge = challenge;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
renderCodeInput() {
|
|
||||||
this.html(/* html */ `
|
|
||||||
<form id="totp-form">
|
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
|
||||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
|
||||||
<div class="form-label-group my-3 has-validation">
|
|
||||||
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
|
|
||||||
${this.renderInputError("code")}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
|
||||||
</form>`);
|
|
||||||
|
|
||||||
$("#totp-form input").trigger("focus");
|
|
||||||
|
|
||||||
$("#totp-form").on("submit", (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
const target = /** @type {HTMLFormElement} */ (ev.target);
|
|
||||||
|
|
||||||
const data = new FormData(target);
|
|
||||||
this.executor.submit(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
renderWebauthn() {
|
|
||||||
this.html(/* html */ `
|
|
||||||
<form id="totp-form">
|
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
|
||||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
|
|
||||||
const challenge = /** @type {PublicKeyCredentialRequestOptions} */ (
|
|
||||||
this.deviceChallenge?.challenge
|
|
||||||
);
|
|
||||||
|
|
||||||
navigator.credentials
|
|
||||||
.get({
|
|
||||||
publicKey: transformCredentialRequestOptions(challenge),
|
|
||||||
})
|
|
||||||
.then((credential) => {
|
|
||||||
if (!credential) {
|
|
||||||
throw new Error("No assertion");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (credential.type !== "public-key") {
|
|
||||||
throw new Error("Invalid assertion type");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// We now have an authentication assertion!
|
|
||||||
// Encode the byte arrays contained in the assertion data as strings
|
|
||||||
// for posting to the server.
|
|
||||||
const transformedAssertionForServer = transformAssertionForServer(
|
|
||||||
/** @type {PublicKeyCredential} */ (credential),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Post the assertion to the server for verification.
|
|
||||||
this.executor.submit({
|
|
||||||
webauthn: transformedAssertionForServer,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Error when validating assertion on server: ${err}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn(error);
|
|
||||||
|
|
||||||
this.deviceChallenge = null;
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { AutosubmitChallenge } from "@goauthentik/api";
|
|
||||||
*/
|
|
||||||
import $ from "jquery";
|
|
||||||
|
|
||||||
import { Stage } from "./Stage.js";
|
|
||||||
import { ak } from "./utils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {AutosubmitChallenge} T
|
|
||||||
* @extends {Stage<T>}
|
|
||||||
*/
|
|
||||||
export class AutosubmitStage extends Stage {
|
|
||||||
render() {
|
|
||||||
this.html(/* html */ `
|
|
||||||
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
|
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
|
||||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
|
||||||
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
|
||||||
return /* html */ `<input
|
|
||||||
type="hidden"
|
|
||||||
name="${key}"
|
|
||||||
value="${value}"
|
|
||||||
/>`;
|
|
||||||
})}
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>`);
|
|
||||||
|
|
||||||
$("#autosubmit-form").submit();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { IdentificationChallenge } from "@goauthentik/api";
|
|
||||||
*/
|
|
||||||
import $ from "jquery";
|
|
||||||
|
|
||||||
import { Stage } from "./Stage.js";
|
|
||||||
import { ak } from "./utils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {IdentificationChallenge} T
|
|
||||||
* @extends {Stage<T>}
|
|
||||||
*/
|
|
||||||
export class IdentificationStage extends Stage {
|
|
||||||
render() {
|
|
||||||
this.html(/* html */ `
|
|
||||||
<form id="ident-form">
|
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
|
||||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
|
||||||
${
|
|
||||||
this.challenge.applicationPre
|
|
||||||
? /* html */ `<p>
|
|
||||||
Log in to continue to ${this.challenge.applicationPre}.
|
|
||||||
</p>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
<div class="form-label-group my-3 has-validation">
|
|
||||||
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
|
|
||||||
</div>
|
|
||||||
${
|
|
||||||
this.challenge.passwordFields
|
|
||||||
? /* html */ `<div class="form-label-group my-3 has-validation">
|
|
||||||
<input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
|
|
||||||
${this.renderInputError("password")}
|
|
||||||
</div>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${this.renderNonFieldErrors()}
|
|
||||||
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
|
|
||||||
</form>`);
|
|
||||||
|
|
||||||
$("#ident-form input[name=uid_field]").trigger("focus");
|
|
||||||
$("#ident-form").on("submit", (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
const target = /** @type {HTMLFormElement} */ (ev.target);
|
|
||||||
|
|
||||||
const data = new FormData(target);
|
|
||||||
this.executor.submit(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { PasswordChallenge } from "@goauthentik/api";
|
|
||||||
*/
|
|
||||||
import $ from "jquery";
|
|
||||||
|
|
||||||
import { Stage } from "./Stage.js";
|
|
||||||
import { ak } from "./utils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {PasswordChallenge} T
|
|
||||||
* @extends {Stage<T>}
|
|
||||||
*/
|
|
||||||
export class PasswordStage extends Stage {
|
|
||||||
render() {
|
|
||||||
this.html(/* html */ `
|
|
||||||
<form id="password-form">
|
|
||||||
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
|
||||||
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
|
||||||
<div class="form-label-group my-3 has-validation">
|
|
||||||
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
|
|
||||||
${this.renderInputError("password")}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
|
||||||
</form>`);
|
|
||||||
|
|
||||||
$("#password-form input").trigger("focus");
|
|
||||||
|
|
||||||
$("#password-form").on("submit", (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
const target = /** @type {HTMLFormElement} */ (ev.target);
|
|
||||||
|
|
||||||
const data = new FormData(target);
|
|
||||||
this.executor.submit(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { RedirectChallenge } from "@goauthentik/api";
|
|
||||||
*/
|
|
||||||
import { Stage } from "./Stage.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {RedirectChallenge} T
|
|
||||||
* @extends {Stage<T>}
|
|
||||||
*/
|
|
||||||
export class RedirectStage extends Stage {
|
|
||||||
render() {
|
|
||||||
window.location.assign(this.challenge.to);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { ChallengeTypes } from "@goauthentik/api";
|
|
||||||
* @import { FlowExecutor } from './Stage.js';
|
|
||||||
*/
|
|
||||||
import $ from "jquery";
|
|
||||||
|
|
||||||
import { ChallengeTypesFromJSON } from "@goauthentik/api";
|
|
||||||
|
|
||||||
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage.js";
|
|
||||||
import { AutosubmitStage } from "./AutosubmitStage.js";
|
|
||||||
import { IdentificationStage } from "./IdentificationStage.js";
|
|
||||||
import { PasswordStage } from "./PasswordStage.js";
|
|
||||||
import { RedirectStage } from "./RedirectStage.js";
|
|
||||||
import { ak } from "./utils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple Flow Executor lifecycle.
|
|
||||||
*
|
|
||||||
* @implements {FlowExecutor}
|
|
||||||
*/
|
|
||||||
export class SimpleFlowExecutor {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {HTMLDivElement} container
|
|
||||||
*/
|
|
||||||
constructor(container) {
|
|
||||||
/**
|
|
||||||
* @type {ChallengeTypes | null} The current challenge.
|
|
||||||
*/
|
|
||||||
this.challenge = null;
|
|
||||||
/**
|
|
||||||
* @type {string} The flow slug.
|
|
||||||
*/
|
|
||||||
this.flowSlug = window.location.pathname.split("/")[3] || "";
|
|
||||||
/**
|
|
||||||
* @type {HTMLDivElement} The container element for the flow executor.
|
|
||||||
*/
|
|
||||||
this.container = container;
|
|
||||||
}
|
|
||||||
|
|
||||||
get apiURL() {
|
|
||||||
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
$.ajax({
|
|
||||||
type: "GET",
|
|
||||||
url: this.apiURL,
|
|
||||||
success: (data) => {
|
|
||||||
this.challenge = ChallengeTypesFromJSON(data);
|
|
||||||
|
|
||||||
this.renderChallenge();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits the form data.
|
|
||||||
* @param {Record<string, unknown> | FormData} payload
|
|
||||||
*/
|
|
||||||
submit(payload) {
|
|
||||||
$("button[type=submit]").addClass("disabled")
|
|
||||||
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
|
||||||
<span role="status">Loading...</span>`);
|
|
||||||
/**
|
|
||||||
* @type {Record<string, unknown>}
|
|
||||||
*/
|
|
||||||
let finalData;
|
|
||||||
|
|
||||||
if (payload instanceof FormData) {
|
|
||||||
finalData = {};
|
|
||||||
|
|
||||||
payload.forEach((value, key) => {
|
|
||||||
finalData[key] = value;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
finalData = payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: this.apiURL,
|
|
||||||
data: JSON.stringify(finalData),
|
|
||||||
success: (data) => {
|
|
||||||
this.challenge = ChallengeTypesFromJSON(data);
|
|
||||||
this.renderChallenge();
|
|
||||||
},
|
|
||||||
contentType: "application/json",
|
|
||||||
dataType: "json",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
renderChallenge() {
|
|
||||||
switch (this.challenge?.component) {
|
|
||||||
case "ak-stage-identification":
|
|
||||||
return new IdentificationStage(this, this.challenge).render();
|
|
||||||
case "ak-stage-password":
|
|
||||||
return new PasswordStage(this, this.challenge).render();
|
|
||||||
case "xak-flow-redirect":
|
|
||||||
return new RedirectStage(this, this.challenge).render();
|
|
||||||
case "ak-stage-autosubmit":
|
|
||||||
return new AutosubmitStage(this, this.challenge).render();
|
|
||||||
case "ak-stage-authenticator-validate":
|
|
||||||
return new AuthenticatorValidateStage(this, this.challenge).render();
|
|
||||||
default:
|
|
||||||
this.container.innerText = `Unsupported stage: ${this.challenge?.component}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
/**
|
|
||||||
* @import { ContextualFlowInfo, ErrorDetail } from "@goauthentik/api";
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} FlowInfoChallenge
|
|
||||||
* @property {ContextualFlowInfo} [flowInfo]
|
|
||||||
* @property {Record<string, Array<ErrorDetail>>} [responseErrors]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export class FlowExecutor {
|
|
||||||
constructor() {
|
|
||||||
/**
|
|
||||||
* The DOM container element.
|
|
||||||
*
|
|
||||||
* @type {HTMLElement}
|
|
||||||
* @abstract
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
||||||
this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits the form data.
|
|
||||||
*
|
|
||||||
* @param {Record<string, unknown> | FormData} data The data to submit.
|
|
||||||
* @abstract
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
submit(data) {
|
|
||||||
throw new Error(`Method 'submit' not implemented in ${this.constructor.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a stage in a flow
|
|
||||||
* @template {FlowInfoChallenge} T
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export class Stage {
|
|
||||||
/**
|
|
||||||
* @param {FlowExecutor} executor - The executor for this stage
|
|
||||||
* @param {T} challenge - The challenge for this stage
|
|
||||||
*/
|
|
||||||
constructor(executor, challenge) {
|
|
||||||
/** @type {FlowExecutor} */
|
|
||||||
this.executor = executor;
|
|
||||||
|
|
||||||
/** @type {T} */
|
|
||||||
this.challenge = challenge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @protected
|
|
||||||
* @param {string} fieldName
|
|
||||||
*/
|
|
||||||
error(fieldName) {
|
|
||||||
if (!this.challenge.responseErrors) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return this.challenge.responseErrors[fieldName] || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @protected
|
|
||||||
* @param {string} fieldName
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
renderInputError(fieldName) {
|
|
||||||
return `${this.error(fieldName)
|
|
||||||
.map((error) => {
|
|
||||||
return /* html */ `<div class="invalid-feedback">
|
|
||||||
${error.string}
|
|
||||||
</div>`;
|
|
||||||
})
|
|
||||||
.join("")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @protected
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
renderNonFieldErrors() {
|
|
||||||
return `${this.error("non_field_errors")
|
|
||||||
.map((error) => {
|
|
||||||
return /* html */ `<div class="alert alert-danger" role="alert">
|
|
||||||
${error.string}
|
|
||||||
</div>`;
|
|
||||||
})
|
|
||||||
.join("")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @protected
|
|
||||||
* @param {string} innerHTML
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
html(innerHTML) {
|
|
||||||
this.executor.container.innerHTML = innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the stage (must be implemented by subclasses)
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
throw new Error("Abstract method");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Simplified Flow Executor (SFE) library module.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./Stage.js";
|
|
||||||
export * from "./SimpleFlowExecutor.js";
|
|
||||||
export * from "./AuthenticatorValidateStage.js";
|
|
||||||
export * from "./AutosubmitStage.js";
|
|
||||||
export * from "./IdentificationStage.js";
|
|
||||||
export * from "./PasswordStage.js";
|
|
||||||
export * from "./RedirectStage.js";
|
|
||||||
export * from "./utils.js";
|
|
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {object} GlobalAuthentik
|
|
||||||
* @property {object} brand
|
|
||||||
* @property {string} brand.branding_logo
|
|
||||||
* @property {object} api
|
|
||||||
* @property {string} api.base
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the global authentik object from the window.
|
|
||||||
* @throws {Error} If the object not found
|
|
||||||
* @returns {GlobalAuthentik}
|
|
||||||
*/
|
|
||||||
export function ak() {
|
|
||||||
if (!("authentik" in window)) {
|
|
||||||
throw new Error("No authentik object found in window");
|
|
||||||
}
|
|
||||||
|
|
||||||
return /** @type {GlobalAuthentik} */ (window.authentik);
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file Simplified Flow Executor (SFE) entry point.
|
|
||||||
*/
|
|
||||||
import "formdata-polyfill";
|
|
||||||
import $ from "jquery";
|
|
||||||
|
|
||||||
import { SimpleFlowExecutor } from "./lib/index.js";
|
|
||||||
|
|
||||||
const flowContainer = /** @type {HTMLDivElement} */ ($("#flow-sfe-container")[0]);
|
|
||||||
|
|
||||||
if (!flowContainer) {
|
|
||||||
throw new Error("No flow container element found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const sfe = new SimpleFlowExecutor(flowContainer);
|
|
||||||
|
|
||||||
sfe.start();
|
|
@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
// TODO: Replace with @goauthentik/tsconfig after project compilation.
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
|
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"@goauthentik/web/authentication": ["../authentication/index.js"]
|
|
||||||
},
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"rootDir": "../",
|
|
||||||
"composite": true,
|
|
||||||
"declaration": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"incremental": true,
|
|
||||||
"emitDeclarationOnly": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"lib": ["DOM", "ES2015", "ES2017"],
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"newLine": "lf",
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitOverride": false,
|
|
||||||
"outDir": "${configDir}/out",
|
|
||||||
"pretty": true,
|
|
||||||
"skipDefaultLibCheck": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"target": "ESNext",
|
|
||||||
"useUnknownInCatchVariables": true
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
// ---
|
|
||||||
"./out/**/*",
|
|
||||||
"./dist/**/*"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
// ---
|
|
||||||
"./**/*.js",
|
|
||||||
"../authentication/**/*.js"
|
|
||||||
]
|
|
||||||
}
|
|
@ -19,7 +19,7 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
|
|||||||
@customElement("ak-about-modal")
|
@customElement("ak-about-modal")
|
||||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
|
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return ModalButton.styles.concat(
|
return super.styles.concat(
|
||||||
PFAbout,
|
PFAbout,
|
||||||
css`
|
css`
|
||||||
.pf-c-about-modal-box__hero {
|
.pf-c-about-modal-box__hero {
|
||||||
|
@ -16,8 +16,8 @@ import "@goauthentik/elements/messages/MessageContainer";
|
|||||||
import "@goauthentik/elements/messages/MessageContainer";
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
import "@goauthentik/elements/notifications/APIDrawer";
|
import "@goauthentik/elements/notifications/APIDrawer";
|
||||||
import "@goauthentik/elements/notifications/NotificationDrawer";
|
import "@goauthentik/elements/notifications/NotificationDrawer";
|
||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
|
||||||
import "@goauthentik/elements/router/RouterOutlet";
|
import "@goauthentik/elements/router/RouterOutlet";
|
||||||
|
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
||||||
import "@goauthentik/elements/sidebar/Sidebar";
|
import "@goauthentik/elements/sidebar/Sidebar";
|
||||||
import "@goauthentik/elements/sidebar/SidebarItem";
|
import "@goauthentik/elements/sidebar/SidebarItem";
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ import "./AdminSidebar";
|
|||||||
@customElement("ak-interface-admin")
|
@customElement("ak-interface-admin")
|
||||||
export class AdminInterface extends AuthenticatedInterface {
|
export class AdminInterface extends AuthenticatedInterface {
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
|
notificationDrawerOpen = getRouteParameter("notificationDrawerOpen", false);
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
apiDrawerOpen = getURLParam("apiDrawerOpen", false);
|
apiDrawerOpen = getRouteParameter("apiDrawerOpen", false);
|
||||||
|
|
||||||
ws: WebsocketClient;
|
ws: WebsocketClient;
|
||||||
|
|
||||||
@ -93,14 +93,14 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
|
|
||||||
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
|
||||||
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
this.notificationDrawerOpen = !this.notificationDrawerOpen;
|
||||||
updateURLParams({
|
patchRouteParams({
|
||||||
notificationDrawerOpen: this.notificationDrawerOpen,
|
notificationDrawerOpen: this.notificationDrawerOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
|
||||||
this.apiDrawerOpen = !this.apiDrawerOpen;
|
this.apiDrawerOpen = !this.apiDrawerOpen;
|
||||||
updateURLParams({
|
patchRouteParams({
|
||||||
apiDrawerOpen: this.apiDrawerOpen,
|
apiDrawerOpen: this.apiDrawerOpen,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -123,7 +123,7 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
||||||
const { ESBuildObserver } = await import("@goauthentik/common/client");
|
const { ESBuildObserver } = await import("src/development/build-observer");
|
||||||
|
|
||||||
new ESBuildObserver(process.env.WATCHER_URL);
|
new ESBuildObserver(process.env.WATCHER_URL);
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@ export class AdminInterface extends AuthenticatedInterface {
|
|||||||
class="pf-c-page__main"
|
class="pf-c-page__main"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
id="main-content"
|
id="main-content"
|
||||||
defaultUrl="/administration/overview"
|
defaultURL="/administration/overview"
|
||||||
.routes=${ROUTES}
|
.routes=${ROUTES}
|
||||||
>
|
>
|
||||||
</ak-router-outlet>
|
</ak-router-outlet>
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
WithCapabilitiesConfig,
|
WithCapabilitiesConfig,
|
||||||
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
} from "@goauthentik/elements/Interface/capabilitiesProvider";
|
||||||
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
|
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
|
||||||
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
import { ID_PATTERN, SLUG_PATTERN, UUID_PATTERN } from "@goauthentik/elements/router";
|
||||||
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
|
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
|
||||||
import { spread } from "@open-wc/lit-helpers";
|
import { spread } from "@open-wc/lit-helpers";
|
||||||
|
|
||||||
@ -95,62 +95,127 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSidebarItems(): TemplateResult {
|
renderSidebarItems(): TemplateResult {
|
||||||
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
|
|
||||||
// commonplace and singular enough to merit its own handler.
|
|
||||||
type SidebarEntry = [
|
type SidebarEntry = [
|
||||||
path: string | null,
|
/**
|
||||||
|
* The pathname to match against. If null, this is a parent item.
|
||||||
|
*/
|
||||||
|
pathname: string | null,
|
||||||
|
/**
|
||||||
|
* The label to display in the sidebar.
|
||||||
|
*/
|
||||||
label: string,
|
label: string,
|
||||||
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
|
/**
|
||||||
|
* The attributes to apply to the sidebar item. This is a map of attribute name to value.
|
||||||
|
*
|
||||||
|
* The second attribute type is of string[] to help with the 'activeWhen' control,
|
||||||
|
* which was commonplace and singular enough to merit its own handler.
|
||||||
|
*/
|
||||||
|
attributes?: Record<string, unknown> | string[] | null,
|
||||||
|
/**
|
||||||
|
* The children of this sidebar item. This is a recursive structure.
|
||||||
|
*/
|
||||||
children?: SidebarEntry[],
|
children?: SidebarEntry[],
|
||||||
];
|
];
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const sidebarContent: SidebarEntry[] = [
|
const sidebarContent: SidebarEntry[] = [
|
||||||
[null, msg("Dashboards"), { "?expanded": true }, [
|
// ---
|
||||||
["/administration/overview", msg("Overview")],
|
[
|
||||||
["/administration/dashboard/users", msg("User Statistics")],
|
null,
|
||||||
["/administration/system-tasks", msg("System Tasks")]]],
|
msg("Dashboards"),
|
||||||
[null, msg("Applications"), null, [
|
{ "?expanded": true },
|
||||||
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
|
[
|
||||||
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
|
["/administration/overview", msg("Overview")],
|
||||||
["/outpost/outposts", msg("Outposts")]]],
|
["/administration/dashboard/users", msg("User Statistics")],
|
||||||
[null, msg("Events"), null, [
|
["/administration/system-tasks", msg("System Tasks")],
|
||||||
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
|
],
|
||||||
["/events/rules", msg("Notification Rules")],
|
],
|
||||||
["/events/transports", msg("Notification Transports")]]],
|
[
|
||||||
[null, msg("Customization"), null, [
|
null,
|
||||||
["/policy/policies", msg("Policies")],
|
msg("Applications"),
|
||||||
["/core/property-mappings", msg("Property Mappings")],
|
null,
|
||||||
["/blueprints/instances", msg("Blueprints")],
|
[
|
||||||
["/policy/reputation", msg("Reputation scores")]]],
|
[
|
||||||
[null, msg("Flows and Stages"), null, [
|
"/core/applications",
|
||||||
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
|
msg("Applications"),
|
||||||
["/flow/stages", msg("Stages")],
|
[`/core/applications/:slug(${SLUG_PATTERN})`],
|
||||||
["/flow/stages/prompts", msg("Prompts")]]],
|
],
|
||||||
[null, msg("Directory"), null, [
|
["/core/providers", msg("Providers"), [`/core/providers/:id(${ID_PATTERN})`]],
|
||||||
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
|
["/outpost/outposts", msg("Outposts")],
|
||||||
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
|
],
|
||||||
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
|
],
|
||||||
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
|
[
|
||||||
["/core/tokens", msg("Tokens and App passwords")],
|
null,
|
||||||
["/flow/stages/invitations", msg("Invitations")]]],
|
msg("Events"),
|
||||||
[null, msg("System"), null, [
|
null,
|
||||||
["/core/brands", msg("Brands")],
|
[
|
||||||
["/crypto/certificates", msg("Certificates")],
|
["/events/log", msg("Logs"), [`/events/log/:id(${UUID_PATTERN})`]],
|
||||||
["/outpost/integrations", msg("Outpost Integrations")],
|
["/events/rules", msg("Notification Rules")],
|
||||||
["/admin/settings", msg("Settings")]]],
|
["/events/transports", msg("Notification Transports")],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
msg("Customization"),
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
["/policy/policies", msg("Policies")],
|
||||||
|
["/core/property-mappings", msg("Property Mappings")],
|
||||||
|
["/blueprints/instances", msg("Blueprints")],
|
||||||
|
["/policy/reputation", msg("Reputation scores")],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
msg("Flows and Stages"),
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
["/flow/flows", msg("Flows"), [`/flow/flows/:slug(${SLUG_PATTERN})`]],
|
||||||
|
["/flow/stages", msg("Stages")],
|
||||||
|
["/flow/stages/prompts", msg("Prompts")],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
msg("Directory"),
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
["/identity/users", msg("Users"), [`/identity/users/:id(${ID_PATTERN})`]],
|
||||||
|
["/identity/groups", msg("Groups"), [`/identity/groups/:id(${UUID_PATTERN})`]],
|
||||||
|
["/identity/roles", msg("Roles"), [`/identity/roles/:id(${UUID_PATTERN})`]],
|
||||||
|
[
|
||||||
|
"/core/sources",
|
||||||
|
msg("Federation and Social login"),
|
||||||
|
[`/core/sources/:slug(${SLUG_PATTERN})`],
|
||||||
|
],
|
||||||
|
["/core/tokens", msg("Tokens and App passwords")],
|
||||||
|
["/flow/stages/invitations", msg("Invitations")],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
msg("System"),
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
["/core/brands", msg("Brands")],
|
||||||
|
["/crypto/certificates", msg("Certificates")],
|
||||||
|
["/outpost/integrations", msg("Outpost Integrations")],
|
||||||
|
["/admin/settings", msg("Settings")],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Typescript requires the type here to correctly type the recursive path
|
// Typescript requires the type here to correctly type the recursive path
|
||||||
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
|
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
|
||||||
|
|
||||||
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
|
const renderOneSidebarItem: SidebarRenderer = ([pathname, label, attributes, children]) => {
|
||||||
const properties = Array.isArray(attributes)
|
const properties = Array.isArray(attributes)
|
||||||
? { ".activeWhen": attributes }
|
? { ".activeWhen": attributes }
|
||||||
: (attributes ?? {});
|
: (attributes ?? {});
|
||||||
if (path) {
|
|
||||||
properties.path = path;
|
if (pathname) {
|
||||||
|
properties.pathname = pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<ak-sidebar-item ${spread(properties)}>
|
return html`<ak-sidebar-item ${spread(properties)}>
|
||||||
${label ? html`<span slot="label">${label}</span>` : nothing}
|
${label ? html`<span slot="label">${label}</span>` : nothing}
|
||||||
${map(children, renderOneSidebarItem)}
|
${map(children, renderOneSidebarItem)}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
|
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/PageHeader";
|
import "@goauthentik/elements/PageHeader";
|
||||||
@ -55,12 +54,10 @@ export class DebugPage extends AKElement {
|
|||||||
message: "Success",
|
message: "Success",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch((exc) => {
|
||||||
const parsedError = await parseAPIResponseError(error);
|
|
||||||
|
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: pluckErrorDetail(parsedError),
|
message: exc,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -1,155 +1,210 @@
|
|||||||
import "@goauthentik/admin/admin-overview/AdminOverviewPage";
|
import "@goauthentik/admin/admin-overview/AdminOverviewPage";
|
||||||
import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
import { Route } from "@goauthentik/elements/router/Route";
|
||||||
|
import { ID_PATTERN, SLUG_PATTERN, UUID_PATTERN } from "@goauthentik/elements/router/constants";
|
||||||
|
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
interface IDParameters {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SlugParameters {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UUIDParameters {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROUTES = [
|
||||||
// Prevent infinite Shell loops
|
// Prevent infinite Shell loops
|
||||||
new Route(new RegExp("^/$")).redirect("/administration/overview"),
|
Route.redirect("^/$", "/administration/overview"),
|
||||||
new Route(new RegExp("^#.*")).redirect("/administration/overview"),
|
Route.redirect("^#.*", "/administration/overview"),
|
||||||
new Route(new RegExp("^/library$")).redirect("/if/user/", true),
|
Route.redirect("^/library$", "/if/user/", true),
|
||||||
// statically imported since this is the default route
|
// statically imported since this is the default route
|
||||||
new Route(new RegExp("^/administration/overview$"), async () => {
|
new Route("/administration/overview", () => {
|
||||||
return html`<ak-admin-overview></ak-admin-overview>`;
|
return html`<ak-admin-overview></ak-admin-overview>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/administration/dashboard/users$"), async () => {
|
new Route("/administration/dashboard/users", async () => {
|
||||||
await import("@goauthentik/admin/admin-overview/DashboardUserPage");
|
await import("@goauthentik/admin/admin-overview/DashboardUserPage");
|
||||||
|
|
||||||
return html`<ak-admin-dashboard-users></ak-admin-dashboard-users>`;
|
return html`<ak-admin-dashboard-users></ak-admin-dashboard-users>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/administration/system-tasks$"), async () => {
|
new Route("/administration/system-tasks", async () => {
|
||||||
await import("@goauthentik/admin/system-tasks/SystemTaskListPage");
|
await import("@goauthentik/admin/system-tasks/SystemTaskListPage");
|
||||||
|
|
||||||
return html`<ak-system-task-list></ak-system-task-list>`;
|
return html`<ak-system-task-list></ak-system-task-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/providers$"), async () => {
|
new Route("/core/providers", async () => {
|
||||||
await import("@goauthentik/admin/providers/ProviderListPage");
|
await import("@goauthentik/admin/providers/ProviderListPage");
|
||||||
|
|
||||||
return html`<ak-provider-list></ak-provider-list>`;
|
return html`<ak-provider-list></ak-provider-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/core/providers/(?<id>${ID_REGEX})$`), async (args) => {
|
new Route<IDParameters>(
|
||||||
await import("@goauthentik/admin/providers/ProviderViewPage");
|
new URLPattern({
|
||||||
return html`<ak-provider-view .providerID=${parseInt(args.id, 10)}></ak-provider-view>`;
|
pathname: `/core/providers/:id(${ID_PATTERN})`,
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/applications$"), async () => {
|
async (params) => {
|
||||||
|
await import("@goauthentik/admin/providers/ProviderViewPage");
|
||||||
|
|
||||||
|
return html`<ak-provider-view
|
||||||
|
.providerID=${parseInt(params.id, 10)}
|
||||||
|
></ak-provider-view>`;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
new Route("/core/applications", async () => {
|
||||||
await import("@goauthentik/admin/applications/ApplicationListPage");
|
await import("@goauthentik/admin/applications/ApplicationListPage");
|
||||||
|
|
||||||
return html`<ak-application-list></ak-application-list>`;
|
return html`<ak-application-list></ak-application-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/core/applications/(?<slug>${SLUG_REGEX})$`), async (args) => {
|
new Route(`/core/applications/:slug(${SLUG_PATTERN})`, async ({ slug }) => {
|
||||||
await import("@goauthentik/admin/applications/ApplicationViewPage");
|
await import("@goauthentik/admin/applications/ApplicationViewPage");
|
||||||
return html`<ak-application-view .applicationSlug=${args.slug}></ak-application-view>`;
|
|
||||||
|
return html`<ak-application-view .applicationSlug=${slug}></ak-application-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/sources$"), async () => {
|
new Route("/core/sources", async () => {
|
||||||
await import("@goauthentik/admin/sources/SourceListPage");
|
await import("@goauthentik/admin/sources/SourceListPage");
|
||||||
|
|
||||||
return html`<ak-source-list></ak-source-list>`;
|
return html`<ak-source-list></ak-source-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/core/sources/(?<slug>${SLUG_REGEX})$`), async (args) => {
|
new Route(`/core/sources/:slug(${SLUG_PATTERN})`, async ({ slug }) => {
|
||||||
await import("@goauthentik/admin/sources/SourceViewPage");
|
await import("@goauthentik/admin/sources/SourceViewPage");
|
||||||
return html`<ak-source-view .sourceSlug=${args.slug}></ak-source-view>`;
|
|
||||||
|
return html`<ak-source-view .sourceSlug=${slug}></ak-source-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/property-mappings$"), async () => {
|
new Route("/core/property-mappings", async () => {
|
||||||
await import("@goauthentik/admin/property-mappings/PropertyMappingListPage");
|
await import("@goauthentik/admin/property-mappings/PropertyMappingListPage");
|
||||||
|
|
||||||
return html`<ak-property-mapping-list></ak-property-mapping-list>`;
|
return html`<ak-property-mapping-list></ak-property-mapping-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/tokens$"), async () => {
|
new Route("/core/tokens", async () => {
|
||||||
await import("@goauthentik/admin/tokens/TokenListPage");
|
await import("@goauthentik/admin/tokens/TokenListPage");
|
||||||
|
|
||||||
return html`<ak-token-list></ak-token-list>`;
|
return html`<ak-token-list></ak-token-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/core/brands"), async () => {
|
new Route("/core/brands", async () => {
|
||||||
await import("@goauthentik/admin/brands/BrandListPage");
|
await import("@goauthentik/admin/brands/BrandListPage");
|
||||||
|
|
||||||
return html`<ak-brand-list></ak-brand-list>`;
|
return html`<ak-brand-list></ak-brand-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/policy/policies$"), async () => {
|
new Route("/policy/policies", async () => {
|
||||||
await import("@goauthentik/admin/policies/PolicyListPage");
|
await import("@goauthentik/admin/policies/PolicyListPage");
|
||||||
|
|
||||||
return html`<ak-policy-list></ak-policy-list>`;
|
return html`<ak-policy-list></ak-policy-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/policy/reputation$"), async () => {
|
new Route("/policy/reputation", async () => {
|
||||||
await import("@goauthentik/admin/policies/reputation/ReputationListPage");
|
await import("@goauthentik/admin/policies/reputation/ReputationListPage");
|
||||||
|
|
||||||
return html`<ak-policy-reputation-list></ak-policy-reputation-list>`;
|
return html`<ak-policy-reputation-list></ak-policy-reputation-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/identity/groups$"), async () => {
|
new Route("/identity/groups", async () => {
|
||||||
await import("@goauthentik/admin/groups/GroupListPage");
|
await import("@goauthentik/admin/groups/GroupListPage");
|
||||||
|
|
||||||
return html`<ak-group-list></ak-group-list>`;
|
return html`<ak-group-list></ak-group-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/identity/groups/(?<uuid>${UUID_REGEX})$`), async (args) => {
|
new Route<UUIDParameters>(`/identity/groups/:uuid(${UUID_PATTERN})`, async ({ uuid }) => {
|
||||||
await import("@goauthentik/admin/groups/GroupViewPage");
|
await import("@goauthentik/admin/groups/GroupViewPage");
|
||||||
return html`<ak-group-view .groupId=${args.uuid}></ak-group-view>`;
|
|
||||||
|
return html`<ak-group-view .groupId=${uuid}></ak-group-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/identity/users$"), async () => {
|
new Route("/identity/users", async () => {
|
||||||
await import("@goauthentik/admin/users/UserListPage");
|
await import("@goauthentik/admin/users/UserListPage");
|
||||||
|
|
||||||
return html`<ak-user-list></ak-user-list>`;
|
return html`<ak-user-list></ak-user-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/identity/users/(?<id>${ID_REGEX})$`), async (args) => {
|
new Route<IDParameters>(`/identity/users/:id(${ID_PATTERN})`, async ({ id }) => {
|
||||||
await import("@goauthentik/admin/users/UserViewPage");
|
await import("@goauthentik/admin/users/UserViewPage");
|
||||||
return html`<ak-user-view .userId=${parseInt(args.id, 10)}></ak-user-view>`;
|
|
||||||
|
return html`<ak-user-view .userId=${parseInt(id, 10)}></ak-user-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/identity/roles$"), async () => {
|
new Route("/identity/roles", async () => {
|
||||||
await import("@goauthentik/admin/roles/RoleListPage");
|
await import("@goauthentik/admin/roles/RoleListPage");
|
||||||
|
|
||||||
return html`<ak-role-list></ak-role-list>`;
|
return html`<ak-role-list></ak-role-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => {
|
new Route<IDParameters>(`/identity/roles/:id(${UUID_PATTERN})`, async ({ id }) => {
|
||||||
await import("@goauthentik/admin/roles/RoleViewPage");
|
await import("@goauthentik/admin/roles/RoleViewPage");
|
||||||
return html`<ak-role-view roleId=${args.id}></ak-role-view>`;
|
|
||||||
|
return html`<ak-role-view roleId=${id}></ak-role-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/flow/stages/invitations$"), async () => {
|
new Route("/flow/stages/invitations", async () => {
|
||||||
await import("@goauthentik/admin/stages/invitation/InvitationListPage");
|
await import("@goauthentik/admin/stages/invitation/InvitationListPage");
|
||||||
|
|
||||||
return html`<ak-stage-invitation-list></ak-stage-invitation-list>`;
|
return html`<ak-stage-invitation-list></ak-stage-invitation-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/flow/stages/prompts$"), async () => {
|
new Route("/flow/stages/prompts", async () => {
|
||||||
await import("@goauthentik/admin/stages/prompt/PromptListPage");
|
await import("@goauthentik/admin/stages/prompt/PromptListPage");
|
||||||
|
|
||||||
return html`<ak-stage-prompt-list></ak-stage-prompt-list>`;
|
return html`<ak-stage-prompt-list></ak-stage-prompt-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/flow/stages$"), async () => {
|
new Route("/flow/stages", async () => {
|
||||||
await import("@goauthentik/admin/stages/StageListPage");
|
await import("@goauthentik/admin/stages/StageListPage");
|
||||||
|
|
||||||
return html`<ak-stage-list></ak-stage-list>`;
|
return html`<ak-stage-list></ak-stage-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/flow/flows$"), async () => {
|
new Route("/flow/flows", async () => {
|
||||||
await import("@goauthentik/admin/flows/FlowListPage");
|
await import("@goauthentik/admin/flows/FlowListPage");
|
||||||
|
|
||||||
return html`<ak-flow-list></ak-flow-list>`;
|
return html`<ak-flow-list></ak-flow-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`), async (args) => {
|
new Route<SlugParameters>(`/flow/flows/:slug(${SLUG_PATTERN})`, async ({ slug }) => {
|
||||||
await import("@goauthentik/admin/flows/FlowViewPage");
|
await import("@goauthentik/admin/flows/FlowViewPage");
|
||||||
return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;
|
|
||||||
|
return html`<ak-flow-view .flowSlug=${slug}></ak-flow-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/events/log$"), async () => {
|
new Route("/events/log", async () => {
|
||||||
await import("@goauthentik/admin/events/EventListPage");
|
await import("@goauthentik/admin/events/EventListPage");
|
||||||
|
|
||||||
return html`<ak-event-list></ak-event-list>`;
|
return html`<ak-event-list></ak-event-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp(`^/events/log/(?<id>${UUID_REGEX})$`), async (args) => {
|
new Route<IDParameters>(`/events/log/:id(${UUID_PATTERN})`, async ({ id }) => {
|
||||||
await import("@goauthentik/admin/events/EventViewPage");
|
await import("@goauthentik/admin/events/EventViewPage");
|
||||||
return html`<ak-event-view .eventID=${args.id}></ak-event-view>`;
|
|
||||||
|
return html`<ak-event-view .eventID=${id}></ak-event-view>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/events/transports$"), async () => {
|
new Route("/events/transports", async () => {
|
||||||
await import("@goauthentik/admin/events/TransportListPage");
|
await import("@goauthentik/admin/events/TransportListPage");
|
||||||
|
|
||||||
return html`<ak-event-transport-list></ak-event-transport-list>`;
|
return html`<ak-event-transport-list></ak-event-transport-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/events/rules$"), async () => {
|
new Route("/events/rules", async () => {
|
||||||
await import("@goauthentik/admin/events/RuleListPage");
|
await import("@goauthentik/admin/events/RuleListPage");
|
||||||
|
|
||||||
return html`<ak-event-rule-list></ak-event-rule-list>`;
|
return html`<ak-event-rule-list></ak-event-rule-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/outpost/outposts$"), async () => {
|
new Route("/outpost/outposts", async () => {
|
||||||
await import("@goauthentik/admin/outposts/OutpostListPage");
|
await import("@goauthentik/admin/outposts/OutpostListPage");
|
||||||
|
|
||||||
return html`<ak-outpost-list></ak-outpost-list>`;
|
return html`<ak-outpost-list></ak-outpost-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/outpost/integrations$"), async () => {
|
new Route("/outpost/integrations", async () => {
|
||||||
await import("@goauthentik/admin/outposts/ServiceConnectionListPage");
|
await import("@goauthentik/admin/outposts/ServiceConnectionListPage");
|
||||||
|
|
||||||
return html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`;
|
return html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/crypto/certificates$"), async () => {
|
new Route("/crypto/certificates", async () => {
|
||||||
await import("@goauthentik/admin/crypto/CertificateKeyPairListPage");
|
await import("@goauthentik/admin/crypto/CertificateKeyPairListPage");
|
||||||
|
|
||||||
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
|
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/admin/settings$"), async () => {
|
new Route("/admin/settings", async () => {
|
||||||
await import("@goauthentik/admin/admin-settings/AdminSettingsPage");
|
await import("@goauthentik/admin/admin-settings/AdminSettingsPage");
|
||||||
|
|
||||||
return html`<ak-admin-settings></ak-admin-settings>`;
|
return html`<ak-admin-settings></ak-admin-settings>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/blueprints/instances$"), async () => {
|
new Route("/blueprints/instances", async () => {
|
||||||
await import("@goauthentik/admin/blueprints/BlueprintListPage");
|
await import("@goauthentik/admin/blueprints/BlueprintListPage");
|
||||||
|
|
||||||
return html`<ak-blueprint-list></ak-blueprint-list>`;
|
return html`<ak-blueprint-list></ak-blueprint-list>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/debug$"), async () => {
|
new Route("/debug", async () => {
|
||||||
await import("@goauthentik/admin/DebugPage");
|
await import("@goauthentik/admin/DebugPage");
|
||||||
|
|
||||||
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
|
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
|
||||||
}),
|
}),
|
||||||
new Route(new RegExp("^/enterprise/licenses$"), async () => {
|
new Route("/enterprise/licenses", async () => {
|
||||||
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
|
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
|
||||||
|
|
||||||
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
||||||
}),
|
}),
|
||||||
];
|
] satisfies Route<never>[];
|
||||||
|
@ -16,7 +16,7 @@ import "@goauthentik/elements/PageHeader";
|
|||||||
import "@goauthentik/elements/cards/AggregatePromiseCard";
|
import "@goauthentik/elements/cards/AggregatePromiseCard";
|
||||||
import "@goauthentik/elements/cards/QuickActionsCard.js";
|
import "@goauthentik/elements/cards/QuickActionsCard.js";
|
||||||
import type { QuickAction } from "@goauthentik/elements/cards/QuickActionsCard.js";
|
import type { QuickAction } from "@goauthentik/elements/cards/QuickActionsCard.js";
|
||||||
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
|
import { formatRouteHash } from "@goauthentik/elements/router";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||||
@ -79,10 +79,13 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
quickActions: QuickAction[] = [
|
quickActions: QuickAction[] = [
|
||||||
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
|
[
|
||||||
[msg("Check the logs"), paramURL("/events/log")],
|
msg("Create a new application"),
|
||||||
|
formatRouteHash("/core/applications", { createForm: true }),
|
||||||
|
],
|
||||||
|
[msg("Check the logs"), formatRouteHash("/events/log")],
|
||||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
||||||
[msg("Manage users"), paramURL("/identity/users")],
|
[msg("Manage users"), formatRouteHash("/identity/users")],
|
||||||
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
|
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -195,10 +198,13 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
|
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
|
||||||
|
|
||||||
const quickActions: [string, string][] = [
|
const quickActions: [string, string][] = [
|
||||||
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
|
[
|
||||||
[msg("Check the logs"), paramURL("/events/log")],
|
msg("Create a new application"),
|
||||||
|
formatRouteHash("/core/applications", { createForm: true }),
|
||||||
|
],
|
||||||
|
[msg("Check the logs"), formatRouteHash("/events/log")],
|
||||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
|
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
|
||||||
[msg("Manage users"), paramURL("/identity/users")],
|
[msg("Manage users"), formatRouteHash("/identity/users")],
|
||||||
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
|
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import {
|
|
||||||
APIError,
|
|
||||||
parseAPIResponseError,
|
|
||||||
pluckErrorDetail,
|
|
||||||
} from "@goauthentik/common/errors/network";
|
|
||||||
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { PropertyValues, TemplateResult, html, nothing } from "lit";
|
import { PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||||
import { state } from "lit/decorators.js";
|
import { state } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { ResponseError } from "@goauthentik/api";
|
||||||
|
|
||||||
export interface AdminStatus {
|
export interface AdminStatus {
|
||||||
icon: string;
|
icon: string;
|
||||||
message?: TemplateResult;
|
message?: TemplateResult;
|
||||||
@ -32,7 +29,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
|||||||
|
|
||||||
// Current error state if any request fails
|
// Current error state if any request fails
|
||||||
@state()
|
@state()
|
||||||
protected error?: APIError;
|
protected error?: string;
|
||||||
|
|
||||||
// Abstract methods to be implemented by subclasses
|
// Abstract methods to be implemented by subclasses
|
||||||
abstract getPrimaryValue(): Promise<T>;
|
abstract getPrimaryValue(): Promise<T>;
|
||||||
@ -62,9 +59,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
|||||||
this.value = value; // Triggers shouldUpdate
|
this.value = value; // Triggers shouldUpdate
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
})
|
})
|
||||||
.catch(async (error: unknown) => {
|
.catch((err: ResponseError) => {
|
||||||
this.status = undefined;
|
this.status = undefined;
|
||||||
this.error = await parseAPIResponseError(error);
|
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,9 +79,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
})
|
})
|
||||||
.catch(async (error: unknown) => {
|
.catch((err: ResponseError) => {
|
||||||
this.status = undefined;
|
this.status = undefined;
|
||||||
this.error = await parseAPIResponseError(error);
|
this.error = err?.response?.statusText ?? msg("Unknown error");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent immediate re-render if only value changed
|
// Prevent immediate re-render if only value changed
|
||||||
@ -123,8 +120,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
|||||||
*/
|
*/
|
||||||
private renderError(error: string): TemplateResult {
|
private renderError(error: string): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<p><i class="fa fa-times"></i> ${msg("Failed to fetch")}</p>
|
<p><i class="fa fa-times"></i> ${error}</p>
|
||||||
<p class="subtext">${error}</p>
|
<p class="subtext">${msg("Failed to fetch")}</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +146,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
|||||||
this.status
|
this.status
|
||||||
? this.renderStatus(this.status) // Status available
|
? this.renderStatus(this.status) // Status available
|
||||||
: this.error
|
: this.error
|
||||||
? this.renderError(pluckErrorDetail(this.error)) // Error state
|
? this.renderError(this.error) // Error state
|
||||||
: this.renderLoading() // Loading state
|
: this.renderLoading() // Loading state
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
@ -10,7 +10,6 @@ import "@goauthentik/elements/buttons/ModalButton";
|
|||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -69,7 +68,7 @@ export class RecentEventsCard extends Table<Event> {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
row(item: EventWithContext): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
|
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
|
||||||
<small>${item.app}</small>`,
|
<small>${item.app}</small>`,
|
||||||
@ -82,11 +81,7 @@ export class RecentEventsCard extends Table<Event> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
|
renderEmpty(): TemplateResult {
|
||||||
if (this.error) {
|
|
||||||
return super.renderEmpty(inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.renderEmpty(
|
return super.renderEmpty(
|
||||||
html`<ak-empty-state header=${msg("No Events found.")}>
|
html`<ak-empty-state header=${msg("No Events found.")}>
|
||||||
<div slot="body">${msg("No matching events could be found.")}</div>
|
<div slot="body">${msg("No matching events could be found.")}</div>
|
||||||
|
@ -7,7 +7,7 @@ import "@goauthentik/elements/ak-mdx";
|
|||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
import { getRouteParameter } from "@goauthentik/elements/router/utils";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -71,7 +71,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return TablePage.styles.concat(PFCard, applicationListStyle);
|
return super.styles.concat(PFCard, applicationListStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
columns(): TableColumn[] {
|
columns(): TableColumn[] {
|
||||||
@ -156,7 +156,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderObjectCreate(): TemplateResult {
|
renderObjectCreate(): TemplateResult {
|
||||||
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
return html` <ak-application-wizard .open=${getRouteParameter("createWizard", false)}>
|
||||||
<button
|
<button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
@ -165,7 +165,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
${msg("Create with Provider")}
|
${msg("Create with Provider")}
|
||||||
</button>
|
</button>
|
||||||
</ak-application-wizard>
|
</ak-application-wizard>
|
||||||
<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
<ak-forms-modal .open=${getRouteParameter("createForm", false)}>
|
||||||
<span slot="submit"> ${msg("Create")} </span>
|
<span slot="submit"> ${msg("Create")} </span>
|
||||||
<span slot="header"> ${msg("Create Application")} </span>
|
<span slot="header"> ${msg("Create Application")} </span>
|
||||||
<ak-application-form slot="form"> </ak-application-form>
|
<ak-application-form slot="form"> </ak-application-form>
|
||||||
|
@ -8,7 +8,7 @@ import "@goauthentik/components/ak-hint/ak-hint-body";
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/Label";
|
import "@goauthentik/elements/Label";
|
||||||
import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
|
import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
|
||||||
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
import { getRouteParameter } from "@goauthentik/elements/router/utils";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { css, html } from "lit";
|
import { css, html } from "lit";
|
||||||
@ -110,7 +110,7 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
|
|||||||
the same time with our new Application Wizard.
|
the same time with our new Application Wizard.
|
||||||
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
|
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
|
||||||
</p>
|
</p>
|
||||||
<ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
<ak-application-wizard .open=${getRouteParameter("createWizard", false)}>
|
||||||
<button
|
<button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js";
|
import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js";
|
||||||
import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js";
|
import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js";
|
||||||
import {
|
import {
|
||||||
NavigationEventInit,
|
NavigationUpdate,
|
||||||
WizardNavigationEvent,
|
WizardNavigationEvent,
|
||||||
WizardUpdateEvent,
|
WizardUpdateEvent,
|
||||||
} from "@goauthentik/components/ak-wizard/events";
|
} from "@goauthentik/components/ak-wizard/events";
|
||||||
@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js";
|
|||||||
import { ValidationError } from "@goauthentik/api";
|
import { ValidationError } from "@goauthentik/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApplicationTransactionValidationError,
|
|
||||||
type ApplicationWizardState,
|
type ApplicationWizardState,
|
||||||
type ApplicationWizardStateUpdate,
|
type ApplicationWizardStateUpdate,
|
||||||
|
ExtendedValidationError,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class ApplicationWizardStep extends WizardStep {
|
export class ApplicationWizardStep extends WizardStep {
|
||||||
@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected removeErrors(
|
protected removeErrors(
|
||||||
keyToDelete: keyof ApplicationTransactionValidationError,
|
keyToDelete: keyof ExtendedValidationError,
|
||||||
): ValidationError | undefined {
|
): ValidationError | undefined {
|
||||||
if (!this.wizard.errors) {
|
if (!this.wizard.errors) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -71,7 +71,7 @@ export class ApplicationWizardStep extends WizardStep {
|
|||||||
public handleUpdate(
|
public handleUpdate(
|
||||||
update?: ApplicationWizardStateUpdate,
|
update?: ApplicationWizardStateUpdate,
|
||||||
destination?: string,
|
destination?: string,
|
||||||
enable?: NavigationEventInit,
|
enable?: NavigationUpdate,
|
||||||
) {
|
) {
|
||||||
// Inform ApplicationWizard of content state
|
// Inform ApplicationWizard of content state
|
||||||
if (update) {
|
if (update) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
|
||||||
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
|
||||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||||
import { isSlug } from "@goauthentik/common/utils.js";
|
|
||||||
import { camelToSnake } from "@goauthentik/common/utils.js";
|
import { camelToSnake } from "@goauthentik/common/utils.js";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/components/ak-slug-input";
|
import "@goauthentik/components/ak-slug-input";
|
||||||
@ -11,6 +10,7 @@ import { type NavigableButton, type WizardButton } from "@goauthentik/components
|
|||||||
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
|
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
|
import { isSlug } from "@goauthentik/elements/router";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { parseAPIResponseError } from "@goauthentik/common/errors/network";
|
import { parseAPIError } from "@goauthentik/common/errors";
|
||||||
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
|
||||||
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
|
||||||
import { showAPIErrorMessage } from "@goauthentik/elements/messages/MessageContainer";
|
|
||||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
import { P, match } from "ts-pattern";
|
import { P, match } from "ts-pattern";
|
||||||
|
|
||||||
@ -31,11 +30,10 @@ import {
|
|||||||
type TransactionApplicationRequest,
|
type TransactionApplicationRequest,
|
||||||
type TransactionApplicationResponse,
|
type TransactionApplicationResponse,
|
||||||
type TransactionPolicyBindingRequest,
|
type TransactionPolicyBindingRequest,
|
||||||
instanceOfValidationError,
|
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
|
||||||
import { OneOfProvider, isApplicationTransactionValidationError } from "../types.js";
|
import { ExtendedValidationError, OneOfProvider } from "../types.js";
|
||||||
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
|
||||||
|
|
||||||
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
const _submitStates = ["reviewing", "running", "submitted"] as const;
|
||||||
@ -133,46 +131,39 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
|
|
||||||
this.state = "running";
|
this.state = "running";
|
||||||
|
|
||||||
return new CoreApi(DEFAULT_CONFIG)
|
return (
|
||||||
.coreTransactionalApplicationsUpdate({
|
new CoreApi(DEFAULT_CONFIG)
|
||||||
transactionApplicationRequest: request,
|
.coreTransactionalApplicationsUpdate({
|
||||||
})
|
transactionApplicationRequest: request,
|
||||||
.then((_response: TransactionApplicationResponse) => {
|
})
|
||||||
this.dispatchCustomEvent(EVENT_REFRESH);
|
.then((_response: TransactionApplicationResponse) => {
|
||||||
this.state = "submitted";
|
this.dispatchCustomEvent(EVENT_REFRESH);
|
||||||
})
|
this.state = "submitted";
|
||||||
|
})
|
||||||
|
|
||||||
.catch(async (error) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const parsedError = await parseAPIResponseError(error);
|
.catch(async (resolution: any) => {
|
||||||
|
const errors = (await parseAPIError(
|
||||||
|
await resolution,
|
||||||
|
)) as ExtendedValidationError;
|
||||||
|
|
||||||
if (!instanceOfValidationError(parsedError)) {
|
// THIS is a really gross special case; if the user is duplicating the name of
|
||||||
showAPIErrorMessage(parsedError);
|
// an existing provider, the error appears on the `app` (!) error object. We
|
||||||
|
// have to move that to the `provider.name` error field so it shows up in the
|
||||||
return;
|
// right place.
|
||||||
}
|
if (Array.isArray(errors?.app?.provider)) {
|
||||||
|
const providerError = errors.app.provider;
|
||||||
if (isApplicationTransactionValidationError(parsedError)) {
|
errors.provider = errors.provider ?? {};
|
||||||
// THIS is a really gross special case; if the user is duplicating the name of an existing provider, the error appears on the `app` (!) error object.
|
errors.provider.name = providerError;
|
||||||
// We have to move that to the `provider.name` error field so it shows up in the right place.
|
delete errors.app.provider;
|
||||||
if (Array.isArray(parsedError.app?.provider)) {
|
if (Object.keys(errors.app).length === 0) {
|
||||||
const providerError = parsedError.app.provider;
|
delete errors.app;
|
||||||
|
|
||||||
parsedError.provider = {
|
|
||||||
...parsedError.provider,
|
|
||||||
name: providerError,
|
|
||||||
};
|
|
||||||
|
|
||||||
delete parsedError.app.provider;
|
|
||||||
|
|
||||||
if (Object.keys(parsedError.app).length === 0) {
|
|
||||||
delete parsedError.app;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
this.handleUpdate({ errors });
|
||||||
|
this.state = "reviewing";
|
||||||
this.handleUpdate({ errors: parsedError });
|
})
|
||||||
this.state = "reviewing";
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override handleButton(button: WizardButton) {
|
override handleButton(button: WizardButton) {
|
||||||
@ -234,20 +225,22 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderError() {
|
renderError() {
|
||||||
const { errors } = this.wizard;
|
if (Object.keys(this.wizard.errors).length === 0) {
|
||||||
|
return nothing;
|
||||||
if (Object.keys(errors).length === 0) return nothing;
|
}
|
||||||
|
|
||||||
|
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
|
||||||
|
const errors = this.wizard.errors;
|
||||||
return html` <hr class="pf-c-divider" />
|
return html` <hr class="pf-c-divider" />
|
||||||
${match(errors)
|
${match(errors as ExtendedValidationError)
|
||||||
.with(
|
.with(
|
||||||
{ app: P.nonNullable },
|
{ app: P.nonNullable },
|
||||||
() =>
|
() =>
|
||||||
html`<p>${msg("There was an error in the application.")}</p>
|
html`<p>${msg("There was an error in the application.")}</p>
|
||||||
<p>
|
<p>
|
||||||
<a @click=${WizardNavigationEvent.toListener(this, "application")}>
|
<a @click=${navTo("application")}
|
||||||
${msg("Review the application.")}
|
>${msg("Review the application.")}</a
|
||||||
</a>
|
>
|
||||||
</p>`,
|
</p>`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
@ -255,20 +248,13 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
() =>
|
() =>
|
||||||
html`<p>${msg("There was an error in the provider.")}</p>
|
html`<p>${msg("There was an error in the provider.")}</p>
|
||||||
<p>
|
<p>
|
||||||
<a @click=${WizardNavigationEvent.toListener(this, "provider")}
|
<a @click=${navTo("provider")}>${msg("Review the provider.")}</a>
|
||||||
>${msg("Review the provider.")}</a
|
|
||||||
>
|
|
||||||
</p>`,
|
</p>`,
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
{ detail: P.nonNullable },
|
{ detail: P.nonNullable },
|
||||||
() =>
|
() =>
|
||||||
html`<p>
|
`<p>${msg("There was an error. Please go back and review the application.")}: ${errors.detail}</p>`,
|
||||||
${msg(
|
|
||||||
"There was an error. Please go back and review the application.",
|
|
||||||
)}:
|
|
||||||
${errors.detail}
|
|
||||||
</p>`,
|
|
||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
{
|
{
|
||||||
@ -278,7 +264,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
|||||||
html`<p>${msg("There was an error:")}:</p>
|
html`<p>${msg("There was an error:")}:</p>
|
||||||
<ul>
|
<ul>
|
||||||
${(errors.nonFieldErrors ?? []).map(
|
${(errors.nonFieldErrors ?? []).map(
|
||||||
(reason) => html`<li>${reason}</li>`,
|
(e: string) => html`<li>${e}</li>`,
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<p>${msg("Please go back and review the application.")}</p>`,
|
<p>${msg("Please go back and review the application.")}</p>`,
|
||||||
|
@ -9,7 +9,7 @@ import { customElement, state } from "lit/decorators.js";
|
|||||||
import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api";
|
import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api";
|
||||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||||
|
|
||||||
import { ApplicationTransactionValidationError } from "../../types.js";
|
import { ExtendedValidationError } from "../../types.js";
|
||||||
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
|
||||||
|
|
||||||
@customElement("ak-application-wizard-provider-for-oauth")
|
@customElement("ak-application-wizard-provider-for-oauth")
|
||||||
@ -34,7 +34,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(provider: OAuth2Provider, errors: ApplicationTransactionValidationError) {
|
renderForm(provider: OAuth2Provider, errors: ExtendedValidationError) {
|
||||||
const showClientSecretCallback = (show: boolean) => {
|
const showClientSecretCallback = (show: boolean) => {
|
||||||
this.showClientSecret = show;
|
this.showClientSecret = show;
|
||||||
};
|
};
|
||||||
|
@ -25,30 +25,16 @@ export type OneOfProvider =
|
|||||||
|
|
||||||
export type ValidationRecord = { [key: string]: string[] };
|
export type ValidationRecord = { [key: string]: string[] };
|
||||||
|
|
||||||
/**
|
// TODO: Elf, extend this type and apply it to every object in the wizard. Then run
|
||||||
* An error that occurs during the creation or modification of an application.
|
// the type-checker again.
|
||||||
*
|
|
||||||
* @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application.
|
export type ExtendedValidationError = ValidationError & {
|
||||||
*/
|
|
||||||
export interface ApplicationTransactionValidationError extends ValidationError {
|
|
||||||
app?: ValidationRecord;
|
app?: ValidationRecord;
|
||||||
provider?: ValidationRecord;
|
provider?: ValidationRecord;
|
||||||
bindings?: ValidationRecord;
|
bindings?: ValidationRecord;
|
||||||
detail?: unknown;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
}
|
detail?: any;
|
||||||
|
};
|
||||||
/**
|
|
||||||
* Type-guard to determine if an API response is shaped like an {@linkcode ApplicationTransactionValidationError}.
|
|
||||||
*/
|
|
||||||
export function isApplicationTransactionValidationError(
|
|
||||||
error: ValidationError,
|
|
||||||
): error is ApplicationTransactionValidationError {
|
|
||||||
if ("app" in error) return true;
|
|
||||||
if ("provider" in error) return true;
|
|
||||||
if ("bindings" in error) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot
|
// We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot
|
||||||
// in which to preserve the retrieved policy, group, or user object from the SearchSelect used to
|
// in which to preserve the retrieved policy, group, or user object from the SearchSelect used to
|
||||||
@ -63,7 +49,7 @@ export interface ApplicationWizardState {
|
|||||||
proxyMode: ProxyMode;
|
proxyMode: ProxyMode;
|
||||||
bindings: PolicyBinding[];
|
bindings: PolicyBinding[];
|
||||||
currentBinding: number;
|
currentBinding: number;
|
||||||
errors: ValidationError | ApplicationTransactionValidationError;
|
errors: ExtendedValidationError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationWizardStateUpdate {
|
export interface ApplicationWizardStateUpdate {
|
||||||
|
@ -8,7 +8,6 @@ import "@goauthentik/components/ak-event-info";
|
|||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
|
||||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
@ -73,7 +72,7 @@ export class EventListPage extends TablePage<Event> {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
row(item: EventWithContext): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`<div>${actionToLabel(item.action)}</div>
|
html`<div>${actionToLabel(item.action)}</div>
|
||||||
<small>${item.app}</small>`,
|
<small>${item.app}</small>`,
|
||||||
|
@ -1,31 +1,27 @@
|
|||||||
import { EventWithContext } from "@goauthentik/common/events";
|
import { EventWithContext } from "@goauthentik/common/events";
|
||||||
import { truncate } from "@goauthentik/common/utils";
|
import { truncate } from "@goauthentik/common/utils";
|
||||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { html, nothing } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
|
|
||||||
/**
|
export function EventGeo(event: EventWithContext): TemplateResult {
|
||||||
* Given event with a geographical context, format it into a string for display.
|
let geo: KeyUnknown | undefined = undefined;
|
||||||
*/
|
if (Object.hasOwn(event.context, "geo")) {
|
||||||
export function EventGeo(event: EventWithContext): SlottedTemplateResult {
|
geo = event.context.geo as KeyUnknown;
|
||||||
if (!event.context.geo) return nothing;
|
const parts = [geo.city, geo.country, geo.continent].filter(
|
||||||
|
(v) => v !== "" && v !== undefined,
|
||||||
const { city, country, continent } = event.context.geo;
|
);
|
||||||
|
return html`${parts.join(", ")}`;
|
||||||
const parts = [city, country, continent].filter(Boolean);
|
}
|
||||||
|
return html``;
|
||||||
return html`${parts.join(", ")}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventUser(
|
export function EventUser(event: EventWithContext, truncateUsername?: number): TemplateResult {
|
||||||
event: EventWithContext,
|
if (!event.user.username) {
|
||||||
truncateUsername?: number,
|
return html`-`;
|
||||||
): SlottedTemplateResult {
|
}
|
||||||
if (!event.user.username) return html`-`;
|
let body = html``;
|
||||||
|
|
||||||
let body: SlottedTemplateResult = nothing;
|
|
||||||
|
|
||||||
if (event.user.is_anonymous) {
|
if (event.user.is_anonymous) {
|
||||||
body = html`<div>${msg("Anonymous user")}</div>`;
|
body = html`<div>${msg("Anonymous user")}</div>`;
|
||||||
} else {
|
} else {
|
||||||
@ -37,14 +33,12 @@ export function EventUser(
|
|||||||
>
|
>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.user.on_behalf_of) {
|
if (event.user.on_behalf_of) {
|
||||||
return html`${body}<small>
|
body = html`${body}<small>
|
||||||
<a href="#/identity/users/${event.user.on_behalf_of.pk}"
|
<a href="#/identity/users/${event.user.on_behalf_of.pk}"
|
||||||
>${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a
|
>${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a
|
||||||
>
|
>
|
||||||
</small>`;
|
</small>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import "@goauthentik/elements/events/LogViewer";
|
import "@goauthentik/elements/events/LogViewer";
|
||||||
import { Form } from "@goauthentik/elements/forms/Form";
|
import { Form } from "@goauthentik/elements/forms/Form";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import "@goauthentik/admin/flows/FlowForm";
|
import "@goauthentik/admin/flows/FlowForm";
|
||||||
import "@goauthentik/admin/flows/FlowImportForm";
|
import "@goauthentik/admin/flows/FlowImportForm";
|
||||||
import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
|
import { DesignationToLabel, formatFlowURL } from "@goauthentik/admin/flows/utils";
|
||||||
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { groupBy } from "@goauthentik/common/utils";
|
import { groupBy } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||||
@ -107,10 +107,9 @@ export class FlowListPage extends TablePage<Flow> {
|
|||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-plain"
|
class="pf-c-button pf-m-plain"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
const finalURL = `${window.location.origin}/if/flow/${item.slug}/${AndNext(
|
const url = formatFlowURL(item);
|
||||||
`${window.location.pathname}#${window.location.hash}`,
|
|
||||||
)}`;
|
window.open(url, "_blank");
|
||||||
window.open(finalURL, "_blank");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pf-tooltip position="top" content=${msg("Execute")}>
|
<pf-tooltip position="top" content=${msg("Execute")}>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import "@goauthentik/admin/flows/BoundStagesList";
|
import "@goauthentik/admin/flows/BoundStagesList";
|
||||||
import "@goauthentik/admin/flows/FlowDiagram";
|
import "@goauthentik/admin/flows/FlowDiagram";
|
||||||
import "@goauthentik/admin/flows/FlowForm";
|
import "@goauthentik/admin/flows/FlowForm";
|
||||||
import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
|
import { DesignationToLabel, applyNextParam, formatFlowURL } from "@goauthentik/admin/flows/utils";
|
||||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||||
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
||||||
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
|
|
||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/PageHeader";
|
import "@goauthentik/elements/PageHeader";
|
||||||
@ -24,7 +23,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
|||||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import { Flow, FlowsApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
|
import {
|
||||||
|
Flow,
|
||||||
|
FlowsApi,
|
||||||
|
RbacPermissionsAssignedByUsersListModelEnum,
|
||||||
|
ResponseError,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-flow-view")
|
@customElement("ak-flow-view")
|
||||||
export class FlowViewPage extends AKElement {
|
export class FlowViewPage extends AKElement {
|
||||||
@ -147,12 +151,9 @@ export class FlowViewPage extends AKElement {
|
|||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-block pf-m-primary"
|
class="pf-c-button pf-m-block pf-m-primary"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
const finalURL = `${
|
const url = formatFlowURL(this.flow);
|
||||||
window.location.origin
|
|
||||||
}/if/flow/${this.flow.slug}/${AndNext(
|
window.open(url, "_blank");
|
||||||
`${window.location.pathname}#${window.location.hash}`,
|
|
||||||
)}`;
|
|
||||||
window.open(finalURL, "_blank");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${msg("Normal")}
|
${msg("Normal")}
|
||||||
@ -164,12 +165,16 @@ export class FlowViewPage extends AKElement {
|
|||||||
.flowsInstancesExecuteRetrieve({
|
.flowsInstancesExecuteRetrieve({
|
||||||
slug: this.flow.slug,
|
slug: this.flow.slug,
|
||||||
})
|
})
|
||||||
.then((link) => {
|
.then(({ link }) => {
|
||||||
const finalURL = `${
|
const finalURL = URL.canParse(link)
|
||||||
link.link
|
? new URL(link)
|
||||||
}${AndNext(
|
: new URL(
|
||||||
`${window.location.pathname}#${window.location.hash}`,
|
link,
|
||||||
)}`;
|
window.location.origin,
|
||||||
|
);
|
||||||
|
|
||||||
|
applyNextParam(finalURL);
|
||||||
|
|
||||||
window.open(finalURL, "_blank");
|
window.open(finalURL, "_blank");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -191,15 +196,13 @@ export class FlowViewPage extends AKElement {
|
|||||||
)}`;
|
)}`;
|
||||||
window.open(finalURL, "_blank");
|
window.open(finalURL, "_blank");
|
||||||
})
|
})
|
||||||
.catch(async (error: unknown) => {
|
.catch((exc: ResponseError) => {
|
||||||
if (isResponseErrorLike(error)) {
|
// This request can return a HTTP 400 when a flow
|
||||||
// This request can return a HTTP 400 when a flow
|
// is not applicable.
|
||||||
// is not applicable.
|
window.open(
|
||||||
window.open(
|
exc.response.url,
|
||||||
error.response.url,
|
"_blank",
|
||||||
"_blank",
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -43,3 +43,51 @@ export function LayoutToLabel(layout: FlowLayoutEnum): string {
|
|||||||
return msg("Unknown layout");
|
return msg("Unknown layout");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the next URL as a query parameter to the given URL or URLSearchParams object.
|
||||||
|
*
|
||||||
|
* @todo deprecate this once hash routing is removed.
|
||||||
|
*/
|
||||||
|
export function applyNextParam(
|
||||||
|
target: URL | URLSearchParams,
|
||||||
|
destination: string | URL = window.location.pathname + "#" + window.location.hash,
|
||||||
|
): void {
|
||||||
|
const searchParams = target instanceof URL ? target.searchParams : target;
|
||||||
|
|
||||||
|
searchParams.set("next", destination.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a URLSearchParams object with the next URL as a query parameter.
|
||||||
|
*
|
||||||
|
* @todo deprecate this once hash routing is removed.
|
||||||
|
*/
|
||||||
|
export function createNextSearchParams(
|
||||||
|
destination: string | URL = window.location.pathname + "#" + window.location.hash,
|
||||||
|
): URLSearchParams {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
applyNextParam(searchParams, destination);
|
||||||
|
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a URL to a flow, with the next URL as a query parameter.
|
||||||
|
*
|
||||||
|
* @param flow The flow to create the URL for.
|
||||||
|
* @param destination The next URL to redirect to after the flow is completed, `true` to use the current route.
|
||||||
|
*/
|
||||||
|
export function formatFlowURL(
|
||||||
|
flow: Flow,
|
||||||
|
destination: string | URL | null = window.location.pathname + "#" + window.location.hash,
|
||||||
|
): URL {
|
||||||
|
const url = new URL(`/if/flow/${flow.slug}/`, window.location.origin);
|
||||||
|
|
||||||
|
if (destination) {
|
||||||
|
applyNextParam(url, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
@ -6,7 +6,6 @@ import "@goauthentik/admin/users/UserPasswordForm";
|
|||||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
|
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
@ -23,7 +22,7 @@ import { Form } from "@goauthentik/elements/forms/Form";
|
|||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { UserOption } from "@goauthentik/elements/user/utils";
|
import { UserOption } from "@goauthentik/elements/user/utils";
|
||||||
@ -38,7 +37,14 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
|||||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api";
|
import {
|
||||||
|
CoreApi,
|
||||||
|
CoreUsersListTypeEnum,
|
||||||
|
Group,
|
||||||
|
ResponseError,
|
||||||
|
SessionUser,
|
||||||
|
User,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-user-related-add")
|
@customElement("ak-user-related-add")
|
||||||
export class RelatedUserAdd extends Form<{ users: number[] }> {
|
export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||||
@ -121,13 +127,13 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
order = "last_login";
|
order = "last_login";
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
|
hideServiceAccounts = getRouteParameter<boolean>("hideServiceAccounts", true);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
me?: SessionUser;
|
me?: SessionUser;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return Table.styles.concat(PFDescriptionList, PFAlert, PFBanner);
|
return super.styles.concat(PFDescriptionList, PFAlert, PFBanner);
|
||||||
}
|
}
|
||||||
|
|
||||||
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||||
@ -313,16 +319,14 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
description: rec.link,
|
description: rec.link,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(async (error: unknown) => {
|
.catch((ex: ResponseError) => {
|
||||||
const parsedError =
|
ex.response.json().then(() => {
|
||||||
await parseAPIResponseError(
|
showMessage({
|
||||||
error,
|
level: MessageLevel.error,
|
||||||
);
|
message: msg(
|
||||||
|
"No recovery flow is configured.",
|
||||||
showMessage({
|
),
|
||||||
level: MessageLevel.error,
|
});
|
||||||
message:
|
|
||||||
pluckErrorDetail(parsedError),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -462,7 +466,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
this.hideServiceAccounts = !this.hideServiceAccounts;
|
this.hideServiceAccounts = !this.hideServiceAccounts;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
updateURLParams({
|
patchRouteParams({
|
||||||
hideServiceAccounts: this.hideServiceAccounts,
|
hideServiceAccounts: this.hideServiceAccounts,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -19,7 +19,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import "@goauthentik/elements/forms/ProxyForm";
|
import "@goauthentik/elements/forms/ProxyForm";
|
||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -54,7 +54,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
|||||||
order = "name";
|
order = "name";
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
hideManaged = getURLParam<boolean>("hideManaged", true);
|
hideManaged = getRouteParameter<boolean>("hideManaged", true);
|
||||||
|
|
||||||
async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
|
async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
|
||||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({
|
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({
|
||||||
@ -148,7 +148,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
|||||||
this.hideManaged = !this.hideManaged;
|
this.hideManaged = !this.hideManaged;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
updateURLParams({
|
patchRouteParams({
|
||||||
hideManaged: this.hideManaged,
|
hideManaged: this.hideManaged,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -3,7 +3,6 @@ import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
|
|||||||
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { convertToSlug } from "@goauthentik/common/utils";
|
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import MDCaddyStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_caddy_standalone.md";
|
import MDCaddyStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_caddy_standalone.md";
|
||||||
@ -21,7 +20,8 @@ import "@goauthentik/elements/ak-mdx";
|
|||||||
import type { Replacer } from "@goauthentik/elements/ak-mdx";
|
import type { Replacer } from "@goauthentik/elements/ak-mdx";
|
||||||
import "@goauthentik/elements/buttons/ModalButton";
|
import "@goauthentik/elements/buttons/ModalButton";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
import { formatAsSlug } from "@goauthentik/elements/router";
|
||||||
|
import { getRouteParameter } from "@goauthentik/elements/router/utils";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
|
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
|
||||||
@ -156,7 +156,7 @@ export class ProxyProviderViewPage extends AKElement {
|
|||||||
(input: string): string => {
|
(input: string): string => {
|
||||||
// The generated config is pretty unreliable currently so
|
// The generated config is pretty unreliable currently so
|
||||||
// put it behind a flag
|
// put it behind a flag
|
||||||
if (!getURLParam("generatedConfig", false)) {
|
if (!getRouteParameter("generatedConfig", false)) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
if (!this.provider) {
|
if (!this.provider) {
|
||||||
@ -183,7 +183,7 @@ export class ProxyProviderViewPage extends AKElement {
|
|||||||
return html`<ak-tabs pageIdentifier="proxy-setup">
|
return html`<ak-tabs pageIdentifier="proxy-setup">
|
||||||
${servers.map((server) => {
|
${servers.map((server) => {
|
||||||
return html`<section
|
return html`<section
|
||||||
slot="page-${convertToSlug(server.label)}"
|
slot="page-${formatAsSlug(server.label)}"
|
||||||
data-tab-title="${server.label}"
|
data-tab-title="${server.label}"
|
||||||
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
|
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
|
||||||
>
|
>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
import { Form } from "@goauthentik/elements/forms/Form";
|
import { Form } from "@goauthentik/elements/forms/Form";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/SearchSelect";
|
import "@goauthentik/elements/forms/SearchSelect";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
|
import { parseAPIError } from "@goauthentik/common/errors";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||||
@ -21,8 +21,9 @@ import {
|
|||||||
Prompt,
|
Prompt,
|
||||||
PromptChallenge,
|
PromptChallenge,
|
||||||
PromptTypeEnum,
|
PromptTypeEnum,
|
||||||
|
ResponseError,
|
||||||
StagesApi,
|
StagesApi,
|
||||||
instanceOfValidationError,
|
ValidationError,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
class PreviewStageHost implements StageHost {
|
class PreviewStageHost implements StageHost {
|
||||||
@ -77,22 +78,15 @@ export class PromptForm extends ModelForm<Prompt, string> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
return new StagesApi(DEFAULT_CONFIG)
|
this.preview = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsPreviewCreate({
|
||||||
.stagesPromptPromptsPreviewCreate({
|
|
||||||
promptRequest: prompt,
|
promptRequest: prompt,
|
||||||
})
|
|
||||||
.then((nextPreview) => {
|
|
||||||
this.preview = nextPreview;
|
|
||||||
this.previewError = undefined;
|
|
||||||
})
|
|
||||||
.catch(async (error: unknown) => {
|
|
||||||
const parsedError = await parseAPIResponseError(error);
|
|
||||||
|
|
||||||
this.previewError = instanceOfValidationError(parsedError)
|
|
||||||
? parsedError.nonFieldErrors
|
|
||||||
: [pluckErrorDetail(parsedError, msg("Failed to preview prompt"))];
|
|
||||||
});
|
});
|
||||||
|
this.previewError = undefined;
|
||||||
|
} catch (exc) {
|
||||||
|
const errorMessage = parseAPIError(exc as ResponseError);
|
||||||
|
this.previewError = (errorMessage as ValidationError).nonFieldErrors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuccessMessage(): string {
|
getSuccessMessage(): string {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
|
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import { DeleteForm } from "@goauthentik/elements/forms/DeleteForm";
|
import { DeleteForm } from "@goauthentik/elements/forms/DeleteForm";
|
||||||
@ -17,14 +16,10 @@ export class UserActiveForm extends DeleteForm {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error: unknown): Promise<void> {
|
onError(e: Error): void {
|
||||||
return parseAPIResponseError(error).then((parsedError) => {
|
showMessage({
|
||||||
showMessage({
|
message: msg(str`Failed to update ${this.objectLabel}: ${e.toString()}`),
|
||||||
message: msg(
|
level: MessageLevel.error,
|
||||||
str`Failed to update ${this.objectLabel}: ${pluckErrorDetail(parsedError)}`,
|
|
||||||
),
|
|
||||||
level: MessageLevel.error,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
import { deviceTypeName } from "@goauthentik/common/labels";
|
import { deviceTypeName } from "@goauthentik/common/labels";
|
||||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
|
||||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
|
@ -7,10 +7,9 @@ import "@goauthentik/admin/users/UserPasswordForm";
|
|||||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import { parseAPIResponseError } from "@goauthentik/common/errors/network";
|
|
||||||
import { userTypeToLabel } from "@goauthentik/common/labels";
|
import { userTypeToLabel } from "@goauthentik/common/labels";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
import { createUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
@ -24,8 +23,8 @@ import "@goauthentik/elements/TreeView";
|
|||||||
import "@goauthentik/elements/buttons/ActionButton";
|
import "@goauthentik/elements/buttons/ActionButton";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { showAPIErrorMessage, showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
import { getRouteParameter, patchRouteParams } from "@goauthentik/elements/router/utils";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -40,7 +39,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
|||||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
import { CoreApi, SessionUser, User, UserPath } from "@goauthentik/api";
|
import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api";
|
||||||
|
|
||||||
export const requestRecoveryLink = (user: User) =>
|
export const requestRecoveryLink = (user: User) =>
|
||||||
new CoreApi(DEFAULT_CONFIG)
|
new CoreApi(DEFAULT_CONFIG)
|
||||||
@ -58,7 +57,16 @@ export const requestRecoveryLink = (user: User) =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.catch((error: unknown) => parseAPIResponseError(error).then(showAPIErrorMessage));
|
.catch((ex: ResponseError) =>
|
||||||
|
ex.response.json().then(() =>
|
||||||
|
showMessage({
|
||||||
|
level: MessageLevel.error,
|
||||||
|
message: msg(
|
||||||
|
"The current brand must have a recovery flow configured to use a recovery link",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const renderRecoveryEmailRequest = (user: User) =>
|
export const renderRecoveryEmailRequest = (user: User) =>
|
||||||
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
|
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
|
||||||
@ -109,7 +117,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
activePath;
|
activePath;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
|
hideDeactivated = getRouteParameter<boolean>("hideDeactivated", false);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
userPaths?: UserPath;
|
userPaths?: UserPath;
|
||||||
@ -118,13 +126,15 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
me?: SessionUser;
|
me?: SessionUser;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [...TablePage.styles, PFDescriptionList, PFCard, PFAlert, recoveryButtonStyles];
|
return [...super.styles, PFDescriptionList, PFCard, PFAlert, recoveryButtonStyles];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const defaultPath = new DefaultUIConfig().defaults.userPath;
|
|
||||||
this.activePath = getURLParam<string>("path", defaultPath);
|
const defaultPath = createUIConfig().defaults.userPath;
|
||||||
|
this.activePath = getRouteParameter("path", defaultPath);
|
||||||
|
|
||||||
uiConfig().then((c) => {
|
uiConfig().then((c) => {
|
||||||
if (c.defaults.userPath !== defaultPath) {
|
if (c.defaults.userPath !== defaultPath) {
|
||||||
this.activePath = c.defaults.userPath;
|
this.activePath = c.defaults.userPath;
|
||||||
@ -135,7 +145,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
|
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
|
||||||
...(await this.defaultEndpointConfig()),
|
...(await this.defaultEndpointConfig()),
|
||||||
pathStartswith: getURLParam("path", ""),
|
pathStartswith: getRouteParameter("path", ""),
|
||||||
isActive: this.hideDeactivated ? true : undefined,
|
isActive: this.hideDeactivated ? true : undefined,
|
||||||
includeGroups: false,
|
includeGroups: false,
|
||||||
});
|
});
|
||||||
@ -217,7 +227,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
this.hideDeactivated = !this.hideDeactivated;
|
this.hideDeactivated = !this.hideDeactivated;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
updateURLParams({
|
patchRouteParams({
|
||||||
hideDeactivated: this.hideDeactivated,
|
hideDeactivated: this.hideDeactivated,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -79,11 +79,4 @@ export const DEFAULT_CONFIG = new Configuration({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is just a function so eslint doesn't complain about
|
|
||||||
// missing-whitespace-between-attributes or
|
|
||||||
// unexpected-character-in-attribute-name
|
|
||||||
export function AndNext(url: string): string {
|
|
||||||
return `?next=${encodeURIComponent(url)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
|
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
|
||||||
import { getCookie } from "@goauthentik/common/utils";
|
import { getCookie } from "@goauthentik/common/http";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CurrentBrand,
|
CurrentBrand,
|
||||||
|
@ -1,170 +1,60 @@
|
|||||||
/**
|
/**
|
||||||
* @file
|
* @file Client-side utilities.
|
||||||
* Client-side observer for ESBuild events.
|
|
||||||
*/
|
*/
|
||||||
import type { Message as ESBuildMessage } from "esbuild";
|
import { TITLE_DEFAULT } from "@goauthentik/common/constants";
|
||||||
|
import { isAdminRoute } from "@goauthentik/elements/router";
|
||||||
|
|
||||||
const logPrefix = "👷 [ESBuild]";
|
import { msg } from "@lit/localize";
|
||||||
const log = console.debug.bind(console, logPrefix);
|
|
||||||
|
|
||||||
type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void;
|
import type { CurrentBrand } from "@goauthentik/api";
|
||||||
|
|
||||||
|
type BrandTitleLike = Partial<Pick<CurrentBrand, "brandingTitle">>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client-side watcher for ESBuild.
|
* Create a title for the page.
|
||||||
*
|
*
|
||||||
* Note that this should be conditionally imported in your code, so that
|
* @param brand - The brand object to append to the title.
|
||||||
* ESBuild may tree-shake it out of production builds.
|
* @param segments - The segments to prepend to the title.
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
|
|
||||||
* const { ESBuildObserver } = await import("@goauthentik/common/client");
|
|
||||||
*
|
|
||||||
* new ESBuildObserver(process.env.WATCHER_URL);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export class ESBuildObserver extends EventSource {
|
export function formatPageTitle(
|
||||||
/**
|
brand: BrandTitleLike | undefined,
|
||||||
* Whether the watcher has a recent connection to the server.
|
...segments: Array<string | undefined>
|
||||||
*/
|
): string;
|
||||||
alive = true;
|
/**
|
||||||
|
* Create a title for the page.
|
||||||
|
*
|
||||||
|
* @param segments - The segments to prepend to the title.
|
||||||
|
*/
|
||||||
|
export function formatPageTitle(...segments: Array<string | undefined>): string;
|
||||||
|
/**
|
||||||
|
* Create a title for the page.
|
||||||
|
*
|
||||||
|
* @param args - The segments to prepend to the title.
|
||||||
|
* @param args - The brand object to append to the title.
|
||||||
|
*/
|
||||||
|
export function formatPageTitle(
|
||||||
|
...args: [BrandTitleLike | string | undefined, ...Array<string | undefined>]
|
||||||
|
): string {
|
||||||
|
const segments: string[] = [];
|
||||||
|
|
||||||
/**
|
if (isAdminRoute()) {
|
||||||
* The number of errors that have occurred since the watcher started.
|
segments.push(msg("Admin"));
|
||||||
*/
|
|
||||||
errorCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a reload has been requested while offline.
|
|
||||||
*/
|
|
||||||
deferredReload = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last time a message was received from the server.
|
|
||||||
*/
|
|
||||||
lastUpdatedAt = Date.now();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the browser considers itself online.
|
|
||||||
*/
|
|
||||||
online = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID of the animation frame for the reload.
|
|
||||||
*/
|
|
||||||
#reloadFrameID = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interval for the keep-alive check.
|
|
||||||
*/
|
|
||||||
#keepAliveInterval: ReturnType<typeof setInterval> | undefined;
|
|
||||||
|
|
||||||
#trackActivity = () => {
|
|
||||||
this.lastUpdatedAt = Date.now();
|
|
||||||
this.alive = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
#startListener: BuildEventListener = () => {
|
|
||||||
this.#trackActivity();
|
|
||||||
log("⏰ Build started...");
|
|
||||||
};
|
|
||||||
|
|
||||||
#internalErrorListener = () => {
|
|
||||||
this.errorCount += 1;
|
|
||||||
|
|
||||||
if (this.errorCount > 100) {
|
|
||||||
clearTimeout(this.#keepAliveInterval);
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
log("⛔️ Closing connection");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#errorListener: BuildEventListener<string> = (event) => {
|
|
||||||
this.#trackActivity();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.group(logPrefix, "⛔️⛔️⛔️ Build error...");
|
|
||||||
|
|
||||||
const esbuildErrorMessages: ESBuildMessage[] = JSON.parse(event.data);
|
|
||||||
|
|
||||||
for (const error of esbuildErrorMessages) {
|
|
||||||
console.warn(error.text);
|
|
||||||
|
|
||||||
if (error.location) {
|
|
||||||
console.debug(
|
|
||||||
`file://${error.location.file}:${error.location.line}:${error.location.column}`,
|
|
||||||
);
|
|
||||||
console.debug(error.location.lineText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.groupEnd();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endListener: BuildEventListener = () => {
|
|
||||||
cancelAnimationFrame(this.#reloadFrameID);
|
|
||||||
|
|
||||||
this.#trackActivity();
|
|
||||||
|
|
||||||
if (!this.online) {
|
|
||||||
log("🚫 Build finished while offline.");
|
|
||||||
this.deferredReload = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log("🛎️ Build completed! Reloading...");
|
|
||||||
|
|
||||||
// We use an animation frame to keep the reload from happening before the
|
|
||||||
// event loop has a chance to process the message.
|
|
||||||
this.#reloadFrameID = requestAnimationFrame(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
#keepAliveListener: BuildEventListener = () => {
|
|
||||||
this.#trackActivity();
|
|
||||||
log("🏓 Keep-alive");
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(url: string | URL) {
|
|
||||||
super(url);
|
|
||||||
|
|
||||||
this.addEventListener("esbuild:start", this.#startListener);
|
|
||||||
this.addEventListener("esbuild:end", this.#endListener);
|
|
||||||
this.addEventListener("esbuild:error", this.#errorListener);
|
|
||||||
this.addEventListener("esbuild:keep-alive", this.#keepAliveListener);
|
|
||||||
|
|
||||||
this.addEventListener("error", this.#internalErrorListener);
|
|
||||||
|
|
||||||
window.addEventListener("offline", () => {
|
|
||||||
this.online = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("online", () => {
|
|
||||||
this.online = true;
|
|
||||||
|
|
||||||
if (!this.deferredReload) return;
|
|
||||||
|
|
||||||
log("🛎️ Reloading after offline build...");
|
|
||||||
this.deferredReload = false;
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
log("🛎️ Listening for build changes...");
|
|
||||||
|
|
||||||
this.#keepAliveInterval = setInterval(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (now - this.lastUpdatedAt < 10_000) return;
|
|
||||||
|
|
||||||
this.alive = false;
|
|
||||||
log("👋 Waiting for build to start...");
|
|
||||||
}, 15_000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [arg1, ...rest] = args;
|
||||||
|
|
||||||
|
if (typeof arg1 === "object") {
|
||||||
|
const { brandingTitle = TITLE_DEFAULT } = arg1;
|
||||||
|
segments.push(brandingTitle);
|
||||||
|
} else {
|
||||||
|
segments.push(TITLE_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const segment of rest) {
|
||||||
|
if (segment) {
|
||||||
|
segments.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.join(" - ");
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ export const PROGRESS_CLASS = "pf-m-in-progress";
|
|||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2025.2.3";
|
export const VERSION = "2025.2.3";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
|
||||||
|
|
||||||
export const EVENT_REFRESH = "ak-refresh";
|
export const EVENT_REFRESH = "ak-refresh";
|
||||||
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
|
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
|
||||||
|
36
web/src/common/errors.ts
Normal file
36
web/src/common/errors.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
GenericError,
|
||||||
|
GenericErrorFromJSON,
|
||||||
|
ResponseError,
|
||||||
|
ValidationError,
|
||||||
|
ValidationErrorFromJSON,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
export class SentryIgnoredError extends Error {}
|
||||||
|
export class NotFoundError extends Error {}
|
||||||
|
export class RequestError extends Error {}
|
||||||
|
|
||||||
|
export type APIErrorTypes = ValidationError | GenericError;
|
||||||
|
|
||||||
|
export const HTTP_BAD_REQUEST = 400;
|
||||||
|
export const HTTP_INTERNAL_SERVICE_ERROR = 500;
|
||||||
|
|
||||||
|
export async function parseAPIError(error: Error): Promise<APIErrorTypes> {
|
||||||
|
if (!(error instanceof ResponseError)) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
error.response.status < HTTP_BAD_REQUEST ||
|
||||||
|
error.response.status >= HTTP_INTERNAL_SERVICE_ERROR
|
||||||
|
) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
const body = await error.response.json();
|
||||||
|
if (error.response.status === 400) {
|
||||||
|
return ValidationErrorFromJSON(body);
|
||||||
|
}
|
||||||
|
if (error.response.status === 403) {
|
||||||
|
return GenericErrorFromJSON(body);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
@ -1,184 +0,0 @@
|
|||||||
import {
|
|
||||||
GenericError,
|
|
||||||
GenericErrorFromJSON,
|
|
||||||
ResponseError,
|
|
||||||
ValidationError,
|
|
||||||
ValidationErrorFromJSON,
|
|
||||||
} from "@goauthentik/api";
|
|
||||||
|
|
||||||
//#region HTTP
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common HTTP status names used in the API and their corresponding codes.
|
|
||||||
*/
|
|
||||||
export const HTTPStatusCode = {
|
|
||||||
BadRequest: 400,
|
|
||||||
Forbidden: 403,
|
|
||||||
InternalServiceError: 500,
|
|
||||||
} as const satisfies Record<string, number>;
|
|
||||||
|
|
||||||
export type HTTPStatusCode = (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode];
|
|
||||||
|
|
||||||
export type HTTPErrorJSONTransformer<T = unknown> = (json: T) => APIError;
|
|
||||||
|
|
||||||
export const HTTPStatusCodeTransformer: Record<number, HTTPErrorJSONTransformer> = {
|
|
||||||
[HTTPStatusCode.BadRequest]: ValidationErrorFromJSON,
|
|
||||||
[HTTPStatusCode.Forbidden]: GenericErrorFromJSON,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to check if a response contains a JSON body.
|
|
||||||
*
|
|
||||||
* This is useful to guard against parsing errors when attempting to read the response body.
|
|
||||||
*/
|
|
||||||
export function isJSONResponse(response: Response): boolean {
|
|
||||||
return Boolean(response.headers.get("content-type")?.includes("application/json"));
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region API
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An API response error, typically derived from a {@linkcode Response} body.
|
|
||||||
*
|
|
||||||
* @see {@linkcode parseAPIResponseError}
|
|
||||||
*/
|
|
||||||
export type APIError = ValidationError | GenericError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an error-like object, attempts to normalize it into a {@linkcode GenericError}
|
|
||||||
* suitable for display to the user.
|
|
||||||
*/
|
|
||||||
export function createSyntheticGenericError(detail?: string): GenericError {
|
|
||||||
const syntheticGenericError: GenericError = {
|
|
||||||
detail: detail || ResponseErrorMessages[HTTPStatusCode.InternalServiceError].reason,
|
|
||||||
};
|
|
||||||
|
|
||||||
return syntheticGenericError;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error that contains a native response object.
|
|
||||||
*
|
|
||||||
* @see {@linkcode isResponseErrorLike} to determine if an error contains a response object.
|
|
||||||
*/
|
|
||||||
export type APIErrorWithResponse = Pick<ResponseError, "response" | "message">;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to check if an error contains a HTTP {@linkcode Response} object.
|
|
||||||
*
|
|
||||||
* @see {@linkcode parseAPIResponseError} to parse the response body into a {@linkcode APIError}.
|
|
||||||
*/
|
|
||||||
export function isResponseErrorLike(errorLike: unknown): errorLike is APIErrorWithResponse {
|
|
||||||
if (!errorLike || typeof errorLike !== "object") return false;
|
|
||||||
|
|
||||||
return "response" in errorLike && errorLike.response instanceof Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A descriptor to provide a human readable error message for a given HTTP status code.
|
|
||||||
*
|
|
||||||
* @see {@linkcode ResponseErrorMessages} for a list of fallback error messages.
|
|
||||||
*/
|
|
||||||
interface ResponseErrorDescriptor {
|
|
||||||
headline: string;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback error messages for HTTP status codes used when a more specific error message is not available in the response.
|
|
||||||
*/
|
|
||||||
export const ResponseErrorMessages: Record<number, ResponseErrorDescriptor> = {
|
|
||||||
[HTTPStatusCode.BadRequest]: {
|
|
||||||
headline: "Bad request",
|
|
||||||
reason: "The server did not understand the request",
|
|
||||||
},
|
|
||||||
[HTTPStatusCode.InternalServiceError]: {
|
|
||||||
headline: "Internal server error",
|
|
||||||
reason: "An unexpected error occurred",
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composes a human readable error message from a {@linkcode ResponseErrorDescriptor}.
|
|
||||||
*
|
|
||||||
* Note that this is kept separate from localization to lower the complexity of the error handling code.
|
|
||||||
*/
|
|
||||||
export function composeResponseErrorDescriptor(descriptor: ResponseErrorDescriptor): string {
|
|
||||||
return `${descriptor.headline}: ${descriptor.reason}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to pluck a human readable error message from a {@linkcode ValidationError}.
|
|
||||||
*/
|
|
||||||
export function pluckErrorDetail(validationError: ValidationError, fallback?: string): string;
|
|
||||||
/**
|
|
||||||
* Attempts to pluck a human readable error message from a {@linkcode GenericError}.
|
|
||||||
*/
|
|
||||||
export function pluckErrorDetail(genericError: GenericError, fallback?: string): string;
|
|
||||||
/**
|
|
||||||
* Attempts to pluck a human readable error message from an `Error` object.
|
|
||||||
*/
|
|
||||||
export function pluckErrorDetail(error: Error, fallback?: string): string;
|
|
||||||
/**
|
|
||||||
* Attempts to pluck a human readable error message from an error-like object.
|
|
||||||
*
|
|
||||||
* Prioritizes the `detail` key, then the `message` key.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function pluckErrorDetail(errorLike: unknown, fallback?: string): string;
|
|
||||||
export function pluckErrorDetail(errorLike: unknown, fallback?: string): string {
|
|
||||||
fallback ||= composeResponseErrorDescriptor(
|
|
||||||
ResponseErrorMessages[HTTPStatusCode.InternalServiceError],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!errorLike || typeof errorLike !== "object") {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("detail" in errorLike && typeof errorLike.detail === "string") {
|
|
||||||
return errorLike.detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("message" in errorLike && typeof errorLike.message === "string") {
|
|
||||||
return errorLike.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given API error, parses the response body and transforms it into a {@linkcode APIError}.
|
|
||||||
*/
|
|
||||||
export async function parseAPIResponseError<T extends APIError = APIError>(
|
|
||||||
error: unknown,
|
|
||||||
): Promise<T> {
|
|
||||||
if (!isResponseErrorLike(error)) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
return createSyntheticGenericError(message) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { response, message } = error;
|
|
||||||
|
|
||||||
if (!isJSONResponse(response)) {
|
|
||||||
return createSyntheticGenericError(message || response.statusText) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
.json()
|
|
||||||
.then((body) => {
|
|
||||||
const transformer = HTTPStatusCodeTransformer[response.status];
|
|
||||||
const transformedBody = transformer ? transformer(body) : body;
|
|
||||||
|
|
||||||
return transformedBody as unknown as T;
|
|
||||||
})
|
|
||||||
.catch((transformerError: unknown) => {
|
|
||||||
console.error("Failed to parse response error body", transformerError);
|
|
||||||
|
|
||||||
return createSyntheticGenericError(message || response.statusText) as T;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
@ -8,10 +8,13 @@ export interface EventUser {
|
|||||||
is_anonymous?: boolean;
|
is_anonymous?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventGeo {
|
export interface EventContext {
|
||||||
city?: string;
|
[key: string]: EventContext | EventModel | string | number | string[];
|
||||||
country?: string;
|
}
|
||||||
continent?: string;
|
|
||||||
|
export interface EventWithContext extends Event {
|
||||||
|
user: EventUser;
|
||||||
|
context: EventContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventModel {
|
export interface EventModel {
|
||||||
@ -25,16 +28,3 @@ export interface EventRequest {
|
|||||||
path: string;
|
path: string;
|
||||||
method: string;
|
method: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventContextProperty = EventModel | EventGeo | string | number | string[] | undefined;
|
|
||||||
|
|
||||||
// TODO: Events should have more specific types.
|
|
||||||
export interface EventContext {
|
|
||||||
[key: string]: EventContext | EventContextProperty;
|
|
||||||
geo?: EventGeo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventWithContext extends Event {
|
|
||||||
user: EventUser;
|
|
||||||
context: EventContext;
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { VERSION } from "@goauthentik/common/constants";
|
import { VERSION } from "@goauthentik/common/constants";
|
||||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
|
|
||||||
export interface PlexPinResponse {
|
export interface PlexPinResponse {
|
||||||
// Only has the fields we care about
|
// Only has the fields we care about
|
||||||
|
145
web/src/common/helpers/webauthn.ts
Normal file
145
web/src/common/helpers/webauthn.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import * as base64js from "base64-js";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
|
||||||
|
export function b64enc(buf: Uint8Array): string {
|
||||||
|
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function b64RawEnc(buf: Uint8Array): string {
|
||||||
|
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function u8arr(input: string): Uint8Array {
|
||||||
|
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkWebAuthnSupport() {
|
||||||
|
if ("credentials" in navigator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
||||||
|
throw new Error(msg("WebAuthn requires this page to be accessed via HTTPS."));
|
||||||
|
}
|
||||||
|
throw new Error(msg("WebAuthn not supported by browser."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms items in the credentialCreateOptions generated on the server
|
||||||
|
* into byte arrays expected by the navigator.credentials.create() call
|
||||||
|
*/
|
||||||
|
export function transformCredentialCreateOptions(
|
||||||
|
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
userId: string,
|
||||||
|
): PublicKeyCredentialCreationOptions {
|
||||||
|
const user = credentialCreateOptions.user;
|
||||||
|
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||||
|
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||||
|
// string, then a byte array, re-encode it and wrap that in an array.
|
||||||
|
const stringId = decodeURIComponent(window.atob(userId));
|
||||||
|
user.id = u8arr(b64enc(u8arr(stringId)));
|
||||||
|
const challenge = u8arr(credentialCreateOptions.challenge.toString());
|
||||||
|
|
||||||
|
return {
|
||||||
|
...credentialCreateOptions,
|
||||||
|
challenge,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Assertion {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
type: string;
|
||||||
|
registrationClientExtensions: string;
|
||||||
|
response: {
|
||||||
|
clientDataJSON: string;
|
||||||
|
attestationObject: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the binary data in the credential into base64 strings
|
||||||
|
* for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||||
|
const attObj = new Uint8Array(
|
||||||
|
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
|
||||||
|
);
|
||||||
|
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
|
||||||
|
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||||
|
response: {
|
||||||
|
clientDataJSON: b64enc(clientDataJSON),
|
||||||
|
attestationObject: b64enc(attObj),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformCredentialRequestOptions(
|
||||||
|
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
): PublicKeyCredentialRequestOptions {
|
||||||
|
const challenge = u8arr(credentialRequestOptions.challenge.toString());
|
||||||
|
|
||||||
|
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||||
|
(credentialDescriptor) => {
|
||||||
|
const id = u8arr(credentialDescriptor.id.toString());
|
||||||
|
return Object.assign({}, credentialDescriptor, { id });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...credentialRequestOptions,
|
||||||
|
challenge,
|
||||||
|
allowCredentials,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthAssertion {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
type: string;
|
||||||
|
assertionClientExtensions: string;
|
||||||
|
response: {
|
||||||
|
clientDataJSON: string;
|
||||||
|
authenticatorData: string;
|
||||||
|
signature: string;
|
||||||
|
userHandle: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||||
|
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
||||||
|
const authData = new Uint8Array(response.authenticatorData);
|
||||||
|
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
const sig = new Uint8Array(response.signature);
|
||||||
|
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
clientDataJSON: b64RawEnc(clientDataJSON),
|
||||||
|
signature: b64RawEnc(sig),
|
||||||
|
authenticatorData: b64RawEnc(authData),
|
||||||
|
userHandle: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
28
web/src/common/http.ts
Normal file
28
web/src/common/http.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @file HTTP utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of a cookie by its name.
|
||||||
|
*
|
||||||
|
* @param cookieName - The name of the cookie to retrieve.
|
||||||
|
* @returns The value of the cookie, or an empty string if the cookie is not found.
|
||||||
|
*/
|
||||||
|
export function getCookie(cookieName: string): string {
|
||||||
|
if (!cookieName) return "";
|
||||||
|
if (typeof document === "undefined") return "";
|
||||||
|
if (typeof document.cookie !== "string") return "";
|
||||||
|
if (!document.cookie) return "";
|
||||||
|
|
||||||
|
const search = cookieName + "=";
|
||||||
|
// Split the cookie string into individual name=value pairs...
|
||||||
|
const keyValPairs = document.cookie.split(";").map((cookie) => cookie.trim());
|
||||||
|
|
||||||
|
for (const pair of keyValPairs) {
|
||||||
|
if (!pair.startsWith(search)) continue;
|
||||||
|
|
||||||
|
return decodeURIComponent(pair.substring(search.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { config } from "@goauthentik/common/api/config";
|
import { config } from "@goauthentik/common/api/config";
|
||||||
import { VERSION } from "@goauthentik/common/constants";
|
import { VERSION } from "@goauthentik/common/constants";
|
||||||
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
|
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
|
||||||
import {
|
import {
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
EventHint,
|
EventHint,
|
||||||
@ -12,11 +14,6 @@ import {
|
|||||||
|
|
||||||
import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api";
|
import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api";
|
||||||
|
|
||||||
/**
|
|
||||||
* A generic error that can be thrown without triggering Sentry's reporting.
|
|
||||||
*/
|
|
||||||
export class SentryIgnoredError extends Error {}
|
|
||||||
|
|
||||||
export const TAG_SENTRY_COMPONENT = "authentik.component";
|
export const TAG_SENTRY_COMPONENT = "authentik.component";
|
||||||
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
|
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
|
||||||
|
|
||||||
@ -68,7 +65,7 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
|
|||||||
});
|
});
|
||||||
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
||||||
if (window.location.pathname.includes("if/")) {
|
if (window.location.pathname.includes("if/")) {
|
||||||
setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface()}`);
|
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
|
||||||
}
|
}
|
||||||
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||||
const Spotlight = await import("@spotlightjs/spotlight");
|
const Spotlight = await import("@spotlightjs/spotlight");
|
||||||
@ -86,13 +83,3 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
|
|||||||
}
|
}
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the interface name from URL
|
|
||||||
export function currentInterface(): string {
|
|
||||||
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
|
|
||||||
let currentInterface = "unknown";
|
|
||||||
if (pathMatches && pathMatches.length >= 2) {
|
|
||||||
currentInterface = pathMatches[1];
|
|
||||||
}
|
|
||||||
return currentInterface.toLowerCase();
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { currentInterface } from "@goauthentik/common/sentry";
|
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
|
import { isUserRoute } from "@goauthentik/elements/router";
|
||||||
|
|
||||||
import { UiThemeEnum, UserSelf } from "@goauthentik/api";
|
import { UiThemeEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
export enum UserDisplay {
|
export enum UserDisplay {
|
||||||
username = "username",
|
username = "username",
|
||||||
@ -18,15 +18,27 @@ export enum LayoutType {
|
|||||||
|
|
||||||
export interface UIConfig {
|
export interface UIConfig {
|
||||||
enabledFeatures: {
|
enabledFeatures: {
|
||||||
// API Request drawer in navbar
|
/**
|
||||||
|
* Whether to show the API request drawer in the navbar.
|
||||||
|
*/
|
||||||
apiDrawer: boolean;
|
apiDrawer: boolean;
|
||||||
// Notification drawer in navbar
|
/**
|
||||||
|
* Whether to show the notification drawer in the navbar.
|
||||||
|
*/
|
||||||
notificationDrawer: boolean;
|
notificationDrawer: boolean;
|
||||||
// Settings in user dropdown
|
/**
|
||||||
|
* Whether to show the settings in the user dropdown.
|
||||||
|
*/
|
||||||
settings: boolean;
|
settings: boolean;
|
||||||
// Application edit in library (only shown when user is superuser)
|
/**
|
||||||
|
* Whether to show the application edit button in the library.
|
||||||
|
*
|
||||||
|
* This is only shown when the user is a superuser.
|
||||||
|
*/
|
||||||
applicationEdit: boolean;
|
applicationEdit: boolean;
|
||||||
// Search bar
|
/**
|
||||||
|
* Whether to show the search bar.
|
||||||
|
*/
|
||||||
search: boolean;
|
search: boolean;
|
||||||
};
|
};
|
||||||
navbar: {
|
navbar: {
|
||||||
@ -38,68 +50,77 @@ export interface UIConfig {
|
|||||||
cardBackground: string;
|
cardBackground: string;
|
||||||
};
|
};
|
||||||
pagination: {
|
pagination: {
|
||||||
|
/**
|
||||||
|
* Number of items to show per page in paginated lists.
|
||||||
|
*/
|
||||||
perPage: number;
|
perPage: number;
|
||||||
};
|
};
|
||||||
layout: {
|
layout: {
|
||||||
|
/**
|
||||||
|
* Layout type to use for the application.
|
||||||
|
*/
|
||||||
type: LayoutType;
|
type: LayoutType;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Locale to use for the application.
|
||||||
|
*/
|
||||||
locale: string;
|
locale: string;
|
||||||
|
/**
|
||||||
|
* Default values.
|
||||||
|
*/
|
||||||
defaults: {
|
defaults: {
|
||||||
|
/**
|
||||||
|
* Default path to use for user API calls.
|
||||||
|
*/
|
||||||
userPath: string;
|
userPath: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultUIConfig implements UIConfig {
|
export function createUIConfig(overrides: Partial<UIConfig> = {}): UIConfig {
|
||||||
enabledFeatures = {
|
const uiConfig: UIConfig = {
|
||||||
apiDrawer: true,
|
enabledFeatures: {
|
||||||
notificationDrawer: true,
|
// TODO: Is the intent that only user routes should have the API drawer disabled,
|
||||||
settings: true,
|
// or only admin routes?
|
||||||
applicationEdit: true,
|
apiDrawer: !isUserRoute(),
|
||||||
search: true,
|
notificationDrawer: true,
|
||||||
};
|
settings: true,
|
||||||
layout = {
|
applicationEdit: true,
|
||||||
type: LayoutType.row,
|
search: true,
|
||||||
};
|
},
|
||||||
navbar = {
|
layout: {
|
||||||
userDisplay: UserDisplay.username,
|
type: LayoutType.row,
|
||||||
};
|
},
|
||||||
theme = {
|
navbar: {
|
||||||
base: UiThemeEnum.Automatic,
|
userDisplay: UserDisplay.username,
|
||||||
background: "",
|
},
|
||||||
cardBackground: "",
|
theme: {
|
||||||
};
|
base: UiThemeEnum.Automatic,
|
||||||
pagination = {
|
background: "",
|
||||||
perPage: 20,
|
cardBackground: "",
|
||||||
};
|
},
|
||||||
locale = "";
|
pagination: {
|
||||||
defaults = {
|
perPage: 20,
|
||||||
userPath: "users",
|
},
|
||||||
|
locale: "",
|
||||||
|
defaults: {
|
||||||
|
userPath: "users",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
// TODO: Should we deep merge the overrides instead of shallow?
|
||||||
if (currentInterface() === "user") {
|
Object.assign(uiConfig, overrides);
|
||||||
this.enabledFeatures.apiDrawer = false;
|
|
||||||
}
|
return uiConfig;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let globalUiConfig: Promise<UIConfig>;
|
let cachedUIConfig: UIConfig | null = null;
|
||||||
|
|
||||||
export function getConfigForUser(user: UserSelf): UIConfig {
|
|
||||||
const settings = user.settings;
|
|
||||||
let config = new DefaultUIConfig();
|
|
||||||
if (!settings) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
config = Object.assign(new DefaultUIConfig(), settings);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uiConfig(): Promise<UIConfig> {
|
export function uiConfig(): Promise<UIConfig> {
|
||||||
if (!globalUiConfig) {
|
if (cachedUIConfig) return Promise.resolve(cachedUIConfig);
|
||||||
globalUiConfig = me().then((user) => {
|
|
||||||
return getConfigForUser(user.user);
|
return me().then((session) => {
|
||||||
});
|
cachedUIConfig = createUIConfig(session.user.settings);
|
||||||
}
|
|
||||||
return globalUiConfig;
|
return cachedUIConfig;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,96 +1,63 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||||
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
|
|
||||||
|
|
||||||
import { CoreApi, SessionUser } from "@goauthentik/api";
|
import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api";
|
||||||
|
|
||||||
/**
|
let globalMePromise: Promise<SessionUser> | undefined;
|
||||||
* Create a guest session for unauthenticated users.
|
|
||||||
*
|
|
||||||
* @see {@linkcode me} for the actual session retrieval.
|
|
||||||
*/
|
|
||||||
function createGuestSession(): SessionUser {
|
|
||||||
const guest: SessionUser = {
|
|
||||||
user: {
|
|
||||||
pk: -1,
|
|
||||||
isSuperuser: false,
|
|
||||||
isActive: true,
|
|
||||||
groups: [],
|
|
||||||
avatar: "",
|
|
||||||
uid: "",
|
|
||||||
username: "",
|
|
||||||
name: "",
|
|
||||||
settings: {},
|
|
||||||
systemPermissions: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return guest;
|
|
||||||
}
|
|
||||||
|
|
||||||
let memoizedSession: SessionUser | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the current user session.
|
|
||||||
*/
|
|
||||||
export function refreshMe(): Promise<SessionUser> {
|
export function refreshMe(): Promise<SessionUser> {
|
||||||
memoizedSession = null;
|
globalMePromise = undefined;
|
||||||
return me();
|
return me();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function me(): Promise<SessionUser> {
|
||||||
* Retrieve the current user session.
|
if (!globalMePromise) {
|
||||||
*
|
globalMePromise = new CoreApi(DEFAULT_CONFIG)
|
||||||
* This is a memoized function, so it will only make one request per page load.
|
.coreUsersMeRetrieve()
|
||||||
*
|
.then((user) => {
|
||||||
* @see {@linkcode refreshMe} to force a refresh.
|
if (!user.user.settings || !("locale" in user.user.settings)) {
|
||||||
*/
|
return user;
|
||||||
export async function me(): Promise<SessionUser> {
|
|
||||||
if (memoizedSession) return memoizedSession;
|
|
||||||
|
|
||||||
return new CoreApi(DEFAULT_CONFIG)
|
|
||||||
.coreUsersMeRetrieve()
|
|
||||||
.then((nextSession) => {
|
|
||||||
const locale: string | undefined = nextSession.user.settings.locale;
|
|
||||||
|
|
||||||
if (locale) {
|
|
||||||
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
|
|
||||||
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
|
||||||
composed: true,
|
|
||||||
bubbles: true,
|
|
||||||
detail: { locale },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextSession;
|
|
||||||
})
|
|
||||||
.catch(async (error: unknown) => {
|
|
||||||
if (isResponseErrorLike(error)) {
|
|
||||||
const { response } = error;
|
|
||||||
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
|
||||||
const { pathname, search, hash } = window.location;
|
|
||||||
|
|
||||||
const authFlowRedirectURL = new URL(
|
|
||||||
`/flows/-/default/authentication/`,
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
|
|
||||||
authFlowRedirectURL.searchParams.set("next", `${pathname}${search}${hash}`);
|
|
||||||
|
|
||||||
window.location.assign(authFlowRedirectURL);
|
|
||||||
}
|
}
|
||||||
}
|
const locale: string | undefined = user.user.settings.locale;
|
||||||
|
if (locale && locale !== "") {
|
||||||
console.debug("authentik/users: Failed to retrieve user session", error);
|
console.debug(
|
||||||
|
`authentik/locale: Activating user's configured locale '${locale}'`,
|
||||||
return createGuestSession();
|
);
|
||||||
})
|
window.dispatchEvent(
|
||||||
.then((nextSession) => {
|
new CustomEvent(EVENT_LOCALE_REQUEST, {
|
||||||
memoizedSession = nextSession;
|
composed: true,
|
||||||
return nextSession;
|
bubbles: true,
|
||||||
});
|
detail: { locale },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
})
|
||||||
|
.catch((ex: ResponseError) => {
|
||||||
|
const defaultUser: SessionUser = {
|
||||||
|
user: {
|
||||||
|
pk: -1,
|
||||||
|
isSuperuser: false,
|
||||||
|
isActive: true,
|
||||||
|
groups: [],
|
||||||
|
avatar: "",
|
||||||
|
uid: "",
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
settings: {},
|
||||||
|
systemPermissions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (ex.response?.status === 401 || ex.response?.status === 403) {
|
||||||
|
const relativeUrl = window.location
|
||||||
|
.toString()
|
||||||
|
.substring(window.location.origin.length);
|
||||||
|
window.location.assign(
|
||||||
|
`/flows/-/default/authentication/?next=${encodeURIComponent(relativeUrl)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return defaultUser;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return globalMePromise;
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,7 @@
|
|||||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
import { SentryIgnoredError } from "@goauthentik/common/errors";
|
||||||
|
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
|
|
||||||
export function getCookie(name: string): string {
|
|
||||||
let cookieValue = "";
|
|
||||||
if (document.cookie && document.cookie !== "") {
|
|
||||||
const cookies = document.cookie.split(";");
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i].trim();
|
|
||||||
// Does this cookie string begin with the name we want?
|
|
||||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertToSlug(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "-")
|
|
||||||
.replace(/[^\w-]+/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSlug(text: string): boolean {
|
|
||||||
const lowered = text.toLowerCase();
|
|
||||||
const forbidden = /([^\w-]|\s)/.test(lowered);
|
|
||||||
return lowered === text && !forbidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate a string based on maximum word count
|
* Truncate a string based on maximum word count
|
||||||
*/
|
*/
|
||||||
@ -63,17 +34,29 @@ export function snakeToCamel(key: string) {
|
|||||||
|
|
||||||
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
||||||
const m = new Map<string, T[]>();
|
const m = new Map<string, T[]>();
|
||||||
|
|
||||||
objects.forEach((obj) => {
|
objects.forEach((obj) => {
|
||||||
const group = callback(obj);
|
const group = callback(obj);
|
||||||
if (!m.has(group)) {
|
if (!m.has(group)) {
|
||||||
m.set(group, []);
|
m.set(group, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tProviders = m.get(group) || [];
|
const tProviders = m.get(group) || [];
|
||||||
tProviders.push(obj);
|
tProviders.push(obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(m).sort();
|
return Array.from(m).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first non-null and non-undefined argument.
|
||||||
|
*
|
||||||
|
* @deprecated Use nullish coalescing operator (??) instead.
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* This needs a deeper look. Some instances of this function use `new Date()`
|
||||||
|
* which may cause issues during rendering.
|
||||||
|
*/
|
||||||
export function first<T>(...args: Array<T | undefined | null>): T {
|
export function first<T>(...args: Array<T | undefined | null>): T {
|
||||||
for (let index = 0; index < args.length; index++) {
|
for (let index = 0; index < args.length; index++) {
|
||||||
const element = args[index];
|
const element = args[index];
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { VERSION } from "@goauthentik/common/constants";
|
import { VERSION } from "@goauthentik/common/constants";
|
||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import {
|
import { EventContext, EventModel, EventWithContext } from "@goauthentik/common/events";
|
||||||
EventContext,
|
|
||||||
EventContextProperty,
|
|
||||||
EventModel,
|
|
||||||
EventWithContext,
|
|
||||||
} from "@goauthentik/common/events";
|
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/Expand";
|
import "@goauthentik/elements/Expand";
|
||||||
import "@goauthentik/elements/Spinner";
|
import "@goauthentik/elements/Spinner";
|
||||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -29,15 +23,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
|
|
||||||
import { EventActions, FlowsApi } from "@goauthentik/api";
|
import { EventActions, FlowsApi } from "@goauthentik/api";
|
||||||
|
|
||||||
// TODO: Settle these types. It's too hard to make sense of what we're expecting here.
|
type Pair = [string, string | number | EventContext | EventModel | string[] | TemplateResult];
|
||||||
type EventSlotValueType =
|
|
||||||
| number
|
|
||||||
| SlottedTemplateResult
|
|
||||||
| undefined
|
|
||||||
| EventContext
|
|
||||||
| EventContextProperty;
|
|
||||||
|
|
||||||
type FieldLabelTuple<V extends EventSlotValueType = EventSlotValueType> = [label: string, value: V];
|
|
||||||
|
|
||||||
// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters
|
// https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters
|
||||||
|
|
||||||
@ -118,7 +104,7 @@ export class EventInfo extends AKElement {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDescriptionGroup([term, description]: FieldLabelTuple) {
|
renderDescriptionGroup([term, description]: Pair) {
|
||||||
return html` <div class="pf-c-description-list__group">
|
return html` <div class="pf-c-description-list__group">
|
||||||
<dt class="pf-c-description-list__term">
|
<dt class="pf-c-description-list__term">
|
||||||
<span class="pf-c-description-list__text">${term}</span>
|
<span class="pf-c-description-list__text">${term}</span>
|
||||||
@ -134,7 +120,7 @@ export class EventInfo extends AKElement {
|
|||||||
return html`<span>-</span>`;
|
return html`<span>-</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelFields: FieldLabelTuple[] = [
|
const modelFields: Pair[] = [
|
||||||
[msg("UID"), context.pk],
|
[msg("UID"), context.pk],
|
||||||
[msg("Name"), context.name],
|
[msg("Name"), context.name],
|
||||||
[msg("App"), context.app],
|
[msg("App"), context.app],
|
||||||
@ -148,23 +134,20 @@ export class EventInfo extends AKElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmailInfo(context: EventContext): SlottedTemplateResult {
|
getEmailInfo(context: EventContext): TemplateResult {
|
||||||
if (context === null) {
|
if (context === null) {
|
||||||
return html`<span>-</span>`;
|
return html`<span>-</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailFields = [
|
// prettier-ignore
|
||||||
// ---
|
const emailFields: Pair[] = [
|
||||||
[msg("Message"), context.message],
|
[msg("Message"), context.message],
|
||||||
[msg("Subject"), context.subject],
|
[msg("Subject"), context.subject],
|
||||||
[msg("From"), context.from_email],
|
[msg("From"), context.from_email],
|
||||||
[
|
[msg("To"), html`${(context.to_email as string[]).map((to) => {
|
||||||
msg("To"),
|
|
||||||
html`${(context.to_email as string[]).map((to) => {
|
|
||||||
return html`<li>${to}</li>`;
|
return html`<li>${to}</li>`;
|
||||||
})}`,
|
})}`],
|
||||||
],
|
];
|
||||||
] satisfies FieldLabelTuple<EventSlotValueType>[];
|
|
||||||
|
|
||||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||||
${map(emailFields, this.renderDescriptionGroup)}
|
${map(emailFields, this.renderDescriptionGroup)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { convertToSlug } from "@goauthentik/common/utils";
|
import { formatAsSlug } from "@goauthentik/elements/router";
|
||||||
|
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators.js";
|
import { customElement, property, query } from "lit/decorators.js";
|
||||||
@ -34,7 +34,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
|
|||||||
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
||||||
// component, such as a custom forms manager, may receive it.
|
// component, such as a custom forms manager, may receive it.
|
||||||
handleTouch(ev: Event) {
|
handleTouch(ev: Event) {
|
||||||
this.input.value = convertToSlug(this.input.value);
|
this.input.value = formatAsSlug(this.input.value);
|
||||||
this.value = this.input.value;
|
this.value = this.input.value;
|
||||||
|
|
||||||
if (this.origin && this.origin.value === "" && this.input.value === "") {
|
if (this.origin && this.origin.value === "" && this.input.value === "") {
|
||||||
@ -67,7 +67,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
|
|||||||
// "any event which adds or removes a character but leaves the rest of the slug looking like
|
// "any event which adds or removes a character but leaves the rest of the slug looking like
|
||||||
// the previous iteration, set it to the current iteration."
|
// the previous iteration, set it to the current iteration."
|
||||||
|
|
||||||
const newSlug = convertToSlug(ev.target.value);
|
const newSlug = formatAsSlug(ev.target.value);
|
||||||
const oldSlug = this.input.value;
|
const oldSlug = this.input.value;
|
||||||
const [shorter, longer] =
|
const [shorter, longer] =
|
||||||
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];
|
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user