mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
26 Commits
sdko/atpro
...
rust-proxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b942ab9f2 | ||
|
|
34d575ec9f | ||
|
|
c1ce21a037 | ||
|
|
59253c4065 | ||
|
|
76e6c33b10 | ||
|
|
76dbfd4051 | ||
|
|
5357f42029 | ||
|
|
716bc6e136 | ||
|
|
60355fdf80 | ||
|
|
828a380569 | ||
|
|
b04f8a6177 | ||
|
|
ff190847f2 | ||
|
|
a7339c7f87 | ||
|
|
38ae472f6c | ||
|
|
7d0656c6fa | ||
|
|
0bbe415b5b | ||
|
|
e52c1b2bdc | ||
|
|
5064167f28 | ||
|
|
bca0f51b53 | ||
|
|
67c197e5a5 | ||
|
|
32b17da699 | ||
|
|
c75eed630a | ||
|
|
9f17d6df96 | ||
|
|
13c8ad5c56 | ||
|
|
28209c03e2 | ||
|
|
f47cf08d8a |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -64,7 +64,7 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@51cd0b8c0499559d9a4d75c0f5c67bec3a894ec8 # v2
|
||||
uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
|
||||
127
Cargo.lock
generated
127
Cargo.lock
generated
@@ -176,10 +176,12 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"argh",
|
||||
"authentik-axum",
|
||||
"authentik-client",
|
||||
"authentik-common",
|
||||
"axum",
|
||||
"color-eyre",
|
||||
"eyre",
|
||||
"futures",
|
||||
"hyper-unix-socket",
|
||||
"hyper-util",
|
||||
"metrics",
|
||||
@@ -187,9 +189,18 @@ dependencies = [
|
||||
"nix 0.31.2",
|
||||
"pyo3",
|
||||
"pyo3-build-config",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"sqlx",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-retry2",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"which",
|
||||
]
|
||||
@@ -542,6 +553,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
@@ -779,6 +801,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
@@ -1003,6 +1034,17 @@ 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"
|
||||
@@ -1219,6 +1261,21 @@ 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"
|
||||
@@ -1265,6 +1322,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
@@ -1300,6 +1358,12 @@ 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"
|
||||
@@ -1857,6 +1921,17 @@ 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"
|
||||
@@ -1928,6 +2003,19 @@ 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"
|
||||
@@ -1977,11 +2065,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "metrics-exporter-prometheus"
|
||||
version = "0.18.1"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda"
|
||||
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"evmap",
|
||||
"indexmap",
|
||||
"metrics",
|
||||
"metrics-util",
|
||||
@@ -2755,6 +2844,17 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -2793,6 +2893,12 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_xoshiro"
|
||||
version = "0.7.0"
|
||||
@@ -3103,6 +3209,12 @@ 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"
|
||||
@@ -3356,7 +3468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -3367,7 +3479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -3937,8 +4049,12 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tungstenite",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4152,8 +4268,11 @@ dependencies = [
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
22
Cargo.toml
22
Cargo.toml
@@ -44,12 +44,13 @@ 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.1", default-features = false }
|
||||
metrics-exporter-prometheus = { version = "= 0.18.3", default-features = false }
|
||||
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
|
||||
notify = "= 8.2.0"
|
||||
pin-project-lite = "= 0.2.17"
|
||||
pyo3 = "= 0.28.3"
|
||||
pyo3-build-config = "= 0.28.3"
|
||||
rand = "= 0.10.1"
|
||||
regex = "= 1.12.3"
|
||||
reqwest = { version = "= 0.13.3", features = [
|
||||
"form",
|
||||
@@ -100,6 +101,10 @@ time = { version = "= 0.3.47", features = ["macros"] }
|
||||
tokio = { version = "= 1.52.1", features = ["full", "tracing"] }
|
||||
tokio-retry2 = "= 0.9.1"
|
||||
tokio-rustls = "= 0.26.4"
|
||||
tokio-tungstenite = { version = "= 0.29.0", features = [
|
||||
"rustls-tls-webpki-roots",
|
||||
"url",
|
||||
] }
|
||||
tokio-util = { version = "= 0.7.18", features = ["full"] }
|
||||
tower = "= 0.5.3"
|
||||
tower-http = { version = "= 0.6.8", features = ["timeout"] }
|
||||
@@ -260,28 +265,39 @@ publish.workspace = true
|
||||
[features]
|
||||
default = ["core", "proxy"]
|
||||
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
|
||||
proxy = ["ak-common/proxy"]
|
||||
proxy = ["ak-common/proxy", "dep:ak-client"]
|
||||
|
||||
[build-dependencies]
|
||||
pyo3-build-config.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ak-axum.workspace = true
|
||||
ak-client = { workspace = true, optional = true }
|
||||
ak-common.workspace = true
|
||||
arc-swap.workspace = true
|
||||
argh.workspace = true
|
||||
axum.workspace = true
|
||||
color-eyre.workspace = true
|
||||
eyre.workspace = true
|
||||
futures.workspace = true
|
||||
hyper-unix-socket.workspace = true
|
||||
hyper-util.workspace = true
|
||||
metrics.workspace = true
|
||||
metrics-exporter-prometheus.workspace = true
|
||||
metrics.workspace = true
|
||||
nix.workspace = true
|
||||
pyo3 = { workspace = true, optional = true }
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_repr.workspace = true
|
||||
sqlx = { workspace = true, optional = true }
|
||||
time.workspace = true
|
||||
tokio-retry2.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
tokio.workspace = true
|
||||
tower.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
|
||||
@@ -32,19 +32,19 @@ from authentik.rbac.decorators import permission_required
|
||||
class UserAgentDeviceDict(TypedDict):
|
||||
"""User agent device"""
|
||||
|
||||
brand: str
|
||||
brand: str | None = None
|
||||
family: str
|
||||
model: str
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class UserAgentOSDict(TypedDict):
|
||||
"""User agent os"""
|
||||
|
||||
family: str
|
||||
major: str
|
||||
minor: str
|
||||
patch: str
|
||||
patch_minor: str
|
||||
major: str | None = None
|
||||
minor: str | None = None
|
||||
patch: str | None = None
|
||||
patch_minor: str | None = None
|
||||
|
||||
|
||||
class UserAgentBrowserDict(TypedDict):
|
||||
|
||||
@@ -187,6 +187,7 @@ 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,7 +33,6 @@ 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):
|
||||
@@ -66,15 +65,6 @@ 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
|
||||
|
||||
@@ -159,7 +149,7 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
"authorization_code_auth_method",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"consumer_secret": {"write_only": True, "allow_blank": True, "required": False},
|
||||
"consumer_secret": {"write_only": True},
|
||||
"request_token_url": {"allow_blank": True},
|
||||
"authorization_url": {"allow_blank": True},
|
||||
"access_token_url": {"allow_blank": True},
|
||||
|
||||
@@ -10,7 +10,6 @@ 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,15 +271,6 @@ 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."""
|
||||
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,486 +0,0 @@
|
||||
"""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,8 +42,6 @@ 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 another flow, potentially with all gathered context."""
|
||||
"""Redirect the user to a static URL or another flow, optionally with all gathered context."""
|
||||
|
||||
keep_context = models.BooleanField(default=True)
|
||||
mode = models.TextField(choices=RedirectMode.choices)
|
||||
|
||||
@@ -12967,7 +12967,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"apple",
|
||||
"atproto",
|
||||
"openidconnect",
|
||||
"entraid",
|
||||
"azuread",
|
||||
@@ -13039,6 +13038,7 @@
|
||||
},
|
||||
"consumer_secret": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Consumer secret"
|
||||
},
|
||||
"additional_scopes": {
|
||||
|
||||
@@ -101,8 +101,6 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
rustc --version && \
|
||||
cargo --version
|
||||
|
||||
RUN cat /root/.rustup/settings.toml
|
||||
|
||||
# Stage: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.11.5@sha256:555ac94f9a22e656fc5f2ce5dfee13b04e94d099e46bb8dd3a73ec7263f2e484 AS uv
|
||||
# Stage: Base python image
|
||||
|
||||
@@ -21,33 +21,45 @@ COPY web .
|
||||
RUN npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.2-trixie@sha256:4a7137ea573f79c86ae451ff05817ed762ef5597fcf732259e97abeb3108d873 AS builder
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG GOOS=$TARGETOS
|
||||
ARG GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
dpkg --add-architecture arm64 && \
|
||||
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Build essentials
|
||||
build-essential \
|
||||
# aws-lc deps
|
||||
cmake clang golang && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain none && \
|
||||
rustup install && \
|
||||
rustup default "$(sed -n 's/channel = "\(.*\)"/\1/p' rust-toolchain.toml)" && \
|
||||
rustc --version && \
|
||||
cargo --version
|
||||
# See https://github.com/aws/aws-lc-rs/issues/569
|
||||
ENV AWS_LC_FIPS_SYS_CC=clang
|
||||
|
||||
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
|
||||
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
||||
go build -o /go/proxy ./cmd/proxy
|
||||
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
|
||||
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
|
||||
--mount=type=bind,target=.cargo/,src=.cargo/ \
|
||||
--mount=type=bind,target=src/,src=src/ \
|
||||
--mount=type=bind,target=packages/,src=packages/ \
|
||||
--mount=type=bind,target=authentik/lib/default.yml,src=authentik/lib/default.yml \
|
||||
# Required otherwise workspace discovery fails
|
||||
--mount=type=bind,target=website/scripts/docsmg/,src=website/scripts/docsmg/ \
|
||||
--mount=type=cache,id=cargo-git-db-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/git/db/ \
|
||||
--mount=type=cache,id=cargo-registry-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/registry/ \
|
||||
--mount=type=cache,id=rust-target-$TARGETARCH$TARGETVARIANT,target=/build/target/ \
|
||||
cargo build --package authentik --no-default-features --features proxy --locked --release && \
|
||||
cp ./target/release/authentik /bin/authentik
|
||||
|
||||
# Stage 3: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341
|
||||
@@ -72,13 +84,13 @@ RUN apt-get update && \
|
||||
apt-get clean && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /go/proxy /
|
||||
COPY --from=builder /bin/authentik /
|
||||
COPY --from=web-builder /static/robots.txt /web/robots.txt
|
||||
COPY --from=web-builder /static/security.txt /web/security.txt
|
||||
COPY --from=web-builder /static/dist/ /web/dist/
|
||||
COPY --from=web-builder /static/authentik/ /web/authentik/
|
||||
|
||||
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/proxy", "healthcheck" ]
|
||||
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/authentik", "healthcheck" ]
|
||||
|
||||
EXPOSE 9000 9300 9443
|
||||
|
||||
@@ -87,4 +99,4 @@ USER 1000
|
||||
ENV TMPDIR=/dev/shm/ \
|
||||
GOFIPS=1
|
||||
|
||||
ENTRYPOINT ["/proxy"]
|
||||
ENTRYPOINT ["/authentik", "proxy"]
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
//! Utilities for working with the authentik API client.
|
||||
|
||||
use ak_client::apis::configuration::Configuration;
|
||||
use ak_client::{apis::configuration::Configuration, models::Pagination};
|
||||
use eyre::{Result, eyre};
|
||||
use url::Url;
|
||||
|
||||
@@ -60,6 +60,42 @@ pub fn make_config() -> Result<Configuration> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch all pages from a paginated API endpoint, returning all results combined.
|
||||
///
|
||||
/// - `fetch`: a function that takes a page number and returns a future resolving to a paginated
|
||||
/// response.
|
||||
/// - `get_pagination`: a function that extracts the [`Pagination`] metadata from a response.
|
||||
/// - `get_results`: a function that extracts the result items from a response.
|
||||
pub async fn fetch_all<T, R, E, F, Fut>(
|
||||
fetch: F,
|
||||
get_pagination: impl Fn(&R) -> &Pagination,
|
||||
get_results: impl Fn(R) -> Vec<T>,
|
||||
) -> std::result::Result<Vec<T>, E>
|
||||
where
|
||||
F: Fn(i32) -> Fut,
|
||||
Fut: Future<Output = std::result::Result<R, E>>,
|
||||
{
|
||||
let mut page = 1;
|
||||
let mut results = Vec::with_capacity(0);
|
||||
|
||||
loop {
|
||||
let response = fetch(page).await?;
|
||||
let next = get_pagination(&response).next;
|
||||
if page == 1 {
|
||||
let count = get_pagination(&response).count as usize;
|
||||
results.reserve(count);
|
||||
}
|
||||
results.extend(get_results(response));
|
||||
if next > 0.0 {
|
||||
page += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -30,12 +30,12 @@ pub fn install() -> Result<()> {
|
||||
}
|
||||
|
||||
if config.debug {
|
||||
let console_layer = console_subscriber::ConsoleLayer::builder()
|
||||
.server_addr(config.listen.debug_tokio)
|
||||
.spawn();
|
||||
// let console_layer = console_subscriber::ConsoleLayer::builder()
|
||||
// .server_addr(config.listen.debug_tokio)
|
||||
// .spawn();
|
||||
tracing_subscriber::registry()
|
||||
.with(ErrorLayer::default())
|
||||
.with(console_layer)
|
||||
// .with(console_layer)
|
||||
.with(
|
||||
fmt::layer()
|
||||
.compact()
|
||||
@@ -186,12 +186,9 @@ pub mod sentry {
|
||||
sentry_dsn: Some(config.sentry_dsn),
|
||||
environment: config.environment,
|
||||
send_pii: config.send_pii,
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "This is fine, we'll never get big values here."
|
||||
)]
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "This is fine, we'll never get big values here."
|
||||
)]
|
||||
sample_rate: config.traces_sample_rate as f32,
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface AuthenticatedSessionUserAgentDevice {
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentDevice
|
||||
*/
|
||||
brand: string;
|
||||
brand: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -35,7 +35,7 @@ export interface AuthenticatedSessionUserAgentDevice {
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentDevice
|
||||
*/
|
||||
model: string;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,25 +29,25 @@ export interface AuthenticatedSessionUserAgentOs {
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
major: string;
|
||||
major: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
minor: string;
|
||||
minor: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
patch: string;
|
||||
patch: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AuthenticatedSessionUserAgentOs
|
||||
*/
|
||||
patchMinor: string;
|
||||
patchMinor: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -162,7 +162,7 @@ export interface OAuthSourceRequest {
|
||||
* @type {string}
|
||||
* @memberof OAuthSourceRequest
|
||||
*/
|
||||
consumerSecret?: string;
|
||||
consumerSecret: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -203,6 +203,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -251,7 +252,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"] == null ? undefined : json["consumer_secret"],
|
||||
consumerSecret: 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"],
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
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
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
10
packages/client-ts/src/models/SourceType.ts
generated
10
packages/client-ts/src/models/SourceType.ts
generated
@@ -72,12 +72,6 @@ export interface SourceType {
|
||||
* @memberof SourceType
|
||||
*/
|
||||
readonly oidcJwksUrl: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SourceType
|
||||
*/
|
||||
clientSecretRequired: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +87,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -116,7 +108,6 @@ 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"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,6 +135,5 @@ 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";
|
||||
|
||||
@@ -9,7 +9,7 @@ dependencies = [
|
||||
"argon2-cffi==25.1.0",
|
||||
"cachetools==7.0.6",
|
||||
"channels==4.3.2",
|
||||
"cryptography==47.0.0",
|
||||
"cryptography==48.0.0",
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"opencontainers==0.0.15",
|
||||
"packaging==26.2",
|
||||
"paramiko==4.0.0",
|
||||
"psycopg[c,pool]==3.3.3",
|
||||
"psycopg[c,pool]==3.3.4",
|
||||
"pydantic-scim==0.0.8",
|
||||
"pydantic==2.13.3",
|
||||
"pyjwt==2.11.0",
|
||||
|
||||
19
schema.yml
19
schema.yml
@@ -34608,10 +34608,12 @@ components:
|
||||
properties:
|
||||
brand:
|
||||
type: string
|
||||
nullable: true
|
||||
family:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- brand
|
||||
- family
|
||||
@@ -34624,12 +34626,16 @@ 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
|
||||
@@ -44643,6 +44649,7 @@ components:
|
||||
consumer_secret:
|
||||
type: string
|
||||
writeOnly: true
|
||||
minLength: 1
|
||||
additional_scopes:
|
||||
type: string
|
||||
oidc_well_known_url:
|
||||
@@ -44659,6 +44666,7 @@ components:
|
||||
token request flow
|
||||
required:
|
||||
- consumer_key
|
||||
- consumer_secret
|
||||
- name
|
||||
- provider_type
|
||||
- slug
|
||||
@@ -49967,6 +49975,7 @@ components:
|
||||
consumer_secret:
|
||||
type: string
|
||||
writeOnly: true
|
||||
minLength: 1
|
||||
additional_scopes:
|
||||
type: string
|
||||
oidc_well_known_url:
|
||||
@@ -52672,7 +52681,6 @@ components:
|
||||
ProviderTypeEnum:
|
||||
enum:
|
||||
- apple
|
||||
- atproto
|
||||
- openidconnect
|
||||
- entraid
|
||||
- azuread
|
||||
@@ -53631,7 +53639,7 @@ components:
|
||||
type: string
|
||||
redirect_uri_type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RedirectUriTypeEnum'
|
||||
- $ref: '#/components/schemas/RedirectURITypeEnum'
|
||||
default: authorization
|
||||
required:
|
||||
- matching_mode
|
||||
@@ -53647,12 +53655,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
|
||||
@@ -56335,12 +56343,9 @@ 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
|
||||
|
||||
10
src/main.rs
10
src/main.rs
@@ -8,6 +8,8 @@ use eyre::{Result, eyre};
|
||||
use tracing::{error, info, trace};
|
||||
|
||||
mod metrics;
|
||||
#[cfg(feature = "proxy")]
|
||||
mod outpost;
|
||||
#[cfg(feature = "core")]
|
||||
mod server;
|
||||
#[cfg(feature = "core")]
|
||||
@@ -29,6 +31,8 @@ enum Command {
|
||||
Server(server::Cli),
|
||||
#[cfg(feature = "core")]
|
||||
Worker(worker::Cli),
|
||||
#[cfg(feature = "proxy")]
|
||||
Proxy(outpost::proxy::Cli),
|
||||
}
|
||||
|
||||
#[derive(Debug, FromArgs, PartialEq)]
|
||||
@@ -53,6 +57,8 @@ fn main() -> Result<()> {
|
||||
Command::Server(_) => Mode::set(Mode::Server)?,
|
||||
#[cfg(feature = "core")]
|
||||
Command::Worker(_) => Mode::set(Mode::Worker)?,
|
||||
#[cfg(feature = "proxy")]
|
||||
Command::Proxy(_) => Mode::set(Mode::Proxy)?,
|
||||
}
|
||||
|
||||
trace!("installing error formatting");
|
||||
@@ -108,6 +114,10 @@ fn main() -> Result<()> {
|
||||
let workers = worker::start(args, &mut tasks)?;
|
||||
metrics.workers.store(Some(workers));
|
||||
}
|
||||
#[cfg(feature = "proxy")]
|
||||
Command::Proxy(args) => {
|
||||
outpost::start::<outpost::proxy::ProxyOutpost>(args, &mut tasks).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let errors = tasks.run().await;
|
||||
|
||||
312
src/outpost/event.rs
Normal file
312
src/outpost/event.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
use ak_common::{Arbiter, Tasks, VERSION, api, arbiter, authentik_build_hash};
|
||||
use axum::http::{HeaderValue, header::AUTHORIZATION};
|
||||
use eyre::{Result, eyre};
|
||||
use futures::{SinkExt as _, StreamExt as _};
|
||||
use nix::unistd::gethostname;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use time::UtcDateTime;
|
||||
use tokio::{
|
||||
signal::unix::SignalKind,
|
||||
time::{Duration, interval, sleep},
|
||||
};
|
||||
use tokio_tungstenite::tungstenite::{Message, client::IntoClientRequest as _};
|
||||
use tracing::{debug, info, instrument, trace, warn};
|
||||
use url::Url;
|
||||
|
||||
use crate::outpost::{Outpost, OutpostController};
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, Copy, Eq)]
|
||||
#[repr(u8)]
|
||||
enum EventKind {
|
||||
/// Code used to acknowledge a previous message.
|
||||
Ack = 0,
|
||||
/// Code used to send a healthcheck keepalive.
|
||||
Hello = 1,
|
||||
/// Code received to trigger a config update.
|
||||
TriggerUpdate = 2,
|
||||
/// Code received to trigger some provider specific function.
|
||||
ProviderSpecific = 3,
|
||||
/// Code received to identify the end of a session.
|
||||
SessionEnd = 4,
|
||||
}
|
||||
|
||||
impl Display for EventKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Ack => write!(f, "Ack"),
|
||||
Self::Hello => write!(f, "Hello"),
|
||||
Self::TriggerUpdate => write!(f, "TriggerUpdate"),
|
||||
Self::ProviderSpecific => write!(f, "ProviderSpecific"),
|
||||
Self::SessionEnd => write!(f, "SessionEnd"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Event {
|
||||
instruction: EventKind,
|
||||
args: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct EventSessionEnd {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
fn build_ws_url(mut url: Url, outpost_pk: &str, instance_uuid: &str, attempt: u32) -> Result<Url> {
|
||||
let ws_scheme = match url.scheme() {
|
||||
"https" => "wss",
|
||||
"http" => "ws",
|
||||
other => return Err(eyre!("Unsupported scheme for WebSocket URL: {other}")),
|
||||
};
|
||||
|
||||
url.set_scheme(ws_scheme)
|
||||
.map_err(|()| eyre!("Failed to set URL scheme to {ws_scheme}"))?;
|
||||
url.set_path(&format!("{}ws/outpost/{outpost_pk}/", url.path()));
|
||||
url.query_pairs_mut()
|
||||
.append_pair("instance_uuid", instance_uuid)
|
||||
.append_pair("attempt", &attempt.to_string());
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn hello_args(instance_uuid: &str) -> serde_json::Value {
|
||||
let raw_hostname = gethostname().unwrap_or_default();
|
||||
let hostname = raw_hostname.to_string_lossy();
|
||||
|
||||
serde_json::json!({
|
||||
"version": VERSION,
|
||||
"buildHash": authentik_build_hash(None),
|
||||
"uuid": instance_uuid,
|
||||
// TODO: rust version and AWS-LC versions
|
||||
"hostname": hostname,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn handle_event<O: Outpost>(
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
event: Event,
|
||||
) -> Result<()> {
|
||||
match event.instruction {
|
||||
EventKind::Ack | EventKind::Hello => {}
|
||||
EventKind::TriggerUpdate => {
|
||||
info!("received update trigger, refreshing outpost");
|
||||
sleep(controller.reload_offset).await;
|
||||
controller.refresh().await?;
|
||||
debug!("outpost controller has been refreshed");
|
||||
outpost.refresh().await?;
|
||||
debug!("outpost has been refreshed");
|
||||
#[expect(
|
||||
clippy::as_conversions,
|
||||
clippy::cast_precision_loss,
|
||||
reason = "This is fine, we'll never get big values here."
|
||||
)]
|
||||
controller
|
||||
.m_last_update
|
||||
.set(UtcDateTime::now().unix_timestamp() as f64);
|
||||
}
|
||||
EventKind::SessionEnd => {
|
||||
let event: EventSessionEnd = serde_json::from_value(event.args)?;
|
||||
outpost.end_session(event).await?;
|
||||
}
|
||||
#[expect(
|
||||
clippy::unimplemented,
|
||||
reason = "this is only relevant for the RAC provider"
|
||||
)]
|
||||
EventKind::ProviderSpecific => unimplemented!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn watch_events_inner<O: Outpost>(
|
||||
arbiter: Arbiter,
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
attempt: u32,
|
||||
) -> Result<()> {
|
||||
let server_config = api::ServerConfig::new()?;
|
||||
let ws_url = build_ws_url(
|
||||
server_config.host,
|
||||
&controller.outpost.load().pk.to_string(),
|
||||
&controller.instance_uuid.to_string(),
|
||||
attempt,
|
||||
)?;
|
||||
|
||||
debug!(url = %ws_url, "connecting to websocket");
|
||||
let mut request = ws_url.into_client_request()?;
|
||||
let token = controller
|
||||
.api_config
|
||||
.bearer_access_token
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
request.headers_mut().insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {token}"))?,
|
||||
);
|
||||
|
||||
let (ws_stream, _response) = tokio_tungstenite::connect_async(request).await?;
|
||||
let (mut ws_write, mut ws_read) = ws_stream.split();
|
||||
|
||||
info!(
|
||||
outpost = %controller.outpost.load().pk,
|
||||
"connected to websocket"
|
||||
);
|
||||
controller.m_connection.set(1_u8);
|
||||
|
||||
let get_refresh_interval = || {
|
||||
let mut interval = controller.outpost.load().refresh_interval_s;
|
||||
// Ensure timer interval is not negative or 0.
|
||||
// If it is, we default to 5 minutes.
|
||||
if interval <= 0_i32 {
|
||||
interval = 60_i32 * 5_i32;
|
||||
}
|
||||
// Clamp interval to be at least 30 seconds.
|
||||
if interval < 30_i32 {
|
||||
interval = 30_i32;
|
||||
}
|
||||
// infallible because we bound it to be positive above
|
||||
Duration::from_secs(interval.try_into().expect("infallible"))
|
||||
};
|
||||
let mut refresh_interval = interval(get_refresh_interval());
|
||||
let mut heartbeat_interval = interval(Duration::from_secs(10));
|
||||
|
||||
let mut events_rx = arbiter.events_subscribe();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = refresh_interval.tick() => {
|
||||
info!("refreshing outpost on interval");
|
||||
if let Err(err) = handle_event(
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
Event {
|
||||
instruction: EventKind::TriggerUpdate,
|
||||
args: serde_json::Value::Null
|
||||
}
|
||||
).await {
|
||||
warn!(?err, "failed to refresh");
|
||||
}
|
||||
refresh_interval = interval(get_refresh_interval());
|
||||
// Since we re-create the interval, we need to make it tick instantly to avoid
|
||||
// ending up in a never-ending tick-loop.
|
||||
refresh_interval.tick().await;
|
||||
},
|
||||
_ = heartbeat_interval.tick() => {
|
||||
let ping = Event {
|
||||
instruction: EventKind::Hello,
|
||||
args: hello_args(&controller.instance_uuid.to_string()),
|
||||
};
|
||||
ws_write.send(Message::text(serde_json::to_string(&ping)?)).await?;
|
||||
trace!("sent websocket hello (heartbeat)");
|
||||
},
|
||||
Ok(arbiter::Event::Signal(signal)) = events_rx.recv() => {
|
||||
if signal == SignalKind::user_defined1() {
|
||||
info!("refreshing outpost on signal");
|
||||
if let Err(err) = handle_event(
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
Event {
|
||||
instruction: EventKind::TriggerUpdate,
|
||||
args: serde_json::Value::Null
|
||||
}
|
||||
).await {
|
||||
warn!(?err, "failed to refresh");
|
||||
}
|
||||
}
|
||||
},
|
||||
msg = ws_read.next() => {
|
||||
let Some(msg) = msg else {
|
||||
break;
|
||||
};
|
||||
let msg = msg?;
|
||||
match msg {
|
||||
Message::Text(text) => {
|
||||
let Ok(event): Result<Event, _> = serde_json::from_str(&text) else {
|
||||
warn!(data = text.as_str(), "failed to parse event");
|
||||
continue;
|
||||
};
|
||||
trace!(event = %event.instruction, "received websocket event");
|
||||
if let Err(err) = handle_event(
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
event,
|
||||
).await {
|
||||
warn!(?err, "failed to handle event");
|
||||
}
|
||||
},
|
||||
Message::Ping(data) => {
|
||||
ws_write.send(Message::Pong(data)).await?;
|
||||
},
|
||||
Message::Close(_) => {
|
||||
break;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
},
|
||||
() = arbiter.shutdown() => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn watch_events<O: Outpost>(
|
||||
arbiter: Arbiter,
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
) -> Result<()> {
|
||||
const MAX_BACKOFF: Duration = Duration::from_mins(5);
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let mut attempt: u32 = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = arbiter.shutdown() => break,
|
||||
res = watch_events_inner(
|
||||
arbiter.clone(),
|
||||
Arc::clone(&controller),
|
||||
Arc::clone(&outpost),
|
||||
attempt
|
||||
) => {
|
||||
controller.m_connection.set(0_u8);
|
||||
match res {
|
||||
Ok(()) => debug!("websocket disconnected cleanly"),
|
||||
Err(err) => warn!(?err, attempt, "websocket error"),
|
||||
}
|
||||
|
||||
info!(attempt, delay = backoff.as_secs(), "reconnecting websocket in {}s...", backoff.as_secs());
|
||||
|
||||
tokio::select! {
|
||||
() = arbiter.shutdown() => break,
|
||||
() = sleep(backoff) => {}
|
||||
}
|
||||
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("stopping event watcher");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start<O: Outpost + 'static>(
|
||||
tasks: &mut Tasks,
|
||||
controller: Arc<OutpostController>,
|
||||
outpost: Arc<O>,
|
||||
) -> Result<()> {
|
||||
let arbiter = tasks.arbiter();
|
||||
tasks
|
||||
.build_task()
|
||||
.name(&format!("{}::watch_events", module_path!()))
|
||||
.spawn(watch_events(arbiter, controller, outpost))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
123
src/outpost/mod.rs
Normal file
123
src/outpost/mod.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use ak_client::{
|
||||
apis::{configuration::Configuration, outposts_api::outposts_instances_list},
|
||||
models::Outpost as OutpostModel,
|
||||
};
|
||||
use ak_common::{Tasks, VERSION, api, authentik_build_hash};
|
||||
use arc_swap::ArcSwap;
|
||||
use eyre::{Result, eyre};
|
||||
use tracing::{debug, info, instrument};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) mod event;
|
||||
#[cfg(feature = "proxy")]
|
||||
pub(crate) mod proxy;
|
||||
|
||||
pub(crate) trait Outpost: Send + Sync + Sized {
|
||||
const OUTPOST_TYPE: &'static str;
|
||||
type Cli: Send + Sync;
|
||||
|
||||
async fn new(controller: Arc<OutpostController>) -> Result<Self>;
|
||||
|
||||
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()>;
|
||||
fn refresh(&self) -> impl Future<Output = Result<()>> + Send;
|
||||
|
||||
fn end_session(&self, event: event::EventSessionEnd)
|
||||
-> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OutpostController {
|
||||
api_config: Configuration,
|
||||
outpost: ArcSwap<OutpostModel>,
|
||||
instance_uuid: Uuid,
|
||||
reload_offset: Duration,
|
||||
m_info: metrics::Gauge,
|
||||
m_last_update: metrics::Gauge,
|
||||
m_connection: metrics::Gauge,
|
||||
}
|
||||
|
||||
impl OutpostController {
|
||||
#[instrument(skip_all)]
|
||||
async fn get_outpost(api_config: &Configuration) -> Result<OutpostModel> {
|
||||
let outposts = outposts_instances_list(
|
||||
api_config, None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(outpost) = outposts.results.into_iter().next() else {
|
||||
return Err(eyre!(
|
||||
"No outposts found with given token, ensure the given token corresponds to an \
|
||||
authentik Outpost"
|
||||
));
|
||||
};
|
||||
debug!(name = outpost.name, "fetched outpost configuration");
|
||||
|
||||
Ok(outpost)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn new<O: Outpost>() -> Result<Self> {
|
||||
let api_config = api::make_config()?;
|
||||
let outpost = Self::get_outpost(&api_config).await?;
|
||||
let instance_uuid = Uuid::new_v4();
|
||||
|
||||
let m_labels = [
|
||||
("outpost_name", outpost.name.clone()),
|
||||
("outpost_type", O::OUTPOST_TYPE.to_owned()),
|
||||
("uuid", instance_uuid.to_string()),
|
||||
("version", VERSION.to_owned()),
|
||||
("build", authentik_build_hash(None)),
|
||||
];
|
||||
metrics::describe_gauge!("authentik_outpost_info", "Outpost info");
|
||||
let m_info = metrics::gauge!("authentik_outpost_info", &m_labels);
|
||||
metrics::describe_gauge!("authentik_outpost_last_update", "Time of last update");
|
||||
let m_last_update = metrics::gauge!("authentik_outpost_last_update", &m_labels);
|
||||
metrics::describe_gauge!("authentik_outpost_connection", "Connection status");
|
||||
let m_connection = metrics::gauge!("authentik_outpost_connection", &m_labels);
|
||||
|
||||
let reload_offset = Duration::from_secs(rand::random_range(0..10));
|
||||
let controller = Self {
|
||||
api_config,
|
||||
outpost: ArcSwap::from_pointee(outpost),
|
||||
instance_uuid,
|
||||
reload_offset,
|
||||
m_info,
|
||||
m_last_update,
|
||||
m_connection,
|
||||
};
|
||||
|
||||
info!(embedded = controller.is_embedded(), "outpost mode");
|
||||
debug!(?reload_offset, "HA Reload offset");
|
||||
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
fn is_embedded(&self) -> bool {
|
||||
self.outpost
|
||||
.load()
|
||||
.managed
|
||||
.as_ref()
|
||||
.and_then(|m| m.as_deref())
|
||||
.is_some_and(|m| m == "goauthentik.io/outposts/embedded")
|
||||
}
|
||||
|
||||
async fn refresh(&self) -> Result<()> {
|
||||
let outpost = Self::get_outpost(&self.api_config).await?;
|
||||
self.outpost.swap(Arc::new(outpost));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub(crate) async fn start<O: Outpost + 'static>(_cli: O::Cli, tasks: &mut Tasks) -> Result<()> {
|
||||
let controller = Arc::new(OutpostController::new::<O>().await?);
|
||||
let outpost = Arc::new(O::new(Arc::clone(&controller)).await?);
|
||||
|
||||
event::start(tasks, Arc::clone(&controller), Arc::clone(&outpost))?;
|
||||
outpost.start(tasks)?;
|
||||
controller.m_info.set(1_u8);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
41
src/outpost/proxy/application.rs
Normal file
41
src/outpost/proxy/application.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use ak_client::models::ProxyOutpostConfig;
|
||||
use axum::Router;
|
||||
use eyre::{Result, eyre};
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
use crate::outpost::proxy::ProxyOutpost;
|
||||
|
||||
const REDIRECT_PARAM: &str = "rd";
|
||||
const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback";
|
||||
const LOGOUT_SIGNATURE: &str = "X-authentik-logout";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Application {
|
||||
pub(super) host: String,
|
||||
pub(super) provider: ProxyOutpostConfig,
|
||||
pub(super) router: Router,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
#[instrument(skip_all)]
|
||||
pub(super) fn new(_existing_apps: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result<Self> {
|
||||
let external_url = Url::parse(&provider.external_host)?;
|
||||
if !external_url.has_authority() {
|
||||
return Err(eyre!("no host in external host"));
|
||||
}
|
||||
let external_host = external_url.authority();
|
||||
|
||||
let _redirect_url = {
|
||||
let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?;
|
||||
redirect_url.set_query(Some(&format!("{CALLBACK_SIGNATURE}=true")));
|
||||
redirect_url
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
host: external_host.to_owned(),
|
||||
provider,
|
||||
router: Router::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
76
src/outpost/proxy/handlers.rs
Normal file
76
src/outpost/proxy/handlers.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::sync::Arc;
|
||||
use tower::util::ServiceExt as _;
|
||||
|
||||
use ak_axum::{error::Result, extract::host::Host};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::{Method, StatusCode, header::CONTENT_TYPE},
|
||||
response::{IntoResponse as _, Response},
|
||||
};
|
||||
use metrics::histogram;
|
||||
use serde_json::json;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{Instrument as _, field, info_span, instrument, trace, warn};
|
||||
|
||||
use crate::outpost::proxy::ProxyOutpost;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub(super) async fn handle_ping(
|
||||
method: Method,
|
||||
Host(host): Host,
|
||||
State(outpost): State<Arc<ProxyOutpost>>,
|
||||
) -> Response {
|
||||
let start = Instant::now();
|
||||
histogram!(
|
||||
"authentik_outpost_proxy_request_duration_seconds",
|
||||
"outpost_name" => outpost.controller.outpost.load().name.clone(),
|
||||
"method" => method.to_string(),
|
||||
"host" => host,
|
||||
"type" => "ping",
|
||||
)
|
||||
.record(start.elapsed().as_secs_f64());
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub(super) async fn default(
|
||||
method: Method,
|
||||
Host(host): Host,
|
||||
State(outpost): State<Arc<ProxyOutpost>>,
|
||||
request: Request,
|
||||
) -> Result<Response> {
|
||||
let span = info_span!("proxy outpost request", user = field::Empty);
|
||||
let start = Instant::now();
|
||||
|
||||
let Some(app) = outpost.lookup_app(&host) else {
|
||||
trace!(headers = ?request.headers(), "tracing headers for no hostname match");
|
||||
warn!("no app for hostname");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.body(
|
||||
json!({
|
||||
"message": "no app for hostname",
|
||||
"host": host,
|
||||
"detail": format!("check the outpost settings and make sure '{host}' is included."),
|
||||
})
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
.expect("infallible"));
|
||||
};
|
||||
|
||||
trace!("passing to application");
|
||||
let response = app.router.clone().oneshot(request).instrument(span).await?;
|
||||
|
||||
histogram!(
|
||||
"authentik_outpost_proxy_request_duration_seconds",
|
||||
"outpost_name" => outpost.controller.outpost.load().name.clone(),
|
||||
"method" => method.to_string(),
|
||||
"host" => host,
|
||||
"type" => "app",
|
||||
)
|
||||
.record(start.elapsed().as_secs_f64());
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
187
src/outpost/proxy/mod.rs
Normal file
187
src/outpost/proxy/mod.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ak_axum::router::wrap_router;
|
||||
use ak_client::{apis::outposts_api::outposts_proxy_list, models::ProxyMode};
|
||||
use ak_common::{Tasks, api::fetch_all, config};
|
||||
use arc_swap::ArcSwap;
|
||||
use argh::FromArgs;
|
||||
use axum::Router;
|
||||
use eyre::Result;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use crate::outpost::{Outpost, OutpostController, proxy::application::Application};
|
||||
|
||||
mod application;
|
||||
mod handlers;
|
||||
|
||||
#[derive(Debug, Default, FromArgs, PartialEq, Eq)]
|
||||
/// Run the authentik proxy outpost.
|
||||
#[argh(subcommand, name = "proxy")]
|
||||
#[expect(
|
||||
clippy::empty_structs_with_brackets,
|
||||
reason = "argh doesn't support unit structs"
|
||||
)]
|
||||
pub(crate) struct Cli {}
|
||||
|
||||
pub(crate) struct ProxyOutpost {
|
||||
controller: Arc<OutpostController>,
|
||||
apps: ArcSwap<HashMap<String, Arc<Application>>>,
|
||||
}
|
||||
|
||||
impl Outpost for ProxyOutpost {
|
||||
type Cli = Cli;
|
||||
|
||||
const OUTPOST_TYPE: &'static str = "proxy";
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn new(controller: Arc<OutpostController>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
controller,
|
||||
apps: ArcSwap::from_pointee(HashMap::with_capacity(0)),
|
||||
})
|
||||
}
|
||||
|
||||
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()> {
|
||||
let router = build_router(self);
|
||||
|
||||
for addr in config::get().listen.http.iter().copied() {
|
||||
ak_axum::server::start_plain(tasks, "proxy-outpost", router.clone(), addr, false)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn refresh(&self) -> Result<()> {
|
||||
debug!(
|
||||
outpost_pk = %self.controller.outpost.load().pk,
|
||||
"requesting providers for outpost"
|
||||
);
|
||||
|
||||
let providers = fetch_all(
|
||||
|page| {
|
||||
outposts_proxy_list(
|
||||
&self.controller.api_config,
|
||||
None,
|
||||
None,
|
||||
Some(page),
|
||||
Some(100_i32),
|
||||
None,
|
||||
)
|
||||
},
|
||||
|r| &r.pagination,
|
||||
|r| r.results,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, "failed to fetch providers"))?;
|
||||
debug!(count = providers.len(), "fetched providers");
|
||||
|
||||
if providers.is_empty() && !self.controller.is_embedded() {
|
||||
warn!(
|
||||
"no providers assigned to this outpost, check outpost configuration in authentik"
|
||||
);
|
||||
}
|
||||
|
||||
for (i, provider) in providers.iter().enumerate() {
|
||||
debug!(
|
||||
index = i,
|
||||
name = provider.name,
|
||||
external_host = provider.external_host,
|
||||
assigned_to_app = provider.assigned_application_name,
|
||||
"provider details"
|
||||
);
|
||||
}
|
||||
|
||||
let mut apps = HashMap::with_capacity(providers.len());
|
||||
|
||||
for provider in providers {
|
||||
let name = provider.name.clone();
|
||||
let Ok(application) = Application::new(self, provider)
|
||||
.inspect_err(|err| warn!(?err, "failed to setup application, skipping provider"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
info!(name, host = application.host, "loaded application");
|
||||
|
||||
apps.insert(application.host.clone(), Arc::new(application));
|
||||
}
|
||||
|
||||
self.apps.store(Arc::new(apps));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn end_session(&self, _event: super::event::EventSessionEnd) -> Result<()> {
|
||||
// todo!()
|
||||
warn!(?_event, "removing session");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyOutpost {
|
||||
#[instrument(skip(self))]
|
||||
fn lookup_app(&self, host: &str) -> Option<Arc<Application>> {
|
||||
let apps = self.apps.load();
|
||||
|
||||
// If we only have a single app, host name switching doesn't matter.
|
||||
if apps.len() == 1
|
||||
&& let Some(app) = apps.values().next()
|
||||
{
|
||||
debug!(app = app.provider.name, "found a single app, using it");
|
||||
return Some(Arc::clone(app));
|
||||
}
|
||||
|
||||
if let Some(app) = apps.get(host) {
|
||||
debug!(app = app.provider.name, "found app based direct host match");
|
||||
return Some(Arc::clone(app));
|
||||
}
|
||||
|
||||
// For forward_auth_domain, we don't have a direct app to domain relationship.
|
||||
// Check through all apps, and check how much of their cookie domain matches the host.
|
||||
// Return the application that has the longest match.
|
||||
let mut longest_match = None;
|
||||
let mut longest_len = 0_usize;
|
||||
|
||||
for app in apps.values() {
|
||||
if app.provider.mode != Some(ProxyMode::ForwardDomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(cookie_domain) = app.provider.cookie_domain.as_deref() {
|
||||
// Check if the cookie domain has a leading period for a wildcard.
|
||||
// This will decrease the weight of a wildcard domain, but a request to example.com
|
||||
// with the cookie domain set to example.com will still be routed correctly.
|
||||
let domain = cookie_domain.trim_start_matches('.');
|
||||
|
||||
if host.ends_with(domain) && domain.len() > longest_len {
|
||||
longest_len = domain.len();
|
||||
longest_match = Some(Arc::clone(app));
|
||||
}
|
||||
// For forward_auth_domain, we need to response on the external domain too.
|
||||
if app.provider.external_host == host {
|
||||
debug!(app = app.provider.name, "found app based on external_host");
|
||||
return Some(Arc::clone(app));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(app) = &longest_match {
|
||||
debug!(app = app.provider.name, "found app based on cookie domain");
|
||||
}
|
||||
|
||||
longest_match
|
||||
}
|
||||
}
|
||||
|
||||
fn build_router(outpost: Arc<ProxyOutpost>) -> Router {
|
||||
wrap_router(
|
||||
Router::new()
|
||||
.nest(
|
||||
"/outpost.goauthentik.io/ping",
|
||||
Router::new().fallback(handlers::handle_ping),
|
||||
)
|
||||
.fallback(handlers::default)
|
||||
.with_state(outpost),
|
||||
true,
|
||||
)
|
||||
}
|
||||
108
uv.lock
generated
108
uv.lock
generated
@@ -318,7 +318,7 @@ requires-dist = [
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "cachetools", specifier = "==7.0.6" },
|
||||
{ name = "channels", specifier = "==4.3.2" },
|
||||
{ name = "cryptography", specifier = "==47.0.0" },
|
||||
{ name = "cryptography", specifier = "==48.0.0" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
{ name = "deepmerge", specifier = "==2.0" },
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
@@ -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.3" },
|
||||
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.4" },
|
||||
{ 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 = "47.0.0"
|
||||
version = "48.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2733,14 +2733,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.3"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2753,9 +2753,9 @@ pool = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-c"
|
||||
version = "3.3.3"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-pool"
|
||||
@@ -2947,14 +2947,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "26.1.0"
|
||||
version = "26.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 566 B |
298
web/package-lock.json
generated
298
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.5",
|
||||
"@storybook/addon-links": "^10.3.5",
|
||||
"@storybook/web-components": "^10.3.5",
|
||||
"@storybook/web-components-vite": "^10.3.5",
|
||||
"@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",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
@@ -114,7 +114,7 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.8",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.1",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
@@ -2895,9 +2895,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2911,9 +2911,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2927,9 +2927,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2943,9 +2943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2959,9 +2959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2975,9 +2975,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2991,9 +2991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3007,9 +3007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -3023,9 +3023,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -3039,9 +3039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3055,9 +3055,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3071,9 +3071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3087,27 +3087,48 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"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_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3121,9 +3142,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3137,9 +3158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs": {
|
||||
@@ -3698,15 +3719,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"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==",
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.6.tgz",
|
||||
"integrity": "sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.3.5",
|
||||
"@storybook/csf-plugin": "10.3.6",
|
||||
"@storybook/icons": "^2.0.1",
|
||||
"@storybook/react-dom-shim": "10.3.5",
|
||||
"@storybook/react-dom-shim": "10.3.6",
|
||||
"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"
|
||||
@@ -3716,13 +3737,13 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.3.5"
|
||||
"storybook": "^10.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"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==",
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.6.tgz",
|
||||
"integrity": "sha512-tv9Xd68qRGBAvEubaxNo3FuFq4GwuMiBriD+gLGuFK0+/u3cnkuA264aoR1v6YCH3sT3er3+MBimuyKM3jLDxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0"
|
||||
@@ -3733,7 +3754,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.3.5"
|
||||
"storybook": "^10.3.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -3742,12 +3763,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.5.tgz",
|
||||
"integrity": "sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "10.3.5",
|
||||
"@storybook/csf-plugin": "10.3.6",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3755,14 +3776,14 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.3.5",
|
||||
"storybook": "^10.3.6",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.5.tgz",
|
||||
"integrity": "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unplugin": "^2.3.5"
|
||||
@@ -3774,7 +3795,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.3.5",
|
||||
"storybook": "^10.3.6",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -3810,9 +3831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.5.tgz",
|
||||
"integrity": "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3821,13 +3842,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.5"
|
||||
"storybook": "^10.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.5.tgz",
|
||||
"integrity": "sha512-tSppZagFCeZ+bWsaHUvdiw17ATWgfGDBz0mFicgEj0/eNuxQH2OvXyRIQUXY39b/55TBwSGeoIX3tOW91WIqpw==",
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.6.tgz",
|
||||
"integrity": "sha512-femDZGYBGQDckL7F6ZCl2S+dNNBjvd9lp6rQrwBdbNprjctLd6d3EB4HyNM502QxtdEo7laq8y1goDu8KwIV3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -3840,24 +3861,24 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lit": "^2.0.0 || ^3.0.0",
|
||||
"storybook": "^10.3.5"
|
||||
"storybook": "^10.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/web-components-vite": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.5.tgz",
|
||||
"integrity": "sha512-6uAw6KAUXFsAPzp8KchcMp3gatEnEAd8ylIvzoMzvsIMiHmzXwvDNmoFZnAJ2tmsQGvF4dZRDCBg7PvWdTx8Rg==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-vite": "10.3.5",
|
||||
"@storybook/web-components": "10.3.5"
|
||||
"@storybook/builder-vite": "10.3.6",
|
||||
"@storybook/web-components": "10.3.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.3.5"
|
||||
"storybook": "^10.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-ast": {
|
||||
@@ -6527,12 +6548,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
@@ -15811,13 +15832,13 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"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==",
|
||||
"version": "1.0.0-rc.17",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.124.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
||||
"@oxc-project/types": "=0.127.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -15826,30 +15847,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
@@ -16598,9 +16610,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.5.tgz",
|
||||
"integrity": "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==",
|
||||
"version": "10.3.6",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.6.tgz",
|
||||
"integrity": "sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
@@ -16625,11 +16637,15 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prettier": "^2 || ^3"
|
||||
"prettier": "^2 || ^3",
|
||||
"vite-plus": "^0.1.15"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prettier": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plus": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18400,16 +18416,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"postcss": "^8.5.10",
|
||||
"rolldown": "1.0.0-rc.17",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -18490,6 +18506,22 @@
|
||||
"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",
|
||||
|
||||
@@ -120,10 +120,10 @@
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@sentry/browser": "^10.50.0",
|
||||
"@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",
|
||||
"@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",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.5",
|
||||
@@ -190,7 +190,7 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.8",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.1",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
|
||||
@@ -52,10 +52,6 @@ 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 >= 1200px)");
|
||||
this.#sidebarMatcher = window.matchMedia("(width > 1210px)");
|
||||
this.sidebarOpen = this.#sidebarMatcher.matches;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,15 +197,17 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
?checked=${this.instance?.openInNewTab ?? false}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
"If checked, 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 User Dashboard")}
|
||||
help=${msg("Whether this application will be shown on the User Dashboard.")}
|
||||
label=${msg("Hide from My applications")}
|
||||
help=${msg(
|
||||
"If checked, this application will not be shown on the user's My applications page.",
|
||||
)}
|
||||
>
|
||||
</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(
|
||||
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
"If checked, 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 User Dashboard")}
|
||||
label=${msg("Hide from My applications")}
|
||||
help=${msg(
|
||||
"Whether this application will be shown on the User Dashboard.",
|
||||
"If checked, this application will not be shown on the user's My applications page.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
@@ -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>
|
||||
<fieldset class="ak-c-fieldset" name="application-details">
|
||||
<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>
|
||||
? html`<fieldset class="ak-c-fieldset" name="provider-details">
|
||||
<legend>${msg("Provider Details")}</legend>
|
||||
${renderer(provider)}
|
||||
</fieldset>`
|
||||
|
||||
@@ -86,7 +86,7 @@ export class ConfigModal extends ModalButton {
|
||||
></ak-codemirror>
|
||||
</ak-expand>
|
||||
</div>
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<fieldset class="ak-c-fieldset 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="pf-c-modal-box__footer">
|
||||
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
|
||||
@@ -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,7 +30,6 @@ import {
|
||||
GroupMatchingModeEnum,
|
||||
OAuthSource,
|
||||
OAuthSourceRequest,
|
||||
PatchedOAuthSourceRequest,
|
||||
PKCEMethodEnum,
|
||||
ProviderTypeEnum,
|
||||
SourcesApi,
|
||||
@@ -82,20 +81,6 @@ 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,
|
||||
@@ -112,20 +97,16 @@ 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: requestData,
|
||||
patchedOAuthSourceRequest: data,
|
||||
});
|
||||
}
|
||||
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
|
||||
oAuthSourceRequest: requestData,
|
||||
oAuthSourceRequest: data as unknown as OAuthSourceRequest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,11 +186,9 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${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.",
|
||||
)}
|
||||
${msg(
|
||||
"URL used to request the initial token. This URL is only required for OAuth 1.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal> `
|
||||
: nothing}
|
||||
@@ -426,22 +405,16 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
|
||||
spellcheck="false"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${this.isAtProtocolSource
|
||||
? msg("Client metadata URL.")
|
||||
: msg("Also known as Client ID.")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
${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-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>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -37,8 +37,6 @@ 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:
|
||||
|
||||
@@ -50,7 +50,9 @@ export class RedirectStageForm extends BaseStageForm<RedirectStage> {
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
return html`<span>
|
||||
${msg("Redirect the user to another flow, potentially with all gathered context")}
|
||||
${msg(
|
||||
"Redirect the user to a static URL or another flow, optionally with all gathered context.",
|
||||
)}
|
||||
</span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
|
||||
@@ -159,7 +159,7 @@ export class UserBulkRevokeSessionsForm extends ModalButton {
|
||||
>
|
||||
</ak-user-bulk-revoke-sessions-table>
|
||||
</section>
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<fieldset class="ak-c-fieldset 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 < 1200px) {
|
||||
@media (width <= 1210px) {
|
||||
column-gap: calc(var(--pf-global--spacer--md) / 2);
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (width < 1200px) {
|
||||
@media (width <= 1210px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@
|
||||
grid-area: toggle;
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
@media (width > 1210px) {
|
||||
slot[name="toggle"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ function resolvePath(...args: string[]): string {
|
||||
* - Intercepts local links and scrolls to the target element.
|
||||
*/
|
||||
export const MDXAnchor = ({
|
||||
href,
|
||||
href: initialHref,
|
||||
children,
|
||||
...props
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const { publicDirectory } = useMDXModule();
|
||||
let href = initialHref;
|
||||
|
||||
if (href?.startsWith(".") && publicDirectory) {
|
||||
const nextPathname = resolvePath(publicDirectory, href);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
);
|
||||
--ak-c-command-palette__group--Color: var(--pf-global--palette--purple-100);
|
||||
|
||||
--ak-fieldset--BorderColor: transparent;
|
||||
--ak-c-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-fieldset--BorderColor: var(--pf-global--palette--purple-500);
|
||||
--ak-c-fieldset--BorderColor: var(--pf-global--palette--purple-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
transition-duration: 0.2s;
|
||||
|
||||
legend {
|
||||
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
--ak-c-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 part="results-group">
|
||||
<fieldset class="ak-c-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-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
padding-block: calc(var(--ak-fieldset__legend--PaddingInlineBase) / 2);
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
|
||||
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
|
||||
border-inline: none;
|
||||
border-block-end: none;
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class ConfirmationForm extends ModalButton {
|
||||
<slot class="pf-c-content" name="body"></slot>
|
||||
</form>
|
||||
</section>
|
||||
<fieldset class="pf-c-modal-box__footer">
|
||||
<fieldset class="ak-c-fieldset 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="pf-c-modal-box__footer">
|
||||
<fieldset class="ak-c-fieldset 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="pf-c-modal-box__footer">
|
||||
return html`<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -126,7 +126,7 @@ export class FlowInspector extends AKElement {
|
||||
|
||||
protected renderNextStage({ currentPlan, isCompleted }: FlowInspection): TemplateResult {
|
||||
return html`<div class="pf-c-card">
|
||||
<fieldset>
|
||||
<fieldset class="ak-c-fieldset">
|
||||
<legend class="pf-c-card__title">${msg("Next stage")}</legend>
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list">
|
||||
@@ -184,7 +184,7 @@ ${stringify(this.getStage(currentPlan?.nextPlannedStage?.stageObj))}</pre
|
||||
currentPlan,
|
||||
}: FlowInspection): TemplateResult {
|
||||
return html`<div class="pf-c-card">
|
||||
<fieldset>
|
||||
<fieldset class="ak-c-fieldset">
|
||||
<legend class="pf-c-card__title">${msg("Plan history")}</legend>
|
||||
<div class="pf-c-card__body">
|
||||
<ol class="pf-c-progress-stepper pf-m-vertical">
|
||||
@@ -248,7 +248,7 @@ ${stringify(this.getStage(currentPlan?.nextPlannedStage?.stageObj))}</pre
|
||||
|
||||
protected renderCurrentPlan({ currentPlan }: FlowInspection): TemplateResult {
|
||||
return html`<div class="pf-c-card">
|
||||
<fieldset>
|
||||
<fieldset class="ak-c-fieldset">
|
||||
<legend class="pf-c-card__title">${msg("Current plan context")}</legend>
|
||||
<pre class="pf-c-card__body"><code>${stringify(
|
||||
currentPlan?.planContext,
|
||||
@@ -259,7 +259,7 @@ ${stringify(this.getStage(currentPlan?.nextPlannedStage?.stageObj))}</pre
|
||||
|
||||
protected renderSession({ currentPlan }: FlowInspection): TemplateResult {
|
||||
return html`<div class="pf-c-card">
|
||||
<fieldset>
|
||||
<fieldset class="ak-c-fieldset">
|
||||
<legend class="pf-c-card__title">${msg("Session ID")}</legend>
|
||||
<div class="pf-c-card__body">
|
||||
<code class="break"> ${currentPlan?.sessionId} </code>
|
||||
|
||||
@@ -118,7 +118,7 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
|
||||
<p>${msg("You're about to be redirected to the following URL.")}</p>
|
||||
<code>${this.getURL()}</code>
|
||||
</div>
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<a
|
||||
type="submit"
|
||||
|
||||
@@ -38,7 +38,7 @@ export class AccessDeniedStage extends BaseStage<
|
||||
: nothing}
|
||||
</ak-empty-state>
|
||||
${this.challenge?.flowInfo?.cancelUrl
|
||||
? html`<fieldset class="pf-c-form__group pf-m-action">
|
||||
? html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<a
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
|
||||
@@ -87,7 +87,7 @@ export class AuthenticatorDuoStage extends BaseStage<
|
||||
</p>
|
||||
<a href=${this.challenge.activationCode}>${msg("Duo activation")}</a>
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -64,7 +64,7 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
${AKFormErrors({ errors: this.challenge?.responseErrors?.email })}
|
||||
</div>
|
||||
${this.renderNonFieldErrors()}
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
@@ -120,7 +120,7 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
${this.renderNonFieldErrors()}
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -67,7 +67,7 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.phone_number })}
|
||||
</div>
|
||||
${this.renderNonFieldErrors()}
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
@@ -106,7 +106,7 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
${this.renderNonFieldErrors()}
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AuthenticatorStaticStage extends BaseStage<
|
||||
</ul>
|
||||
<p>${msg("Make sure to keep these tokens in a safe place.")}</p>
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -175,7 +175,7 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
.ak-c-fieldset.ak-c-fieldset.pf-c-form__group.pf-m-action[name="device-challenges"] {
|
||||
--ak-c-fieldset--BorderWidth: thin;
|
||||
|
||||
--ak-c-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
|
||||
|
||||
--ak-c-fieldset--RowGap: 0;
|
||||
--ak-c-fieldset--ColumnGap: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.authenticator-button,
|
||||
ak-stage-authenticator-validate.style-scope .authenticator-button {
|
||||
align-items: center;
|
||||
@@ -5,6 +15,7 @@ ak-stage-authenticator-validate.style-scope .authenticator-button {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(auto, 2rem) minmax(33%, max-content);
|
||||
gap: var(--pf-global--spacer--lg);
|
||||
padding-block: calc(var(--pf-global--spacer--form-element) * 2);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--pf-global--BackgroundColor--200);
|
||||
|
||||
@@ -267,7 +267,10 @@ export class AuthenticatorValidateStage
|
||||
},
|
||||
);
|
||||
|
||||
return html`<fieldset class="pf-c-form__group pf-m-action" name="device-challenges">
|
||||
return html`<fieldset
|
||||
class="ak-c-fieldset pf-c-form__group pf-m-action"
|
||||
name="device-challenges"
|
||||
>
|
||||
<legend class="pf-c-title">${msg("Select an authentication method")}</legend>
|
||||
${deviceChallengeButtons}
|
||||
</fieldset>`;
|
||||
@@ -300,7 +303,7 @@ export class AuthenticatorValidateStage
|
||||
},
|
||||
);
|
||||
|
||||
return html`<fieldset class="pf-c-form__group pf-m-action" name="stages">
|
||||
return html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action" name="stages">
|
||||
<legend class="sr-only">${msg("Select a configuration stage")}</legend>
|
||||
${stageButtons}
|
||||
</fieldset>`;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
|
||||
return html`<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
${this.renderUserInfo()}
|
||||
<fieldset class="pf-c-form__group">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group">
|
||||
<legend class="sr-only">${msg("Authentication code")}</legend>
|
||||
${AKLabel(
|
||||
{
|
||||
@@ -62,7 +62,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
${AKFormErrors({ errors: this.challenge?.responseErrors?.code })}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button name="continue" type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
|
||||
@@ -63,7 +63,7 @@ export class AuthenticatorValidateStageWebDuo extends BaseDeviceStage<
|
||||
>
|
||||
</ak-empty-state>
|
||||
${this.showBackButton
|
||||
? html`<fieldset class="pf-c-form__group pf-m-action">
|
||||
? html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
${this.renderReturnToDevicePicker()}
|
||||
</fieldset>`
|
||||
|
||||
@@ -133,7 +133,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
|
||||
>
|
||||
</ak-empty-state>
|
||||
${!this.authenticating || this.showBackButton
|
||||
? html`<fieldset class="pf-c-form__group pf-m-action">
|
||||
? html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
${!this.authenticating
|
||||
? html`<button
|
||||
|
||||
@@ -153,7 +153,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
${this.challenge?.responseErrors
|
||||
? html`<p>${this.challenge.responseErrors.response[0].string}</p>`
|
||||
: nothing}
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
${!this.registerRunning
|
||||
? html` <button
|
||||
|
||||
@@ -126,7 +126,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||
? this.renderAdditional()
|
||||
: this.renderNoPrevious()}
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -23,7 +23,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<p>${msg(str`Stage name: ${this.challenge?.name}`)}</p>
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -25,7 +25,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
|
||||
<p>${msg("Check your Inbox for a verification email.")}</p>
|
||||
</div>
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
|
||||
@@ -296,8 +296,15 @@ export class IdentificationStage extends BaseStage<
|
||||
type: string,
|
||||
label: string,
|
||||
initialUserIdentification: string | null,
|
||||
autocomplete: string,
|
||||
passwordFields?: boolean,
|
||||
) {
|
||||
// When webauthn is enabled, add "webauthn" to autocomplete to enable passkey autofill
|
||||
let autocomplete: AutoFill = type === "email" ? "email" : "username";
|
||||
|
||||
if (this.#webauthn.live) {
|
||||
autocomplete = `${autocomplete} webauthn`;
|
||||
}
|
||||
|
||||
return html`<input
|
||||
${ref(this.autofocusTarget.reference)}
|
||||
id=${id}
|
||||
@@ -307,6 +314,9 @@ export class IdentificationStage extends BaseStage<
|
||||
autofocus
|
||||
autocomplete=${autocomplete}
|
||||
spellcheck="false"
|
||||
inputmode=${type === "email" ? "email" : "text"}
|
||||
autocapitalize="none"
|
||||
enterkeyhint=${passwordFields ? "next" : "go"}
|
||||
class="pf-c-form-control"
|
||||
value=${initialUserIdentification ?? ""}
|
||||
required
|
||||
@@ -345,19 +355,11 @@ export class IdentificationStage extends BaseStage<
|
||||
const type = fields.length === 1 && fields[0] === UserFieldsEnum.Email ? "email" : "text";
|
||||
const label = OR_LIST_FORMATTERS.format(fields.map((f) => UI_FIELDS[f]));
|
||||
|
||||
// When webauthn is enabled, add "webauthn" to autocomplete to enable passkey autofill
|
||||
const autocomplete: AutoFill = this.#webauthn.live ? "username webauthn" : "username";
|
||||
|
||||
console.debug(
|
||||
"Rendering identification stage with fields:",
|
||||
fields,
|
||||
initialUserIdentification,
|
||||
);
|
||||
// prettier-ignore
|
||||
return html`${offerRecovery ? this.renderRecoveryMessage() : nothing}
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: inputID }, label)}
|
||||
${this.renderUidField(inputID, type, label, initialUserIdentification, autocomplete)}
|
||||
${this.renderUidField(inputID, type, label, initialUserIdentification, passwordFields)}
|
||||
${rememberMeController?.renderToggleInput() ?? null}
|
||||
${AKFormErrors({ errors: challenge.responseErrors?.uid_field })}
|
||||
</div>
|
||||
@@ -433,9 +435,8 @@ export class IdentificationStage extends BaseStage<
|
||||
return html`<fieldset
|
||||
slot="footer"
|
||||
part="source-list"
|
||||
role="group"
|
||||
name="login-sources"
|
||||
class="pf-c-form__group"
|
||||
class="ak-c-fieldset pf-c-form__group"
|
||||
>
|
||||
<legend class="sr-only">${msg("Login sources")}</legend>
|
||||
${repeat(
|
||||
@@ -467,7 +468,8 @@ export class IdentificationStage extends BaseStage<
|
||||
return html`<fieldset
|
||||
slot="footer-band"
|
||||
part="additional-actions"
|
||||
class="pf-c-login__main-footer-band"
|
||||
name="additional-actions"
|
||||
class="ak-c-fieldset pf-c-login__main-footer-band"
|
||||
>
|
||||
<legend class="sr-only">${msg("Additional actions")}</legend>
|
||||
${enrollUrl
|
||||
|
||||
@@ -52,7 +52,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
?allow-show-password=${!!this.challenge?.allowShowPassword}
|
||||
prefill=${PasswordManagerPrefill.password ?? ""}
|
||||
></ak-flow-input-password>
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
name="continue"
|
||||
@@ -67,7 +67,8 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
? html`<fieldset
|
||||
slot="footer-band"
|
||||
part="additional-actions"
|
||||
class="pf-c-login__main-footer-band"
|
||||
name="additional-actions"
|
||||
class="ak-c-fieldset pf-c-login__main-footer-band"
|
||||
>
|
||||
<legend class="sr-only">${msg("Additional actions")}</legend>
|
||||
<div class="pf-c-login__main-footer-band-item">
|
||||
|
||||
@@ -322,7 +322,7 @@ ${prompt.initialValue}</textarea
|
||||
}
|
||||
|
||||
protected renderContinue(): SlottedTemplateResult {
|
||||
return html`<fieldset class="pf-c-form__group pf-m-action">
|
||||
return html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button name="continue" type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class PasswordStage extends BaseStage<
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<fieldset class="pf-c-form__group pf-m-action">
|
||||
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button name="remember-me" type="submit" class="pf-c-button pf-m-primary">
|
||||
${msg("Yes")}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@import "./components/Content/content.css";
|
||||
@import "./components/Table/table.css";
|
||||
@import "./components/Form/form.css";
|
||||
@import "./components/Fieldset/fieldset.css";
|
||||
@import "./components/Switch/switch.css";
|
||||
@import "./components/Select/select.css";
|
||||
@import "./components/Modal/modal.css";
|
||||
|
||||
@@ -245,3 +245,94 @@ html[data-theme="dark"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.ak-c-fieldset {
|
||||
--ak-c-fieldset--BorderWidth: thin;
|
||||
--ak-c-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
|
||||
|
||||
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BackgroundColor--light-100));
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BorderColor--200));
|
||||
}
|
||||
|
||||
@media (prefers-contrast: less) {
|
||||
border-color: var(--ak-c-fieldset--BorderColor, transparent);
|
||||
}
|
||||
|
||||
border-width: var(--ak-c-fieldset--BorderWidth);
|
||||
|
||||
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
|
||||
|
||||
& > legend {
|
||||
line-height: 1;
|
||||
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
|
||||
margin-inline-start: var(
|
||||
--ak-c-fieldset__legend--MarginInlineStart,
|
||||
var(--ak-c-fieldset__legend--MarginInlineBase)
|
||||
) !important;
|
||||
margin-inline-end: var(
|
||||
--ak-legend-margin-inline-end,
|
||||
var(--ak-c-fieldset__legend--MarginInlineBase)
|
||||
) !important;
|
||||
}
|
||||
|
||||
&:has(legend.sr-only:not(.more-contrast-only)) {
|
||||
border-width: 0;
|
||||
|
||||
&:not(.pf-c-modal-box__footer) {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: 0;
|
||||
--ak-c-fieldset__legend--MarginInlineBase: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.pf-c-form__group {
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
}
|
||||
|
||||
&.pf-c-form__group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.pf-m-action {
|
||||
gap: var(--pf-global--spacer--md) var(--pf-global--spacer--sm);
|
||||
margin-block-start: 0;
|
||||
|
||||
/* Fallback for action-only fieldsets when :has() does not suppress
|
||||
* the browser's native groove border. Keep the device picker
|
||||
* bordered because it has a visible legend.
|
||||
*/
|
||||
&:not([name="device-challenges"]) {
|
||||
border-width: 0;
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: 0;
|
||||
--ak-c-fieldset__legend--MarginInlineBase: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pf-c-login__main-footer-band {
|
||||
& > *:last-child {
|
||||
padding-block-end: var(--pf-c-login__main-footer-band-item--PaddingTop);
|
||||
}
|
||||
}
|
||||
|
||||
&.pf-c-modal-box__footer {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--lg);
|
||||
--pf-c-modal-box__footer--c-button--MarginRight: 0;
|
||||
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
justify-content: end;
|
||||
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
|
||||
border-inline: none;
|
||||
border-block-end: none;
|
||||
|
||||
--pf-c-modal-box__footer--c-button--sm--MarginRight: var(
|
||||
--pf-c-modal-box__footer--c-button--MarginRight
|
||||
);
|
||||
|
||||
& > ak-spinner-button:not(:last-child) {
|
||||
margin-right: var(--pf-c-modal-box__footer--c-button--MarginRight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.pf-c-card > fieldset {
|
||||
.pf-c-card > .ak-c-fieldset {
|
||||
margin-inline: var(--pf-global--spacer--md);
|
||||
margin-block-end: var(--pf-global--spacer--md);
|
||||
|
||||
@media not (prefers-contrast: more) {
|
||||
--ak-fieldset__legend--MarginInlineStart: calc(
|
||||
var(--pf-c-card--child--PaddingLeft) - var(--ak-fieldset__legend--PaddingInlineBase)
|
||||
--ak-c-fieldset__legend--MarginInlineStart: calc(
|
||||
var(--pf-c-card--child--PaddingLeft) - var(--ak-c-fieldset__legend--PaddingInlineBase)
|
||||
);
|
||||
--ak-legend-margin-inline-end: calc(
|
||||
var(--pf-c-card--child--PaddingRight) - var(--ak-fieldset__legend--PaddingInlineBase)
|
||||
var(--pf-c-card--child--PaddingRight) - var(--ak-c-fieldset__legend--PaddingInlineBase)
|
||||
);
|
||||
|
||||
border-width: 0;
|
||||
|
||||
102
web/src/styles/authentik/components/Fieldset/fieldset.css
Normal file
102
web/src/styles/authentik/components/Fieldset/fieldset.css
Normal file
@@ -0,0 +1,102 @@
|
||||
.ak-c-fieldset {
|
||||
--ak-c-fieldset--BorderWidth: thin;
|
||||
|
||||
--ak-c-fieldset--RowGap: var(--pf-global--spacer--md);
|
||||
--ak-c-fieldset--ColumnGap: var(--pf-global--spacer--sm);
|
||||
|
||||
--ak-c-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
|
||||
|
||||
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BackgroundColor--light-100));
|
||||
border-width: var(--ak-c-fieldset--BorderWidth);
|
||||
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BorderColor--200));
|
||||
}
|
||||
|
||||
@media (prefers-contrast: less) {
|
||||
border-color: var(--ak-c-fieldset--BorderColor, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.ak-c-fieldset > legend {
|
||||
line-height: 1;
|
||||
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
|
||||
margin-inline-start: var(
|
||||
--ak-c-fieldset__legend--MarginInlineStart,
|
||||
var(--ak-c-fieldset__legend--MarginInlineBase)
|
||||
) !important;
|
||||
margin-inline-end: var(
|
||||
--ak-legend-margin-inline-end,
|
||||
var(--ak-c-fieldset__legend--MarginInlineBase)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.ak-c-fieldset:has(legend.sr-only:not(.more-contrast-only)) {
|
||||
border-width: 0;
|
||||
|
||||
&:not(.pf-c-modal-box__footer) {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: 0;
|
||||
--ak-c-fieldset__legend--MarginInlineBase: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ak-c-fieldset.pf-c-form__group {
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
}
|
||||
|
||||
.ak-c-fieldset.pf-c-form__group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.pf-m-action {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: 0;
|
||||
--ak-c-fieldset__legend--MarginInlineBase: 0;
|
||||
--ak-c-fieldset--BorderWidth: 0;
|
||||
|
||||
row-gap: var(--ak-c-fieldset--RowGap);
|
||||
column-gap: var(--ak-c-fieldset--ColumnGap);
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ak-c-fieldset.pf-c-login__main-footer-band > *:last-child {
|
||||
padding-block-end: var(--pf-c-login__main-footer-band-item--PaddingTop);
|
||||
}
|
||||
|
||||
/* TODO: Remove after ak-modal migration. */
|
||||
.ak-c-fieldset.pf-c-modal-box__footer {
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--lg);
|
||||
|
||||
--pf-c-modal-box__footer--c-button--MarginRight: 0;
|
||||
--pf-c-modal-box__footer--c-button--sm--MarginRight: var(
|
||||
--pf-c-modal-box__footer--c-button--MarginRight
|
||||
);
|
||||
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
justify-content: end;
|
||||
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
|
||||
border-inline: none;
|
||||
border-block-end: none;
|
||||
|
||||
& > ak-spinner-button:not(:last-child) {
|
||||
margin-right: var(--pf-c-modal-box__footer--c-button--MarginRight);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] .ak-c-fieldset,
|
||||
:host([theme="dark"]) .ak-c-fieldset {
|
||||
border-color: var(
|
||||
--ak-c-fieldset--BorderColor,
|
||||
var(--pf-global--BackgroundColor--dark-transparent-200)
|
||||
);
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BorderColor--300));
|
||||
}
|
||||
|
||||
@media (prefers-contrast: less) {
|
||||
border-color: var(--ak-c-fieldset--BorderColor, transparent);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@
|
||||
);
|
||||
}
|
||||
|
||||
.pf-c-form__alert {
|
||||
display: grid;
|
||||
gap: var(--pf-global--spacer--form-element);
|
||||
}
|
||||
|
||||
.pf-c-form.ak-m-content-center {
|
||||
--pf-c-form--GridGap: var(--pf-global--spacer--sm);
|
||||
|
||||
@@ -90,91 +95,6 @@ ak-form-element-horizontal:has(.pf-c-form__helper-text + ak-checkbox-group) {
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Fields */
|
||||
|
||||
fieldset {
|
||||
--ak-fieldset--BorderWidth: thin;
|
||||
--ak-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
|
||||
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
|
||||
|
||||
border-color: var(--ak-fieldset--BorderColor, var(--pf-global--BackgroundColor--light-100));
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
border-color: var(--ak-fieldset--BorderColor, var(--pf-global--BorderColor--200));
|
||||
}
|
||||
|
||||
@media (prefers-contrast: less) {
|
||||
border-color: var(--ak-fieldset--BorderColor, transparent);
|
||||
}
|
||||
|
||||
border-width: var(--ak-fieldset--BorderWidth);
|
||||
|
||||
padding: var(--ak-fieldset__legend--PaddingInlineBase) !important;
|
||||
|
||||
& > legend {
|
||||
line-height: 1;
|
||||
padding: var(--ak-fieldset__legend--PaddingInlineBase) !important;
|
||||
margin-inline-start: var(
|
||||
--ak-fieldset__legend--MarginInlineStart,
|
||||
var(--ak-fieldset__legend--MarginInlineBase)
|
||||
) !important;
|
||||
margin-inline-end: var(
|
||||
--ak-legend-margin-inline-end,
|
||||
var(--ak-fieldset__legend--MarginInlineBase)
|
||||
) !important;
|
||||
}
|
||||
|
||||
&:has(legend.sr-only:not(.more-contrast-only)) {
|
||||
border-width: 0;
|
||||
|
||||
&:not(.pf-c-modal-box__footer) {
|
||||
--ak-fieldset__legend--PaddingInlineBase: 0;
|
||||
--ak-fieldset__legend--MarginInlineBase: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.pf-c-form__group {
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
}
|
||||
|
||||
&.pf-c-form__group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.pf-m-action {
|
||||
gap: var(--pf-global--spacer--md) var(--pf-global--spacer--sm);
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.pf-c-login__main-footer-band {
|
||||
& > *:last-child {
|
||||
padding-block-end: var(--pf-c-login__main-footer-band-item--PaddingTop);
|
||||
}
|
||||
}
|
||||
|
||||
&.pf-c-modal-box__footer {
|
||||
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--lg);
|
||||
--pf-c-modal-box__footer--c-button--MarginRight: 0;
|
||||
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
justify-content: end;
|
||||
padding-block: calc(var(--ak-fieldset__legend--PaddingInlineBase) / 2);
|
||||
border-inline: none;
|
||||
border-block-end: none;
|
||||
|
||||
--pf-c-modal-box__footer--c-button--sm--MarginRight: var(
|
||||
--pf-c-modal-box__footer--c-button--MarginRight
|
||||
);
|
||||
|
||||
& > ak-spinner-button:not(:last-child) {
|
||||
margin-right: var(--pf-c-modal-box__footer--c-button--MarginRight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Radio */
|
||||
|
||||
.pf-c-radio {
|
||||
@@ -402,21 +322,6 @@ ak-switch-input {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-color: var(
|
||||
--ak-fieldset--BorderColor,
|
||||
var(--pf-global--BackgroundColor--dark-transparent-200)
|
||||
);
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
border-color: var(--ak-fieldset--BorderColor, var(--pf-global--BorderColor--300));
|
||||
}
|
||||
|
||||
@media (prefers-contrast: less) {
|
||||
border-color: var(--ak-fieldset--BorderColor, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Radio */
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
@media (width > 1210px) {
|
||||
.pf-c-page__sidebar.pf-m-expanded {
|
||||
--pf-c-page__sidebar--BoxShadow: var(--pf-global--BoxShadow--sm-right);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width < 1200px) {
|
||||
@media (width <= 1210px) {
|
||||
.pf-c-page__sidebar.pf-m-expanded ~ .pf-c-page__drawer .pf-c-page__sidebar-backdrop::after {
|
||||
background-color: var(--pf-global--BackgroundColor--dark-transparent-100);
|
||||
content: "";
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.pf-c-wizard__main-body {
|
||||
--ak-fieldset--BorderColor: var(--pf-global--BackgroundColor--150);
|
||||
--ak-c-fieldset--BorderColor: var(--pf-global--BackgroundColor--150);
|
||||
|
||||
gap: var(--pf-global--spacer--lg);
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
.pf-c-wizard__footer {
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
/** Approximation of the height of navigation buttons to avoid excessive layout shifts when they are added or removed. */
|
||||
min-height: calc(var(--pf-global--spacer--3xl) + (var(--pf-global--spacer--form-element) * 2));
|
||||
}
|
||||
|
||||
.pf-c-wizard__nav-link {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@import "@patternfly/patternfly/utilities/Text/text.css";
|
||||
@import "./components/Drawer/drawer.css";
|
||||
@import "./components/Form/form.css";
|
||||
@import "./components/Fieldset/fieldset.css";
|
||||
@import "./components/Login/login.css";
|
||||
@import "./components/Icon/icon.css";
|
||||
@import "#elements/locale/ak-locale-select.css";
|
||||
|
||||
@@ -132,7 +132,7 @@ ak-app-icon {
|
||||
|
||||
[part="app-group-header"] {
|
||||
@media not (prefers-contrast: more) {
|
||||
--ak-fieldset__legend--PaddingInlineBase: 1rem;
|
||||
--ak-c-fieldset__legend--PaddingInlineBase: 1rem;
|
||||
padding-block-start: 0 !important;
|
||||
padding-inline: 0 !important;
|
||||
margin-inline: 0 !important;
|
||||
|
||||
@@ -63,6 +63,7 @@ export const AKLibraryApplicationList: LitFC<AKLibraryApplicationListProps> = ({
|
||||
const groupID = kebabCase(groupLabel);
|
||||
|
||||
return html`<fieldset
|
||||
class="ak-c-fieldset"
|
||||
data-group-id=${ifPresent(groupID)}
|
||||
part="app-group"
|
||||
data-group-index=${groupIndex}
|
||||
|
||||
@@ -178,7 +178,7 @@ export class LibraryPage extends WithSession(AKElement) {
|
||||
threshold: 0.3,
|
||||
});
|
||||
|
||||
public pageTitle = msg("User Dashboard - Applications");
|
||||
public pageTitle = msg("My Applications");
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
@@ -432,7 +432,7 @@ export class LibraryPage extends WithSession(AKElement) {
|
||||
protected override render() {
|
||||
return html`<div class="pf-c-page__main">
|
||||
<div class="pf-c-page__header pf-c-content">
|
||||
<h1 class="pf-c-page__title">${msg("User Dashboard")}</h1>
|
||||
<h1 class="pf-c-page__title">${msg("My applications")}</h1>
|
||||
${this.searchEnabled ? this.renderSearch() : nothing}
|
||||
</div>
|
||||
<main
|
||||
|
||||
@@ -93,7 +93,7 @@ export class LibraryPage extends AKElement {
|
||||
);
|
||||
}
|
||||
|
||||
public pageTitle = msg("User Dashboard - Applications");
|
||||
public pageTitle = msg("My Applications");
|
||||
|
||||
render() {
|
||||
if (this.apps.loading) {
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe("Session management", () => {
|
||||
page.getByRole("heading", {
|
||||
level: 1,
|
||||
}),
|
||||
).toHaveText("User Dashboard", {
|
||||
).toHaveText("My applications", {
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
"aria-description",
|
||||
"inert",
|
||||
"enterkeyhint",
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
@@ -1302,10 +1302,6 @@
|
||||
<source>Open in new tab</source>
|
||||
<target>Otevřít v nové záložce</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8655c52824caac63">
|
||||
<source>If checked, the launch URL will open in a new browser tab or window from the user's application library.</source>
|
||||
<target>Pokud je zaškrtnuto, spouštěcí URL se otevře v nové záložce nebo okně prohlížeče z knihovny aplikací uživatele.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s909e876731a8febb">
|
||||
<source>Select all rows</source>
|
||||
<target>Vybrat všechny řádky</target>
|
||||
@@ -1393,18 +1389,10 @@
|
||||
<source>Policy</source>
|
||||
<target>Zásada</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b85380416964890">
|
||||
<source>Negate result</source>
|
||||
<target>Negovat výsledek</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3bfa0258999fb629">
|
||||
<source>Negates the outcome of the binding. Messages are unaffected.</source>
|
||||
<target>Neguje výsledek vazby. Zprávy nejsou ovlivněny.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b1ed7507f26cb4a">
|
||||
<source>Failure result</source>
|
||||
<target>Výsledek selhání</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfdfa5bb4ddd99d70">
|
||||
<source>Enterprise only</source>
|
||||
<target>Pouze podnik</target>
|
||||
@@ -6143,10 +6131,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
|
||||
<source>Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.</source>
|
||||
<target>Krok používaný k ověření jakéhokoli autentikátoru. Tento krok by měl být použit během ověřovacích nebo autorizačních toků.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s73c13e5a6f5e38a3">
|
||||
<source>Device classes</source>
|
||||
<target>Třídy zařízení</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd8d9451f86502d1a">
|
||||
<source>Device classes which can be used to authenticate.</source>
|
||||
<target>Třídy zařízení, které lze použít k ověření.</target>
|
||||
@@ -6391,10 +6375,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
|
||||
<source>Let the user identify themselves with their username or Email address.</source>
|
||||
<target>Umožnit uživateli identifikovat se uživatelským jménem nebo emailovou adresou.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s592ab7d2bc1b8973">
|
||||
<source>User fields</source>
|
||||
<target>Uživatelská pole</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4cdae7635e757555">
|
||||
<source>Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.</source>
|
||||
<target>Pole, kterými se uživatel může identifikovat. Pokud nejsou vybrána žádná pole, uživatel bude moci používat pouze zdroje.</target>
|
||||
@@ -8608,10 +8588,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s45915ee0293ce5d4">
|
||||
<source>Ungrouped</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cf2298d92c327a6">
|
||||
<source>My Applications</source>
|
||||
<target>Moje aplikace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s65b433c52c2ad8eb">
|
||||
<source>Search for an application by name...</source>
|
||||
</trans-unit>
|
||||
@@ -8619,10 +8595,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<source>Search returned no results.</source>
|
||||
<target>Hledání nevrátilo žádné výsledky.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2656433a3b1f7e86">
|
||||
<source>My applications</source>
|
||||
<target>Moje aplikace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91cfe4fac0957ee7">
|
||||
<source>Application list</source>
|
||||
</trans-unit>
|
||||
@@ -10959,12 +10931,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="s71724d2ff6637203">
|
||||
<source>Altered behavior for usage with VMware vCenter.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9eda7101f63a8652">
|
||||
<source>Hide from My applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30f30e9c42594a33">
|
||||
<source>If checked, this application will not be shown on the user's My applications page.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2ea2e39e4b470249">
|
||||
<source>EntityID/Issuer override</source>
|
||||
</trans-unit>
|
||||
@@ -11098,6 +11064,33 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<trans-unit id="sdcd1a9744efdbd7e">
|
||||
<source>Choose Policy Type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf4eb7c0c8e92e6b2">
|
||||
<source>Whether the launch URL will open in a new browser tab or window from the user's application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8ab0176c9cf77b1a">
|
||||
<source>Hide from User Dashboard</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b5847edb7150911">
|
||||
<source>Whether this application will be shown on the User Dashboard.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa5ed8446baaba70">
|
||||
<source>Negate Result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5387aa645962312a">
|
||||
<source>Failure Result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sea62de98f2e25e03">
|
||||
<source>Device Classes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s41938ae69656ef53">
|
||||
<source>User Fields</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s977b2dff8b9def14">
|
||||
<source>User Dashboard - Applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s190cbdd5b62e4746">
|
||||
<source>User Dashboard</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -1313,10 +1313,6 @@
|
||||
<source>Open in new tab</source>
|
||||
<target>Im neuen Tab öffnen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8655c52824caac63">
|
||||
<source>If checked, the launch URL will open in a new browser tab or window from the user's application library.</source>
|
||||
<target>Wenn diese Option aktiviert ist, wird die Aufruf-URL in einer neuen Browser-Registerkarte oder einem neuen Fenster der Anwendungsbibliothek des Benutzers geöffnet.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s909e876731a8febb">
|
||||
<source>Select all rows</source>
|
||||
<target>Wählen Sie alle Zeilen aus</target>
|
||||
@@ -1404,18 +1400,10 @@
|
||||
<source>Policy</source>
|
||||
<target>Richtlinie</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b85380416964890">
|
||||
<source>Negate result</source>
|
||||
<target>Ergebnis verneinen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3bfa0258999fb629">
|
||||
<source>Negates the outcome of the binding. Messages are unaffected.</source>
|
||||
<target>Negiert das Ergebnis der Bindung. Nachrichten sind nicht betroffen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b1ed7507f26cb4a">
|
||||
<source>Failure result</source>
|
||||
<target>Fehlergebnis</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfdfa5bb4ddd99d70">
|
||||
<source>Enterprise only</source>
|
||||
<target>Enterprise-Feature</target>
|
||||
@@ -6169,10 +6157,6 @@ Beim Erstellen eines festen Auswahlfelds aktiviere „Als Ausdruck interpretiere
|
||||
<source>Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.</source>
|
||||
<target>Stage, die verwendet wird, um einen beliebigen Authentifikator zu validieren. Diese Stage sollte während Authentifizierungs- oder Autorisierungs-Flows verwendet werden.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s73c13e5a6f5e38a3">
|
||||
<source>Device classes</source>
|
||||
<target>Geräteklassen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd8d9451f86502d1a">
|
||||
<source>Device classes which can be used to authenticate.</source>
|
||||
<target>Geräteklassen, die zur Authentifizierung verwendet werden können.</target>
|
||||
@@ -6417,10 +6401,6 @@ Beim Erstellen eines festen Auswahlfelds aktiviere „Als Ausdruck interpretiere
|
||||
<source>Let the user identify themselves with their username or Email address.</source>
|
||||
<target>Lassen Sie den Benutzer sich mit seinem Benutzernamen oder seiner E-Mail-Adresse identifizieren.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s592ab7d2bc1b8973">
|
||||
<source>User fields</source>
|
||||
<target>Benutzerfelder</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4cdae7635e757555">
|
||||
<source>Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.</source>
|
||||
<target>Felder, mit denen sich ein Benutzer identifizieren kann. Wenn keine Felder ausgewählt sind, kann der Benutzer nur Quellen verwenden.</target>
|
||||
@@ -8640,10 +8620,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s45915ee0293ce5d4">
|
||||
<source>Ungrouped</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cf2298d92c327a6">
|
||||
<source>My Applications</source>
|
||||
<target>Meine Anwendungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s65b433c52c2ad8eb">
|
||||
<source>Search for an application by name...</source>
|
||||
</trans-unit>
|
||||
@@ -8651,10 +8627,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<source>Search returned no results.</source>
|
||||
<target>Suche ergab keine Treffer.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2656433a3b1f7e86">
|
||||
<source>My applications</source>
|
||||
<target>Meine Anwendungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91cfe4fac0957ee7">
|
||||
<source>Application list</source>
|
||||
</trans-unit>
|
||||
@@ -10991,12 +10963,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="s71724d2ff6637203">
|
||||
<source>Altered behavior for usage with VMware vCenter.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9eda7101f63a8652">
|
||||
<source>Hide from My applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30f30e9c42594a33">
|
||||
<source>If checked, this application will not be shown on the user's My applications page.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2ea2e39e4b470249">
|
||||
<source>EntityID/Issuer override</source>
|
||||
</trans-unit>
|
||||
@@ -11130,6 +11096,33 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<trans-unit id="sdcd1a9744efdbd7e">
|
||||
<source>Choose Policy Type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf4eb7c0c8e92e6b2">
|
||||
<source>Whether the launch URL will open in a new browser tab or window from the user's application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8ab0176c9cf77b1a">
|
||||
<source>Hide from User Dashboard</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b5847edb7150911">
|
||||
<source>Whether this application will be shown on the User Dashboard.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa5ed8446baaba70">
|
||||
<source>Negate Result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5387aa645962312a">
|
||||
<source>Failure Result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sea62de98f2e25e03">
|
||||
<source>Device Classes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s41938ae69656ef53">
|
||||
<source>User Fields</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s977b2dff8b9def14">
|
||||
<source>User Dashboard - Applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s190cbdd5b62e4746">
|
||||
<source>User Dashboard</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -998,9 +998,6 @@
|
||||
<trans-unit id="s2348f46ebf436671">
|
||||
<source>Open in new tab</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8655c52824caac63">
|
||||
<source>If checked, the launch URL will open in a new browser tab or window from the user's application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s909e876731a8febb">
|
||||
<source>Select all rows</source>
|
||||
</trans-unit>
|
||||
@@ -1064,15 +1061,9 @@
|
||||
<trans-unit id="s042baf59902a711f">
|
||||
<source>Policy</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b85380416964890">
|
||||
<source>Negate result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3bfa0258999fb629">
|
||||
<source>Negates the outcome of the binding. Messages are unaffected.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b1ed7507f26cb4a">
|
||||
<source>Failure result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfdfa5bb4ddd99d70">
|
||||
<source>Enterprise only</source>
|
||||
</trans-unit>
|
||||
@@ -4748,9 +4739,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s0e15f678445dfc45">
|
||||
<source>Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s73c13e5a6f5e38a3">
|
||||
<source>Device classes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd8d9451f86502d1a">
|
||||
<source>Device classes which can be used to authenticate.</source>
|
||||
</trans-unit>
|
||||
@@ -4934,9 +4922,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s4af8a3ce5a600855">
|
||||
<source>Let the user identify themselves with their username or Email address.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s592ab7d2bc1b8973">
|
||||
<source>User fields</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4cdae7635e757555">
|
||||
<source>Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.</source>
|
||||
</trans-unit>
|
||||
@@ -6632,18 +6617,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s45915ee0293ce5d4">
|
||||
<source>Ungrouped</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cf2298d92c327a6">
|
||||
<source>My Applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s65b433c52c2ad8eb">
|
||||
<source>Search for an application by name...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4facec1106c91cf9">
|
||||
<source>Search returned no results.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2656433a3b1f7e86">
|
||||
<source>My applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91cfe4fac0957ee7">
|
||||
<source>Application list</source>
|
||||
</trans-unit>
|
||||
@@ -8965,12 +8944,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s71724d2ff6637203">
|
||||
<source>Altered behavior for usage with VMware vCenter.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9eda7101f63a8652">
|
||||
<source>Hide from My applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s30f30e9c42594a33">
|
||||
<source>If checked, this application will not be shown on the user's My applications page.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2ea2e39e4b470249">
|
||||
<source>EntityID/Issuer override</source>
|
||||
</trans-unit>
|
||||
@@ -9104,6 +9077,33 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sdcd1a9744efdbd7e">
|
||||
<source>Choose Policy Type</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf4eb7c0c8e92e6b2">
|
||||
<source>Whether the launch URL will open in a new browser tab or window from the user's application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8ab0176c9cf77b1a">
|
||||
<source>Hide from User Dashboard</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0b5847edb7150911">
|
||||
<source>Whether this application will be shown on the User Dashboard.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="saa5ed8446baaba70">
|
||||
<source>Negate Result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5387aa645962312a">
|
||||
<source>Failure Result</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sea62de98f2e25e03">
|
||||
<source>Device Classes</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s41938ae69656ef53">
|
||||
<source>User Fields</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s977b2dff8b9def14">
|
||||
<source>User Dashboard - Applications</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s190cbdd5b62e4746">
|
||||
<source>User Dashboard</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user