mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 23:52:38 +02:00
Compare commits
1 Commits
blueprint_
...
sdko/atpro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d4f85579 |
4
.github/actions/setup/action.yml
vendored
4
.github/actions/setup/action.yml
vendored
@@ -49,7 +49,7 @@ runs:
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: uv sync --all-extras --dev --locked
|
||||
run: uv sync --all-extras --dev --frozen
|
||||
- name: Setup rust (stable)
|
||||
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies, 'rust-nightly') }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
|
||||
@@ -64,7 +64,7 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2
|
||||
uses: taiki-e/install-action@51cd0b8c0499559d9a4d75c0f5c67bec3a894ec8 # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
|
||||
117
Cargo.lock
generated
117
Cargo.lock
generated
@@ -1003,17 +1003,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evmap"
|
||||
version = "11.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
|
||||
dependencies = [
|
||||
"hashbag",
|
||||
"left-right",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
@@ -1230,21 +1219,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -1326,12 +1300,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbag"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -1889,17 +1857,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "left-right"
|
||||
version = "0.11.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"loom",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -1971,19 +1928,6 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
@@ -2033,12 +1977,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "metrics-exporter-prometheus"
|
||||
version = "0.18.3"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
|
||||
checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"evmap",
|
||||
"indexmap",
|
||||
"metrics",
|
||||
"metrics-util",
|
||||
@@ -2057,7 +2000,7 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"metrics",
|
||||
"quanta",
|
||||
"rand 0.9.4",
|
||||
"rand 0.9.2",
|
||||
"rand_xoshiro",
|
||||
"sketches-ddsketch",
|
||||
]
|
||||
@@ -2744,7 +2687,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
@@ -2804,9 +2747,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
@@ -3160,12 +3103,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -3203,9 +3140,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.48.0"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8ac94aab850a23d7507307cc505332ed2bafd36c65930dfc5c43610f9e9b477"
|
||||
checksum = "eb25f439f97d26fea01d717fa626167ceffcd981addaa670001e70505b72acbb"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"httpdate",
|
||||
@@ -3224,9 +3161,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc84c325ace9ca2388e510fe7d6672b5d60cd8b3bd0eb4bb4ee8314c323cd686"
|
||||
checksum = "46a8c2c1bd5c1f735e84f28b48e7d72efcaafc362b7541bc8253e60e8fcdffc6"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"regex",
|
||||
@@ -3235,9 +3172,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "896c1ab62dbfe1746fb262bbf72e6feb2fb9dfb2c14709077bf71beb532e44b2"
|
||||
checksum = "9b88a90baa654d7f0e1f4b667f6b434293d9f72c71bef16b197c76af5b7d5803"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
@@ -3249,11 +3186,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-core"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f5abf20c42cb1593ec1638976e2647da55f79bccac956444c1707b6cce259a"
|
||||
checksum = "0ac170a5bba8bec6e3339c90432569d89641fa7a3d3e4f44987d24f0762e6adf"
|
||||
dependencies = [
|
||||
"rand 0.9.4",
|
||||
"rand 0.9.2",
|
||||
"sentry-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3262,9 +3199,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-debug-images"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b88bbe6a760d5724bb40689827e82e8db1e275947df2c59abe171bfc30bb671"
|
||||
checksum = "dd9646a972b57896d4a92ed200cf76139f8e30b3cfd03b6662ae59926d26633c"
|
||||
dependencies = [
|
||||
"findshlibs",
|
||||
"sentry-core",
|
||||
@@ -3272,9 +3209,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0260dcb52562b6a79ae7702312a26dba94b79fb5baee7301087529e5ca4e872e"
|
||||
checksum = "6127d3d304ba5ce0409401e85aae538e303a569f8dbb031bf64f9ba0f7174346"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
@@ -3282,9 +3219,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tower"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d669616d5d5279b5712febfc80c343acc3695e499de0d101ed70fceacadf37f2"
|
||||
checksum = "61c5253dc4ad89863a866b93aeaaac1c9d60f2f774663b5024afe2d57e0a101c"
|
||||
dependencies = [
|
||||
"sentry-core",
|
||||
"tower-layer",
|
||||
@@ -3293,9 +3230,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tracing"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1c035f3a0a8671ae1a231c5b457abb68b71acba2bf3054dab2a09a9d4ea487e"
|
||||
checksum = "27701acc51e68db5281802b709010395bfcbcb128b1d0a4e5873680d3b47ff0c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"sentry-backtrace",
|
||||
@@ -3306,13 +3243,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.48.1"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82d8e81058ec155992191f61c7b29bfa7b2cf12012131e7cdc0678020898a7c9"
|
||||
checksum = "56780cb5597d676bf22e6c11d1f062eb4def46390ea3bfb047bcbcf7dfd19bdb"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"hex",
|
||||
"rand 0.9.4",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
@@ -4214,7 +4151,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.4",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
@@ -44,7 +44,7 @@ hyper-util = "= 0.1.20"
|
||||
ipnet = { version = "= 2.12.0", features = ["serde"] }
|
||||
json-subscriber = "= 0.2.8"
|
||||
metrics = "= 0.24.5"
|
||||
metrics-exporter-prometheus = { version = "= 0.18.3", default-features = false }
|
||||
metrics-exporter-prometheus = { version = "= 0.18.1", default-features = false }
|
||||
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
|
||||
notify = "= 8.2.0"
|
||||
pin-project-lite = "= 0.2.17"
|
||||
@@ -67,7 +67,7 @@ reqwest-middleware = { version = "= 0.5.1", features = [
|
||||
"rustls",
|
||||
] }
|
||||
rustls = { version = "= 0.23.40", features = ["fips"] }
|
||||
sentry = { version = "= 0.48.0", default-features = false, features = [
|
||||
sentry = { version = "= 0.47.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Serializer mixin for managed models"""
|
||||
|
||||
from json import JSONDecodeError, loads
|
||||
from typing import cast
|
||||
|
||||
from django.conf import settings
|
||||
@@ -45,7 +44,6 @@ class BlueprintUploadSerializer(PassiveSerializer):
|
||||
|
||||
file = FileField(required=False)
|
||||
path = CharField(required=False)
|
||||
context = CharField(required=False, allow_blank=True)
|
||||
|
||||
def validate_path(self, path: str) -> str:
|
||||
"""Ensure the path (if set) specified is retrievable"""
|
||||
@@ -56,18 +54,6 @@ class BlueprintUploadSerializer(PassiveSerializer):
|
||||
raise ValidationError(_("Blueprint file does not exist"))
|
||||
return path
|
||||
|
||||
def validate_context(self, context: str) -> dict:
|
||||
"""Parse context as a JSON object"""
|
||||
if not context:
|
||||
return {}
|
||||
try:
|
||||
parsed = loads(context)
|
||||
except JSONDecodeError as exc:
|
||||
raise ValidationError(_("Context must be valid JSON")) from exc
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValidationError(_("Context must be a JSON object"))
|
||||
return parsed
|
||||
|
||||
|
||||
class ManagedSerializer:
|
||||
"""Managed Serializer"""
|
||||
@@ -217,7 +203,10 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@extend_schema(
|
||||
request={"multipart/form-data": BlueprintUploadSerializer},
|
||||
responses={200: BlueprintImportResultSerializer},
|
||||
responses={
|
||||
204: BlueprintImportResultSerializer,
|
||||
400: BlueprintImportResultSerializer,
|
||||
},
|
||||
)
|
||||
@action(url_path="import", detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||
@validate(
|
||||
@@ -235,8 +224,7 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
).retrieve_file()
|
||||
else:
|
||||
raise ValidationError("Either path or file must be set")
|
||||
context = body.validated_data.get("context") or {}
|
||||
importer = Importer.from_string(string_contents, context)
|
||||
importer = Importer.from_string(string_contents)
|
||||
|
||||
check_blueprint_perms(importer.blueprint, request.user)
|
||||
|
||||
@@ -244,13 +232,21 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
import_response = self.BlueprintImportResultSerializer(
|
||||
data={
|
||||
"logs": [LogEventSerializer(log).data for log in logs],
|
||||
"success": valid,
|
||||
"logs": [],
|
||||
"success": False,
|
||||
}
|
||||
)
|
||||
import_response.is_valid(raise_exception=True)
|
||||
|
||||
if valid:
|
||||
import_response.initial_data["success"] = importer.apply()
|
||||
import_response.is_valid()
|
||||
import_response.initial_data["logs"] = [LogEventSerializer(log).data for log in logs]
|
||||
import_response.initial_data["success"] = valid
|
||||
import_response.is_valid()
|
||||
if not valid:
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
|
||||
successful = importer.apply()
|
||||
import_response.initial_data["success"] = successful
|
||||
import_response.is_valid()
|
||||
if not successful:
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
return Response(data=import_response.initial_data, status=200)
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
"""Test blueprints v1 api"""
|
||||
|
||||
from json import dumps, loads
|
||||
from json import loads
|
||||
from tempfile import NamedTemporaryFile, mkdtemp
|
||||
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from yaml import dump
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.invitation.models import InvitationStage
|
||||
from authentik.stages.user_write.models import UserWriteStage
|
||||
|
||||
TMP = mkdtemp("authentik-blueprints")
|
||||
|
||||
@@ -85,121 +80,3 @@ class TestBlueprintsV1API(APITestCase):
|
||||
res.content.decode(),
|
||||
{"content": ["Failed to validate blueprint", "- Invalid blueprint version"]},
|
||||
)
|
||||
|
||||
def test_api_import_with_context(self):
|
||||
"""Test that the import endpoint applies the supplied context to the real blueprint"""
|
||||
slug = f"invitation-enrollment-{generate_id()}"
|
||||
flow_name = f"Invitation Enrollment {generate_id()}"
|
||||
stage_name = f"invitation-stage-{generate_id()}"
|
||||
user_type = "internal"
|
||||
continue_without_invitation = True
|
||||
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={
|
||||
"path": "example/flows-invitation-enrollment-minimal.yaml",
|
||||
"context": dumps(
|
||||
{
|
||||
"flow_slug": slug,
|
||||
"flow_name": flow_name,
|
||||
"stage_name": stage_name,
|
||||
"continue_flow_without_invitation": continue_without_invitation,
|
||||
"user_type": user_type,
|
||||
}
|
||||
),
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(res.json()["success"])
|
||||
|
||||
flow = Flow.objects.get(slug=slug)
|
||||
self.assertEqual(flow.name, flow_name)
|
||||
self.assertEqual(flow.title, flow_name)
|
||||
|
||||
invitation_stage = InvitationStage.objects.get(name=stage_name)
|
||||
self.assertEqual(
|
||||
invitation_stage.continue_flow_without_invitation,
|
||||
continue_without_invitation,
|
||||
)
|
||||
|
||||
user_write_stage = UserWriteStage.objects.get(
|
||||
name=f"invitation-enrollment-user-write-{slug}"
|
||||
)
|
||||
self.assertEqual(user_write_stage.user_type, user_type)
|
||||
self.assertEqual(user_write_stage.user_path_template, f"users/{user_type}")
|
||||
|
||||
def test_api_import_blank_path(self):
|
||||
"""Validator returns empty path unchanged (covers api.py:53)."""
|
||||
with NamedTemporaryFile(mode="w+", suffix=".yaml") as file:
|
||||
file.write(dump({"version": 1, "entries": []}))
|
||||
file.flush()
|
||||
file.seek(0)
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={"path": "", "file": file},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_api_import_invalid_blueprint_returns_result_payload(self):
|
||||
"""Invalid blueprint content returns a result payload instead of a 400 response."""
|
||||
file = SimpleUploadedFile("invalid-blueprint.yaml", b'{"version": 3}')
|
||||
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={"file": file},
|
||||
format="multipart",
|
||||
)
|
||||
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertFalse(res.json()["success"])
|
||||
self.assertGreater(len(res.json()["logs"]), 0)
|
||||
|
||||
def test_api_import_unknown_path(self):
|
||||
"""Path not in available blueprints is rejected (covers api.py:56)."""
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={"path": "does/not/exist.yaml"},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertIn("Blueprint file does not exist", res.content.decode())
|
||||
|
||||
def test_api_import_blank_context(self):
|
||||
"""Blank context is normalized to empty dict (covers api.py:62)."""
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={
|
||||
"path": "example/flows-invitation-enrollment-minimal.yaml",
|
||||
"context": "",
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_api_import_invalid_json_context(self):
|
||||
"""Malformed JSON context raises ValidationError (covers api.py:65-66)."""
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={
|
||||
"path": "example/flows-invitation-enrollment-minimal.yaml",
|
||||
"context": "{not json",
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertIn("Context must be valid JSON", res.content.decode())
|
||||
|
||||
def test_api_import_non_object_context(self):
|
||||
"""JSON context that isn't an object is rejected (covers api.py:68)."""
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:blueprintinstance-import-"),
|
||||
data={
|
||||
"path": "example/flows-invitation-enrollment-minimal.yaml",
|
||||
"context": "[1, 2, 3]",
|
||||
},
|
||||
format="multipart",
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertIn("Context must be a JSON object", res.content.decode())
|
||||
|
||||
@@ -32,19 +32,19 @@ from authentik.rbac.decorators import permission_required
|
||||
class UserAgentDeviceDict(TypedDict):
|
||||
"""User agent device"""
|
||||
|
||||
brand: str | None = None
|
||||
brand: str
|
||||
family: str
|
||||
model: str | None = None
|
||||
model: str
|
||||
|
||||
|
||||
class UserAgentOSDict(TypedDict):
|
||||
"""User agent os"""
|
||||
|
||||
family: str
|
||||
major: str | None = None
|
||||
minor: str | None = None
|
||||
patch: str | None = None
|
||||
patch_minor: str | None = None
|
||||
major: str
|
||||
minor: str
|
||||
patch: str
|
||||
patch_minor: str
|
||||
|
||||
|
||||
class UserAgentBrowserDict(TypedDict):
|
||||
|
||||
@@ -29,7 +29,6 @@ class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others
|
||||
default = False
|
||||
visibility = "public"
|
||||
description = _("Refresh other tabs after successful authentication.")
|
||||
deprecated = True
|
||||
|
||||
|
||||
class ContinuousLogin(Flag[bool], key="flows_continuous_login"):
|
||||
|
||||
@@ -187,7 +187,6 @@ SPECTACULAR_SETTINGS = {
|
||||
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
|
||||
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
|
||||
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
|
||||
"RedirectURITypeEnum": "authentik.providers.oauth2.models.RedirectURIType",
|
||||
"SAMLBindingsEnum": "authentik.providers.saml.models.SAMLBindings",
|
||||
"SAMLLogoutMethods": "authentik.providers.saml.models.SAMLLogoutMethods",
|
||||
"SAMLNameIDPolicyEnum": "authentik.sources.saml.models.SAMLNameIDPolicy",
|
||||
|
||||
@@ -33,6 +33,7 @@ class SourceTypeSerializer(PassiveSerializer):
|
||||
profile_url = CharField(read_only=True, allow_null=True)
|
||||
oidc_well_known_url = CharField(read_only=True, allow_null=True)
|
||||
oidc_jwks_url = CharField(read_only=True, allow_null=True)
|
||||
client_secret_required = BooleanField()
|
||||
|
||||
|
||||
class OAuthSourceSerializer(SourceSerializer):
|
||||
@@ -65,6 +66,15 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
)
|
||||
source_type = registry.find_type(provider_type_name)
|
||||
|
||||
if not source_type.client_secret_required and "consumer_secret" not in attrs:
|
||||
attrs["consumer_secret"] = ""
|
||||
if (
|
||||
source_type.client_secret_required
|
||||
and not self.instance
|
||||
and not attrs.get("consumer_secret")
|
||||
):
|
||||
raise ValidationError({"consumer_secret": "This field is required."})
|
||||
|
||||
well_known = attrs.get("oidc_well_known_url") or source_type.oidc_well_known_url
|
||||
inferred_oidc_jwks_url = None
|
||||
|
||||
@@ -149,7 +159,7 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
"authorization_code_auth_method",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"consumer_secret": {"write_only": True},
|
||||
"consumer_secret": {"write_only": True, "allow_blank": True, "required": False},
|
||||
"request_token_url": {"allow_blank": True},
|
||||
"authorization_url": {"allow_blank": True},
|
||||
"access_token_url": {"allow_blank": True},
|
||||
|
||||
@@ -10,6 +10,7 @@ LOGGER = get_logger()
|
||||
|
||||
AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.apple",
|
||||
"authentik.sources.oauth.types.atproto",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.discord",
|
||||
"authentik.sources.oauth.types.entra_id",
|
||||
|
||||
@@ -271,6 +271,15 @@ class EntraIDOAuthSource(CreatableType, OAuthSource):
|
||||
verbose_name_plural = _("Entra ID OAuth Sources")
|
||||
|
||||
|
||||
class AtProtoOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using AT Protocol."""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = _("AT Protocol OAuth Source")
|
||||
verbose_name_plural = _("AT Protocol OAuth Sources")
|
||||
|
||||
|
||||
class OpenIDConnectOAuthSource(CreatableType, OAuthSource):
|
||||
"""Login using a Generic OpenID-Connect compliant provider."""
|
||||
|
||||
|
||||
284
authentik/sources/oauth/tests/test_type_atproto.py
Normal file
284
authentik/sources/oauth/tests/test_type_atproto.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""AT Protocol OAuth Source tests"""
|
||||
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat
|
||||
from django.test import RequestFactory, SimpleTestCase
|
||||
from jwt import decode, get_unverified_header
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.sources.oauth.api.source import OAuthSourceSerializer
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.atproto import (
|
||||
BSKY_AUTHORIZATION_URL_DEFAULT,
|
||||
BSKY_PAR_URL_DEFAULT,
|
||||
BSKY_PUBLIC_PROFILE_URL_DEFAULT,
|
||||
BSKY_TOKEN_URL_DEFAULT,
|
||||
AtProtoOAuthClient,
|
||||
AtProtoType,
|
||||
)
|
||||
|
||||
ATPROTO_DID = "did:plc:z72i7hdynmk6r22z27h6tvur"
|
||||
ATPROTO_PDS = "https://puffball.us-east.host.bsky.network"
|
||||
ATPROTO_CLIENT_ID = "https://authentik.example/application/o/atproto/client-metadata.json"
|
||||
|
||||
ATPROTO_DID_DOCUMENT = {
|
||||
"id": ATPROTO_DID,
|
||||
"alsoKnownAs": ["at://bsky.app"],
|
||||
"service": [
|
||||
{
|
||||
"id": "#atproto_pds",
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"serviceEndpoint": ATPROTO_PDS,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
ATPROTO_PROFILE = {
|
||||
"did": ATPROTO_DID,
|
||||
"handle": "bsky.app",
|
||||
"displayName": "Bluesky",
|
||||
}
|
||||
CUSTOM_ISSUER = "https://auth.example"
|
||||
CUSTOM_AUTHORIZATION_URL = f"{CUSTOM_ISSUER}/oauth/authorize"
|
||||
CUSTOM_PAR_URL = f"{CUSTOM_ISSUER}/oauth/par"
|
||||
CUSTOM_TOKEN_URL = f"{CUSTOM_ISSUER}/oauth/token"
|
||||
CUSTOM_PROFILE_URL = f"{CUSTOM_ISSUER}/xrpc/app.bsky.actor.getProfile"
|
||||
|
||||
|
||||
def private_key_pem() -> str:
|
||||
"""Generate an ES256 private key for DPoP tests."""
|
||||
return (
|
||||
ec.generate_private_key(ec.SECP256R1())
|
||||
.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
||||
.decode()
|
||||
)
|
||||
|
||||
|
||||
class TestTypeAtProto(SimpleTestCase):
|
||||
"""AT Protocol OAuth Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = OAuthSource(
|
||||
name="test",
|
||||
slug="test",
|
||||
provider_type="atproto",
|
||||
consumer_key=ATPROTO_CLIENT_ID,
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def get_request(self):
|
||||
request = self.factory.get("/")
|
||||
request.session = {}
|
||||
return request
|
||||
|
||||
def get_callback_request(self, issuer: str = "https://bsky.social"):
|
||||
request = self.factory.get(f"/?state=state&iss={issuer}&code=code")
|
||||
request.session = {
|
||||
"authentik/sources/oauth/atproto/test": {
|
||||
"state": "state",
|
||||
"code_verifier": "verifier",
|
||||
"issuer": issuer,
|
||||
"private_key": private_key_pem(),
|
||||
"dpop_nonce": "nonce-1",
|
||||
"login_hint": None,
|
||||
"expected_did": None,
|
||||
}
|
||||
}
|
||||
return request
|
||||
|
||||
def test_enroll_context(self):
|
||||
"""Test AT Protocol enrollment context."""
|
||||
ak_context = AtProtoType().get_base_user_properties(
|
||||
source=self.source,
|
||||
info=ATPROTO_PROFILE,
|
||||
)
|
||||
self.assertEqual(ak_context["username"], ATPROTO_PROFILE["handle"])
|
||||
self.assertEqual(ak_context["name"], ATPROTO_PROFILE["displayName"])
|
||||
self.assertIsNone(ak_context["email"])
|
||||
|
||||
def test_serializer_allows_missing_secret(self):
|
||||
"""Test AT Protocol sources can be created without a client secret."""
|
||||
serializer = OAuthSourceSerializer()
|
||||
validated = serializer.validate(
|
||||
{
|
||||
"name": "test-atproto",
|
||||
"slug": "test-atproto",
|
||||
"provider_type": "atproto",
|
||||
"consumer_key": ATPROTO_CLIENT_ID,
|
||||
}
|
||||
)
|
||||
self.assertEqual(validated["consumer_secret"], "")
|
||||
|
||||
@Mocker()
|
||||
def test_redirect_uses_par_dpop_pkce_and_no_secret(self, mock: Mocker):
|
||||
"""Test authorization starts with a DPoP-bound pushed authorization request."""
|
||||
mock.post(
|
||||
BSKY_PAR_URL_DEFAULT,
|
||||
json={"request_uri": "urn:request:123"},
|
||||
headers={"DPoP-Nonce": "nonce-1"},
|
||||
)
|
||||
|
||||
request = self.get_request()
|
||||
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
|
||||
redirect_url = client.get_redirect_url({"scope": ["atproto", "transition:generic"]})
|
||||
|
||||
parsed_redirect = urlparse(redirect_url)
|
||||
parsed_query = parse_qs(parsed_redirect.query)
|
||||
parsed_redirect_url = (
|
||||
f"{parsed_redirect.scheme}://{parsed_redirect.netloc}{parsed_redirect.path}"
|
||||
)
|
||||
self.assertEqual(parsed_redirect_url, BSKY_AUTHORIZATION_URL_DEFAULT)
|
||||
self.assertEqual(parsed_query["client_id"], [ATPROTO_CLIENT_ID])
|
||||
self.assertEqual(parsed_query["request_uri"], ["urn:request:123"])
|
||||
self.assertEqual(len(mock.request_history), 1)
|
||||
par_request = mock.request_history[0]
|
||||
self.assertIn("DPoP", par_request.headers)
|
||||
self.assertEqual(par_request.text.count("client_secret"), 0)
|
||||
self.assertIn("client_id=https%3A%2F%2Fauthentik.example", par_request.text)
|
||||
self.assertIn("code_challenge_method=S256", par_request.text)
|
||||
self.assertIn("scope=atproto+transition%3Ageneric", par_request.text)
|
||||
|
||||
header = get_unverified_header(par_request.headers["DPoP"])
|
||||
payload = decode(par_request.headers["DPoP"], options={"verify_signature": False})
|
||||
self.assertEqual(header["typ"], "dpop+jwt")
|
||||
self.assertEqual(header["alg"], "ES256")
|
||||
self.assertEqual(payload["htm"], "POST")
|
||||
self.assertEqual(payload["htu"], BSKY_PAR_URL_DEFAULT)
|
||||
|
||||
@Mocker()
|
||||
def test_custom_urls_override_bluesky_defaults(self, mock: Mocker):
|
||||
"""Test non-Bluesky AT Protocol endpoint configuration."""
|
||||
source = OAuthSource(
|
||||
name="test",
|
||||
slug="test",
|
||||
provider_type="atproto",
|
||||
consumer_key=ATPROTO_CLIENT_ID,
|
||||
authorization_url=CUSTOM_AUTHORIZATION_URL,
|
||||
request_token_url=CUSTOM_PAR_URL,
|
||||
access_token_url=CUSTOM_TOKEN_URL,
|
||||
profile_url=CUSTOM_PROFILE_URL,
|
||||
)
|
||||
mock.post(
|
||||
CUSTOM_PAR_URL,
|
||||
json={"request_uri": "urn:request:custom"},
|
||||
headers={"DPoP-Nonce": "nonce-custom"},
|
||||
)
|
||||
|
||||
request = self.get_request()
|
||||
client = AtProtoOAuthClient(source, request, callback="/callback/")
|
||||
redirect_url = client.get_redirect_url({"scope": ["atproto"]})
|
||||
|
||||
parsed_redirect = urlparse(redirect_url)
|
||||
self.assertEqual(
|
||||
f"{parsed_redirect.scheme}://{parsed_redirect.netloc}{parsed_redirect.path}",
|
||||
CUSTOM_AUTHORIZATION_URL,
|
||||
)
|
||||
self.assertEqual(request.session[client.session_key]["issuer"], CUSTOM_ISSUER)
|
||||
self.assertEqual(mock.request_history[0].url, CUSTOM_PAR_URL)
|
||||
|
||||
@Mocker()
|
||||
def test_access_token_validates_subject_scope_and_issuer(self, mock: Mocker):
|
||||
"""Test callback token response validation."""
|
||||
mock.post(
|
||||
BSKY_TOKEN_URL_DEFAULT,
|
||||
json={
|
||||
"access_token": "access",
|
||||
"refresh_token": "refresh",
|
||||
"token_type": "DPoP",
|
||||
"expires_in": 300,
|
||||
"sub": ATPROTO_DID,
|
||||
"scope": "atproto transition:generic",
|
||||
},
|
||||
headers={"DPoP-Nonce": "nonce-2"},
|
||||
)
|
||||
mock.get(f"https://plc.directory/{ATPROTO_DID}", json=ATPROTO_DID_DOCUMENT)
|
||||
mock.get(
|
||||
f"{ATPROTO_PDS}/.well-known/oauth-protected-resource",
|
||||
json={"authorization_servers": ["https://bsky.social"]},
|
||||
)
|
||||
|
||||
request = self.get_callback_request()
|
||||
|
||||
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
|
||||
token = client.get_access_token()
|
||||
|
||||
self.assertEqual(token["sub"], ATPROTO_DID)
|
||||
self.assertEqual(token["pds_url"], ATPROTO_PDS)
|
||||
token_request = mock.request_history[0]
|
||||
self.assertIn("DPoP", token_request.headers)
|
||||
self.assertEqual(token_request.text.count("client_secret"), 0)
|
||||
self.assertIn("code_verifier=verifier", token_request.text)
|
||||
|
||||
@Mocker()
|
||||
def test_access_token_rejects_non_dpop_token_type(self, mock: Mocker):
|
||||
"""Test callback rejects token responses that are not DPoP-bound."""
|
||||
mock.post(
|
||||
BSKY_TOKEN_URL_DEFAULT,
|
||||
json={
|
||||
"access_token": "access",
|
||||
"token_type": "Bearer",
|
||||
"sub": ATPROTO_DID,
|
||||
"scope": "atproto",
|
||||
},
|
||||
headers={"DPoP-Nonce": "nonce-2"},
|
||||
)
|
||||
|
||||
client = AtProtoOAuthClient(self.source, self.get_callback_request(), callback="/callback/")
|
||||
token = client.get_access_token()
|
||||
|
||||
self.assertEqual(token["error"], "Token response did not include a DPoP token type.")
|
||||
|
||||
@Mocker()
|
||||
def test_did_web_localhost_uses_http_for_local_testing(self, mock: Mocker):
|
||||
"""Test did:web localhost resolution for the local AT Protocol simulator."""
|
||||
mock.get("http://localhost:8787/.well-known/did.json", json={"id": "did:web:localhost"})
|
||||
client = AtProtoOAuthClient(self.source, self.get_request(), callback="/callback/")
|
||||
document = client.get_did_document("did:web:localhost%3A8787")
|
||||
self.assertEqual(document["id"], "did:web:localhost")
|
||||
|
||||
@Mocker()
|
||||
def test_profile_info(self, mock: Mocker):
|
||||
"""Test public Bluesky profile lookup."""
|
||||
mock.get(BSKY_PUBLIC_PROFILE_URL_DEFAULT, json=ATPROTO_PROFILE)
|
||||
client = AtProtoOAuthClient(self.source, self.get_request(), callback="/callback/")
|
||||
profile = client.get_profile_info({"sub": ATPROTO_DID})
|
||||
self.assertEqual(profile["did"], ATPROTO_DID)
|
||||
self.assertEqual(profile["handle"], "bsky.app")
|
||||
|
||||
@Mocker()
|
||||
def test_profile_info_with_transition_email(self, mock: Mocker):
|
||||
"""Test private session email lookup when transition:email is granted."""
|
||||
mock.get(BSKY_PUBLIC_PROFILE_URL_DEFAULT, json=ATPROTO_PROFILE)
|
||||
mock.get(
|
||||
f"{ATPROTO_PDS}/xrpc/com.atproto.server.getSession",
|
||||
json={"email": "user@example.com", "emailConfirmed": True},
|
||||
headers={"DPoP-Nonce": "nonce-3"},
|
||||
)
|
||||
request = self.get_request()
|
||||
request.session = {
|
||||
"authentik/sources/oauth/atproto/test": {
|
||||
"state": "state",
|
||||
"code_verifier": "verifier",
|
||||
"issuer": "https://bsky.social",
|
||||
"private_key": private_key_pem(),
|
||||
"dpop_nonce": "nonce-2",
|
||||
"login_hint": None,
|
||||
"expected_did": None,
|
||||
}
|
||||
}
|
||||
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
|
||||
profile = client.get_profile_info(
|
||||
{
|
||||
"sub": ATPROTO_DID,
|
||||
"scope": "atproto transition:email",
|
||||
"access_token": "access",
|
||||
"pds_url": ATPROTO_PDS,
|
||||
}
|
||||
)
|
||||
self.assertEqual(profile["email"], "user@example.com")
|
||||
session_request = mock.request_history[1]
|
||||
self.assertEqual(session_request.headers["Authorization"], "DPoP access")
|
||||
payload = decode(session_request.headers["DPoP"], options={"verify_signature": False})
|
||||
self.assertIn("ath", payload)
|
||||
486
authentik/sources/oauth/types/atproto.py
Normal file
486
authentik/sources/oauth/types/atproto.py
Normal file
@@ -0,0 +1,486 @@
|
||||
"""AT Protocol OAuth Views"""
|
||||
|
||||
from time import time
|
||||
from typing import Any
|
||||
from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse, urlunparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
||||
from cryptography.hazmat.primitives.hashes import SHA256, Hash
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding,
|
||||
NoEncryption,
|
||||
PrivateFormat,
|
||||
load_pem_private_key,
|
||||
)
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import constant_time_compare, get_random_string
|
||||
from jwt import encode
|
||||
from jwt.algorithms import ECAlgorithm
|
||||
from jwt.utils import base64url_encode
|
||||
from requests.exceptions import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.utils import pkce_s256_challenge
|
||||
from authentik.sources.oauth.clients.base import BaseOAuthClient
|
||||
from authentik.sources.oauth.models import OAuthSource, PKCEMethod
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
# Bluesky defaults. AT Protocol OAuth requires these endpoint roles, but
|
||||
# non-Bluesky deployments can use different hosts through the source URL fields.
|
||||
BSKY_AUTHORIZATION_URL_DEFAULT = "https://bsky.social/oauth/authorize"
|
||||
BSKY_TOKEN_URL_DEFAULT = "https://bsky.social/oauth/token" # nosec
|
||||
BSKY_PAR_URL_DEFAULT = "https://bsky.social/oauth/par"
|
||||
BSKY_ISSUER_DEFAULT = "https://bsky.social"
|
||||
BSKY_PUBLIC_PROFILE_URL_DEFAULT = "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile"
|
||||
HTTP_STATUS_BAD_REQUEST = 400
|
||||
|
||||
SESSION_KEY_ATPROTO = "authentik/sources/oauth/atproto"
|
||||
|
||||
|
||||
class AtProtoOAuthClient(BaseOAuthClient):
|
||||
"""AT Protocol OAuth client.
|
||||
|
||||
AT Protocol looks like OAuth2 from a distance, but the required security
|
||||
profile is different enough that sharing the generic OAuth2 client would
|
||||
hide important behavior: PAR is mandatory, access tokens are DPoP-bound,
|
||||
public clients use metadata URLs instead of secrets, and the token subject
|
||||
is the user's DID rather than an OIDC userinfo subject.
|
||||
"""
|
||||
|
||||
def get_client_id(self) -> str:
|
||||
"""Return the public client metadata URL."""
|
||||
return self.source.consumer_key
|
||||
|
||||
@property
|
||||
def session_key(self) -> str:
|
||||
return f"{SESSION_KEY_ATPROTO}/{self.source.slug}"
|
||||
|
||||
def get_authorization_url(self) -> str:
|
||||
if self.source.source_type.urls_customizable and self.source.authorization_url:
|
||||
return self.source.authorization_url
|
||||
return self.source.source_type.authorization_url or BSKY_AUTHORIZATION_URL_DEFAULT
|
||||
|
||||
def get_token_url(self) -> str:
|
||||
if self.source.source_type.urls_customizable and self.source.access_token_url:
|
||||
return self.source.access_token_url
|
||||
return self.source.source_type.access_token_url or BSKY_TOKEN_URL_DEFAULT
|
||||
|
||||
def get_par_url(self) -> str:
|
||||
if self.source.source_type.urls_customizable and self.source.request_token_url:
|
||||
return self.source.request_token_url
|
||||
return self.source.source_type.request_token_url or BSKY_PAR_URL_DEFAULT
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
parsed_url = urlparse(self.get_authorization_url())
|
||||
if parsed_url.scheme and parsed_url.netloc:
|
||||
return f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
return BSKY_ISSUER_DEFAULT
|
||||
|
||||
def get_redirect_args(self) -> dict[str, str]:
|
||||
"""AT Protocol redirects are built from PAR responses instead."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_redirect_url(self, parameters=None):
|
||||
"""Create a PAR request and redirect with request_uri."""
|
||||
request_uri = self.create_pushed_authorization_request(parameters or {})
|
||||
parsed_url = urlparse(self.get_authorization_url())
|
||||
parsed_args = parse_qs(parsed_url.query)
|
||||
args = {
|
||||
"client_id": self.get_client_id(),
|
||||
"request_uri": request_uri,
|
||||
}
|
||||
args.update(parsed_args)
|
||||
params = urlencode(args, quote_via=quote, doseq=True)
|
||||
return urlunparse(parsed_url._replace(query=params))
|
||||
|
||||
def create_pushed_authorization_request(self, parameters: dict[str, Any]) -> str:
|
||||
"""Create the pushed authorization request and persist session data."""
|
||||
state = get_random_string(32)
|
||||
code_verifier = generate_id(length=128)
|
||||
private_key = ec.generate_private_key(ec.SECP256R1())
|
||||
login_hint = parameters.pop("login_hint", None)
|
||||
scope = parameters.pop("scope", [])
|
||||
if isinstance(scope, str):
|
||||
scopes = scope.split()
|
||||
else:
|
||||
scopes = list(scope)
|
||||
if "atproto" not in scopes:
|
||||
scopes.append("atproto")
|
||||
|
||||
# The DPoP key and PKCE verifier must survive the browser redirect so
|
||||
# the callback can prove it is the same client that created the PAR.
|
||||
session_data = {
|
||||
"state": state,
|
||||
"code_verifier": code_verifier,
|
||||
"issuer": self.get_issuer(),
|
||||
"private_key": private_key.private_bytes(
|
||||
Encoding.PEM,
|
||||
PrivateFormat.PKCS8,
|
||||
NoEncryption(),
|
||||
).decode(),
|
||||
"dpop_nonce": None,
|
||||
"login_hint": login_hint,
|
||||
"expected_did": self.resolve_identifier(login_hint) if login_hint else None,
|
||||
}
|
||||
self.request.session[self.session_key] = session_data
|
||||
|
||||
# AT Protocol starts the browser flow with a PAR request. The browser
|
||||
# only receives a request_uri, not the full authorization parameters.
|
||||
body = {
|
||||
"client_id": self.get_client_id(),
|
||||
"response_type": "code",
|
||||
"redirect_uri": self.request.build_absolute_uri(self.callback),
|
||||
"scope": " ".join(sorted(set(scopes))),
|
||||
"state": state,
|
||||
"code_challenge": pkce_s256_challenge(code_verifier),
|
||||
"code_challenge_method": PKCEMethod.S256,
|
||||
}
|
||||
if login_hint:
|
||||
body["login_hint"] = login_hint
|
||||
body.update(parameters)
|
||||
response = self.request_with_dpop("post", self.get_par_url(), data=body)
|
||||
try:
|
||||
request_uri = response.json().get("request_uri")
|
||||
except ValueError as exc:
|
||||
raise RequestException("PAR response was not valid JSON", response=response) from exc
|
||||
if not request_uri:
|
||||
raise RequestException("PAR response did not include request_uri", response=response)
|
||||
return request_uri
|
||||
|
||||
def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
|
||||
"""Fetch the initial access token from the callback code."""
|
||||
session_data = self.request.session.get(self.session_key)
|
||||
if not session_data:
|
||||
LOGGER.warning("No AT Protocol OAuth session found")
|
||||
return {"error": "No AT Protocol OAuth session found."}
|
||||
if not constant_time_compare(session_data["state"], self.get_request_arg("state", "")):
|
||||
LOGGER.warning("AT Protocol OAuth state check failed")
|
||||
return {"error": "State check failed."}
|
||||
issuer = self.get_request_arg("iss")
|
||||
if not issuer or not constant_time_compare(session_data["issuer"], issuer):
|
||||
LOGGER.warning("AT Protocol OAuth issuer check failed", issuer=issuer)
|
||||
return {"error": "Issuer check failed."}
|
||||
code = self.get_request_arg("code")
|
||||
if not code:
|
||||
return {"error": self.get_request_arg("error_description") or "No token received."}
|
||||
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": self.get_client_id(),
|
||||
"redirect_uri": self.request.build_absolute_uri(self.callback),
|
||||
"code": code,
|
||||
"code_verifier": session_data["code_verifier"],
|
||||
}
|
||||
try:
|
||||
response = self.request_with_dpop("post", self.get_token_url(), data=data)
|
||||
token = response.json()
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("AT Protocol token response was not valid JSON", exc=exc)
|
||||
return None
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to fetch AT Protocol access token",
|
||||
exc=exc,
|
||||
response=exc.response.text if exc.response is not None else str(exc),
|
||||
)
|
||||
return None
|
||||
|
||||
validation_error = self.validate_token_response(token, session_data, issuer)
|
||||
if validation_error:
|
||||
return {"error": validation_error}
|
||||
return token
|
||||
|
||||
def validate_token_response(
|
||||
self,
|
||||
token: dict[str, Any],
|
||||
session_data: dict[str, Any],
|
||||
issuer: str,
|
||||
) -> str | None:
|
||||
"""Validate AT Protocol token claims and attach the verified PDS URL."""
|
||||
# The token response identifies the account by DID. That DID becomes
|
||||
# the stable source connection identifier in authentik.
|
||||
did = token.get("sub")
|
||||
if not did:
|
||||
return "Token response did not include an account DID."
|
||||
if "atproto" not in token.get("scope", "").split():
|
||||
return "Token response did not include the atproto scope."
|
||||
if token.get("token_type") != "DPoP":
|
||||
return "Token response did not include a DPoP token type."
|
||||
expected_did = session_data.get("expected_did")
|
||||
if expected_did and not constant_time_compare(expected_did, did):
|
||||
LOGGER.warning("AT Protocol OAuth subject check failed", expected=expected_did, did=did)
|
||||
return "Subject check failed."
|
||||
# Verify the DID document's PDS points back to the authorization server
|
||||
# that issued the callback, otherwise a token could claim another DID.
|
||||
pds_url = self.get_pds_url_for_subject(did, issuer)
|
||||
if not pds_url:
|
||||
return "Issuer is not authoritative for this account."
|
||||
token["pds_url"] = pds_url
|
||||
return None
|
||||
|
||||
def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Fetch public profile data for the authenticated DID."""
|
||||
did = token.get("sub")
|
||||
if not did:
|
||||
return None
|
||||
profile_url = BSKY_PUBLIC_PROFILE_URL_DEFAULT
|
||||
if self.source.source_type.urls_customizable and self.source.profile_url:
|
||||
profile_url = self.source.profile_url
|
||||
response = self.session.get(profile_url, params={"actor": did})
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to fetch AT Protocol profile",
|
||||
exc=exc,
|
||||
response=exc.response.text if exc.response is not None else str(exc),
|
||||
)
|
||||
return {"did": did}
|
||||
profile = response.json()
|
||||
profile["did"] = did
|
||||
if "transition:email" in token.get("scope", "").split() and token.get("pds_url"):
|
||||
profile.update(self.get_session_info(token))
|
||||
return profile
|
||||
|
||||
def request_with_dpop(self, method: str, url: str, **kwargs):
|
||||
"""Make a DPoP request, retrying once when the server provides a fresh nonce."""
|
||||
response = self.do_dpop_request(method, url, **kwargs)
|
||||
if response.status_code == HTTP_STATUS_BAD_REQUEST and response.headers.get("DPoP-Nonce"):
|
||||
self.update_dpop_nonce(response.headers["DPoP-Nonce"])
|
||||
response = self.do_dpop_request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
nonce = response.headers.get("DPoP-Nonce")
|
||||
if not nonce:
|
||||
raise RequestException("DPoP response did not include DPoP-Nonce", response=response)
|
||||
self.update_dpop_nonce(nonce)
|
||||
return response
|
||||
|
||||
def get_session_info(self, token: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Fetch private session data when transition:email was granted."""
|
||||
pds_url = token["pds_url"].rstrip("/")
|
||||
session_url = f"{pds_url}/xrpc/com.atproto.server.getSession"
|
||||
headers = {
|
||||
"Authorization": f"DPoP {token['access_token']}",
|
||||
}
|
||||
response = self.do_dpop_request(
|
||||
"get",
|
||||
session_url,
|
||||
headers=headers,
|
||||
access_token=token["access_token"],
|
||||
)
|
||||
if response.status_code == HTTP_STATUS_BAD_REQUEST and response.headers.get("DPoP-Nonce"):
|
||||
self.update_dpop_nonce(response.headers["DPoP-Nonce"])
|
||||
response = self.do_dpop_request(
|
||||
"get",
|
||||
session_url,
|
||||
headers=headers,
|
||||
access_token=token["access_token"],
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to fetch AT Protocol session info",
|
||||
exc=exc,
|
||||
response=exc.response.text if exc.response is not None else str(exc),
|
||||
)
|
||||
return {}
|
||||
nonce = response.headers.get("DPoP-Nonce")
|
||||
if nonce:
|
||||
self.update_dpop_nonce(nonce)
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("AT Protocol session response was not valid JSON", exc=exc)
|
||||
return {}
|
||||
|
||||
def do_dpop_request(self, method: str, url: str, **kwargs):
|
||||
access_token = kwargs.pop("access_token", None)
|
||||
headers = dict(kwargs.pop("headers", {}))
|
||||
headers["Accept"] = "application/json"
|
||||
headers["DPoP"] = self.build_dpop_proof(method, url, access_token)
|
||||
return self.session.request(method, url, headers=headers, **kwargs)
|
||||
|
||||
def build_dpop_proof(self, method: str, url: str, access_token: str | None = None) -> str:
|
||||
session_data = self.request.session[self.session_key]
|
||||
private_key = load_pem_private_key(session_data["private_key"].encode(), password=None)
|
||||
if not isinstance(private_key, EllipticCurvePrivateKey):
|
||||
raise TypeError("DPoP private key must be an EC key")
|
||||
payload = {
|
||||
"jti": generate_id(),
|
||||
"htm": method.upper(),
|
||||
"htu": url,
|
||||
"iat": int(time()),
|
||||
}
|
||||
if session_data.get("dpop_nonce"):
|
||||
payload["nonce"] = session_data["dpop_nonce"]
|
||||
if access_token:
|
||||
# Resource requests bind the proof to the access token with ath.
|
||||
digest = Hash(SHA256())
|
||||
digest.update(access_token.encode())
|
||||
payload["ath"] = base64url_encode(digest.finalize()).decode()
|
||||
public_jwk = ECAlgorithm.to_jwk(private_key.public_key(), as_dict=True)
|
||||
public_jwk.pop("kid", None)
|
||||
return encode(
|
||||
payload,
|
||||
private_key,
|
||||
algorithm="ES256",
|
||||
headers={
|
||||
"typ": "dpop+jwt",
|
||||
"jwk": public_jwk,
|
||||
},
|
||||
)
|
||||
|
||||
def update_dpop_nonce(self, nonce: str) -> None:
|
||||
session_data = self.request.session[self.session_key]
|
||||
session_data["dpop_nonce"] = nonce
|
||||
self.request.session[self.session_key] = session_data
|
||||
|
||||
def get_request_arg(self, key: str, default: Any | None = None) -> Any:
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST.get(key, default)
|
||||
return self.request.GET.get(key, default)
|
||||
|
||||
def resolve_identifier(self, identifier: str | None) -> str | None:
|
||||
"""Resolve a handle or DID to a DID."""
|
||||
if not identifier:
|
||||
return None
|
||||
if identifier.startswith("did:"):
|
||||
return identifier
|
||||
response = self.session.get(
|
||||
f"{self.get_issuer()}/xrpc/com.atproto.identity.resolveHandle",
|
||||
params={"handle": identifier.removeprefix("@")},
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to resolve AT Protocol login hint",
|
||||
identifier=identifier,
|
||||
exc=exc,
|
||||
)
|
||||
return None
|
||||
try:
|
||||
return response.json().get("did")
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("AT Protocol handle resolution response was not valid JSON", exc=exc)
|
||||
return None
|
||||
|
||||
def get_pds_url_for_subject(self, did: str, issuer: str) -> str | None:
|
||||
"""Verify that the DID's PDS resolves to the callback issuer."""
|
||||
try:
|
||||
did_document = self.get_did_document(did)
|
||||
pds_url = self.get_pds_url(did_document)
|
||||
if not pds_url:
|
||||
LOGGER.warning("DID document does not include an atproto PDS", did=did)
|
||||
return None
|
||||
resource_metadata = self.session.get(
|
||||
f"{pds_url.rstrip('/')}/.well-known/oauth-protected-resource"
|
||||
)
|
||||
resource_metadata.raise_for_status()
|
||||
try:
|
||||
authorization_servers = resource_metadata.json().get("authorization_servers", [])
|
||||
except ValueError as exc:
|
||||
raise RequestException(
|
||||
"OAuth protected resource metadata was not valid JSON",
|
||||
response=resource_metadata,
|
||||
) from exc
|
||||
except RequestException as exc:
|
||||
LOGGER.warning("Unable to verify AT Protocol issuer", did=did, issuer=issuer, exc=exc)
|
||||
return None
|
||||
if issuer in authorization_servers:
|
||||
return pds_url
|
||||
return None
|
||||
|
||||
def get_did_document(self, did: str) -> dict[str, Any]:
|
||||
if did.startswith("did:plc:"):
|
||||
response = self.session.get(f"https://plc.directory/{did}")
|
||||
elif did.startswith("did:web:"):
|
||||
# did:web resolves by fetching a DID document from the hostname in the DID.
|
||||
# The AT Protocol local simulator uses did:web:localhost, which cannot use
|
||||
# HTTPS locally; real did:web identities should resolve over HTTPS.
|
||||
did_parts = [unquote(part) for part in did.removeprefix("did:web:").split(":")]
|
||||
host = did_parts[0]
|
||||
path = "/".join(did_parts[1:])
|
||||
scheme = "http" if host.startswith(("localhost", "127.0.0.1")) else "https"
|
||||
did_path = f"{path}/did.json" if path else ".well-known/did.json"
|
||||
response = self.session.get(f"{scheme}://{host}/{did_path}")
|
||||
else:
|
||||
raise RequestException(f"Unsupported DID method: {did}")
|
||||
response.raise_for_status()
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError as exc:
|
||||
raise RequestException("DID document was not valid JSON", response=response) from exc
|
||||
|
||||
def get_pds_url(self, did_document: dict[str, Any]) -> str | None:
|
||||
for service in did_document.get("service", []):
|
||||
if service.get("id") == "#atproto_pds":
|
||||
return service.get("serviceEndpoint")
|
||||
if service.get("type") == "AtprotoPersonalDataServer":
|
||||
return service.get("serviceEndpoint")
|
||||
return None
|
||||
|
||||
|
||||
class AtProtoOAuthRedirect(OAuthRedirect):
|
||||
"""AT Protocol OAuth redirect."""
|
||||
|
||||
client_class = AtProtoOAuthClient
|
||||
|
||||
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
|
||||
return {
|
||||
"scope": ["atproto"],
|
||||
}
|
||||
|
||||
|
||||
class AtProtoOAuthCallback(OAuthCallback):
|
||||
"""AT Protocol OAuth callback."""
|
||||
|
||||
client_class = AtProtoOAuthClient
|
||||
|
||||
def get_callback_url(self, source: OAuthSource) -> str:
|
||||
return reverse(
|
||||
"authentik_sources_oauth:oauth-client-callback",
|
||||
kwargs={"source_slug": source.slug},
|
||||
)
|
||||
|
||||
def get_user_id(self, info: dict[str, Any]) -> str | None:
|
||||
return info.get("did")
|
||||
|
||||
|
||||
@registry.register()
|
||||
class AtProtoType(SourceType):
|
||||
"""AT Protocol Type definition"""
|
||||
|
||||
callback_view = AtProtoOAuthCallback
|
||||
redirect_view = AtProtoOAuthRedirect
|
||||
verbose_name = "AT Protocol"
|
||||
name = "atproto"
|
||||
|
||||
# Defaults target Bluesky. They are editable because other AT Protocol
|
||||
# authorization servers can expose the same endpoint roles on different URLs.
|
||||
authorization_url = BSKY_AUTHORIZATION_URL_DEFAULT
|
||||
request_token_url = BSKY_PAR_URL_DEFAULT
|
||||
access_token_url = BSKY_TOKEN_URL_DEFAULT
|
||||
profile_url = BSKY_PUBLIC_PROFILE_URL_DEFAULT
|
||||
|
||||
urls_customizable = True
|
||||
pkce = PKCEMethod.S256
|
||||
client_secret_required = False
|
||||
|
||||
def icon_url(self) -> str:
|
||||
return static("authentik/sources/atproto.svg")
|
||||
|
||||
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
|
||||
return {
|
||||
"username": info.get("handle") or info.get("did"),
|
||||
"email": info.get("email"),
|
||||
"name": info.get("displayName") or info.get("handle"),
|
||||
}
|
||||
@@ -42,6 +42,8 @@ class SourceType:
|
||||
oidc_jwks_url: str | None = None
|
||||
pkce: PKCEMethod = PKCEMethod.NONE
|
||||
|
||||
client_secret_required = True
|
||||
|
||||
authorization_code_auth_method: AuthorizationCodeAuthMethod = (
|
||||
AuthorizationCodeAuthMethod.BASIC_AUTH
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ class RedirectMode(models.TextChoices):
|
||||
|
||||
|
||||
class RedirectStage(Stage):
|
||||
"""Redirect the user to a static URL or another flow, optionally with all gathered context."""
|
||||
"""Redirect the user to another flow, potentially with all gathered context."""
|
||||
|
||||
keep_context = models.BooleanField(default=True)
|
||||
mode = models.TextField(choices=RedirectMode.choices)
|
||||
|
||||
@@ -7,7 +7,7 @@ from dramatiq.broker import Broker, MessageProxy, get_broker
|
||||
from dramatiq.middleware.middleware import Middleware
|
||||
from dramatiq.middleware.retries import Retries
|
||||
from dramatiq.results.middleware import Results
|
||||
from dramatiq.worker import ConsumerThread, Worker, WorkerThread
|
||||
from dramatiq.worker import Worker, _ConsumerThread, _WorkerThread
|
||||
|
||||
from authentik.tasks.broker import PostgresBroker
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestWorker(Worker):
|
||||
self.worker_id = 1000
|
||||
self.work_queue = PriorityQueue()
|
||||
self.consumers = {
|
||||
TESTING_QUEUE: ConsumerThread(
|
||||
TESTING_QUEUE: _ConsumerThread(
|
||||
broker=self.broker,
|
||||
queue_name=TESTING_QUEUE,
|
||||
prefetch=2,
|
||||
@@ -33,7 +33,7 @@ class TestWorker(Worker):
|
||||
prefetch=2,
|
||||
timeout=1,
|
||||
)
|
||||
self._worker = WorkerThread(
|
||||
self._worker = _WorkerThread(
|
||||
broker=self.broker,
|
||||
consumers=self.consumers,
|
||||
work_queue=self.work_queue,
|
||||
@@ -78,18 +78,17 @@ def use_test_broker():
|
||||
actor.broker = broker
|
||||
actor.broker.declare_actor(actor)
|
||||
|
||||
for middleware_class_path, middleware_kwargs in Conf().middlewares:
|
||||
middleware_class = import_string(middleware_class_path)
|
||||
if issubclass(middleware_class, Results):
|
||||
middleware_kwargs["backend"] = import_string(Conf().result_backend)(
|
||||
*Conf().result_backend_args,
|
||||
**Conf().result_backend_kwargs,
|
||||
)
|
||||
middleware: Middleware = middleware_class(
|
||||
for middleware_class, middleware_kwargs in Conf().middlewares:
|
||||
middleware: Middleware = import_string(middleware_class)(
|
||||
**middleware_kwargs,
|
||||
)
|
||||
if isinstance(middleware, Retries):
|
||||
middleware.max_retries = 0
|
||||
if isinstance(middleware, Results):
|
||||
middleware.backend = import_string(Conf().result_backend)(
|
||||
*Conf().result_backend_args,
|
||||
**Conf().result_backend_kwargs,
|
||||
)
|
||||
broker.add_middleware(middleware)
|
||||
|
||||
broker.start()
|
||||
|
||||
@@ -59,8 +59,6 @@ class FlagsJSONExtension(OpenApiSerializerFieldExtension):
|
||||
props[_flag.key] = build_basic_type(get_args(_flag.__orig_bases__[0])[0])
|
||||
if _flag.description:
|
||||
props[_flag.key]["description"] = _flag.description
|
||||
if _flag.deprecated:
|
||||
props[_flag.key]["deprecated"] = _flag.deprecated
|
||||
return build_object_type(props, required=props.keys())
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ class Flag[T]:
|
||||
Literal["none"] | Literal["public"] | Literal["authenticated"] | Literal["system"]
|
||||
) = "none"
|
||||
description: str | None = None
|
||||
deprecated = False
|
||||
|
||||
def __init_subclass__(cls, key: str, **kwargs):
|
||||
cls.__key = key
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
# Minimal Invitation-based Enrollment Blueprint
|
||||
#
|
||||
# Companion to flows-invitation-enrollment.yaml, intended for the "New Invitation"
|
||||
# wizard in the admin UI. Creates a single enrollment flow with an invitation stage
|
||||
# bound to it, plus the supporting prompt/user-write/user-login stages.
|
||||
#
|
||||
# All user-facing fields are parameterized via !Context with fallback defaults, so
|
||||
# this blueprint can be imported directly (without context) or through the wizard
|
||||
# with custom values.
|
||||
#
|
||||
# Context keys (all optional):
|
||||
# flow_name Display name of the enrollment flow.
|
||||
# flow_slug URL slug of the flow and suffix for sub-entity
|
||||
# identifiers (so repeated imports with different
|
||||
# slugs don't overwrite each other).
|
||||
# stage_name Name of the invitation stage.
|
||||
# continue_flow_without_invitation Whether the flow continues when no invitation
|
||||
# is supplied (default: false).
|
||||
# user_type "external" or "internal" (default: "external").
|
||||
# Drives the user-write stage's user_type and
|
||||
# user_path_template.
|
||||
version: 1
|
||||
metadata:
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "false"
|
||||
name: Invitation-based Enrollment (minimal)
|
||||
entries:
|
||||
- identifiers:
|
||||
slug: !Context [flow_slug, invitation-enrollment-flow]
|
||||
model: authentik_flows.flow
|
||||
id: flow
|
||||
attrs:
|
||||
name: !Context [flow_name, Invitation Enrollment Flow]
|
||||
title: !Context [flow_name, Invitation Enrollment Flow]
|
||||
designation: enrollment
|
||||
authentication: require_unauthenticated
|
||||
|
||||
- identifiers:
|
||||
name: !Context [stage_name, invitation-stage]
|
||||
id: invitation-stage
|
||||
model: authentik_stages_invitation.invitationstage
|
||||
attrs:
|
||||
continue_flow_without_invitation: !Context [continue_flow_without_invitation, false]
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-field-username-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-field-username
|
||||
model: authentik_stages_prompt.prompt
|
||||
attrs:
|
||||
field_key: username
|
||||
label: Username
|
||||
type: username
|
||||
required: true
|
||||
placeholder: Username
|
||||
placeholder_expression: false
|
||||
order: 0
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-field-password-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-field-password
|
||||
model: authentik_stages_prompt.prompt
|
||||
attrs:
|
||||
field_key: password
|
||||
label: Password
|
||||
type: password
|
||||
required: true
|
||||
placeholder: Password
|
||||
placeholder_expression: false
|
||||
order: 1
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-field-password-repeat-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-field-password-repeat
|
||||
model: authentik_stages_prompt.prompt
|
||||
attrs:
|
||||
field_key: password_repeat
|
||||
label: Password (repeat)
|
||||
type: password
|
||||
required: true
|
||||
placeholder: Password (repeat)
|
||||
placeholder_expression: false
|
||||
order: 2
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-field-name-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-field-name
|
||||
model: authentik_stages_prompt.prompt
|
||||
attrs:
|
||||
field_key: name
|
||||
label: Name
|
||||
type: text
|
||||
required: true
|
||||
placeholder: Name
|
||||
placeholder_expression: false
|
||||
order: 0
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-field-email-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-field-email
|
||||
model: authentik_stages_prompt.prompt
|
||||
attrs:
|
||||
field_key: email
|
||||
label: Email
|
||||
type: email
|
||||
required: true
|
||||
placeholder: Email
|
||||
placeholder_expression: false
|
||||
order: 1
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-prompt-credentials-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-stage-credentials
|
||||
model: authentik_stages_prompt.promptstage
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-username
|
||||
- !KeyOf prompt-field-password
|
||||
- !KeyOf prompt-field-password-repeat
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-prompt-details-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: prompt-stage-details
|
||||
model: authentik_stages_prompt.promptstage
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf prompt-field-name
|
||||
- !KeyOf prompt-field-email
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-user-write-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: user-write-stage
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs:
|
||||
user_creation_mode: always_create
|
||||
user_type: !Context [user_type, external]
|
||||
user_path_template:
|
||||
!Format ["users/%s", !Context [user_type, external]]
|
||||
|
||||
- identifiers:
|
||||
name:
|
||||
!Format [
|
||||
"invitation-enrollment-user-login-%s",
|
||||
!Context [flow_slug, invitation-enrollment-flow],
|
||||
]
|
||||
id: user-login-stage
|
||||
model: authentik_stages_user_login.userloginstage
|
||||
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf invitation-stage
|
||||
order: 5
|
||||
model: authentik_flows.flowstagebinding
|
||||
attrs:
|
||||
evaluate_on_plan: true
|
||||
re_evaluate_policies: true
|
||||
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf prompt-stage-credentials
|
||||
order: 10
|
||||
model: authentik_flows.flowstagebinding
|
||||
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf prompt-stage-details
|
||||
order: 15
|
||||
model: authentik_flows.flowstagebinding
|
||||
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf user-write-stage
|
||||
order: 20
|
||||
model: authentik_flows.flowstagebinding
|
||||
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf user-login-stage
|
||||
order: 100
|
||||
model: authentik_flows.flowstagebinding
|
||||
@@ -12967,6 +12967,7 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"apple",
|
||||
"atproto",
|
||||
"openidconnect",
|
||||
"entraid",
|
||||
"azuread",
|
||||
@@ -13038,7 +13039,6 @@
|
||||
},
|
||||
"consumer_secret": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Consumer secret"
|
||||
},
|
||||
"additional_scopes": {
|
||||
|
||||
@@ -79,7 +79,7 @@ function prepare_debug {
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server libkrb5-dev gcc
|
||||
source "${VENV_PATH}/bin/activate"
|
||||
uv sync --active --locked
|
||||
uv sync --active --frozen
|
||||
touch /unittest.xml
|
||||
chown authentik:authentik /unittest.xml
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
||||
--mount=type=bind,target=packages/django-postgres-cache,src=packages/django-postgres-cache \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=cache,id=uv-python-deps-$TARGETARCH$TARGETVARIANT,target=/root/.cache/uv \
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
# Stage: Run
|
||||
FROM python-base AS final-image
|
||||
|
||||
@@ -28,7 +28,12 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
_ = db_conn.cursor()
|
||||
|
||||
def do_GET(self):
|
||||
from django.db import DatabaseError, InterfaceError, OperationalError, connections
|
||||
from django.db import (
|
||||
DatabaseError,
|
||||
InterfaceError,
|
||||
OperationalError,
|
||||
connections,
|
||||
)
|
||||
from psycopg.errors import AdminShutdown
|
||||
|
||||
from authentik.root.monitoring import monitoring_set
|
||||
@@ -37,6 +42,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
AdminShutdown,
|
||||
InterfaceError,
|
||||
DatabaseError,
|
||||
ConnectionError,
|
||||
OperationalError,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-06 00:27+0000\n"
|
||||
"POT-Creation-Date: 2026-05-01 03:47+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -101,14 +101,6 @@ msgstr ""
|
||||
msgid "Blueprint file does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/blueprints/api.py
|
||||
msgid "Context must be valid JSON"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/blueprints/api.py
|
||||
msgid "Context must be a JSON object"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/blueprints/api.py
|
||||
msgid "Failed to validate blueprint"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
3
packages/client-ts/package.json
generated
3
packages/client-ts/package.json
generated
@@ -8,8 +8,7 @@
|
||||
"url": "https://github.com/goauthentik/authentik.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run clean && tsc -b tsconfig.json tsconfig.esm.json",
|
||||
"clean": "tsc -b --clean tsconfig.json tsconfig.esm.json",
|
||||
"build": "tsc && tsc -p tsconfig.esm.json",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
5
packages/client-ts/src/apis/ManagedApi.ts
generated
5
packages/client-ts/src/apis/ManagedApi.ts
generated
@@ -47,7 +47,6 @@ export interface ManagedBlueprintsDestroyRequest {
|
||||
export interface ManagedBlueprintsImportCreateRequest {
|
||||
file?: Blob;
|
||||
path?: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface ManagedBlueprintsListRequest {
|
||||
@@ -370,10 +369,6 @@ export class ManagedApi extends runtime.BaseAPI {
|
||||
formParams.append("path", requestParameters["path"] as any);
|
||||
}
|
||||
|
||||
if (requestParameters["context"] != null) {
|
||||
formParams.append("context", requestParameters["context"] as any);
|
||||
}
|
||||
|
||||
let urlPath = `/managed/blueprints/import/`;
|
||||
|
||||
return {
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface AuthenticatedSessionUserAgentDevice {
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentDevice
|
||||
*/
|
||||
brand: string | null;
|
||||
brand: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -35,7 +35,7 @@ export interface AuthenticatedSessionUserAgentDevice {
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentDevice
|
||||
*/
|
||||
model: string | null;
|
||||
model: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,25 +29,25 @@ export interface AuthenticatedSessionUserAgentOs {
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
major: string | null;
|
||||
major: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
minor: string | null;
|
||||
minor: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
patch: string | null;
|
||||
patch: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
patchMinor: string | null;
|
||||
patchMinor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,6 @@ export interface CurrentBrandFlags {
|
||||
* Refresh other tabs after successful authentication.
|
||||
* @type {boolean}
|
||||
* @memberof CurrentBrandFlags
|
||||
* @deprecated
|
||||
*/
|
||||
flowsRefreshOthers: boolean;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export interface OAuthSourceRequest {
|
||||
* @type {string}
|
||||
* @memberof OAuthSourceRequest
|
||||
*/
|
||||
consumerSecret: string;
|
||||
consumerSecret?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -203,7 +203,6 @@ export function instanceOfOAuthSourceRequest(value: object): value is OAuthSourc
|
||||
if (!("slug" in value) || value["slug"] === undefined) return false;
|
||||
if (!("providerType" in value) || value["providerType"] === undefined) return false;
|
||||
if (!("consumerKey" in value) || value["consumerKey"] === undefined) return false;
|
||||
if (!("consumerSecret" in value) || value["consumerSecret"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -252,7 +251,7 @@ export function OAuthSourceRequestFromJSONTyped(
|
||||
profileUrl: json["profile_url"] == null ? undefined : json["profile_url"],
|
||||
pkce: json["pkce"] == null ? undefined : PKCEMethodEnumFromJSON(json["pkce"]),
|
||||
consumerKey: json["consumer_key"],
|
||||
consumerSecret: json["consumer_secret"],
|
||||
consumerSecret: json["consumer_secret"] == null ? undefined : json["consumer_secret"],
|
||||
additionalScopes: json["additional_scopes"] == null ? undefined : json["additional_scopes"],
|
||||
oidcWellKnownUrl:
|
||||
json["oidc_well_known_url"] == null ? undefined : json["oidc_well_known_url"],
|
||||
|
||||
@@ -40,7 +40,6 @@ export interface PatchedSettingsRequestFlags {
|
||||
* Refresh other tabs after successful authentication.
|
||||
* @type {boolean}
|
||||
* @memberof PatchedSettingsRequestFlags
|
||||
* @deprecated
|
||||
*/
|
||||
flowsRefreshOthers: boolean;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
export const ProviderTypeEnum = {
|
||||
Apple: "apple",
|
||||
Atproto: "atproto",
|
||||
Openidconnect: "openidconnect",
|
||||
Entraid: "entraid",
|
||||
Azuread: "azuread",
|
||||
|
||||
12
packages/client-ts/src/models/RedirectURI.ts
generated
12
packages/client-ts/src/models/RedirectURI.ts
generated
@@ -14,8 +14,8 @@
|
||||
|
||||
import type { MatchingModeEnum } from "./MatchingModeEnum";
|
||||
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
|
||||
import type { RedirectURITypeEnum } from "./RedirectURITypeEnum";
|
||||
import { RedirectURITypeEnumFromJSON, RedirectURITypeEnumToJSON } from "./RedirectURITypeEnum";
|
||||
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
|
||||
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
|
||||
|
||||
/**
|
||||
* A single allowed redirect URI entry
|
||||
@@ -37,10 +37,10 @@ export interface RedirectURI {
|
||||
url: string;
|
||||
/**
|
||||
*
|
||||
* @type {RedirectURITypeEnum}
|
||||
* @type {RedirectUriTypeEnum}
|
||||
* @memberof RedirectURI
|
||||
*/
|
||||
redirectUriType?: RedirectURITypeEnum;
|
||||
redirectUriType?: RedirectUriTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ export function RedirectURIFromJSONTyped(json: any, ignoreDiscriminator: boolean
|
||||
redirectUriType:
|
||||
json["redirect_uri_type"] == null
|
||||
? undefined
|
||||
: RedirectURITypeEnumFromJSON(json["redirect_uri_type"]),
|
||||
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +85,6 @@ export function RedirectURIToJSONTyped(
|
||||
return {
|
||||
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
|
||||
url: value["url"],
|
||||
redirect_uri_type: RedirectURITypeEnumToJSON(value["redirectUriType"]),
|
||||
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
|
||||
};
|
||||
}
|
||||
|
||||
12
packages/client-ts/src/models/RedirectURIRequest.ts
generated
12
packages/client-ts/src/models/RedirectURIRequest.ts
generated
@@ -14,8 +14,8 @@
|
||||
|
||||
import type { MatchingModeEnum } from "./MatchingModeEnum";
|
||||
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
|
||||
import type { RedirectURITypeEnum } from "./RedirectURITypeEnum";
|
||||
import { RedirectURITypeEnumFromJSON, RedirectURITypeEnumToJSON } from "./RedirectURITypeEnum";
|
||||
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
|
||||
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
|
||||
|
||||
/**
|
||||
* A single allowed redirect URI entry
|
||||
@@ -37,10 +37,10 @@ export interface RedirectURIRequest {
|
||||
url: string;
|
||||
/**
|
||||
*
|
||||
* @type {RedirectURITypeEnum}
|
||||
* @type {RedirectUriTypeEnum}
|
||||
* @memberof RedirectURIRequest
|
||||
*/
|
||||
redirectUriType?: RedirectURITypeEnum;
|
||||
redirectUriType?: RedirectUriTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ export function RedirectURIRequestFromJSONTyped(
|
||||
redirectUriType:
|
||||
json["redirect_uri_type"] == null
|
||||
? undefined
|
||||
: RedirectURITypeEnumFromJSON(json["redirect_uri_type"]),
|
||||
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,6 +88,6 @@ export function RedirectURIRequestToJSONTyped(
|
||||
return {
|
||||
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
|
||||
url: value["url"],
|
||||
redirect_uri_type: RedirectURITypeEnumToJSON(value["redirectUriType"]),
|
||||
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
|
||||
};
|
||||
}
|
||||
|
||||
57
packages/client-ts/src/models/RedirectURITypeEnum.ts
generated
57
packages/client-ts/src/models/RedirectURITypeEnum.ts
generated
@@ -1,57 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const RedirectURITypeEnum = {
|
||||
Authorization: "authorization",
|
||||
Logout: "logout",
|
||||
UnknownDefaultOpenApi: "11184809",
|
||||
} as const;
|
||||
export type RedirectURITypeEnum = (typeof RedirectURITypeEnum)[keyof typeof RedirectURITypeEnum];
|
||||
|
||||
export function instanceOfRedirectURITypeEnum(value: any): boolean {
|
||||
for (const key in RedirectURITypeEnum) {
|
||||
if (Object.prototype.hasOwnProperty.call(RedirectURITypeEnum, key)) {
|
||||
if (RedirectURITypeEnum[key as keyof typeof RedirectURITypeEnum] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function RedirectURITypeEnumFromJSON(json: any): RedirectURITypeEnum {
|
||||
return RedirectURITypeEnumFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function RedirectURITypeEnumFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): RedirectURITypeEnum {
|
||||
return json as RedirectURITypeEnum;
|
||||
}
|
||||
|
||||
export function RedirectURITypeEnumToJSON(value?: RedirectURITypeEnum | null): any {
|
||||
return value as any;
|
||||
}
|
||||
|
||||
export function RedirectURITypeEnumToJSONTyped(
|
||||
value: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): RedirectURITypeEnum {
|
||||
return value as RedirectURITypeEnum;
|
||||
}
|
||||
57
packages/client-ts/src/models/RedirectUriTypeEnum.ts
generated
Normal file
57
packages/client-ts/src/models/RedirectUriTypeEnum.ts
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const RedirectUriTypeEnum = {
|
||||
Authorization: "authorization",
|
||||
Logout: "logout",
|
||||
UnknownDefaultOpenApi: "11184809",
|
||||
} as const;
|
||||
export type RedirectUriTypeEnum = (typeof RedirectUriTypeEnum)[keyof typeof RedirectUriTypeEnum];
|
||||
|
||||
export function instanceOfRedirectUriTypeEnum(value: any): boolean {
|
||||
for (const key in RedirectUriTypeEnum) {
|
||||
if (Object.prototype.hasOwnProperty.call(RedirectUriTypeEnum, key)) {
|
||||
if (RedirectUriTypeEnum[key as keyof typeof RedirectUriTypeEnum] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function RedirectUriTypeEnumFromJSON(json: any): RedirectUriTypeEnum {
|
||||
return RedirectUriTypeEnumFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function RedirectUriTypeEnumFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): RedirectUriTypeEnum {
|
||||
return json as RedirectUriTypeEnum;
|
||||
}
|
||||
|
||||
export function RedirectUriTypeEnumToJSON(value?: RedirectUriTypeEnum | null): any {
|
||||
return value as any;
|
||||
}
|
||||
|
||||
export function RedirectUriTypeEnumToJSONTyped(
|
||||
value: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): RedirectUriTypeEnum {
|
||||
return value as RedirectUriTypeEnum;
|
||||
}
|
||||
10
packages/client-ts/src/models/SourceType.ts
generated
10
packages/client-ts/src/models/SourceType.ts
generated
@@ -72,6 +72,12 @@ export interface SourceType {
|
||||
* @memberof SourceType
|
||||
*/
|
||||
readonly oidcJwksUrl: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SourceType
|
||||
*/
|
||||
clientSecretRequired: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +93,8 @@ export function instanceOfSourceType(value: object): value is SourceType {
|
||||
if (!("profileUrl" in value) || value["profileUrl"] === undefined) return false;
|
||||
if (!("oidcWellKnownUrl" in value) || value["oidcWellKnownUrl"] === undefined) return false;
|
||||
if (!("oidcJwksUrl" in value) || value["oidcJwksUrl"] === undefined) return false;
|
||||
if (!("clientSecretRequired" in value) || value["clientSecretRequired"] === undefined)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -108,6 +116,7 @@ export function SourceTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean)
|
||||
profileUrl: json["profile_url"],
|
||||
oidcWellKnownUrl: json["oidc_well_known_url"],
|
||||
oidcJwksUrl: json["oidc_jwks_url"],
|
||||
clientSecretRequired: json["client_secret_required"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,5 +144,6 @@ export function SourceTypeToJSONTyped(
|
||||
name: value["name"],
|
||||
verbose_name: value["verboseName"],
|
||||
urls_customizable: value["urlsCustomizable"],
|
||||
client_secret_required: value["clientSecretRequired"],
|
||||
};
|
||||
}
|
||||
|
||||
2
packages/client-ts/src/models/index.ts
generated
2
packages/client-ts/src/models/index.ts
generated
@@ -707,7 +707,7 @@ export * from "./RedirectStageModeEnum";
|
||||
export * from "./RedirectStageRequest";
|
||||
export * from "./RedirectURI";
|
||||
export * from "./RedirectURIRequest";
|
||||
export * from "./RedirectURITypeEnum";
|
||||
export * from "./RedirectUriTypeEnum";
|
||||
export * from "./RelatedGroup";
|
||||
export * from "./RelatedRule";
|
||||
export * from "./Reputation";
|
||||
|
||||
2
packages/client-ts/templates/tsconfig.mustache
generated
2
packages/client-ts/templates/tsconfig.mustache
generated
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"newLine": "lf",
|
||||
|
||||
2
packages/client-ts/tsconfig.json
generated
2
packages/client-ts/tsconfig.json
generated
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"newLine": "lf",
|
||||
|
||||
@@ -32,17 +32,16 @@ class DjangoDramatiqPostgres(AppConfig):
|
||||
middleware=[],
|
||||
)
|
||||
|
||||
for middleware_class_path, middleware_kwargs in Conf().middlewares:
|
||||
middleware_class = import_string(middleware_class_path)
|
||||
if issubclass(middleware_class, Results):
|
||||
middleware_kwargs["backend"] = import_string(Conf().result_backend)(
|
||||
for middleware_class, middleware_kwargs in Conf().middlewares:
|
||||
middleware: dramatiq.middleware.middleware.Middleware = import_string(middleware_class)(
|
||||
**middleware_kwargs,
|
||||
)
|
||||
if isinstance(middleware, Results):
|
||||
middleware.backend = import_string(Conf().result_backend)(
|
||||
*Conf().result_backend_args,
|
||||
**Conf().result_backend_kwargs,
|
||||
)
|
||||
middleware: dramatiq.middleware.middleware.Middleware = middleware_class(
|
||||
**middleware_kwargs,
|
||||
)
|
||||
broker.add_middleware(middleware)
|
||||
broker.add_middleware(middleware) # type: ignore[no-untyped-call]
|
||||
|
||||
dramatiq.set_broker(broker)
|
||||
|
||||
|
||||
@@ -23,9 +23,11 @@ from django.utils.functional import cached_property
|
||||
from django.utils.module_loading import import_string
|
||||
from dramatiq.broker import Broker, Consumer, MessageProxy
|
||||
from dramatiq.common import compute_backoff, current_millis, dq_name, q_name, xq_name
|
||||
from dramatiq.errors import BrokerConnectionError, QueueJoinTimeout
|
||||
from dramatiq.errors import ConnectionError, QueueJoinTimeout
|
||||
from dramatiq.message import Message
|
||||
from dramatiq.middleware import Middleware
|
||||
from dramatiq.middleware import (
|
||||
Middleware,
|
||||
)
|
||||
from pglock.core import _cast_lock_id
|
||||
from psycopg import sql
|
||||
from psycopg.errors import AdminShutdown
|
||||
@@ -44,6 +46,7 @@ DATABASE_ERRORS = (
|
||||
AdminShutdown,
|
||||
InterfaceError,
|
||||
DatabaseError,
|
||||
ConnectionError,
|
||||
OperationalError,
|
||||
)
|
||||
|
||||
@@ -52,7 +55,7 @@ def channel_name(queue_name: str, identifier: ChannelIdentifier) -> str:
|
||||
return f"{CHANNEL_PREFIX}.{queue_name}.{identifier.value}"
|
||||
|
||||
|
||||
def raise_broker_connection_error(func: Callable[P, R]) -> Callable[P, R]: # noqa: UP047
|
||||
def raise_connection_error(func: Callable[P, R]) -> Callable[P, R]: # noqa: UP047
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
try:
|
||||
@@ -63,13 +66,13 @@ def raise_broker_connection_error(func: Callable[P, R]) -> Callable[P, R]: # no
|
||||
connections.close_all()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
raise BrokerConnectionError(str(exc)) from exc # type: ignore[no-untyped-call]
|
||||
raise ConnectionError(str(exc)) from exc # type: ignore[no-untyped-call]
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class PostgresBroker(Broker):
|
||||
queues: set[str]
|
||||
queues: set[str] # type: ignore[assignment]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -78,7 +81,7 @@ class PostgresBroker(Broker):
|
||||
db_alias: str = DEFAULT_DB_ALIAS,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, middleware=[], **kwargs) # type: ignore[misc]
|
||||
super().__init__(*args, middleware=[], **kwargs) # type: ignore[no-untyped-call,misc]
|
||||
self.logger = get_logger(__name__, type(self))
|
||||
|
||||
self.queues = set()
|
||||
@@ -119,10 +122,10 @@ class PostgresBroker(Broker):
|
||||
|
||||
def declare_queue(self, queue_name: str) -> None:
|
||||
if queue_name not in self.queues:
|
||||
self.emit_before("declare_queue", queue_name)
|
||||
self.emit_before("declare_queue", queue_name) # type: ignore[no-untyped-call]
|
||||
self.queues.add(queue_name)
|
||||
# Nothing more to do, all queues are in the same table
|
||||
self.emit_after("declare_queue", queue_name)
|
||||
self.emit_after("declare_queue", queue_name) # type: ignore[no-untyped-call]
|
||||
|
||||
def model_defaults(self, message: Message[Any]) -> dict[str, Any]:
|
||||
eta = None
|
||||
@@ -138,7 +141,7 @@ class PostgresBroker(Broker):
|
||||
}
|
||||
|
||||
@tenacity.retry(
|
||||
retry=tenacity.retry_if_exception_type(BrokerConnectionError),
|
||||
retry=tenacity.retry_if_exception_type(ConnectionError),
|
||||
reraise=True,
|
||||
wait=tenacity.wait_random_exponential(multiplier=1, max=5),
|
||||
stop=tenacity.stop_after_attempt(3),
|
||||
@@ -146,11 +149,11 @@ class PostgresBroker(Broker):
|
||||
cast(logging.Logger, logger), logging.INFO, exc_info=True
|
||||
),
|
||||
)
|
||||
@raise_broker_connection_error
|
||||
@raise_connection_error
|
||||
def enqueue(self, message: Message[Any], *, delay: int | None = None) -> Message[Any]:
|
||||
queue_name = q_name(message.queue_name)
|
||||
queue_name = q_name(message.queue_name) # type: ignore[no-untyped-call]
|
||||
if delay:
|
||||
message_eta = current_millis() + delay
|
||||
message_eta = current_millis() + delay # type: ignore[no-untyped-call]
|
||||
message.options["eta"] = message_eta
|
||||
|
||||
self.declare_queue(queue_name)
|
||||
@@ -160,7 +163,7 @@ class PostgresBroker(Broker):
|
||||
|
||||
message.options["model_defaults"] = self.model_defaults(message)
|
||||
message.options["model_create_defaults"] = {}
|
||||
self.emit_before("enqueue", message, delay)
|
||||
self.emit_before("enqueue", message, delay) # type: ignore[no-untyped-call]
|
||||
|
||||
with transaction.atomic(using=self.db_alias):
|
||||
query = {
|
||||
@@ -182,7 +185,7 @@ class PostgresBroker(Broker):
|
||||
message.options["task"] = task
|
||||
message.options["task_created"] = created
|
||||
|
||||
self.emit_after("enqueue", message, delay)
|
||||
self.emit_after("enqueue", message, delay) # type: ignore[no-untyped-call]
|
||||
return message
|
||||
|
||||
def get_declared_queues(self) -> set[str]:
|
||||
@@ -190,7 +193,7 @@ class PostgresBroker(Broker):
|
||||
|
||||
def flush(self, queue_name: str) -> None:
|
||||
self.query_set.filter(
|
||||
queue_name__in=(queue_name, dq_name(queue_name), xq_name(queue_name))
|
||||
queue_name__in=(queue_name, dq_name(queue_name), xq_name(queue_name)) # type: ignore[no-untyped-call]
|
||||
).delete()
|
||||
|
||||
def flush_all(self) -> None:
|
||||
@@ -372,7 +375,7 @@ class _PostgresConsumer(Consumer):
|
||||
self.in_processing.add(str(message_id))
|
||||
return message
|
||||
|
||||
@raise_broker_connection_error
|
||||
@raise_connection_error
|
||||
def __next__(self) -> MessageProxy | None:
|
||||
# This method is called every second
|
||||
|
||||
@@ -392,7 +395,7 @@ class _PostgresConsumer(Consumer):
|
||||
if processing >= self.prefetch:
|
||||
# If we have too many messages already processing, wait and don't consume a message
|
||||
# straight away, other workers will be faster.
|
||||
self.misses, backoff_ms = compute_backoff(self.misses, max_backoff=1000)
|
||||
self.misses, backoff_ms = compute_backoff(self.misses, max_backoff=1000) # type: ignore[no-untyped-call]
|
||||
self.logger.debug(
|
||||
"Too many messages in processing, Sleeping",
|
||||
processing=processing,
|
||||
@@ -417,7 +420,7 @@ class _PostgresConsumer(Consumer):
|
||||
break
|
||||
message = self._consume_one(str(message_id))
|
||||
if message is not None:
|
||||
return MessageProxy(message)
|
||||
return MessageProxy(message) # type: ignore[no-untyped-call]
|
||||
else:
|
||||
self.logger.debug("Message already consumed. Skipping.", message_id=message_id)
|
||||
continue
|
||||
@@ -441,7 +444,7 @@ class _PostgresConsumer(Consumer):
|
||||
self.to_unlock.add(str(message_id))
|
||||
return False
|
||||
|
||||
def _post_process_message(self, message: MessageProxy, state: TaskState) -> None:
|
||||
def _post_process_message(self, message: Message[Any], state: TaskState) -> None:
|
||||
self.logger.debug("Post-processing message", message=message.message_id, state=state)
|
||||
try:
|
||||
self.in_processing.remove(str(message.message_id))
|
||||
@@ -463,16 +466,16 @@ class _PostgresConsumer(Consumer):
|
||||
)
|
||||
message.options["task"] = task
|
||||
|
||||
@raise_broker_connection_error
|
||||
def ack(self, message: MessageProxy) -> None:
|
||||
@raise_connection_error
|
||||
def ack(self, message: Message[Any]) -> None:
|
||||
self._post_process_message(message, TaskState.DONE)
|
||||
|
||||
@raise_broker_connection_error
|
||||
def nack(self, message: MessageProxy) -> None:
|
||||
@raise_connection_error
|
||||
def nack(self, message: Message[Any]) -> None:
|
||||
self._post_process_message(message, TaskState.REJECTED)
|
||||
|
||||
@raise_broker_connection_error
|
||||
def requeue(self, messages: Iterable[MessageProxy]) -> None:
|
||||
@raise_connection_error
|
||||
def requeue(self, messages: Iterable[Message[Any]]) -> None:
|
||||
self.query_set.filter(
|
||||
message_id__in=[message.message_id for message in messages],
|
||||
).update(
|
||||
@@ -511,7 +514,7 @@ class _PostgresConsumer(Consumer):
|
||||
self.logger.info("Purged messages in all queues", count=count)
|
||||
self.task_purge_last_run = timezone.now()
|
||||
|
||||
@raise_broker_connection_error
|
||||
@raise_connection_error
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self._purge_locks()
|
||||
|
||||
@@ -5,7 +5,7 @@ from signal import pause
|
||||
from django_dramatiq_postgres.conf import Conf
|
||||
|
||||
|
||||
def worker_metrics() -> int:
|
||||
def worker_metrics() -> None:
|
||||
import_module(Conf().autodiscovery["setup_module"])
|
||||
|
||||
from django_dramatiq_postgres.middleware import MetricsMiddleware
|
||||
@@ -15,4 +15,3 @@ def worker_metrics() -> int:
|
||||
int(os.getenv("dramatiq_prom_port", "9191")),
|
||||
)
|
||||
pause()
|
||||
return 0
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from django.db import DatabaseError, close_old_connections, connections
|
||||
from dramatiq.actor import Actor
|
||||
from dramatiq.broker import Broker, MessageProxy
|
||||
from dramatiq.broker import Broker
|
||||
from dramatiq.common import current_millis
|
||||
from dramatiq.message import Message
|
||||
from dramatiq.middleware.middleware import Middleware
|
||||
@@ -79,7 +79,7 @@ class DbConnectionMiddleware(Middleware):
|
||||
|
||||
|
||||
class TaskStateBeforeMiddleware(Middleware):
|
||||
def before_process_message(self, broker: PostgresBroker, message: Message[Any]) -> None: # type: ignore[override]
|
||||
def before_process_message(self, broker: PostgresBroker, message: Message[Any]) -> None:
|
||||
broker.query_set.filter(
|
||||
message_id=message.message_id,
|
||||
queue_name=message.queue_name,
|
||||
@@ -90,7 +90,7 @@ class TaskStateBeforeMiddleware(Middleware):
|
||||
|
||||
|
||||
class TaskStateAfterMiddleware(Middleware):
|
||||
def before_process_message(self, broker: PostgresBroker, message: MessageProxy) -> None: # type: ignore[override]
|
||||
def before_process_message(self, broker: PostgresBroker, message: Message[Any]) -> None:
|
||||
broker.query_set.filter(
|
||||
message_id=message.message_id,
|
||||
queue_name=message.queue_name,
|
||||
@@ -99,7 +99,7 @@ class TaskStateAfterMiddleware(Middleware):
|
||||
state=TaskState.RUNNING,
|
||||
)
|
||||
|
||||
def after_skip_message(self, broker: PostgresBroker, message: MessageProxy) -> None: # type: ignore[override]
|
||||
def after_skip_message(self, broker: PostgresBroker, message: Message[Any]) -> None:
|
||||
broker.query_set.filter(
|
||||
message_id=message.message_id,
|
||||
queue_name=message.queue_name,
|
||||
@@ -110,11 +110,11 @@ class TaskStateAfterMiddleware(Middleware):
|
||||
|
||||
def after_process_message(
|
||||
self,
|
||||
broker: PostgresBroker, # type: ignore[override]
|
||||
message: MessageProxy,
|
||||
broker: PostgresBroker,
|
||||
message: Message[Any],
|
||||
*,
|
||||
result: Any | None = None,
|
||||
exception: BaseException | None = None,
|
||||
exception: Exception | None = None,
|
||||
) -> None:
|
||||
self.after_skip_message(broker, message)
|
||||
|
||||
@@ -147,7 +147,7 @@ class CurrentTask(Middleware):
|
||||
raise CurrentTaskNotFound()
|
||||
return task[-1]
|
||||
|
||||
def before_process_message(self, broker: Broker, message: MessageProxy) -> None:
|
||||
def before_process_message(self, broker: Broker, message: Message[Any]) -> None:
|
||||
tasks = self._TASKS.get()
|
||||
if tasks is None:
|
||||
tasks = []
|
||||
@@ -157,10 +157,10 @@ class CurrentTask(Middleware):
|
||||
def after_process_message(
|
||||
self,
|
||||
broker: Broker,
|
||||
message: MessageProxy,
|
||||
message: Message[Any],
|
||||
*,
|
||||
result: Any | None = None,
|
||||
exception: BaseException | None = None,
|
||||
exception: Exception | None = None,
|
||||
) -> None:
|
||||
tasks: list[TaskBase] | None = self._TASKS.get()
|
||||
if tasks is None or len(tasks) == 0:
|
||||
@@ -194,7 +194,7 @@ class CurrentTask(Middleware):
|
||||
pass
|
||||
self._TASKS.set(tasks[:-1])
|
||||
|
||||
def after_skip_message(self, broker: Broker, message: MessageProxy) -> None:
|
||||
def after_skip_message(self, broker: Broker, message: Message[Any]) -> None:
|
||||
self.after_process_message(broker, message)
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ class MetricsMiddleware(Middleware):
|
||||
self.message_start_times: dict[str, int] = {}
|
||||
|
||||
@property
|
||||
def forks(self) -> list[Callable[[], int]]:
|
||||
def forks(self) -> list[Callable[[], None]]:
|
||||
from django_dramatiq_postgres.forks import worker_metrics
|
||||
|
||||
return [worker_metrics]
|
||||
@@ -310,41 +310,41 @@ class MetricsMiddleware(Middleware):
|
||||
# TODO: worker_id
|
||||
multiprocess.mark_process_dead(os.getpid()) # type: ignore[no-untyped-call]
|
||||
|
||||
def _make_labels(self, message: MessageProxy | Message[Any]) -> list[str]:
|
||||
def _make_labels(self, message: Message[Any]) -> list[str]:
|
||||
return [message.queue_name, message.actor_name]
|
||||
|
||||
def after_nack(self, broker: Broker, message: MessageProxy) -> None:
|
||||
def after_nack(self, broker: Broker, message: Message[Any]) -> None:
|
||||
self.total_rejected_messages.labels(*self._make_labels(message)).inc()
|
||||
|
||||
def after_enqueue(self, broker: Broker, message: Message[Any], delay: int) -> None:
|
||||
if "retries" in message.options:
|
||||
self.total_retried_messages.labels(*self._make_labels(message)).inc()
|
||||
|
||||
def before_delay_message(self, broker: Broker, message: MessageProxy) -> None:
|
||||
def before_delay_message(self, broker: Broker, message: Message[Any]) -> None:
|
||||
self.delayed_messages.add(message.message_id)
|
||||
self.in_progress_delayed_messages.labels(*self._make_labels(message)).inc()
|
||||
|
||||
def before_process_message(self, broker: Broker, message: MessageProxy) -> None:
|
||||
def before_process_message(self, broker: Broker, message: Message[Any]) -> None:
|
||||
labels = self._make_labels(message)
|
||||
if message.message_id in self.delayed_messages:
|
||||
self.delayed_messages.remove(message.message_id)
|
||||
self.in_progress_delayed_messages.labels(*labels).dec()
|
||||
|
||||
self.in_progress_messages.labels(*labels).inc()
|
||||
self.message_start_times[message.message_id] = current_millis()
|
||||
self.message_start_times[message.message_id] = current_millis() # type: ignore[no-untyped-call]
|
||||
|
||||
def after_process_message(
|
||||
self,
|
||||
broker: Broker,
|
||||
message: MessageProxy,
|
||||
message: Message[Any],
|
||||
*,
|
||||
result: Any | None = None,
|
||||
exception: BaseException | None = None,
|
||||
exception: Exception | None = None,
|
||||
) -> None:
|
||||
labels = self._make_labels(message)
|
||||
|
||||
message_start_time = self.message_start_times.pop(message.message_id, current_millis())
|
||||
message_duration = current_millis() - message_start_time
|
||||
message_start_time = self.message_start_times.pop(message.message_id, current_millis()) # type: ignore[no-untyped-call]
|
||||
message_duration = current_millis() - message_start_time # type: ignore[no-untyped-call]
|
||||
self.messages_durations.labels(*labels).observe(message_duration)
|
||||
|
||||
self.in_progress_messages.labels(*labels).dec()
|
||||
|
||||
@@ -159,7 +159,7 @@ class ScheduleBase(models.Model):
|
||||
|
||||
def send(self, broker: Broker | None = None) -> Message[Any]:
|
||||
broker = broker or get_broker()
|
||||
actor: Actor[Any, Any] = broker.get_actor(self.actor_name)
|
||||
actor: Actor[Any, Any] = broker.get_actor(self.actor_name) # type: ignore[no-untyped-call]
|
||||
return actor.send_with_options(
|
||||
args=pickle.loads(self.args), # nosec
|
||||
kwargs=pickle.loads(self.kwargs), # nosec
|
||||
|
||||
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"django >=4.2,<6.0",
|
||||
"django-pglock >=1.7,<2",
|
||||
"django-pgtrigger >=4,<5",
|
||||
"dramatiq >=2,<3",
|
||||
"dramatiq >=1.17,<1.18",
|
||||
"tenacity >=9,<10",
|
||||
"structlog >=25,<26",
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies = [
|
||||
"argon2-cffi==25.1.0",
|
||||
"cachetools==7.0.6",
|
||||
"channels==4.3.2",
|
||||
"cryptography==48.0.0",
|
||||
"cryptography==47.0.0",
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
@@ -25,7 +25,7 @@ dependencies = [
|
||||
"django-prometheus==2.4.1",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-tenants==3.10.1",
|
||||
"django==5.2.14",
|
||||
"django==5.2.13",
|
||||
"djangoql==0.19.1",
|
||||
"djangorestframework==3.17.1",
|
||||
"docker==7.1.0",
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"opencontainers==0.0.15",
|
||||
"packaging==26.2",
|
||||
"paramiko==4.0.0",
|
||||
"psycopg[c,pool]==3.3.4",
|
||||
"psycopg[c,pool]==3.3.3",
|
||||
"pydantic-scim==0.0.8",
|
||||
"pydantic==2.13.3",
|
||||
"pyjwt==2.11.0",
|
||||
|
||||
33
schema.yml
33
schema.yml
@@ -9678,14 +9678,18 @@ paths:
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
'204':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlueprintImportResult'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlueprintImportResult'
|
||||
description: ''
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/oauth2/access_tokens/:
|
||||
@@ -34604,12 +34608,10 @@ components:
|
||||
properties:
|
||||
brand:
|
||||
type: string
|
||||
nullable: true
|
||||
family:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- brand
|
||||
- family
|
||||
@@ -34622,16 +34624,12 @@ components:
|
||||
type: string
|
||||
major:
|
||||
type: string
|
||||
nullable: true
|
||||
minor:
|
||||
type: string
|
||||
nullable: true
|
||||
patch:
|
||||
type: string
|
||||
nullable: true
|
||||
patch_minor:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- family
|
||||
- major
|
||||
@@ -36046,8 +36044,6 @@ components:
|
||||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
context:
|
||||
type: string
|
||||
Brand:
|
||||
type: object
|
||||
description: Brand Serializer
|
||||
@@ -37187,7 +37183,6 @@ components:
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
description: Refresh other tabs after successful authentication.
|
||||
deprecated: true
|
||||
required:
|
||||
- core_default_app_access
|
||||
- enterprise_audit_include_expanded_diff
|
||||
@@ -44648,7 +44643,6 @@ components:
|
||||
consumer_secret:
|
||||
type: string
|
||||
writeOnly: true
|
||||
minLength: 1
|
||||
additional_scopes:
|
||||
type: string
|
||||
oidc_well_known_url:
|
||||
@@ -44665,7 +44659,6 @@ components:
|
||||
token request flow
|
||||
required:
|
||||
- consumer_key
|
||||
- consumer_secret
|
||||
- name
|
||||
- provider_type
|
||||
- slug
|
||||
@@ -49974,7 +49967,6 @@ components:
|
||||
consumer_secret:
|
||||
type: string
|
||||
writeOnly: true
|
||||
minLength: 1
|
||||
additional_scopes:
|
||||
type: string
|
||||
oidc_well_known_url:
|
||||
@@ -51198,7 +51190,6 @@ components:
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
description: Refresh other tabs after successful authentication.
|
||||
deprecated: true
|
||||
required:
|
||||
- core_default_app_access
|
||||
- enterprise_audit_include_expanded_diff
|
||||
@@ -52681,6 +52672,7 @@ components:
|
||||
ProviderTypeEnum:
|
||||
enum:
|
||||
- apple
|
||||
- atproto
|
||||
- openidconnect
|
||||
- entraid
|
||||
- azuread
|
||||
@@ -53639,7 +53631,7 @@ components:
|
||||
type: string
|
||||
redirect_uri_type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RedirectURITypeEnum'
|
||||
- $ref: '#/components/schemas/RedirectUriTypeEnum'
|
||||
default: authorization
|
||||
required:
|
||||
- matching_mode
|
||||
@@ -53655,12 +53647,12 @@ components:
|
||||
minLength: 1
|
||||
redirect_uri_type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RedirectURITypeEnum'
|
||||
- $ref: '#/components/schemas/RedirectUriTypeEnum'
|
||||
default: authorization
|
||||
required:
|
||||
- matching_mode
|
||||
- url
|
||||
RedirectURITypeEnum:
|
||||
RedirectUriTypeEnum:
|
||||
enum:
|
||||
- authorization
|
||||
- logout
|
||||
@@ -55981,7 +55973,6 @@ components:
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
description: Refresh other tabs after successful authentication.
|
||||
deprecated: true
|
||||
required:
|
||||
- core_default_app_access
|
||||
- enterprise_audit_include_expanded_diff
|
||||
@@ -56070,7 +56061,6 @@ components:
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
description: Refresh other tabs after successful authentication.
|
||||
deprecated: true
|
||||
required:
|
||||
- core_default_app_access
|
||||
- enterprise_audit_include_expanded_diff
|
||||
@@ -56345,9 +56335,12 @@ components:
|
||||
type: string
|
||||
readOnly: true
|
||||
nullable: true
|
||||
client_secret_required:
|
||||
type: boolean
|
||||
required:
|
||||
- access_token_url
|
||||
- authorization_url
|
||||
- client_secret_required
|
||||
- name
|
||||
- oidc_jwks_url
|
||||
- oidc_well_known_url
|
||||
|
||||
@@ -111,12 +111,8 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "a[data-ouia-component-id='enroll']"))
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, "a[data-ouia-component-id='enroll']"
|
||||
).click()
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[ouiaId='enroll']")))
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "a[ouiaId='enroll']").click()
|
||||
|
||||
# First prompt stage
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
|
||||
@@ -27,14 +27,8 @@ class TestFlowsRecovery(SeleniumTestCase):
|
||||
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, "a[data-ouia-component-id='recovery']")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, "a[data-ouia-component-id='recovery']"
|
||||
).click()
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[ouiaId='recovery']")))
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "a[ouiaId='recovery']").click()
|
||||
|
||||
# First prompt stage
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
|
||||
127
uv.lock
generated
127
uv.lock
generated
@@ -318,11 +318,11 @@ requires-dist = [
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.6" },
|
||||
{ name = "channels", specifier = "==4.3.2" },
|
||||
{ name = "cryptography", specifier = "==48.0.0" },
|
||||
{ name = "cryptography", specifier = "==47.0.0" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
{ name = "deepmerge", specifier = "==2.0" },
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
{ name = "django", specifier = "==5.2.14" },
|
||||
{ name = "django", specifier = "==5.2.13" },
|
||||
{ name = "django-channels-postgres", editable = "packages/django-channels-postgres" },
|
||||
{ name = "django-countries", specifier = "==8.2.0" },
|
||||
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
|
||||
@@ -357,7 +357,7 @@ requires-dist = [
|
||||
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
||||
{ name = "packaging", specifier = "==26.2" },
|
||||
{ name = "paramiko", specifier = "==4.0.0" },
|
||||
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.4" },
|
||||
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.3" },
|
||||
{ name = "pydantic", specifier = "==2.13.3" },
|
||||
{ name = "pydantic-scim", specifier = "==0.0.8" },
|
||||
{ name = "pyjwt", specifier = "==2.11.0" },
|
||||
@@ -917,55 +917,55 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "48.0.0"
|
||||
version = "47.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1075,16 +1075,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.2.14"
|
||||
version = "5.2.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/95/95f7faa0950867afaa0bef2460c6263afd6a2c78cc9434046ed28160b015/django-5.2.14.tar.gz", hash = "sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2", size = 10895118, upload-time = "2026-05-05T13:57:31.104Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/44/f172870cf87aa25afef48fb72adba89ee8b77fcab6f3b23d240b923f1528/django-5.2.14-py3-none-any.whl", hash = "sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76", size = 8311320, upload-time = "2026-05-05T13:57:25.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/b1/51ab36b2eefcf8cdb9338c7188668a157e29e30306bfc98a379704c9e10d/django-5.2.13-py3-none-any.whl", hash = "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", size = 8310982, upload-time = "2026-04-07T14:02:08.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1143,7 +1143,7 @@ requires-dist = [
|
||||
{ name = "django", specifier = ">=4.2,<6.0" },
|
||||
{ name = "django-pglock", specifier = ">=1.7,<2" },
|
||||
{ name = "django-pgtrigger", specifier = ">=4,<5" },
|
||||
{ name = "dramatiq", specifier = ">=2,<3" },
|
||||
{ name = "dramatiq", specifier = ">=1.17,<1.18" },
|
||||
{ name = "structlog", specifier = ">=25,<26" },
|
||||
{ name = "tenacity", specifier = ">=9,<10" },
|
||||
]
|
||||
@@ -1381,11 +1381,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dramatiq"
|
||||
version = "2.1.0"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/69/02b54e3fc4fe75721b322bc578054b4f03cec258ba614fa98a1a5bbe1efe/dramatiq-2.1.0.tar.gz", hash = "sha256:cf81550729de6cf64234b05bd63970645654aaf38967faa7a2b6e401384bb090", size = 105444, upload-time = "2026-03-03T11:22:10.067Z" }
|
||||
dependencies = [
|
||||
{ name = "prometheus-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/7a/6792ddc64a77d22bfd97261b751a7a76cf2f9d62edc59aafb679ac48b77d/dramatiq-1.17.1.tar.gz", hash = "sha256:2675d2f57e0d82db3a7d2a60f1f9c536365349db78c7f8d80a63e4c54697647a", size = 99071, upload-time = "2024-10-26T05:09:28.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/91/422960c8c415fd31ca1519d71d6f7e4bcabb2cdcc5872f784467e9fe7237/dramatiq-2.1.0-py3-none-any.whl", hash = "sha256:3ef940c2815722d3679aed79ef96c805f02fd33d4361529b2de30f01511ca44d", size = 125543, upload-time = "2026-03-03T11:22:08.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/36/925c7afd5db4f1a3f00676b9c3c58f31ff7ae29a347282d86c8d429280a5/dramatiq-1.17.1-py3-none-any.whl", hash = "sha256:951cdc334478dff8e5150bb02a6f7a947d215ee24b5aedaf738eff20e17913df", size = 120382, upload-time = "2024-10-26T05:09:26.436Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2730,14 +2733,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.4"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2750,9 +2753,9 @@ pool = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-c"
|
||||
version = "3.3.4"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/7c/c08364f2eab2913e4068b3b955d963e7a3491986a85429990969525def30/psycopg_c-3.3.4.tar.gz", hash = "sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22", size = 647111, upload-time = "2026-05-01T23:31:58.032Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/a0/8feb0ca8c7c20a8b9ac4d46b335ddd57e48e593b714262f006880f34fee5/psycopg_c-3.3.3.tar.gz", hash = "sha256:86ef6f4424348247828e83fb0882c9f8acb33e64d0a5ce66c1b4a5107ee73edd", size = 631965, upload-time = "2026-02-18T16:52:18.084Z" }
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-pool"
|
||||
@@ -2944,14 +2947,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "26.2.0"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/a8/26d36401e3ab8eed9030ad33f381da7856fcfad5691780fccd1b019718fc/pyopenssl-26.1.0.tar.gz", hash = "sha256:737f0a2275c5bc54f3b02137687e1a765931fb3949b9a92a825e4d33b9eec08b", size = 186181, upload-time = "2026-04-24T20:23:48.115Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/41/52f3a3e812b816a91e89aa504199d8bf989a1f873192b10762be66cf2009/pyopenssl-26.1.0-py3-none-any.whl", hash = "sha256:115563879b2c8ccb207975705d3e491434d8c9d7c79667c902ecbf5f3bbd2ece", size = 58109, upload-time = "2026-04-24T20:23:46.273Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
3
web/authentik/sources/atproto.svg
Normal file
3
web/authentik/sources/atproto.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
||||
<path fill="#1185fe" d="M320 291.1C293.9 240.4 222.9 145.9 156.9 99.3C93.6 54.6 69.5 62.3 53.6 69.5C35.3 77.8 32 105.9 32 122.4C32 138.9 41.1 258 47 277.9C66.5 343.6 136.1 365.8 200.2 358.6C106.3 372.6 22.9 406.8 132.3 528.5C252.6 653.1 297.1 501.8 320 425.1C342.9 501.8 369.2 647.6 505.6 528.5C608 425.1 533.7 372.5 439.8 358.6C503.9 365.7 573.4 343.5 593 277.9C598.9 258 608 139 608 122.4C608 105.8 604.7 77.7 586.4 69.5C570.6 62.4 546.4 54.6 483.2 99.3C417.1 145.9 346.1 240.4 320 291.1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 566 B |
424
web/package-lock.json
generated
424
web/package-lock.json
generated
@@ -44,10 +44,10 @@
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@sentry/browser": "^10.50.0",
|
||||
"@storybook/addon-docs": "^10.3.6",
|
||||
"@storybook/addon-links": "^10.3.6",
|
||||
"@storybook/web-components": "^10.3.6",
|
||||
"@storybook/web-components-vite": "^10.3.6",
|
||||
"@storybook/addon-docs": "^10.3.5",
|
||||
"@storybook/addon-links": "^10.3.5",
|
||||
"@storybook/web-components": "^10.3.5",
|
||||
"@storybook/web-components-vite": "^10.3.5",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
@@ -66,7 +66,7 @@
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"core-js": "^3.49.0",
|
||||
"country-flag-icons": "^1.6.17",
|
||||
"country-flag-icons": "^1.6.16",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"dompurify": "^3.4.2",
|
||||
@@ -114,11 +114,11 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.10",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.1",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
"yaml": "^2.8.4"
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
@@ -2895,9 +2895,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2911,9 +2911,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2927,9 +2927,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2943,9 +2943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2959,9 +2959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2975,9 +2975,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2991,9 +2991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3007,9 +3007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -3023,9 +3023,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -3039,9 +3039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3055,9 +3055,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3071,9 +3071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3087,48 +3087,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3142,9 +3121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3158,9 +3137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
@@ -3719,15 +3698,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.6.tgz",
|
||||
"integrity": "sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.5.tgz",
|
||||
"integrity": "sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.3.6",
|
||||
"@storybook/csf-plugin": "10.3.5",
|
||||
"@storybook/icons": "^2.0.1",
|
||||
"@storybook/react-dom-shim": "10.3.6",
|
||||
"@storybook/react-dom-shim": "10.3.5",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -3737,13 +3716,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.3.6"
|
||||
"storybook": "^10.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.6.tgz",
|
||||
"integrity": "sha512-tv9Xd68qRGBAvEubaxNo3FuFq4GwuMiBriD+gLGuFK0+/u3cnkuA264aoR1v6YCH3sT3er3+MBimuyKM3jLDxg==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.5.tgz",
|
||||
"integrity": "sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0"
|
||||
@@ -3754,7 +3733,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.3.6"
|
||||
"storybook": "^10.3.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -3763,12 +3742,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.6.tgz",
|
||||
"integrity": "sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.5.tgz",
|
||||
"integrity": "sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "10.3.6",
|
||||
"@storybook/csf-plugin": "10.3.5",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3776,14 +3755,14 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.3.6",
|
||||
"storybook": "^10.3.5",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.6.tgz",
|
||||
"integrity": "sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.5.tgz",
|
||||
"integrity": "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unplugin": "^2.3.5"
|
||||
@@ -3795,7 +3774,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.3.6",
|
||||
"storybook": "^10.3.5",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -3831,9 +3810,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.6.tgz",
|
||||
"integrity": "sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.5.tgz",
|
||||
"integrity": "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3842,13 +3821,13 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.3.6"
|
||||
"storybook": "^10.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.6.tgz",
|
||||
"integrity": "sha512-femDZGYBGQDckL7F6ZCl2S+dNNBjvd9lp6rQrwBdbNprjctLd6d3EB4HyNM502QxtdEo7laq8y1goDu8KwIV3A==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.5.tgz",
|
||||
"integrity": "sha512-tSppZagFCeZ+bWsaHUvdiw17ATWgfGDBz0mFicgEj0/eNuxQH2OvXyRIQUXY39b/55TBwSGeoIX3tOW91WIqpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -3861,24 +3840,24 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lit": "^2.0.0 || ^3.0.0",
|
||||
"storybook": "^10.3.6"
|
||||
"storybook": "^10.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components-vite": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.6.tgz",
|
||||
"integrity": "sha512-VeDEAJuOOQV6VAqEF0pilXucS6kp+1ILJVkI+ets6Ku2D+RKeu167YrQAzh1NwzRTv0e5H0anDDNke+sWvg2dg==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.5.tgz",
|
||||
"integrity": "sha512-6uAw6KAUXFsAPzp8KchcMp3gatEnEAd8ylIvzoMzvsIMiHmzXwvDNmoFZnAJ2tmsQGvF4dZRDCBg7PvWdTx8Rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-vite": "10.3.6",
|
||||
"@storybook/web-components": "10.3.6"
|
||||
"@storybook/builder-vite": "10.3.5",
|
||||
"@storybook/web-components": "10.3.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.3.6"
|
||||
"storybook": "^10.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-ast": {
|
||||
@@ -4577,9 +4556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
|
||||
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.32.tgz",
|
||||
"integrity": "sha512-/eWL0n43D64QWEUHLtTE+jDqjkJhyidjkDhv6f0uJohOUAhywxQ9wXYp845DNNds0JpCdI4Uo0a9bl+vbXf+ew==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -4594,18 +4573,18 @@
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.15.33",
|
||||
"@swc/core-darwin-x64": "1.15.33",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.33",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.33",
|
||||
"@swc/core-linux-arm64-musl": "1.15.33",
|
||||
"@swc/core-linux-ppc64-gnu": "1.15.33",
|
||||
"@swc/core-linux-s390x-gnu": "1.15.33",
|
||||
"@swc/core-linux-x64-gnu": "1.15.33",
|
||||
"@swc/core-linux-x64-musl": "1.15.33",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.33",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.33",
|
||||
"@swc/core-win32-x64-msvc": "1.15.33"
|
||||
"@swc/core-darwin-arm64": "1.15.32",
|
||||
"@swc/core-darwin-x64": "1.15.32",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.32",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.32",
|
||||
"@swc/core-linux-arm64-musl": "1.15.32",
|
||||
"@swc/core-linux-ppc64-gnu": "1.15.32",
|
||||
"@swc/core-linux-s390x-gnu": "1.15.32",
|
||||
"@swc/core-linux-x64-gnu": "1.15.32",
|
||||
"@swc/core-linux-x64-musl": "1.15.32",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.32",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.32",
|
||||
"@swc/core-win32-x64-msvc": "1.15.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
@@ -4617,9 +4596,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz",
|
||||
"integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.32.tgz",
|
||||
"integrity": "sha512-/YWMvJDPu+AAwuUsM2G+DNQ/7zhodURGzdQyewEqcvgklAdDHs3LwQmLLnyn6SJl8DT8UOxkbzK+D1PmPeelRg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4633,9 +4612,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz",
|
||||
"integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.32.tgz",
|
||||
"integrity": "sha512-KOTXJXdAhWL+hZ77MYP3z+4pcMFaQhQ74yqyN1uz093q0YnbxpqMtYpPISbYvMHzVRNNx5kN+9RZAXEaadhWVA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4649,9 +4628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz",
|
||||
"integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.32.tgz",
|
||||
"integrity": "sha512-oOoxLweljlc0A4X8ybsgxV7cVaYTwBOg2iMDJcFR3Sr48C+lsv9VzSmqdK/IVIXF4W4GjLc3VqTAdSMXlfVLuQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4665,9 +4644,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz",
|
||||
"integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.32.tgz",
|
||||
"integrity": "sha512-oDzEkdl6D6BAWdMtU5KGO7y3HR5fJcvByNLyEk9+ugj8nP5Ovb7P4kBcStBXc4MPExFGQryehiINMlmY8HlclA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4681,9 +4660,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz",
|
||||
"integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.32.tgz",
|
||||
"integrity": "sha512-omcqjoZP/b8D8PuczVoRwJieC6ibj7qIxTftNYokz4/aSmKFHvsd7nIFfPk5ZvtzncbH4AY7+Dkr/Lp2gWxYeA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4697,9 +4676,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-ppc64-gnu": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz",
|
||||
"integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.32.tgz",
|
||||
"integrity": "sha512-KGkTMyz/Tbn3PBNu0AVZ4GTDFKnICrYcTiNPZq8DrvK42pnFsf3GNDrIG9E5AtQlTmC0YigkWKmu0eMcfTrmgA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -4713,9 +4692,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-s390x-gnu": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz",
|
||||
"integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.32.tgz",
|
||||
"integrity": "sha512-G3Aa4tVS/3OGZBkoNIwUF9F6RAy+Osb4GOlo62SinLmDiErz/ykmM7KH0wkz6l9kM8jJq1HyAM6atJTUEbBk7g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -4729,9 +4708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz",
|
||||
"integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.32.tgz",
|
||||
"integrity": "sha512-ERsjfGcj6CBmj3vJnGDO8m8rTvw6RqMcWo1dogOtNx3/+/0+NNpJiXDobJrr1GwInI/BHAEkvSFIH6d2LqPcUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4745,9 +4724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz",
|
||||
"integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.32.tgz",
|
||||
"integrity": "sha512-N4Ggahe/8SUbTX50P6EdhbW9YWcgbZVb52R4cq6MK+zsoMjRq7rGvV5ztA05QnbaCYqMYx8rTY7KAIA3Crdo4Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4761,9 +4740,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz",
|
||||
"integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.32.tgz",
|
||||
"integrity": "sha512-01yN0o9jvo8xBTP12aPK2wW8b41jmOlGbDDlAnoynotc4pO6xA0zby9f1z6j++qXDpGBttLySq1omgVrlQKYcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4777,9 +4756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz",
|
||||
"integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.32.tgz",
|
||||
"integrity": "sha512-fLagI9XZYNpTcmlqAcp3KBtmj7E19WCmYD80Jxj1Kn5tGNa7yxNLd3NNdWxuZGUPl5iC0/KqZru7g08gF6Fsrw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -4793,9 +4772,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.33",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz",
|
||||
"integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==",
|
||||
"version": "1.15.32",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.32.tgz",
|
||||
"integrity": "sha512-gbc2bQ/T2CiR+w0OvcVKwLOFAcPZBvmWmolbwpg1E8UrpeC03DGtyMUApOHNXNYWA3SHFrYXCQtosrcMza1YFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -6548,12 +6527,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
@@ -7475,9 +7454,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/country-flag-icons": {
|
||||
"version": "1.6.17",
|
||||
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.17.tgz",
|
||||
"integrity": "sha512-Nmik0289ZVZSI3c7mJR/amg6DyY7Z59b0sTFSKayeX72mHfPzCPJygwJs2pYgQULzuAyWeCUgwAJ+Dq8OR+JFw==",
|
||||
"version": "1.6.16",
|
||||
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.16.tgz",
|
||||
"integrity": "sha512-HxJVoE/aaZGcUMx1vK/u9430uKGB3ODZDDZJJOqVJQzoHk5v42c0fSp1rk4tDfyr1dVOJjwxRiaBPliBMo2Liw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
@@ -10748,9 +10727,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -15832,13 +15811,13 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.127.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||
"@oxc-project/types": "=0.124.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -15847,21 +15826,30 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@oxc-project/types": {
|
||||
"version": "0.124.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
@@ -16610,9 +16598,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.6.tgz",
|
||||
"integrity": "sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==",
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.5.tgz",
|
||||
"integrity": "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -16637,15 +16625,11 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^2 || ^3",
|
||||
"vite-plus": "^0.1.15"
|
||||
"prettier": "^2 || ^3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prettier": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plus": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18416,16 +18400,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.17",
|
||||
"tinyglobby": "^0.2.16"
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -18506,22 +18490,6 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz",
|
||||
@@ -19093,9 +19061,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz",
|
||||
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
@@ -19280,7 +19248,7 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-swc": "^0.4.0",
|
||||
"@swc/cli": "^0.8.1",
|
||||
"@swc/core": "^1.15.33",
|
||||
"@swc/core": "^1.15.32",
|
||||
"@webcomponents/template": "^1.5.1",
|
||||
"base64-js": "^1.5.1",
|
||||
"core-js": "^3.49.0",
|
||||
|
||||
@@ -120,10 +120,10 @@
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@sentry/browser": "^10.50.0",
|
||||
"@storybook/addon-docs": "^10.3.6",
|
||||
"@storybook/addon-links": "^10.3.6",
|
||||
"@storybook/web-components": "^10.3.6",
|
||||
"@storybook/web-components-vite": "^10.3.6",
|
||||
"@storybook/addon-docs": "^10.3.5",
|
||||
"@storybook/addon-links": "^10.3.5",
|
||||
"@storybook/web-components": "^10.3.5",
|
||||
"@storybook/web-components-vite": "^10.3.5",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
@@ -142,7 +142,7 @@
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"core-js": "^3.49.0",
|
||||
"country-flag-icons": "^1.6.17",
|
||||
"country-flag-icons": "^1.6.16",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"dompurify": "^3.4.2",
|
||||
@@ -190,11 +190,11 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.10",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.1",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
"yaml": "^2.8.4"
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.28.0",
|
||||
|
||||
267
web/packages/lex/index.js
vendored
267
web/packages/lex/index.js
vendored
@@ -7,186 +7,152 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A token produced by a {@link LexerAction}. The lexer is agnostic to the
|
||||
* concrete token shape; consumers pick whatever representation suits them.
|
||||
*
|
||||
* @typedef {unknown} Token
|
||||
* @typedef {(this: Lexer, chr: string) => any} DefunctFunction
|
||||
*/
|
||||
|
||||
/**
|
||||
* A rule action. Invoked with the regex match (full match followed by capture
|
||||
* groups) bound to the owning {@link Lexer} so it can read or set `state`,
|
||||
* `index`, and `reject`.
|
||||
*
|
||||
* Return values:
|
||||
* - `null` (or `undefined` from an implicit return) — discard the match and continue scanning.
|
||||
* - a single token — yield it from {@link Lexer.lex}.
|
||||
* - an array of tokens — yield the first; queue the rest for subsequent calls.
|
||||
*
|
||||
* @callback LexerAction
|
||||
* @this {Lexer}
|
||||
* @param {...string} match
|
||||
* @returns {Token | Token[] | null | void}
|
||||
* @typedef {(this: Lexer, ...args: RegExpExecArray) => string | string[] | undefined} RuleAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} LexerRule
|
||||
* @property {RegExp} pattern Sticky-compiled pattern used to probe the input.
|
||||
* @property {boolean} global Whether the user-supplied pattern was global.
|
||||
* @property {LexerAction} action
|
||||
* @property {number[]} start States in which the rule is active. `[0]` is the default state; an empty array means "any state".
|
||||
* @typedef {Object} Rule
|
||||
* @property {RegExp} pattern
|
||||
* @property {boolean} global
|
||||
* @property {RuleAction} action
|
||||
* @property {number[]} start
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} LexerMatch
|
||||
* @typedef {Object} Match
|
||||
* @property {RegExpExecArray} result
|
||||
* @property {LexerAction} action
|
||||
* @property {RuleAction} action
|
||||
* @property {number} length
|
||||
* @property {boolean} global Whether the producing rule was declared with the `g` flag.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handler invoked when no rule matches at the current position.
|
||||
*
|
||||
* @callback DefunctHandler
|
||||
* @this {Lexer}
|
||||
* @param {string} chr The unexpected character.
|
||||
* @returns {Token | Token[] | null | void}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {DefunctHandler}
|
||||
*/
|
||||
function defaultDefunct(chr) {
|
||||
throw new Error(`Unexpected character at index ${this.index - 1}: ${chr}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexer class for tokenizing input strings.
|
||||
*/
|
||||
export class Lexer {
|
||||
/**
|
||||
* Current lexer state. Rules whose `start` array contains this value (or
|
||||
* is empty) are eligible to match. Odd-numbered states are also matched
|
||||
* by rules declared with `start: [0]`, mirroring flex's inclusive states.
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
tokens = [];
|
||||
/**
|
||||
* @type {Rule[]}
|
||||
*/
|
||||
rules = [];
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
remove = 0;
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
state = 0;
|
||||
|
||||
/** @type {number} */
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
index = 0;
|
||||
|
||||
/** @type {string} */
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
input = "";
|
||||
|
||||
/**
|
||||
* When set to `true` from inside an action, the current match is rolled
|
||||
* back and the next-best match is tried instead.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
reject = false;
|
||||
|
||||
/** @type {LexerRule[]} */
|
||||
#rules = [];
|
||||
|
||||
/** @type {Token[]} */
|
||||
#tokens = [];
|
||||
|
||||
/** @type {number} */
|
||||
#remove = 0;
|
||||
|
||||
/** @type {DefunctHandler} */
|
||||
#defunct;
|
||||
|
||||
/**
|
||||
* @param {DefunctHandler} [defunct] Optional handler for unexpected characters.
|
||||
* @param {DefunctFunction} [defunct]
|
||||
*/
|
||||
constructor(defunct) {
|
||||
this.#defunct = typeof defunct === "function" ? defunct : defaultDefunct;
|
||||
defunct ||= function (chr) {
|
||||
throw new Error("Unexpected character at index " + (this.index - 1) + ": " + chr);
|
||||
};
|
||||
|
||||
this.defunct = defunct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a tokenization rule.
|
||||
* Add a lexing rule.
|
||||
*
|
||||
* @param {RegExp} pattern
|
||||
* @param {LexerAction} action
|
||||
* @param {number[]} [start] States in which the rule is active. Defaults to `[0]`.
|
||||
* @returns {this}
|
||||
* @param {RuleAction} action
|
||||
* @param {number[]} [start]
|
||||
* @returns {Lexer}
|
||||
*/
|
||||
addRule(pattern, action, start) {
|
||||
addRule = (pattern, action, start) => {
|
||||
const global = pattern.global;
|
||||
|
||||
if (!global || !pattern.sticky) {
|
||||
let flags = "gy";
|
||||
|
||||
if (pattern.multiline) flags += "m";
|
||||
if (pattern.ignoreCase) flags += "i";
|
||||
if (pattern.unicode) flags += "u";
|
||||
pattern = new RegExp(pattern.source, flags);
|
||||
}
|
||||
|
||||
this.#rules.push({
|
||||
pattern,
|
||||
global,
|
||||
action,
|
||||
start: Array.isArray(start) ? start : [0],
|
||||
if (!Array.isArray(start)) start = [0];
|
||||
|
||||
this.rules.push({
|
||||
pattern: pattern,
|
||||
global: global,
|
||||
action: action,
|
||||
start: start,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the lexer and load a new input string.
|
||||
* Set the input string for lexing.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {this}
|
||||
* @returns {Lexer}
|
||||
*/
|
||||
setInput(input) {
|
||||
this.#remove = 0;
|
||||
setInput = (input) => {
|
||||
this.remove = 0;
|
||||
this.state = 0;
|
||||
this.index = 0;
|
||||
this.#tokens.length = 0;
|
||||
this.tokens.length = 0;
|
||||
this.input = input;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Produce the next token from the input, or `null` once exhausted.
|
||||
* Lex the next token from the input.
|
||||
*
|
||||
* @returns {Token | null}
|
||||
* @returns {string | string[] | undefined}
|
||||
*/
|
||||
lex() {
|
||||
if (this.#tokens.length) return /** @type {Token} */ (this.#tokens.shift());
|
||||
lex = () => {
|
||||
if (this.tokens.length) return this.tokens.shift();
|
||||
|
||||
this.reject = true;
|
||||
|
||||
while (this.index <= this.input.length) {
|
||||
const matches = this.#scan().splice(this.#remove);
|
||||
const matches = this.scan().splice(this.remove);
|
||||
const index = this.index;
|
||||
|
||||
while (matches.length) {
|
||||
if (!this.reject) break;
|
||||
if (!this.reject) {
|
||||
break;
|
||||
}
|
||||
const match = matches.shift();
|
||||
|
||||
const match = /** @type {LexerMatch} */ (matches.shift());
|
||||
const { result, length } = match;
|
||||
if (!match) break;
|
||||
|
||||
const result = match.result;
|
||||
const length = match.length;
|
||||
this.index += length;
|
||||
this.reject = false;
|
||||
this.#remove++;
|
||||
this.remove++;
|
||||
|
||||
let token = match.action.apply(
|
||||
this,
|
||||
/** @type {string[]} */ (/** @type {unknown} */ (result)),
|
||||
);
|
||||
let token = match.action.apply(this, result);
|
||||
|
||||
if (this.reject) {
|
||||
this.index = result.index;
|
||||
} else if (token !== null && token !== undefined) {
|
||||
if (Array.isArray(token)) {
|
||||
this.#tokens = token.slice(1);
|
||||
token = token[0];
|
||||
}
|
||||
if (length) this.#remove = 0;
|
||||
} else if (Array.isArray(token)) {
|
||||
this.tokens = token.slice(1);
|
||||
token = token[0];
|
||||
} else {
|
||||
if (length) this.remove = 0;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -195,82 +161,79 @@ export class Lexer {
|
||||
|
||||
if (index < input.length) {
|
||||
if (this.reject) {
|
||||
this.#remove = 0;
|
||||
const token = this.#defunct(input.charAt(this.index++));
|
||||
if (token !== null && token !== undefined) {
|
||||
this.remove = 0;
|
||||
const token = this.defunct(input.charAt(this.index++));
|
||||
if (typeof token !== "undefined") {
|
||||
if (Array.isArray(token)) {
|
||||
this.#tokens = token.slice(1);
|
||||
this.tokens = token.slice(1);
|
||||
return token[0];
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
} else {
|
||||
if (this.index !== index) this.#remove = 0;
|
||||
if (this.index !== index) this.remove = 0;
|
||||
this.reject = true;
|
||||
}
|
||||
} else if (matches.length) {
|
||||
this.reject = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (matches.length) this.reject = true;
|
||||
else break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Probe every state-eligible rule at the current position, returning the
|
||||
* matches sorted by length (longest first), with global rules pinned
|
||||
* after non-global ones to preserve flex's "longest non-global wins"
|
||||
* tie-breaking.
|
||||
* Scan the input for matches.
|
||||
*
|
||||
* @returns {LexerMatch[]}
|
||||
* @returns {Match[]}
|
||||
*/
|
||||
#scan() {
|
||||
/** @type {LexerMatch[]} */
|
||||
scan = () => {
|
||||
/**
|
||||
* @type {Match[]}
|
||||
*/
|
||||
const matches = [];
|
||||
let index = 0;
|
||||
|
||||
const state = this.state;
|
||||
const lastIndex = this.index;
|
||||
const input = this.input;
|
||||
|
||||
for (const rule of this.#rules) {
|
||||
for (let i = 0, length = this.rules.length; i < length; i++) {
|
||||
const rule = this.rules[i];
|
||||
const start = rule.start;
|
||||
const states = start.length;
|
||||
const eligible =
|
||||
!states || start.indexOf(state) >= 0 || (state % 2 && states === 1 && !start[0]);
|
||||
|
||||
if (!eligible) continue;
|
||||
if (!states || start.indexOf(state) >= 0 || (state % 2 && states === 1 && !start[0])) {
|
||||
const pattern = rule.pattern;
|
||||
pattern.lastIndex = lastIndex;
|
||||
const result = pattern.exec(input);
|
||||
|
||||
const pattern = rule.pattern;
|
||||
pattern.lastIndex = lastIndex;
|
||||
const result = pattern.exec(input);
|
||||
if (!result || result.index !== lastIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!result || result.index !== lastIndex) continue;
|
||||
let j = matches.push({
|
||||
result: result,
|
||||
action: rule.action,
|
||||
length: result[0].length,
|
||||
});
|
||||
|
||||
let j = matches.push({
|
||||
result,
|
||||
action: rule.action,
|
||||
length: result[0].length,
|
||||
global: rule.global,
|
||||
});
|
||||
if (rule.global) {
|
||||
index = j;
|
||||
}
|
||||
|
||||
while (--j > 0) {
|
||||
const k = j - 1;
|
||||
const cur = matches[j];
|
||||
const prev = matches[k];
|
||||
const longer = cur.length > prev.length;
|
||||
const tieFavorsCur = cur.length === prev.length && prev.global && !cur.global;
|
||||
while (--j > index) {
|
||||
const k = j - 1;
|
||||
|
||||
if (!longer && !tieFavorsCur) break;
|
||||
|
||||
matches[j] = prev;
|
||||
matches[k] = cur;
|
||||
if (matches[j].length > matches[k].length) {
|
||||
const temple = matches[j];
|
||||
matches[j] = matches[k];
|
||||
matches[k] = temple;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Lexer;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-swc": "^0.4.0",
|
||||
"@swc/cli": "^0.8.1",
|
||||
"@swc/core": "^1.15.33",
|
||||
"@swc/core": "^1.15.32",
|
||||
"@webcomponents/template": "^1.5.1",
|
||||
"base64-js": "^1.5.1",
|
||||
"core-js": "^3.49.0",
|
||||
|
||||
@@ -8,7 +8,6 @@ import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#elements/utils/TimeDeltaHelp";
|
||||
import "./AdminSettingsFooterLinks.js";
|
||||
import "#elements/Alert";
|
||||
|
||||
import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.js";
|
||||
|
||||
@@ -288,9 +287,6 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
help=${msg(
|
||||
"When enabled, other flow tabs in a session will refresh upon a successful authentication.",
|
||||
)}
|
||||
.bighelp=${html`<ak-alert class="pf-c-radio__description" inline plain>
|
||||
${msg("This flag is deprecated.")}
|
||||
</ak-alert>`}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
|
||||
@@ -52,6 +52,10 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
|
||||
...AKModal.styles,
|
||||
PFAbout,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pf-c-about-modal-box {
|
||||
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-dialog--BackgroundColor);
|
||||
width: unset;
|
||||
|
||||
@@ -135,7 +135,7 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
|
||||
WebsocketClient.connect();
|
||||
|
||||
this.#sidebarMatcher = window.matchMedia("(width > 1210px)");
|
||||
this.#sidebarMatcher = window.matchMedia("(width >= 1200px)");
|
||||
this.sidebarOpen = this.#sidebarMatcher.matches;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,17 +197,15 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
?checked=${this.instance?.openInNewTab ?? false}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="metaHide"
|
||||
?checked=${this.instance?.metaHide ?? false}
|
||||
label=${msg("Hide from My applications")}
|
||||
help=${msg(
|
||||
"If checked, this application will not be shown on the user's My applications page.",
|
||||
)}
|
||||
label=${msg("Hide from User Dashboard")}
|
||||
help=${msg("Whether this application will be shown on the User Dashboard.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-file-search-input
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
RACProvider,
|
||||
RadiusProvider,
|
||||
RedirectURI,
|
||||
RedirectURITypeEnum,
|
||||
RedirectUriTypeEnum,
|
||||
SAMLProvider,
|
||||
SCIMProvider,
|
||||
WSFederationProvider,
|
||||
@@ -87,7 +87,7 @@ function formatRedirectUris(uris: RedirectURI[] = []) {
|
||||
(${uri.matchingMode === MatchingModeEnum.Strict
|
||||
? msg("strict")
|
||||
: msg("regexp")},
|
||||
${uri.redirectUriType === RedirectURITypeEnum.Logout
|
||||
${uri.redirectUriType === RedirectUriTypeEnum.Logout
|
||||
? msg("post logout")
|
||||
: msg("authorization")})
|
||||
</li>`,
|
||||
|
||||
@@ -183,16 +183,16 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
?checked=${app.openInNewTab ?? false}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="metaHide"
|
||||
?checked=${app.metaHide ?? false}
|
||||
label=${msg("Hide from My applications")}
|
||||
label=${msg("Hide from User Dashboard")}
|
||||
help=${msg(
|
||||
"If checked, this application will not be shown on the user's My applications page.",
|
||||
"Whether this application will be shown on the User Dashboard.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
@@ -124,22 +124,19 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
||||
order="order"
|
||||
.columns=${COLUMNS}
|
||||
.content=${[]}
|
||||
>
|
||||
<ak-empty-state slot="empty-table" icon="pf-icon-module"
|
||||
><span>${msg("No bound policies.")}</span>
|
||||
<div slot="body">
|
||||
${msg("No policies are currently bound to this object.")}
|
||||
</div>
|
||||
<div slot="primary">
|
||||
<button
|
||||
@click=${() => this.onBindingEvent()}
|
||||
class="pf-c-button pf-m-primary"
|
||||
>
|
||||
${msg("Bind policy/group/user")}
|
||||
</button>
|
||||
</div>
|
||||
</ak-empty-state>
|
||||
</ak-select-table>
|
||||
></ak-select-table>
|
||||
<ak-empty-state icon="pf-icon-module"
|
||||
><span>${msg("No bound policies.")}</span>
|
||||
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
|
||||
<div slot="primary">
|
||||
<button
|
||||
@click=${() => this.onBindingEvent()}
|
||||
class="pf-c-button pf-m-primary"
|
||||
>
|
||||
${msg("Bind policy/group/user")}
|
||||
</button>
|
||||
</div>
|
||||
</ak-empty-state>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,7 +381,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
return html`<h2 class="pf-c-wizard__main-title">
|
||||
${msg("Review the Application and Provider")}
|
||||
</h2>
|
||||
<fieldset class="ak-c-fieldset" name="application-details">
|
||||
<fieldset>
|
||||
<legend>${msg("Application Details")}</legend>
|
||||
<dl class="pf-c-description-list">
|
||||
<div class="pf-c-description-list__group">
|
||||
@@ -419,7 +419,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
|
||||
${
|
||||
renderer
|
||||
? html`<fieldset class="ak-c-fieldset" name="provider-details">
|
||||
? html`<fieldset>
|
||||
<legend>${msg("Provider Details")}</legend>
|
||||
${renderer(provider)}
|
||||
</fieldset>`
|
||||
|
||||
@@ -23,7 +23,6 @@ import { certificateProvider, certificateSelector } from "#admin/brands/Certific
|
||||
|
||||
import {
|
||||
Application,
|
||||
AuthenticationEnum,
|
||||
Brand,
|
||||
CoreApi,
|
||||
CoreApplicationsListRequest,
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
FlowsApi,
|
||||
UsageEnum,
|
||||
} from "@goauthentik/api";
|
||||
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum.js";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export class ConfigModal extends ModalButton {
|
||||
></ak-codemirror>
|
||||
</ak-expand>
|
||||
</div>
|
||||
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
|
||||
@@ -65,7 +65,7 @@ export class DeviceAddHowTo extends ModalButton {
|
||||
})}
|
||||
</ak-tabs>`}
|
||||
</div>
|
||||
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
|
||||
@@ -17,7 +17,6 @@ import { DesignationToLabel, LayoutToLabel } from "#admin/flows/utils";
|
||||
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
|
||||
|
||||
import {
|
||||
AuthenticationEnum,
|
||||
DeniedActionEnum,
|
||||
Flow,
|
||||
FlowDesignationEnum,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
FlowsApi,
|
||||
UsageEnum,
|
||||
} from "@goauthentik/api";
|
||||
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
OAuth2Provider,
|
||||
OAuth2ProviderLogoutMethodEnum,
|
||||
RedirectURI,
|
||||
RedirectURITypeEnum,
|
||||
RedirectUriTypeEnum,
|
||||
SubModeEnum,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
@@ -270,7 +270,7 @@ export function renderForm({
|
||||
.newItem=${() => ({
|
||||
matchingMode: MatchingModeEnum.Strict,
|
||||
url: "",
|
||||
redirectUriType: RedirectURITypeEnum.Authorization,
|
||||
redirectUriType: RedirectUriTypeEnum.Authorization,
|
||||
})}
|
||||
.row=${(redirectURI: RedirectURI, idx: number) => {
|
||||
return html`<ak-provider-oauth2-redirect-uri
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AKControlElement } from "#elements/ControlElement";
|
||||
import { LitPropertyRecord } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { MatchingModeEnum, RedirectURI, RedirectURITypeEnum } from "@goauthentik/api";
|
||||
import { MatchingModeEnum, RedirectURI, RedirectUriTypeEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html } from "lit";
|
||||
@@ -37,7 +37,7 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
|
||||
public redirectURI: RedirectURI = {
|
||||
matchingMode: MatchingModeEnum.Strict,
|
||||
url: "",
|
||||
redirectUriType: RedirectURITypeEnum.Authorization,
|
||||
redirectUriType: RedirectUriTypeEnum.Authorization,
|
||||
};
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
@@ -89,15 +89,15 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
|
||||
@change=${onChange}
|
||||
>
|
||||
<option
|
||||
value="${RedirectURITypeEnum.Authorization}"
|
||||
value="${RedirectUriTypeEnum.Authorization}"
|
||||
?selected=${(this.redirectURI.redirectUriType ??
|
||||
RedirectURITypeEnum.Authorization) === RedirectURITypeEnum.Authorization}
|
||||
RedirectUriTypeEnum.Authorization) === RedirectUriTypeEnum.Authorization}
|
||||
>
|
||||
${msg("Authorization")}
|
||||
</option>
|
||||
<option
|
||||
value="${RedirectURITypeEnum.Logout}"
|
||||
?selected=${this.redirectURI.redirectUriType === RedirectURITypeEnum.Logout}
|
||||
value="${RedirectUriTypeEnum.Logout}"
|
||||
?selected=${this.redirectURI.redirectUriType === RedirectUriTypeEnum.Logout}
|
||||
>
|
||||
${msg("Post Logout")}
|
||||
</option>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
GroupMatchingModeEnum,
|
||||
OAuthSource,
|
||||
OAuthSourceRequest,
|
||||
PatchedOAuthSourceRequest,
|
||||
PKCEMethodEnum,
|
||||
ProviderTypeEnum,
|
||||
SourcesApi,
|
||||
@@ -81,6 +82,20 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
private get isAtProtocolSource(): boolean {
|
||||
return (
|
||||
this.providerType?.name === ProviderTypeEnum.Atproto ||
|
||||
this.modelName?.includes("atproto") === true
|
||||
);
|
||||
}
|
||||
|
||||
private get isClientSecretRequired(): boolean {
|
||||
if (this.isAtProtocolSource) {
|
||||
return false;
|
||||
}
|
||||
return this.providerType?.clientSecretRequired !== false;
|
||||
}
|
||||
|
||||
protected async loadInstance(pk: string): Promise<OAuthSource> {
|
||||
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthRetrieve({
|
||||
slug: pk,
|
||||
@@ -97,16 +112,20 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
|
||||
|
||||
protected async send(data: OAuthSource): Promise<OAuthSource> {
|
||||
data.providerType = (this.providerType?.name || "") as ProviderTypeEnum;
|
||||
const requestData = data as unknown as OAuthSourceRequest & PatchedOAuthSourceRequest;
|
||||
if (!this.isClientSecretRequired) {
|
||||
requestData.consumerSecret = "";
|
||||
}
|
||||
|
||||
if (this.instance) {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
|
||||
slug: this.instance.slug,
|
||||
patchedOAuthSourceRequest: data,
|
||||
patchedOAuthSourceRequest: requestData,
|
||||
});
|
||||
}
|
||||
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
|
||||
oAuthSourceRequest: data as unknown as OAuthSourceRequest,
|
||||
oAuthSourceRequest: requestData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,9 +205,11 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"URL used to request the initial token. This URL is only required for OAuth 1.",
|
||||
)}
|
||||
${this.isAtProtocolSource
|
||||
? msg("URL used to create pushed authorization requests.")
|
||||
: msg(
|
||||
"URL used to request the initial token. This URL is only required for OAuth 1.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal> `
|
||||
: nothing}
|
||||
@@ -405,16 +426,22 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
|
||||
spellcheck="false"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${this.isAtProtocolSource
|
||||
? msg("Client metadata URL.")
|
||||
: msg("Also known as Client ID.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Consumer secret")}
|
||||
name="consumerSecret"
|
||||
input-hint="code"
|
||||
help=${msg("Also known as Client Secret.")}
|
||||
?required=${!this.instance}
|
||||
?revealed=${!this.instance}
|
||||
></ak-secret-textarea-input>
|
||||
${this.isClientSecretRequired
|
||||
? html`<ak-secret-textarea-input
|
||||
label=${msg("Consumer secret")}
|
||||
name="consumerSecret"
|
||||
input-hint="code"
|
||||
help=${msg("Also known as Client Secret.")}
|
||||
?required=${!this.instance}
|
||||
?revealed=${!this.instance}
|
||||
></ak-secret-textarea-input>`
|
||||
: nothing}
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -37,6 +37,8 @@ export function ProviderToLabel(provider?: ProviderTypeEnum): string {
|
||||
return "";
|
||||
case ProviderTypeEnum.Apple:
|
||||
return "Apple";
|
||||
case ProviderTypeEnum.Atproto:
|
||||
return "AT Protocol";
|
||||
case ProviderTypeEnum.Azuread:
|
||||
return "Azure Active Directory (Deprecated)";
|
||||
case ProviderTypeEnum.Discord:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import type { Stage } from "@goauthentik/api";
|
||||
import type { Stage } from "@goauthentik/api/dist/models/Stage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
|
||||
@@ -158,33 +158,24 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg(
|
||||
"No preference: the browser may offer any available authenticator",
|
||||
),
|
||||
label: msg("No preference is sent"),
|
||||
value: null,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg(
|
||||
"Platform: a non-removable authenticator built into the device, such as Touch ID, Face ID, or Windows Hello",
|
||||
"A non-removable authenticator, like TouchID or Windows Hello",
|
||||
),
|
||||
value: AuthenticatorAttachmentEnum.Platform,
|
||||
},
|
||||
{
|
||||
label: msg(
|
||||
"Cross-platform: a roaming authenticator, such as a YubiKey or Google Titan",
|
||||
),
|
||||
label: msg('A "roaming" authenticator, like a YubiKey'),
|
||||
value: AuthenticatorAttachmentEnum.CrossPlatform,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.authenticatorAttachment}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Controls the authenticatorAttachment parameter sent to the browser during WebAuthn registration. If Hints are configured and this is left as 'No preference', a value is inferred from the selected hints for backward compatibility with older browsers.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Hints")} name="hints">
|
||||
<ak-dual-select-provider
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AKElement } from "#elements/Base";
|
||||
import { Invitation, StagesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
@@ -27,30 +27,7 @@ export class InvitationListLink extends AKElement {
|
||||
@property()
|
||||
selectedFlow?: string;
|
||||
|
||||
/**
|
||||
* When true, the "Send via Email" button dispatches the
|
||||
* `ak-invitation-send-email-inline` event instead of opening the nested
|
||||
* email modal. Used by the invitation wizard's success step so the email
|
||||
* form can be rendered as its own wizard step.
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "inline-send-email" })
|
||||
inlineSendEmail = false;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFDescriptionList,
|
||||
PFButton,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
input.pf-c-form-control {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList, PFButton];
|
||||
|
||||
renderLink(): string {
|
||||
if (this.invitation?.flowObj) {
|
||||
@@ -126,7 +103,6 @@ export class InvitationListLink extends AKElement {
|
||||
class="pf-c-form-control"
|
||||
readonly
|
||||
type="text"
|
||||
style="width: 100%;"
|
||||
value=${this.renderLink()}
|
||||
/>
|
||||
</div>
|
||||
@@ -146,32 +122,18 @@ export class InvitationListLink extends AKElement {
|
||||
>
|
||||
${msg("Copy Link")}
|
||||
</button>
|
||||
${this.inlineSendEmail
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ak-invitation-send-email-inline", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
${msg("Send via Email")}
|
||||
</button>`
|
||||
: html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Send")}</span>
|
||||
<span slot="header">${msg("Send Invitation via Email")}</span>
|
||||
<ak-invitation-send-email-form
|
||||
slot="form"
|
||||
.invitation=${this.invitation}
|
||||
>
|
||||
</ak-invitation-send-email-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Send via Email")}
|
||||
</button>
|
||||
</ak-forms-modal>`}
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Send")}</span>
|
||||
<span slot="header">${msg("Send Invitation via Email")}</span>
|
||||
<ak-invitation-send-email-form
|
||||
slot="form"
|
||||
.invitation=${this.invitation}
|
||||
>
|
||||
</ak-invitation-send-email-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Send via Email")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#admin/stages/invitation/InvitationForm";
|
||||
import "#admin/stages/invitation/InvitationListLink";
|
||||
import "#admin/stages/invitation/wizard/InvitationWizard";
|
||||
import "#elements/buttons/Dropdown";
|
||||
import "#elements/buttons/ModalButton";
|
||||
import "#elements/buttons/SpinnerButton/ak-spinner-button";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
@@ -11,7 +9,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { IconEditButton, modalInvoker } from "#elements/dialogs";
|
||||
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
|
||||
import { PFColor } from "#elements/Label";
|
||||
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
@@ -20,12 +18,11 @@ import { SlottedTemplateResult } from "#elements/types";
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
|
||||
import { InvitationForm } from "#admin/stages/invitation/InvitationForm";
|
||||
import { InvitationWizard } from "#admin/stages/invitation/wizard/InvitationWizard";
|
||||
|
||||
import { FlowDesignationEnum, Invitation, ModelEnum, StagesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, PropertyValues, TemplateResult } from "lit";
|
||||
import { CSSResult, html, PropertyValues } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
@@ -142,66 +139,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
}
|
||||
|
||||
protected override renderObjectCreate(): SlottedTemplateResult {
|
||||
return html`${this.renderNewInvitationDropdown()}`;
|
||||
}
|
||||
|
||||
protected renderNewInvitationDropdown(): TemplateResult {
|
||||
return html`<ak-dropdown class="pf-c-dropdown">
|
||||
<div class="pf-c-dropdown__toggle pf-m-primary pf-m-split-button pf-m-action">
|
||||
<button
|
||||
class="pf-c-dropdown__toggle-button"
|
||||
type="button"
|
||||
${modalInvoker(InvitationWizard, { mode: "existing" })}
|
||||
>
|
||||
${msg("New Invitation")}
|
||||
</button>
|
||||
<button
|
||||
class="pf-c-dropdown__toggle-button"
|
||||
type="button"
|
||||
id="new-invitation-toggle"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="new-invitation-menu"
|
||||
tabindex="0"
|
||||
aria-label=${msg("New Invitation options")}
|
||||
>
|
||||
<i class="fas fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<menu
|
||||
class="pf-c-dropdown__menu"
|
||||
hidden
|
||||
id="new-invitation-menu"
|
||||
aria-labelledby="new-invitation-toggle"
|
||||
tabindex="-1"
|
||||
>
|
||||
<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
${modalInvoker(InvitationWizard, { mode: "existing" })}
|
||||
aria-description=${msg(
|
||||
"Opens the new invitation wizard and binds the invitation to an existing enrollment flow.",
|
||||
)}
|
||||
>
|
||||
${msg("with Existing Enrollment Flow...")}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="pf-c-dropdown__menu-item"
|
||||
${modalInvoker(InvitationWizard, { mode: "create" })}
|
||||
aria-description=${msg(
|
||||
"Opens the new invitation wizard, which will create a new enrollment flow and invitation stage.",
|
||||
)}
|
||||
>
|
||||
${msg("with New Enrollment Flow and Invitation Stage...")}
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
</ak-dropdown>`;
|
||||
return ModalInvokerButton(InvitationForm);
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import "#admin/stages/invitation/wizard/InvitationWizardDetailsStep";
|
||||
import "#admin/stages/invitation/wizard/InvitationWizardEmailStep";
|
||||
import "#admin/stages/invitation/wizard/InvitationWizardFlowStep";
|
||||
import "#admin/stages/invitation/wizard/InvitationWizardSuccessStep";
|
||||
import "#elements/wizard/Wizard";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { TransclusionChildElement, TransclusionChildSymbol } from "#elements/dialogs";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { property } from "@lit/reactive-element/decorators/property.js";
|
||||
import { html } from "lit";
|
||||
|
||||
export type InvitationWizardFlowMode = "existing" | "create";
|
||||
|
||||
@customElement("ak-invitation-wizard")
|
||||
export class InvitationWizard extends AKElement implements TransclusionChildElement {
|
||||
public static verboseName = msg("Invitation");
|
||||
|
||||
public [TransclusionChildSymbol] = true;
|
||||
|
||||
@property({ type: String })
|
||||
public mode: InvitationWizardFlowMode = "existing";
|
||||
|
||||
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
return html`<ak-wizard
|
||||
entity-singular=${msg("Invitation")}
|
||||
description=${msg("Create a new invitation with an enrollment flow.")}
|
||||
.initialSteps=${["flow-step", "details-step", "success-step"]}
|
||||
>
|
||||
<ak-invitation-wizard-flow-step
|
||||
slot="flow-step"
|
||||
headline=${msg("Enrollment Flow")}
|
||||
.mode=${this.mode}
|
||||
></ak-invitation-wizard-flow-step>
|
||||
<ak-invitation-wizard-details-step
|
||||
slot="details-step"
|
||||
headline=${msg("Invitation Details")}
|
||||
></ak-invitation-wizard-details-step>
|
||||
<ak-invitation-wizard-success-step
|
||||
slot="success-step"
|
||||
headline=${msg("Invitation Link")}
|
||||
></ak-invitation-wizard-success-step>
|
||||
<ak-invitation-wizard-email-step
|
||||
slot="email-step"
|
||||
headline=${msg("Send via Email")}
|
||||
></ak-invitation-wizard-email-step>
|
||||
</ak-wizard>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-invitation-wizard": InvitationWizard;
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
import "#components/ak-switch-input";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import type { InvitationWizardState } from "./types";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import {
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
pluckFallbackFieldErrors,
|
||||
} from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
import { dateTimeLocal } from "#common/temporal";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import { FlowsApi, ManagedApi, StagesApi } from "@goauthentik/api";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
const MINIMAL_BLUEPRINT_PATH = "example/flows-invitation-enrollment-minimal.yaml";
|
||||
|
||||
@customElement("ak-invitation-wizard-details-step")
|
||||
export class InvitationWizardDetailsStep extends WizardPage {
|
||||
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
|
||||
|
||||
@state()
|
||||
invitationName = "";
|
||||
|
||||
@state()
|
||||
invitationExpires: string = dateTimeLocal(new Date(Date.now() + 48 * 60 * 60 * 1000));
|
||||
|
||||
@state()
|
||||
fixedDataRaw = "{}";
|
||||
|
||||
@state()
|
||||
singleUse = true;
|
||||
|
||||
activeCallback = async (): Promise<void> => {
|
||||
this.host.valid = this.invitationName.length > 0;
|
||||
};
|
||||
|
||||
async #fail(step: string, err: unknown): Promise<false> {
|
||||
const parsed = await parseAPIResponseError(err);
|
||||
const fieldErrors = pluckFallbackFieldErrors(parsed);
|
||||
const detail = fieldErrors.length > 0 ? fieldErrors.join(" ") : pluckErrorDetail(parsed);
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg(str`${step} failed`),
|
||||
description: detail,
|
||||
});
|
||||
this.logger.error("Invitation wizard step failed", { step, error: err });
|
||||
return false;
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
let validYaml = true;
|
||||
try {
|
||||
YAML.parse(this.fixedDataRaw);
|
||||
} catch {
|
||||
validYaml = false;
|
||||
}
|
||||
this.host.valid =
|
||||
this.invitationName.length > 0 && this.invitationExpires.length > 0 && validYaml;
|
||||
}
|
||||
|
||||
nextCallback = async (): Promise<boolean> => {
|
||||
if (!this.invitationName) return false;
|
||||
|
||||
let fixedData: Record<string, unknown> = {};
|
||||
try {
|
||||
fixedData = YAML.parse(this.fixedDataRaw) || {};
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const wizardState = this.host.state as unknown as InvitationWizardState;
|
||||
|
||||
if (wizardState.createdInvitationPk) {
|
||||
return true;
|
||||
}
|
||||
|
||||
wizardState.invitationName = this.invitationName;
|
||||
wizardState.invitationExpires = this.invitationExpires;
|
||||
wizardState.invitationFixedData = fixedData;
|
||||
wizardState.invitationSingleUse = this.singleUse;
|
||||
|
||||
if (wizardState.needsFlow) {
|
||||
try {
|
||||
const result = await new ManagedApi(DEFAULT_CONFIG).managedBlueprintsImportCreate({
|
||||
path: MINIMAL_BLUEPRINT_PATH,
|
||||
context: JSON.stringify({
|
||||
flow_name: wizardState.newFlowName,
|
||||
flow_slug: wizardState.newFlowSlug,
|
||||
stage_name: wizardState.newStageName,
|
||||
continue_flow_without_invitation: wizardState.continueFlowWithoutInvitation,
|
||||
user_type: wizardState.newUserType,
|
||||
}),
|
||||
});
|
||||
if (!result.success) {
|
||||
const logs = (result.logs || [])
|
||||
.map((l) => l.event)
|
||||
.filter((m) => !!m)
|
||||
.join("\n");
|
||||
return this.#fail(
|
||||
msg("Importing enrollment flow blueprint"),
|
||||
new Error(logs || msg("Blueprint validation failed")),
|
||||
);
|
||||
}
|
||||
|
||||
const slugToLookup = wizardState.newFlowSlug!;
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
slug: slugToLookup,
|
||||
});
|
||||
const createdFlow = flows.results[0];
|
||||
if (!createdFlow) {
|
||||
return this.#fail(
|
||||
msg("Importing enrollment flow blueprint"),
|
||||
new Error(
|
||||
msg(str`Flow with slug "${slugToLookup}" not found after import`),
|
||||
),
|
||||
);
|
||||
}
|
||||
wizardState.createdFlowPk = createdFlow.pk;
|
||||
wizardState.createdFlowSlug = createdFlow.slug;
|
||||
wizardState.needsFlow = false;
|
||||
wizardState.needsStage = false;
|
||||
wizardState.needsBinding = false;
|
||||
} catch (err) {
|
||||
return this.#fail(msg("Importing enrollment flow blueprint"), err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const flowPk = wizardState.createdFlowPk || wizardState.selectedFlowPk || undefined;
|
||||
const invitation = await new StagesApi(
|
||||
DEFAULT_CONFIG,
|
||||
).stagesInvitationInvitationsCreate({
|
||||
invitationRequest: {
|
||||
name: wizardState.invitationName!,
|
||||
expires: wizardState.invitationExpires
|
||||
? new Date(wizardState.invitationExpires)
|
||||
: undefined,
|
||||
fixedData: wizardState.invitationFixedData,
|
||||
singleUse: wizardState.invitationSingleUse,
|
||||
flow: flowPk || null,
|
||||
},
|
||||
});
|
||||
wizardState.createdInvitationPk = invitation.pk;
|
||||
wizardState.createdInvitation = invitation;
|
||||
} catch (err) {
|
||||
return this.#fail(msg("Creating invitation"), err);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
override reset(): void {
|
||||
this.invitationName = "";
|
||||
this.invitationExpires = dateTimeLocal(new Date(Date.now() + 48 * 60 * 60 * 1000));
|
||||
this.fixedDataRaw = "{}";
|
||||
this.singleUse = true;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const wizardState = this.host.state as unknown as InvitationWizardState;
|
||||
const flowDisplay =
|
||||
wizardState.flowMode === "existing"
|
||||
? wizardState.selectedFlowSlug
|
||||
: wizardState.newFlowSlug;
|
||||
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Name")} required>
|
||||
<input
|
||||
type="text"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
.value=${this.invitationName}
|
||||
@input=${(ev: InputEvent) => {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.invitationName = target.value.replace(/[^a-z0-9-]/g, "");
|
||||
target.value = this.invitationName;
|
||||
this.validate();
|
||||
}}
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The name of an invitation must be a slug: only lower case letters, numbers, and the hyphen are permitted here.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Expires")} required>
|
||||
<input
|
||||
type="datetime-local"
|
||||
data-type="datetime-local"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
.value=${this.invitationExpires}
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.invitationExpires = (ev.target as HTMLInputElement).value;
|
||||
this.validate();
|
||||
}}
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Flow")}>
|
||||
<input
|
||||
type="text"
|
||||
class="pf-c-form-control"
|
||||
readonly
|
||||
disabled
|
||||
.value=${flowDisplay || ""}
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The flow selected in the previous step. The invitation will be bound to this flow.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Custom attributes")}>
|
||||
<ak-codemirror
|
||||
mode="yaml"
|
||||
.value=${this.fixedDataRaw}
|
||||
@change=${(ev: CustomEvent) => {
|
||||
this.fixedDataRaw = ev.detail.value;
|
||||
this.validate();
|
||||
}}
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-switch-input
|
||||
label=${msg("Single use")}
|
||||
?checked=${this.singleUse}
|
||||
@change=${(ev: Event) => {
|
||||
this.singleUse = (ev.target as HTMLInputElement).checked;
|
||||
}}
|
||||
help=${msg("When enabled, the invitation will be deleted after usage.")}
|
||||
></ak-switch-input>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-invitation-wizard-details-step": InvitationWizardDetailsStep;
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import "#components/ak-textarea-input";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import type { InvitationWizardState } from "./types";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import {
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
pluckFallbackFieldErrors,
|
||||
} from "#common/errors/network";
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import { StagesApi, TypeCreate } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-invitation-wizard-email-step")
|
||||
export class InvitationWizardEmailStep extends WizardPage {
|
||||
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
|
||||
|
||||
@state()
|
||||
toAddresses = "";
|
||||
|
||||
@state()
|
||||
ccAddresses = "";
|
||||
|
||||
@state()
|
||||
bccAddresses = "";
|
||||
|
||||
@state()
|
||||
template = "email/invitation.html";
|
||||
|
||||
@state()
|
||||
availableTemplates: TypeCreate[] = [];
|
||||
|
||||
override formatNextLabel(): SlottedTemplateResult {
|
||||
return html`${msg("Send")}
|
||||
<span class="pf-c-button__icon pf-m-end">
|
||||
<i class="fas fa-paper-plane" aria-hidden="true"></i>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
activeCallback = async (): Promise<void> => {
|
||||
this.host.valid = this.toAddresses.trim().length > 0;
|
||||
try {
|
||||
this.availableTemplates = await new StagesApi(
|
||||
DEFAULT_CONFIG,
|
||||
).stagesEmailTemplatesList();
|
||||
} catch {
|
||||
this.availableTemplates = [];
|
||||
}
|
||||
};
|
||||
|
||||
parseEmailAddresses(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,;]/)
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
this.host.valid = this.parseEmailAddresses(this.toAddresses).length > 0;
|
||||
}
|
||||
|
||||
nextCallback = async (): Promise<boolean> => {
|
||||
const wizardState = this.host.state as unknown as InvitationWizardState;
|
||||
const invitationPk = wizardState.createdInvitationPk;
|
||||
if (!invitationPk) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg("No invitation available to send"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const to = this.parseEmailAddresses(this.toAddresses);
|
||||
if (to.length === 0) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg("Please enter at least one email address"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const cc = this.parseEmailAddresses(this.ccAddresses);
|
||||
const bcc = this.parseEmailAddresses(this.bccAddresses);
|
||||
|
||||
try {
|
||||
await new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsSendEmailCreate({
|
||||
inviteUuid: invitationPk,
|
||||
invitationSendEmailRequest: {
|
||||
emailAddresses: to,
|
||||
ccAddresses: cc.length > 0 ? cc : undefined,
|
||||
bccAddresses: bcc.length > 0 ? bcc : undefined,
|
||||
template: this.template,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const parsed = await parseAPIResponseError(err);
|
||||
const fieldErrors = pluckFallbackFieldErrors(parsed);
|
||||
const detail =
|
||||
fieldErrors.length > 0 ? fieldErrors.join(" ") : pluckErrorDetail(parsed);
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg("Failed to queue invitation emails"),
|
||||
description: detail,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg(
|
||||
str`Invitation emails queued for sending to ${to.length} recipient(s). Check the System Tasks for more information.`,
|
||||
),
|
||||
});
|
||||
this.dispatchEvent(new AKRefreshEvent());
|
||||
return true;
|
||||
};
|
||||
|
||||
override reset(): void {
|
||||
this.toAddresses = "";
|
||||
this.ccAddresses = "";
|
||||
this.bccAddresses = "";
|
||||
this.template = "email/invitation.html";
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("To")} required>
|
||||
<textarea
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
rows="3"
|
||||
.value=${this.toAddresses}
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.toAddresses = (ev.target as HTMLTextAreaElement).value;
|
||||
this.validate();
|
||||
}}
|
||||
></textarea>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"One email address per line, or comma/semicolon separated. Each recipient will receive a separate email with an invitation link.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("CC")}>
|
||||
<textarea
|
||||
class="pf-c-form-control"
|
||||
rows="2"
|
||||
.value=${this.ccAddresses}
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.ccAddresses = (ev.target as HTMLTextAreaElement).value;
|
||||
}}
|
||||
></textarea>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"A comma-separated list of addresses to receive copies of the invitation. Recipients will receive the full list of other addresses in this list.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("BCC")}>
|
||||
<textarea
|
||||
class="pf-c-form-control"
|
||||
rows="2"
|
||||
.value=${this.bccAddresses}
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.bccAddresses = (ev.target as HTMLTextAreaElement).value;
|
||||
}}
|
||||
></textarea>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"A comma-separated list of addresses to receive copies of the invitation. Recipients will not receive the addresses of other recipients.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Template")} required>
|
||||
<select
|
||||
class="pf-c-form-control"
|
||||
@change=${(ev: Event) => {
|
||||
this.template = (ev.target as HTMLSelectElement).value;
|
||||
}}
|
||||
>
|
||||
${this.availableTemplates.map(
|
||||
(template) =>
|
||||
html`<option
|
||||
value=${template.name}
|
||||
?selected=${template.name === this.template}
|
||||
>
|
||||
${template.description}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Select the email template to use for sending invitations.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-invitation-wizard-email-step": InvitationWizardEmailStep;
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
import "#components/ak-radio-input";
|
||||
import "#components/ak-switch-input";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
|
||||
import type { InvitationWizardState } from "./types";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import {
|
||||
FlowDesignationEnum,
|
||||
type FlowSet,
|
||||
type InvitationStage,
|
||||
StagesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
interface EnrollmentFlow {
|
||||
slug: string;
|
||||
pk: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement("ak-invitation-wizard-flow-step")
|
||||
export class InvitationWizardFlowStep extends WizardPage {
|
||||
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl, PFButton, PFAlert];
|
||||
|
||||
@property({ type: String })
|
||||
public mode: "existing" | "create" = "existing";
|
||||
|
||||
@state()
|
||||
enrollmentFlows: EnrollmentFlow[] = [];
|
||||
|
||||
@state()
|
||||
loading = true;
|
||||
|
||||
@state()
|
||||
selectedFlowSlug?: string;
|
||||
|
||||
@state()
|
||||
selectedFlowPk?: string;
|
||||
|
||||
@state()
|
||||
newFlowName = "Enrollment with invitation";
|
||||
|
||||
@state()
|
||||
newFlowSlug = "enrollment-with-invitation";
|
||||
|
||||
@state()
|
||||
newStageName = "invitation-stage";
|
||||
|
||||
@state()
|
||||
newUserType: "external" | "internal" = "external";
|
||||
|
||||
@state()
|
||||
continueFlowWithoutInvitation = true;
|
||||
|
||||
activeCallback = async (): Promise<void> => {
|
||||
this.host.valid = false;
|
||||
|
||||
if (this.mode === "create") {
|
||||
this.loading = false;
|
||||
this.validate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({
|
||||
noFlows: false,
|
||||
});
|
||||
|
||||
const flowMap = new Map<string, EnrollmentFlow>();
|
||||
|
||||
stages.results.forEach((stage: InvitationStage) => {
|
||||
(stage.flowSet || [])
|
||||
.filter((flow: FlowSet) => flow.designation === FlowDesignationEnum.Enrollment)
|
||||
.forEach((flow: FlowSet) => {
|
||||
if (!flowMap.has(flow.slug)) {
|
||||
flowMap.set(flow.slug, {
|
||||
slug: flow.slug,
|
||||
pk: flow.pk,
|
||||
name: flow.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.enrollmentFlows = Array.from(flowMap.values());
|
||||
|
||||
if (this.enrollmentFlows.length > 0) {
|
||||
this.selectedFlowSlug = this.enrollmentFlows[0].slug;
|
||||
this.selectedFlowPk = this.enrollmentFlows[0].pk;
|
||||
this.host.valid = true;
|
||||
}
|
||||
} catch {
|
||||
this.enrollmentFlows = [];
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
// If there's exactly one eligible flow, skip this step so the user goes
|
||||
// straight to the invitation details. Drop ourselves from the step list
|
||||
// so the back button from the next step doesn't bounce back here.
|
||||
if (this.mode === "existing" && this.enrollmentFlows.length === 1) {
|
||||
const currentSlot = this.slot;
|
||||
const advanced = await this.host.navigateNext();
|
||||
if (advanced) {
|
||||
this.host.steps = this.host.steps.filter((s) => s !== currentSlot);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
validate(): void {
|
||||
if (this.mode === "existing") {
|
||||
this.host.valid = !!this.selectedFlowSlug;
|
||||
} else {
|
||||
this.host.valid =
|
||||
this.newFlowName.length > 0 &&
|
||||
this.newFlowSlug.length > 0 &&
|
||||
this.newStageName.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
nextCallback = async (): Promise<boolean> => {
|
||||
const state = this.host.state as unknown as InvitationWizardState;
|
||||
|
||||
state.flowMode = this.mode;
|
||||
|
||||
if (this.mode === "existing") {
|
||||
if (!this.selectedFlowSlug) return false;
|
||||
state.selectedFlowSlug = this.selectedFlowSlug;
|
||||
state.selectedFlowPk = this.selectedFlowPk;
|
||||
state.needsFlow = false;
|
||||
state.needsStage = false;
|
||||
state.needsBinding = false;
|
||||
} else {
|
||||
if (!this.newFlowName || !this.newFlowSlug || !this.newStageName) return false;
|
||||
state.newFlowName = this.newFlowName;
|
||||
state.newFlowSlug = this.newFlowSlug;
|
||||
state.newStageName = this.newStageName;
|
||||
state.newUserType = this.newUserType;
|
||||
state.continueFlowWithoutInvitation = this.continueFlowWithoutInvitation;
|
||||
state.needsFlow = true;
|
||||
state.needsStage = true;
|
||||
state.needsBinding = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
override reset(): void {
|
||||
this.enrollmentFlows = [];
|
||||
this.loading = true;
|
||||
this.selectedFlowSlug = undefined;
|
||||
this.selectedFlowPk = undefined;
|
||||
this.newFlowName = "Enrollment with invitation";
|
||||
this.newFlowSlug = "enrollment-with-invitation";
|
||||
this.newStageName = "invitation-stage";
|
||||
this.newUserType = "external";
|
||||
this.continueFlowWithoutInvitation = true;
|
||||
}
|
||||
|
||||
slugify(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
renderExistingFlowSelector(): TemplateResult {
|
||||
if (this.enrollmentFlows.length === 0) {
|
||||
return html`
|
||||
<div class="pf-c-alert pf-m-warning pf-m-inline">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">
|
||||
${msg("No enrollment flows with invitation stages found")}
|
||||
</h4>
|
||||
<div class="pf-c-alert__description">
|
||||
<p>
|
||||
${msg(
|
||||
"You can create a new enrollment flow and invitation stage right here, or cancel and bind an invitation stage to an existing flow manually.",
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${() => {
|
||||
this.mode = "create";
|
||||
this.validate();
|
||||
}}
|
||||
>
|
||||
${msg("Create a new enrollment flow")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Enrollment flow")} required>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<EnrollmentFlow[]> => {
|
||||
if (!query) return this.enrollmentFlows;
|
||||
const needle = query.toLowerCase();
|
||||
return this.enrollmentFlows.filter(
|
||||
(flow) =>
|
||||
flow.name.toLowerCase().includes(needle) ||
|
||||
flow.slug.toLowerCase().includes(needle),
|
||||
);
|
||||
}}
|
||||
.renderElement=${(flow: EnrollmentFlow): string => flow.name}
|
||||
.renderDescription=${(flow: EnrollmentFlow): TemplateResult =>
|
||||
html`${flow.slug}`}
|
||||
.value=${(flow: EnrollmentFlow | undefined): string | undefined => flow?.pk}
|
||||
.selected=${(flow: EnrollmentFlow): boolean => flow.pk === this.selectedFlowPk}
|
||||
@ak-change=${(ev: CustomEvent<{ value: EnrollmentFlow | null }>) => {
|
||||
const flow = ev.detail.value;
|
||||
this.selectedFlowSlug = flow?.slug;
|
||||
this.selectedFlowPk = flow?.pk;
|
||||
this.validate();
|
||||
}}
|
||||
style="display: block; width: 100%;"
|
||||
></ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Only enrollment flows that have an invitation stage bound to them are listed here.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
`;
|
||||
}
|
||||
|
||||
renderCreateForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Flow name")} required>
|
||||
<input
|
||||
type="text"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
.value=${this.newFlowName}
|
||||
@input=${(ev: InputEvent) => {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.newFlowName = target.value;
|
||||
this.newFlowSlug = this.slugify(target.value);
|
||||
this.validate();
|
||||
}}
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Name for the new enrollment flow.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Flow slug")} required>
|
||||
<input
|
||||
type="text"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
.value=${this.newFlowSlug}
|
||||
@input=${(ev: InputEvent) => {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.newFlowSlug = target.value.replace(/[^a-z0-9-]/g, "");
|
||||
this.validate();
|
||||
}}
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Visible in the URL.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Invitation stage name")} required>
|
||||
<input
|
||||
type="text"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
.value=${this.newStageName}
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.newStageName = (ev.target as HTMLInputElement).value;
|
||||
this.validate();
|
||||
}}
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Name for the new invitation stage.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-radio-input
|
||||
label=${msg("User type")}
|
||||
.value=${this.newUserType}
|
||||
.options=${[
|
||||
{
|
||||
label: msg("External"),
|
||||
value: "external",
|
||||
description: html`${msg(
|
||||
"Enrolled users are created as external (e.g. customers, guests). New users will be placed under users/external.",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Internal"),
|
||||
value: "internal",
|
||||
description: html`${msg(
|
||||
"Enrolled users are created as internal (e.g. employees). New users will be placed under users/internal.",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
@input=${(ev: CustomEvent<{ value: "external" | "internal" }>) => {
|
||||
this.newUserType = ev.detail.value;
|
||||
}}
|
||||
></ak-radio-input>
|
||||
<ak-switch-input
|
||||
label=${msg("Continue flow without invitation")}
|
||||
?checked=${this.continueFlowWithoutInvitation}
|
||||
@change=${(ev: Event) => {
|
||||
this.continueFlowWithoutInvitation = (ev.target as HTMLInputElement).checked;
|
||||
}}
|
||||
help=${msg(
|
||||
"If enabled, the stage will jump to the next stage when no invitation is given. If disabled, the flow will be cancelled without a valid invitation.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`<div class="pf-c-form">
|
||||
<p>${msg("Loading...")}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
${this.mode === "existing"
|
||||
? this.renderExistingFlowSelector()
|
||||
: this.renderCreateForm()}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-invitation-wizard-flow-step": InvitationWizardFlowStep;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import "#admin/stages/invitation/InvitationListLink";
|
||||
|
||||
import type { InvitationWizardState } from "./types";
|
||||
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import { Invitation } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-invitation-wizard-success-step")
|
||||
export class InvitationWizardSuccessStep extends WizardPage {
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFForm,
|
||||
PFAlert,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ak-stage-invitation-list-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
invitation?: Invitation;
|
||||
|
||||
#notified = false;
|
||||
|
||||
activeCallback = async (): Promise<void> => {
|
||||
const wizardState = this.host.state as unknown as InvitationWizardState;
|
||||
this.invitation = wizardState.createdInvitation;
|
||||
this.host.valid = true;
|
||||
|
||||
if (this.invitation && !this.#notified) {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg("Successfully created invitation."),
|
||||
});
|
||||
this.#notified = true;
|
||||
}
|
||||
};
|
||||
|
||||
nextCallback = async (): Promise<boolean> => {
|
||||
this.dispatchEvent(new AKRefreshEvent());
|
||||
return true;
|
||||
};
|
||||
|
||||
override reset(): void {
|
||||
this.invitation = undefined;
|
||||
this.#notified = false;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const invitation = this.invitation;
|
||||
|
||||
if (!invitation) {
|
||||
return html`<div class="pf-c-alert pf-m-warning pf-m-inline">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">${msg("No invitation was created.")}</h4>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ak-stage-invitation-list-link
|
||||
.invitation=${invitation}
|
||||
?inline-send-email=${true}
|
||||
@ak-invitation-send-email-inline=${this.onSendViaEmail}
|
||||
></ak-stage-invitation-list-link>
|
||||
`;
|
||||
}
|
||||
|
||||
onSendViaEmail = async (): Promise<void> => {
|
||||
const steps = this.host.steps;
|
||||
if (!steps.includes("email-step")) {
|
||||
this.host.steps = [...steps, "email-step"];
|
||||
}
|
||||
await this.host.navigateNext();
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-invitation-wizard-success-step": InvitationWizardSuccessStep;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { Invitation } from "@goauthentik/api";
|
||||
|
||||
export interface InvitationWizardState {
|
||||
// Step 1: Flow selection
|
||||
flowMode: "existing" | "create";
|
||||
selectedFlowSlug?: string;
|
||||
selectedFlowPk?: string;
|
||||
newFlowName?: string;
|
||||
newFlowSlug?: string;
|
||||
newStageName?: string;
|
||||
newUserType?: "external" | "internal";
|
||||
continueFlowWithoutInvitation: boolean;
|
||||
|
||||
// Flags for which API calls to make
|
||||
needsFlow: boolean;
|
||||
needsStage: boolean;
|
||||
needsBinding: boolean;
|
||||
|
||||
// Step 2: Invitation details
|
||||
invitationName?: string;
|
||||
invitationExpires?: string;
|
||||
invitationFixedData?: Record<string, unknown>;
|
||||
invitationSingleUse: boolean;
|
||||
|
||||
// Results from API calls
|
||||
createdStagePk?: string;
|
||||
createdFlowPk?: string;
|
||||
createdFlowSlug?: string;
|
||||
createdInvitationPk?: string;
|
||||
createdInvitation?: Invitation;
|
||||
}
|
||||
@@ -50,9 +50,7 @@ export class RedirectStageForm extends BaseStageForm<RedirectStage> {
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
return html`<span>
|
||||
${msg(
|
||||
"Redirect the user to a static URL or another flow, optionally with all gathered context.",
|
||||
)}
|
||||
${msg("Redirect the user to another flow, potentially with all gathered context")}
|
||||
</span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
StagesApi,
|
||||
UserCreationModeEnum,
|
||||
UserTypeEnum,
|
||||
UserWriteStage,
|
||||
} from "@goauthentik/api";
|
||||
import { UserCreationModeEnum } from "@goauthentik/api/dist/models/UserCreationModeEnum.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
@@ -159,7 +159,7 @@ export class UserBulkRevokeSessionsForm extends ModalButton {
|
||||
>
|
||||
</ak-user-bulk-revoke-sessions-table>
|
||||
</section>
|
||||
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (width <= 1210px) {
|
||||
@media (width < 1200px) {
|
||||
column-gap: calc(var(--pf-global--spacer--md) / 2);
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width <= 1210px) {
|
||||
@media (width < 1200px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@
|
||||
grid-area: toggle;
|
||||
}
|
||||
|
||||
@media (width > 1210px) {
|
||||
@media (width >= 1200px) {
|
||||
slot[name="toggle"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ function resolvePath(...args: string[]): string {
|
||||
* - Intercepts local links and scrolls to the target element.
|
||||
*/
|
||||
export const MDXAnchor = ({
|
||||
href: initialHref,
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const { publicDirectory } = useMDXModule();
|
||||
let href = initialHref;
|
||||
|
||||
if (href?.startsWith(".") && publicDirectory) {
|
||||
const nextPathname = resolvePath(publicDirectory, href);
|
||||
|
||||
@@ -74,10 +74,6 @@ svg[id^="mermaid-svg-"] {
|
||||
}
|
||||
}
|
||||
|
||||
ak-alert + :is(h2, p) {
|
||||
padding-top: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
/* #region Dark Theme */
|
||||
|
||||
:host([theme="dark"]) {
|
||||
|
||||
@@ -211,11 +211,9 @@ export class SimpleTable
|
||||
);
|
||||
|
||||
return html`<tr role="presentation">
|
||||
<td role="presentation" colspan=${columnCount + 1}>
|
||||
<td role="presentation" colspan=${columnCount}>
|
||||
<div class="pf-l-bullseye">
|
||||
<slot name="empty-table">
|
||||
<ak-empty-state><span>${message}</span></ak-empty-state>
|
||||
</slot>
|
||||
<ak-empty-state><span>${message}</span></ak-empty-state>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
);
|
||||
--ak-c-command-palette__group--Color: var(--pf-global--palette--purple-100);
|
||||
|
||||
--ak-c-fieldset--BorderColor: transparent;
|
||||
--ak-fieldset--BorderColor: transparent;
|
||||
|
||||
--ak-c-command-palette__item--BackgroundColor: transparent;
|
||||
--ak-c-command-palette__item--Color: var(--pf-global--palette--purple-50);
|
||||
@@ -37,7 +37,7 @@
|
||||
transform: translate3d(0, 0, 0); /* Fixes rendering artifacts. */
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
--ak-c-fieldset--BorderColor: var(--pf-global--palette--purple-500);
|
||||
--ak-fieldset--BorderColor: var(--pf-global--palette--purple-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
transition-duration: 0.2s;
|
||||
|
||||
legend {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
|
||||
cursor: pointer;
|
||||
color: var(--ak-c-command-palette__group--Color);
|
||||
|
||||
@@ -524,7 +524,7 @@ export class AKCommandPaletteModal extends AKModal {
|
||||
Object.entries(grouped),
|
||||
(_group, groupIdx) => `group-${groupIdx}`,
|
||||
([groupLabel, commands], groupIdx) => html`
|
||||
<fieldset class="ak-c-fieldset" part="results-group">
|
||||
<fieldset part="results-group">
|
||||
<legend
|
||||
class="${!groupLabel ? "sr-only more-contrast-only" : ""}"
|
||||
data-label=${ifPresent(groupLabel)}
|
||||
|
||||
@@ -115,8 +115,8 @@
|
||||
/* #region Footer */
|
||||
|
||||
fieldset.ak-c-dialog__footer {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
|
||||
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
padding-block: calc(var(--ak-fieldset__legend--PaddingInlineBase) / 2);
|
||||
border-inline: none;
|
||||
border-block-end: none;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { checkObjectShallowEquality } from "#common/collections";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { asInvoker, type ModalTemplate } from "#elements/dialogs/invokers";
|
||||
import type { DialogInit, TransclusionElementConstructor } from "#elements/dialogs/shared";
|
||||
import { ElementConstructorBoundary } from "#elements/errors/boundaries";
|
||||
import type { LitPropertyRecord } from "#elements/types";
|
||||
import { isAKElementConstructor, StrictUnsafe } from "#elements/utils/unsafe";
|
||||
|
||||
@@ -160,22 +159,10 @@ export function lookupElementConstructor<T extends CustomElementConstructor>(
|
||||
tagName: string,
|
||||
registry: CustomElementRegistry = window.customElements,
|
||||
): T {
|
||||
if (!tagName) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(
|
||||
"No tag name provided for lookup. Did this value come from a different version of authentik?",
|
||||
);
|
||||
|
||||
return ElementConstructorBoundary as unknown as T;
|
||||
}
|
||||
|
||||
const ElementConstructor = registry.get(tagName);
|
||||
|
||||
if (!ElementConstructor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(`No custom element defined for tag name: ${tagName}`);
|
||||
|
||||
return ElementConstructorBoundary as unknown as T;
|
||||
throw new TypeError(`No custom element defined for tag name: ${tagName}`);
|
||||
}
|
||||
|
||||
return ElementConstructor as unknown as T;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { CapabilitiesEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html } from "lit-html";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
|
||||
/**
|
||||
* A fallback element to render when a custom element fails to load, either due to a missing import,
|
||||
* or a version mismatch between the element's definition and its usage.
|
||||
*/
|
||||
@customElement("ak-element-missing")
|
||||
export class ElementConstructorBoundary extends AKElement {
|
||||
public styles = [PFAlert];
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const debug = globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug);
|
||||
|
||||
const description = debug
|
||||
? msg(
|
||||
"The element could not be loaded. This may be due to a missing import or a version mismatch.",
|
||||
)
|
||||
: msg(
|
||||
"An element could not be loaded. Please try refreshing the page or clearing your cache.",
|
||||
);
|
||||
|
||||
return html`<div class="pf-c-alert pf-m-danger" role="alert">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">${msg("Failed to load element")}</h4>
|
||||
<div class="pf-c-alert__description">${description}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export class ConfirmationForm extends ModalButton {
|
||||
<slot class="pf-c-content" name="body"></slot>
|
||||
</form>
|
||||
</section>
|
||||
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
|
||||
@@ -119,7 +119,7 @@ export class DeleteBulkForm<T> extends ModalButton {
|
||||
>
|
||||
</ak-used-by-table>
|
||||
</section>
|
||||
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
|
||||
@@ -218,7 +218,7 @@ export class ModalForm extends ModalButton {
|
||||
}
|
||||
|
||||
protected renderActions(): SlottedTemplateResult {
|
||||
return html`<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
return html`<fieldset class="pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -20,14 +20,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* P5 puts a line separating the entries, but this looks odd in our stacked usage. Specifying
|
||||
* `.pf-m-stack` here also raises the specificity above the P4 default.
|
||||
*/
|
||||
.pf-m-stack label.pf-c-radio:not(:last-child) {
|
||||
--pf-c-radio--BoxShadowColor: transparent;
|
||||
}
|
||||
|
||||
.pf-c-radio__description {
|
||||
text-wrap: balance;
|
||||
text-wrap: pretty;
|
||||
|
||||
@@ -996,9 +996,7 @@ export abstract class Table<T extends object, D = T>
|
||||
* A simple pagination display, shown at both the top and bottom of the page.
|
||||
*/
|
||||
protected renderTablePagination(): SlottedTemplateResult {
|
||||
if (!this.paginated || !this.data || this.data?.pagination.totalPages < 2) {
|
||||
return nothing;
|
||||
}
|
||||
if (!this.paginated) return nothing;
|
||||
|
||||
const handler = (page: number) => {
|
||||
this.page = page;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user