Compare commits

...

23 Commits

Author SHA1 Message Date
Marcelo Elizeche Landó
731163200a Add invitation wizard docs 2026-05-05 14:03:54 -03:00
Marcelo Elizeche Landó
a8db2882ec stages/invitation: Invitation wizard (#20399) 2026-05-05 11:47:31 -05:00
Ken Sternberg
befc15ad92 Web/release202604/nits 2 (#22040)
* ## What

         window.authentik.flow = {
             "layout": "{{ flow.layout }}",
    +        "background": "{{ flow.background }}",
    +        "title": "{{ flow.title }}",
         };

Amends the `flow.html` template and `GlobalAuthentik` parser to include new parameters, `background` and `title`, in the flow-specific part of the configuration written to the HTML `<head>` object, and to provide those parameters to client code.

## Why

The `layout` is start-up critical: it tells the Flow interface how the admin wants the Flow page to look, and allows the HTML and CSS to be pre-aligned to that condition. `layout` is determined on a per-Flow bases, not a per-Stage basis; Flows are derived from a tuple of `(Brand, Application?)`, where the opening policy *may* direct a user to a different flow if the user reached authentik via a redirect from a specific application, but will otherwise fall back to the default Flow for the Brand.

The `background` is a field that is required if the `Flow`’s layout is of type `frame_background`; in this case, the part of the viewport not dedicated to the FlowExecutor is reserved for an `<iframe>` that will be filled in with whatever the administrator specifies. Although this gives it the same priority as `layout` (whether it’s provided or undefined) for describing the [chrome](https://developer.mozilla.org/en-US/docs/Glossary/Chrome) around a challenge, it is currently not provided to the application in the start-up config; it is provided in the `challenge` and renders the IFrame as part of the initial challenge.

This patch fixes that; if `layout` is provided, `background` ought to be as well, even if it’s empty. The execution of a Challenge ought not have any influence over the look and feel of the Flow-defined appearance *around* that Challenge.

I have added `title` as well; with that, all of the current theme-and-appearance related configuration details are placed into `<head>` and can be removed from the FlowExecutor.

Server-side, `background` is currently specified: `background = FileField(blank=True, default="")` which is … interesting since we also appear to store URLs in it. I don’t see anything in the FlowSerializer that would change that from a client’s point of view.

This patch furthers the effort to separate flow execution from flow presentation.

- \[🐰\] The code has been formatted (`make web`)

* The status label was using HTML booleans incorrectly. It is impossible for a boolean to be null. The default red was alarming, so I chose a neutral grey for the 'not default' state.

* It is not enough to provide a blank cell to ensure the header is spaced correctly; if the table is empty, that will collapse to zero width.  Providing the classes that go with the 'this cell may contain a toggle' provides the correct spacing as well.

* Fix inconsistent wording between menu and page; make the 'select type' radiocard and radiolist interfaces flush with the top of the form container, removing a weird jagged visual line between the menu and the content.

* Document adding 'toggle' to Table classes.

* Fix how the buttons for TablePage's empty state align; slots are still wonky when responding to content layout that we do not control ourselves.

* Do not show pagination controls when there are no pages to turn.

* Fix spacing after ak-alert in documentation show in the front-end.  Without this, headers and paragraphs were edging well into the alert's drop-shadow.

* Remove separator line from radio entries; P4-ism that was visually confusing.

* Make the empty state a slot, so it can be easily overriden, and provide a default if the slot isn't filled from a lightDOM entry. Add one to the columnWidth, since columnWidth doesn't include the action column; this fixes a visual tic where the empty state did not look correctly centered.
2026-05-05 09:43:53 -07:00
Teffen Ellis
2b48c27760 web: Gracefully handle missing element construction. (#21787)
* web: Gracefully handle missing element construction.

* web: Tailor missing element message based on debug capability. (#22048)

Show a developer-oriented hint when CanDebug is set, and an
end-user-friendly suggestion (refresh / clear cache) otherwise.

Co-authored-by: Agent (authentik-i21787-graceful-gross-chrome) <279763771+playpen-agent@users.noreply.github.com>

---------

Co-authored-by: Agent (authentik-i21787-graceful-gross-chrome) <279763771+playpen-agent@users.noreply.github.com>
2026-05-05 18:41:33 +02:00
Jens L.
6be7b2f7b7 root: update django to 5.2.14 (#22064) 2026-05-05 15:49:16 +00:00
Jens L.
7cffbb4d07 tenants: add option to mark flag as deprecated (#22063)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-05-05 17:25:01 +02:00
Marcelo Elizeche Landó
5d629bec9b web/stages: better wording for webauthn authenticator attachments options (#22062)
better wording for webauthn authenticator attachments options
2026-05-05 17:02:55 +02:00
dependabot[bot]
5357f42029 web: bump vite from 8.0.8 to 8.0.10 in /web (#21842)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.8 to 8.0.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 14:50:15 +02:00
Dewi Roberts
716bc6e136 api: set authenticated session user agent nullable properties (#22059)
* Set properties to nullable and regenerate schema

* Make gen

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-05-05 14:47:27 +02:00
Dewi Roberts
60355fdf80 web/admin: redirect stage: adds mention of static url (#22060)
Adds mention of static url, not just flow redirect
2026-05-05 14:46:56 +02:00
dependabot[bot]
828a380569 web: bump axios from 1.15.0 to 1.16.0 in /web (#22058)
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 14:25:07 +02:00
Luca Sannitu
b04f8a6177 providers/oauth2: override RedirectURITypeEnum capitalization for generated API (#22037)
* fix(providers/oauth2): correct RedirectURITypeEnum capitalization in API schema

* fix: remove encoding artifacts introduced during client regeneration
2026-05-05 14:18:02 +02:00
Dominic R
ff190847f2 website/docs: document language settings (#21968) 2026-05-05 08:08:00 -04:00
Dominic R
a7339c7f87 website/docs: document supported PostgreSQL versions (#21967) 2026-05-05 08:07:24 -04:00
dependabot[bot]
38ae472f6c website: bump docusaurus-theme-openapi-docs from 5.0.1 to 5.0.2 in /website (#22052)
* website: bump docusaurus-theme-openapi-docs in /website

Bumps [docusaurus-theme-openapi-docs](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/HEAD/packages/docusaurus-theme-openapi-docs) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/releases)
- [Changelog](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/commits/v5.0.2/packages/docusaurus-theme-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-theme-openapi-docs
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-05-05 13:36:39 +02:00
dependabot[bot]
7d0656c6fa web: bump the storybook group across 1 directory with 5 updates (#22024)
Bumps the storybook group with 4 updates in the /web directory: [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs), [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links), [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) and [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite).


Updates `@storybook/addon-docs` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/docs)

Updates `@storybook/addon-links` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/links)

Updates `@storybook/web-components` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/frameworks/web-components-vite)

Updates `storybook` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:18:52 +02:00
Teffen Ellis
0bbe415b5b revert: web: Consistent use of "User Dashboard" (#22038) (#22046)
Revert "web: Consistent use of "User Dashboard" (#22038)"

This reverts commit d69433b314.
2026-05-05 13:17:40 +02:00
dependabot[bot]
e52c1b2bdc core: bump metrics-exporter-prometheus from 0.18.1 to 0.18.3 (#22057)
Bumps [metrics-exporter-prometheus](https://github.com/metrics-rs/metrics) from 0.18.1 to 0.18.3.
- [Changelog](https://github.com/metrics-rs/metrics/blob/main/release.toml)
- [Commits](https://github.com/metrics-rs/metrics/compare/metrics-exporter-prometheus-v0.18.1...metrics-exporter-prometheus-v0.18.3)

---
updated-dependencies:
- dependency-name: metrics-exporter-prometheus
  dependency-version: 0.18.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:09:40 +02:00
authentik-automation[bot]
5064167f28 core, web: update translations (#22047)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-05-05 13:08:29 +02:00
dependabot[bot]
bca0f51b53 core: bump cryptography from 47.0.0 to 48.0.0 (#22053)
Bumps [cryptography](https://github.com/pyca/cryptography) from 47.0.0 to 48.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/47.0.0...48.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 48.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:08:25 +02:00
dependabot[bot]
67c197e5a5 core: bump psycopg[c,pool] from 3.3.3 to 3.3.4 (#22054)
Bumps [psycopg[c,pool]](https://github.com/psycopg/psycopg) from 3.3.3 to 3.3.4.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.3...3.3.4)

---
updated-dependencies:
- dependency-name: psycopg[c,pool]
  dependency-version: 3.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:08:21 +02:00
dependabot[bot]
32b17da699 ci: bump taiki-e/install-action from 2.75.28 to 2.75.29 in /.github/actions/setup (#22056)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.28 to 2.75.29.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](51cd0b8c04...b5fddbb536)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:08:16 +02:00
Dominic R
c75eed630a web: remove native fieldset borders from action groups (#21334)
* web: remove native fieldset borders from action groups

Refs:\n- https://authentiksecurity.slack.com/archives/C08C0SCU2JV/p1775085687040019\n- https://authentiksecurity.slack.com/archives/C08C0SCU2JV/p1774988472501059

* Use consistent naming.

* Fix up styles, selector specifics, compatibility mode.

* Fix field autocapitalization, keyboard behavior.

* Fix default height.

* Fix for mid-size tablet viewports.

- Helped with debugging on mobile.

* Fix linter warning.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-05-05 06:17:23 +02:00
288 changed files with 3035 additions and 1251 deletions

View File

@@ -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)

67
Cargo.lock generated
View File

@@ -1003,6 +1003,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 +1230,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"
@@ -1300,6 +1326,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 +1889,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 +1971,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 +2033,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",
@@ -3103,6 +3160,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"

View File

@@ -44,7 +44,7 @@ hyper-util = "= 0.1.20"
ipnet = { version = "= 2.12.0", features = ["serde"] }
json-subscriber = "= 0.2.8"
metrics = "= 0.24.5"
metrics-exporter-prometheus = { version = "= 0.18.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"

View File

@@ -1,5 +1,6 @@
"""Serializer mixin for managed models"""
from json import JSONDecodeError, loads
from typing import cast
from django.conf import settings
@@ -44,6 +45,7 @@ class BlueprintUploadSerializer(PassiveSerializer):
file = FileField(required=False)
path = CharField(required=False)
context = CharField(required=False, allow_blank=True)
def validate_path(self, path: str) -> str:
"""Ensure the path (if set) specified is retrievable"""
@@ -54,6 +56,18 @@ class BlueprintUploadSerializer(PassiveSerializer):
raise ValidationError(_("Blueprint file does not exist"))
return path
def validate_context(self, context: str) -> dict:
"""Parse context as a JSON object"""
if not context:
return {}
try:
parsed = loads(context)
except JSONDecodeError as exc:
raise ValidationError(_("Context must be valid JSON")) from exc
if not isinstance(parsed, dict):
raise ValidationError(_("Context must be a JSON object"))
return parsed
class ManagedSerializer:
"""Managed Serializer"""
@@ -224,7 +238,8 @@ class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
).retrieve_file()
else:
raise ValidationError("Either path or file must be set")
importer = Importer.from_string(string_contents)
context = body.validated_data.get("context") or {}
importer = Importer.from_string(string_contents, context)
check_blueprint_perms(importer.blueprint, request.user)

View File

@@ -1,6 +1,6 @@
"""Test blueprints v1 api"""
from json import loads
from json import dumps, loads
from tempfile import NamedTemporaryFile, mkdtemp
from django.urls import reverse
@@ -8,7 +8,11 @@ from rest_framework.test import APITestCase
from yaml import dump
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.invitation.models import InvitationStage
from authentik.stages.user_write.models import UserWriteStage
TMP = mkdtemp("authentik-blueprints")
@@ -80,3 +84,107 @@ class TestBlueprintsV1API(APITestCase):
res.content.decode(),
{"content": ["Failed to validate blueprint", "- Invalid blueprint version"]},
)
def test_api_import_with_context(self):
"""Test that the import endpoint applies the supplied context to the real blueprint"""
slug = f"invitation-enrollment-{generate_id()}"
flow_name = f"Invitation Enrollment {generate_id()}"
stage_name = f"invitation-stage-{generate_id()}"
user_type = "internal"
continue_without_invitation = True
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": dumps(
{
"flow_slug": slug,
"flow_name": flow_name,
"stage_name": stage_name,
"continue_flow_without_invitation": continue_without_invitation,
"user_type": user_type,
}
),
},
format="multipart",
)
self.assertEqual(res.status_code, 200)
self.assertTrue(res.json()["success"])
flow = Flow.objects.get(slug=slug)
self.assertEqual(flow.name, flow_name)
self.assertEqual(flow.title, flow_name)
invitation_stage = InvitationStage.objects.get(name=stage_name)
self.assertEqual(
invitation_stage.continue_flow_without_invitation,
continue_without_invitation,
)
user_write_stage = UserWriteStage.objects.get(
name=f"invitation-enrollment-user-write-{slug}"
)
self.assertEqual(user_write_stage.user_type, user_type)
self.assertEqual(user_write_stage.user_path_template, f"users/{user_type}")
def test_api_import_blank_path(self):
"""Validator returns empty path unchanged (covers api.py:53)."""
with NamedTemporaryFile(mode="w+", suffix=".yaml") as file:
file.write(dump({"version": 1, "entries": []}))
file.flush()
file.seek(0)
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={"path": "", "file": file},
format="multipart",
)
self.assertEqual(res.status_code, 200)
def test_api_import_unknown_path(self):
"""Path not in available blueprints is rejected (covers api.py:56)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={"path": "does/not/exist.yaml"},
format="multipart",
)
self.assertEqual(res.status_code, 400)
self.assertIn("Blueprint file does not exist", res.content.decode())
def test_api_import_blank_context(self):
"""Blank context is normalized to empty dict (covers api.py:62)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": "",
},
format="multipart",
)
self.assertEqual(res.status_code, 200)
def test_api_import_invalid_json_context(self):
"""Malformed JSON context raises ValidationError (covers api.py:65-66)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": "{not json",
},
format="multipart",
)
self.assertEqual(res.status_code, 400)
self.assertIn("Context must be valid JSON", res.content.decode())
def test_api_import_non_object_context(self):
"""JSON context that isn't an object is rejected (covers api.py:68)."""
res = self.client.post(
reverse("authentik_api:blueprintinstance-import-"),
data={
"path": "example/flows-invitation-enrollment-minimal.yaml",
"context": "[1, 2, 3]",
},
format="multipart",
)
self.assertEqual(res.status_code, 400)
self.assertIn("Context must be a JSON object", res.content.decode())

View File

@@ -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):

View File

@@ -29,6 +29,7 @@ class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others
default = False
visibility = "public"
description = _("Refresh other tabs after successful authentication.")
deprecated = True
class ContinuousLogin(Flag[bool], key="flows_continuous_login"):

View File

@@ -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",

View File

@@ -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)

View File

@@ -59,6 +59,8 @@ class FlagsJSONExtension(OpenApiSerializerFieldExtension):
props[_flag.key] = build_basic_type(get_args(_flag.__orig_bases__[0])[0])
if _flag.description:
props[_flag.key]["description"] = _flag.description
if _flag.deprecated:
props[_flag.key]["deprecated"] = _flag.deprecated
return build_object_type(props, required=props.keys())

View File

@@ -18,6 +18,7 @@ class Flag[T]:
Literal["none"] | Literal["public"] | Literal["authenticated"] | Literal["system"]
) = "none"
description: str | None = None
deprecated = False
def __init_subclass__(cls, key: str, **kwargs):
cls.__key = key

View File

@@ -0,0 +1,211 @@
# Minimal Invitation-based Enrollment Blueprint
#
# Companion to flows-invitation-enrollment.yaml, intended for the "New Invitation"
# wizard in the admin UI. Creates a single enrollment flow with an invitation stage
# bound to it, plus the supporting prompt/user-write/user-login stages.
#
# All user-facing fields are parameterized via !Context with fallback defaults, so
# this blueprint can be imported directly (without context) or through the wizard
# with custom values.
#
# Context keys (all optional):
# flow_name Display name of the enrollment flow.
# flow_slug URL slug of the flow and suffix for sub-entity
# identifiers (so repeated imports with different
# slugs don't overwrite each other).
# stage_name Name of the invitation stage.
# continue_flow_without_invitation Whether the flow continues when no invitation
# is supplied (default: false).
# user_type "external" or "internal" (default: "external").
# Drives the user-write stage's user_type and
# user_path_template.
version: 1
metadata:
labels:
blueprints.goauthentik.io/instantiate: "false"
name: Invitation-based Enrollment (minimal)
entries:
- identifiers:
slug: !Context [flow_slug, invitation-enrollment-flow]
model: authentik_flows.flow
id: flow
attrs:
name: !Context [flow_name, Invitation Enrollment Flow]
title: !Context [flow_name, Invitation Enrollment Flow]
designation: enrollment
authentication: require_unauthenticated
- identifiers:
name: !Context [stage_name, invitation-stage]
id: invitation-stage
model: authentik_stages_invitation.invitationstage
attrs:
continue_flow_without_invitation: !Context [continue_flow_without_invitation, false]
- identifiers:
name:
!Format [
"invitation-enrollment-field-username-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-username
model: authentik_stages_prompt.prompt
attrs:
field_key: username
label: Username
type: username
required: true
placeholder: Username
placeholder_expression: false
order: 0
- identifiers:
name:
!Format [
"invitation-enrollment-field-password-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-password
model: authentik_stages_prompt.prompt
attrs:
field_key: password
label: Password
type: password
required: true
placeholder: Password
placeholder_expression: false
order: 1
- identifiers:
name:
!Format [
"invitation-enrollment-field-password-repeat-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-password-repeat
model: authentik_stages_prompt.prompt
attrs:
field_key: password_repeat
label: Password (repeat)
type: password
required: true
placeholder: Password (repeat)
placeholder_expression: false
order: 2
- identifiers:
name:
!Format [
"invitation-enrollment-field-name-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-name
model: authentik_stages_prompt.prompt
attrs:
field_key: name
label: Name
type: text
required: true
placeholder: Name
placeholder_expression: false
order: 0
- identifiers:
name:
!Format [
"invitation-enrollment-field-email-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-field-email
model: authentik_stages_prompt.prompt
attrs:
field_key: email
label: Email
type: email
required: true
placeholder: Email
placeholder_expression: false
order: 1
- identifiers:
name:
!Format [
"invitation-enrollment-prompt-credentials-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-stage-credentials
model: authentik_stages_prompt.promptstage
attrs:
fields:
- !KeyOf prompt-field-username
- !KeyOf prompt-field-password
- !KeyOf prompt-field-password-repeat
- identifiers:
name:
!Format [
"invitation-enrollment-prompt-details-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: prompt-stage-details
model: authentik_stages_prompt.promptstage
attrs:
fields:
- !KeyOf prompt-field-name
- !KeyOf prompt-field-email
- identifiers:
name:
!Format [
"invitation-enrollment-user-write-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: user-write-stage
model: authentik_stages_user_write.userwritestage
attrs:
user_creation_mode: always_create
user_type: !Context [user_type, external]
user_path_template:
!Format ["users/%s", !Context [user_type, external]]
- identifiers:
name:
!Format [
"invitation-enrollment-user-login-%s",
!Context [flow_slug, invitation-enrollment-flow],
]
id: user-login-stage
model: authentik_stages_user_login.userloginstage
- identifiers:
target: !KeyOf flow
stage: !KeyOf invitation-stage
order: 5
model: authentik_flows.flowstagebinding
attrs:
evaluate_on_plan: true
re_evaluate_policies: true
- identifiers:
target: !KeyOf flow
stage: !KeyOf prompt-stage-credentials
order: 10
model: authentik_flows.flowstagebinding
- identifiers:
target: !KeyOf flow
stage: !KeyOf prompt-stage-details
order: 15
model: authentik_flows.flowstagebinding
- identifiers:
target: !KeyOf flow
stage: !KeyOf user-write-stage
order: 20
model: authentik_flows.flowstagebinding
- identifiers:
target: !KeyOf flow
stage: !KeyOf user-login-stage
order: 100
model: authentik_flows.flowstagebinding

Binary file not shown.

View File

@@ -47,6 +47,7 @@ export interface ManagedBlueprintsDestroyRequest {
export interface ManagedBlueprintsImportCreateRequest {
file?: Blob;
path?: string;
context?: string;
}
export interface ManagedBlueprintsListRequest {
@@ -369,6 +370,10 @@ export class ManagedApi extends runtime.BaseAPI {
formParams.append("path", requestParameters["path"] as any);
}
if (requestParameters["context"] != null) {
formParams.append("context", requestParameters["context"] as any);
}
let urlPath = `/managed/blueprints/import/`;
return {

View File

@@ -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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -40,6 +40,7 @@ export interface CurrentBrandFlags {
* Refresh other tabs after successful authentication.
* @type {boolean}
* @memberof CurrentBrandFlags
* @deprecated
*/
flowsRefreshOthers: boolean;
}

View File

@@ -40,6 +40,7 @@ export interface PatchedSettingsRequestFlags {
* Refresh other tabs after successful authentication.
* @type {boolean}
* @memberof PatchedSettingsRequestFlags
* @deprecated
*/
flowsRefreshOthers: boolean;
}

View File

@@ -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"]),
};
}

View File

@@ -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"]),
};
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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",
@@ -25,7 +25,7 @@ dependencies = [
"django-prometheus==2.4.1",
"django-storages[s3]==1.14.6",
"django-tenants==3.10.1",
"django==5.2.13",
"django==5.2.14",
"djangoql==0.19.1",
"djangorestframework==3.17.1",
"docker==7.1.0",
@@ -48,7 +48,7 @@ dependencies = [
"opencontainers==0.0.15",
"packaging==26.2",
"paramiko==4.0.0",
"psycopg[c,pool]==3.3.3",
"psycopg[c,pool]==3.3.4",
"pydantic-scim==0.0.8",
"pydantic==2.13.3",
"pyjwt==2.11.0",

View File

@@ -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
@@ -36044,6 +36050,8 @@ components:
path:
type: string
minLength: 1
context:
type: string
Brand:
type: object
description: Brand Serializer
@@ -37183,6 +37191,7 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -51193,6 +51202,7 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -53633,7 +53643,7 @@ components:
type: string
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectUriTypeEnum'
- $ref: '#/components/schemas/RedirectURITypeEnum'
default: authorization
required:
- matching_mode
@@ -53649,12 +53659,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
@@ -55975,6 +55985,7 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff
@@ -56063,6 +56074,7 @@ components:
flows_refresh_others:
type: boolean
description: Refresh other tabs after successful authentication.
deprecated: true
required:
- core_default_app_access
- enterprise_audit_include_expanded_diff

116
uv.lock generated
View File

@@ -318,11 +318,11 @@ requires-dist = [
{ name = "argon2-cffi", specifier = "==25.1.0" },
{ name = "cachetools", specifier = "==7.0.6" },
{ name = "channels", specifier = "==4.3.2" },
{ name = "cryptography", specifier = "==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" },
{ name = "django", specifier = "==5.2.13" },
{ name = "django", specifier = "==5.2.14" },
{ name = "django-channels-postgres", editable = "packages/django-channels-postgres" },
{ name = "django-countries", specifier = "==8.2.0" },
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
@@ -357,7 +357,7 @@ requires-dist = [
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
{ name = "packaging", specifier = "==26.2" },
{ name = "paramiko", specifier = "==4.0.0" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.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]]
@@ -1075,16 +1075,16 @@ wheels = [
[[package]]
name = "django"
version = "5.2.13"
version = "5.2.14"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" }
sdist = { url = "https://files.pythonhosted.org/packages/65/95/95f7faa0950867afaa0bef2460c6263afd6a2c78cc9434046ed28160b015/django-5.2.14.tar.gz", hash = "sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2", size = 10895118, upload-time = "2026-05-05T13:57:31.104Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/b1/51ab36b2eefcf8cdb9338c7188668a157e29e30306bfc98a379704c9e10d/django-5.2.13-py3-none-any.whl", hash = "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", size = 8310982, upload-time = "2026-04-07T14:02:08.883Z" },
{ url = "https://files.pythonhosted.org/packages/14/44/f172870cf87aa25afef48fb72adba89ee8b77fcab6f3b23d240b923f1528/django-5.2.14-py3-none-any.whl", hash = "sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76", size = 8311320, upload-time = "2026-05-05T13:57:25.795Z" },
]
[[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]]

298
web/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -8,6 +8,7 @@ import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
import "#elements/utils/TimeDeltaHelp";
import "./AdminSettingsFooterLinks.js";
import "#elements/Alert";
import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.js";
@@ -287,6 +288,9 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
help=${msg(
"When enabled, other flow tabs in a session will refresh upon a successful authentication.",
)}
.bighelp=${html`<ak-alert class="pf-c-radio__description" inline plain>
${msg("This flag is deprecated.")}
</ak-alert>`}
>
</ak-switch-input>
<ak-switch-input

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>`,

View File

@@ -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>

View File

@@ -124,19 +124,22 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
order="order"
.columns=${COLUMNS}
.content=${[]}
></ak-select-table>
<ak-empty-state icon="pf-icon-module"
><span>${msg("No bound policies.")}</span>
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
<div slot="primary">
<button
@click=${() => this.onBindingEvent()}
class="pf-c-button pf-m-primary"
>
${msg("Bind policy/group/user")}
</button>
</div>
</ak-empty-state>
>
<ak-empty-state slot="empty-table" icon="pf-icon-module"
><span>${msg("No bound policies.")}</span>
<div slot="body">
${msg("No policies are currently bound to this object.")}
</div>
<div slot="primary">
<button
@click=${() => this.onBindingEvent()}
class="pf-c-button pf-m-primary"
>
${msg("Bind policy/group/user")}
</button>
</div>
</ak-empty-state>
</ak-select-table>
</div>`;
}

View File

@@ -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>`

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -158,24 +158,33 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
<ak-radio
.options=${[
{
label: msg("No preference is sent"),
label: msg(
"No preference: the browser may offer any available authenticator",
),
value: null,
default: true,
},
{
label: msg(
"A non-removable authenticator, like TouchID or Windows Hello",
"Platform: a non-removable authenticator built into the device, such as Touch ID, Face ID, or Windows Hello",
),
value: AuthenticatorAttachmentEnum.Platform,
},
{
label: msg('A "roaming" authenticator, like a YubiKey'),
label: msg(
"Cross-platform: a roaming authenticator, such as a YubiKey or Google Titan",
),
value: AuthenticatorAttachmentEnum.CrossPlatform,
},
]}
.value=${this.instance?.authenticatorAttachment}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg(
"Controls the authenticatorAttachment parameter sent to the browser during WebAuthn registration. If Hints are configured and this is left as 'No preference', a value is inferred from the selected hints for backward compatibility with older browsers.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Hints")} name="hints">
<ak-dual-select-provider

View File

@@ -10,7 +10,7 @@ import { AKElement } from "#elements/Base";
import { Invitation, StagesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing, TemplateResult } from "lit";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
@@ -27,7 +27,30 @@ export class InvitationListLink extends AKElement {
@property()
selectedFlow?: string;
static styles: CSSResult[] = [PFForm, PFFormControl, PFDescriptionList, PFButton];
/**
* When true, the "Send via Email" button dispatches the
* `ak-invitation-send-email-inline` event instead of opening the nested
* email modal. Used by the invitation wizard's success step so the email
* form can be rendered as its own wizard step.
*/
@property({ type: Boolean, attribute: "inline-send-email" })
inlineSendEmail = false;
static styles: CSSResult[] = [
PFForm,
PFFormControl,
PFDescriptionList,
PFButton,
css`
:host {
display: block;
width: 100%;
}
input.pf-c-form-control {
width: 100%;
}
`,
];
renderLink(): string {
if (this.invitation?.flowObj) {
@@ -103,6 +126,7 @@ export class InvitationListLink extends AKElement {
class="pf-c-form-control"
readonly
type="text"
style="width: 100%;"
value=${this.renderLink()}
/>
</div>
@@ -122,18 +146,32 @@ export class InvitationListLink extends AKElement {
>
${msg("Copy Link")}
</button>
<ak-forms-modal>
<span slot="submit">${msg("Send")}</span>
<span slot="header">${msg("Send Invitation via Email")}</span>
<ak-invitation-send-email-form
slot="form"
.invitation=${this.invitation}
>
</ak-invitation-send-email-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Send via Email")}
</button>
</ak-forms-modal>
${this.inlineSendEmail
? html`<button
class="pf-c-button pf-m-secondary"
@click=${() => {
this.dispatchEvent(
new CustomEvent("ak-invitation-send-email-inline", {
bubbles: true,
composed: true,
}),
);
}}
>
${msg("Send via Email")}
</button>`
: html`<ak-forms-modal>
<span slot="submit">${msg("Send")}</span>
<span slot="header">${msg("Send Invitation via Email")}</span>
<ak-invitation-send-email-form
slot="form"
.invitation=${this.invitation}
>
</ak-invitation-send-email-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Send via Email")}
</button>
</ak-forms-modal>`}
</div>
</dd>
</div>

View File

@@ -1,6 +1,8 @@
import "#admin/rbac/ObjectPermissionModal";
import "#admin/stages/invitation/InvitationForm";
import "#admin/stages/invitation/InvitationListLink";
import "#admin/stages/invitation/wizard/InvitationWizard";
import "#elements/buttons/Dropdown";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import "#elements/forms/DeleteBulkForm";
@@ -9,7 +11,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
import { IconEditButton, modalInvoker } from "#elements/dialogs";
import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
@@ -18,11 +20,12 @@ import { SlottedTemplateResult } from "#elements/types";
import { setPageDetails } from "#components/ak-page-navbar";
import { InvitationForm } from "#admin/stages/invitation/InvitationForm";
import { InvitationWizard } from "#admin/stages/invitation/wizard/InvitationWizard";
import { FlowDesignationEnum, Invitation, ModelEnum, StagesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, PropertyValues } from "lit";
import { CSSResult, html, PropertyValues, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
@@ -139,7 +142,66 @@ export class InvitationListPage extends TablePage<Invitation> {
}
protected override renderObjectCreate(): SlottedTemplateResult {
return ModalInvokerButton(InvitationForm);
return html`${this.renderNewInvitationDropdown()}`;
}
protected renderNewInvitationDropdown(): TemplateResult {
return html`<ak-dropdown class="pf-c-dropdown">
<div class="pf-c-dropdown__toggle pf-m-primary pf-m-split-button pf-m-action">
<button
class="pf-c-dropdown__toggle-button"
type="button"
${modalInvoker(InvitationWizard, { mode: "existing" })}
>
${msg("New Invitation")}
</button>
<button
class="pf-c-dropdown__toggle-button"
type="button"
id="new-invitation-toggle"
aria-haspopup="menu"
aria-controls="new-invitation-menu"
tabindex="0"
aria-label=${msg("New Invitation options")}
>
<i class="fas fa-caret-down" aria-hidden="true"></i>
</button>
</div>
<menu
class="pf-c-dropdown__menu"
hidden
id="new-invitation-menu"
aria-labelledby="new-invitation-toggle"
tabindex="-1"
>
<li role="presentation">
<button
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
${modalInvoker(InvitationWizard, { mode: "existing" })}
aria-description=${msg(
"Opens the new invitation wizard and binds the invitation to an existing enrollment flow.",
)}
>
${msg("with Existing Enrollment Flow...")}
</button>
</li>
<li role="presentation">
<button
type="button"
role="menuitem"
class="pf-c-dropdown__menu-item"
${modalInvoker(InvitationWizard, { mode: "create" })}
aria-description=${msg(
"Opens the new invitation wizard, which will create a new enrollment flow and invitation stage.",
)}
>
${msg("with New Enrollment Flow and Invitation Stage...")}
</button>
</li>
</menu>
</ak-dropdown>`;
}
protected override render(): SlottedTemplateResult {

View File

@@ -0,0 +1,62 @@
import "#admin/stages/invitation/wizard/InvitationWizardDetailsStep";
import "#admin/stages/invitation/wizard/InvitationWizardEmailStep";
import "#admin/stages/invitation/wizard/InvitationWizardFlowStep";
import "#admin/stages/invitation/wizard/InvitationWizardSuccessStep";
import "#elements/wizard/Wizard";
import { AKElement } from "#elements/Base";
import { TransclusionChildElement, TransclusionChildSymbol } from "#elements/dialogs";
import { SlottedTemplateResult } from "#elements/types";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { property } from "@lit/reactive-element/decorators/property.js";
import { html } from "lit";
export type InvitationWizardFlowMode = "existing" | "create";
@customElement("ak-invitation-wizard")
export class InvitationWizard extends AKElement implements TransclusionChildElement {
public static verboseName = msg("Invitation");
public [TransclusionChildSymbol] = true;
@property({ type: String })
public mode: InvitationWizardFlowMode = "existing";
protected override createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
protected override render(): SlottedTemplateResult {
return html`<ak-wizard
entity-singular=${msg("Invitation")}
description=${msg("Create a new invitation with an enrollment flow.")}
.initialSteps=${["flow-step", "details-step", "success-step"]}
>
<ak-invitation-wizard-flow-step
slot="flow-step"
headline=${msg("Enrollment Flow")}
.mode=${this.mode}
></ak-invitation-wizard-flow-step>
<ak-invitation-wizard-details-step
slot="details-step"
headline=${msg("Invitation Details")}
></ak-invitation-wizard-details-step>
<ak-invitation-wizard-success-step
slot="success-step"
headline=${msg("Invitation Link")}
></ak-invitation-wizard-success-step>
<ak-invitation-wizard-email-step
slot="email-step"
headline=${msg("Send via Email")}
></ak-invitation-wizard-email-step>
</ak-wizard>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard": InvitationWizard;
}
}

View File

@@ -0,0 +1,261 @@
import "#components/ak-switch-input";
import "#elements/CodeMirror";
import "#elements/forms/HorizontalFormElement";
import type { InvitationWizardState } from "./types";
import { DEFAULT_CONFIG } from "#common/api/config";
import {
parseAPIResponseError,
pluckErrorDetail,
pluckFallbackFieldErrors,
} from "#common/errors/network";
import { MessageLevel } from "#common/messages";
import { dateTimeLocal } from "#common/temporal";
import { showMessage } from "#elements/messages/MessageContainer";
import { WizardPage } from "#elements/wizard/WizardPage";
import { FlowsApi, ManagedApi, StagesApi } from "@goauthentik/api";
import YAML from "yaml";
import { msg, str } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
const MINIMAL_BLUEPRINT_PATH = "example/flows-invitation-enrollment-minimal.yaml";
@customElement("ak-invitation-wizard-details-step")
export class InvitationWizardDetailsStep extends WizardPage {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
@state()
invitationName = "";
@state()
invitationExpires: string = dateTimeLocal(new Date(Date.now() + 48 * 60 * 60 * 1000));
@state()
fixedDataRaw = "{}";
@state()
singleUse = true;
activeCallback = async (): Promise<void> => {
this.host.valid = this.invitationName.length > 0;
};
async #fail(step: string, err: unknown): Promise<false> {
const parsed = await parseAPIResponseError(err);
const fieldErrors = pluckFallbackFieldErrors(parsed);
const detail = fieldErrors.length > 0 ? fieldErrors.join(" ") : pluckErrorDetail(parsed);
showMessage({
level: MessageLevel.error,
message: msg(str`${step} failed`),
description: detail,
});
this.logger.error("Invitation wizard step failed", { step, error: err });
return false;
}
validate(): void {
let validYaml = true;
try {
YAML.parse(this.fixedDataRaw);
} catch {
validYaml = false;
}
this.host.valid =
this.invitationName.length > 0 && this.invitationExpires.length > 0 && validYaml;
}
nextCallback = async (): Promise<boolean> => {
if (!this.invitationName) return false;
let fixedData: Record<string, unknown> = {};
try {
fixedData = YAML.parse(this.fixedDataRaw) || {};
} catch {
return false;
}
const wizardState = this.host.state as unknown as InvitationWizardState;
if (wizardState.createdInvitationPk) {
return true;
}
wizardState.invitationName = this.invitationName;
wizardState.invitationExpires = this.invitationExpires;
wizardState.invitationFixedData = fixedData;
wizardState.invitationSingleUse = this.singleUse;
if (wizardState.needsFlow) {
try {
const result = await new ManagedApi(DEFAULT_CONFIG).managedBlueprintsImportCreate({
path: MINIMAL_BLUEPRINT_PATH,
context: JSON.stringify({
flow_name: wizardState.newFlowName,
flow_slug: wizardState.newFlowSlug,
stage_name: wizardState.newStageName,
continue_flow_without_invitation: wizardState.continueFlowWithoutInvitation,
user_type: wizardState.newUserType,
}),
});
if (!result.success) {
const logs = (result.logs || [])
.map((l) => l.event)
.filter((m) => !!m)
.join("\n");
return this.#fail(
msg("Importing enrollment flow blueprint"),
new Error(logs || msg("Blueprint validation failed")),
);
}
const slugToLookup = wizardState.newFlowSlug!;
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
slug: slugToLookup,
});
const createdFlow = flows.results[0];
if (!createdFlow) {
return this.#fail(
msg("Importing enrollment flow blueprint"),
new Error(
msg(str`Flow with slug "${slugToLookup}" not found after import`),
),
);
}
wizardState.createdFlowPk = createdFlow.pk;
wizardState.createdFlowSlug = createdFlow.slug;
wizardState.needsFlow = false;
wizardState.needsStage = false;
wizardState.needsBinding = false;
} catch (err) {
return this.#fail(msg("Importing enrollment flow blueprint"), err);
}
}
try {
const flowPk = wizardState.createdFlowPk || wizardState.selectedFlowPk || undefined;
const invitation = await new StagesApi(
DEFAULT_CONFIG,
).stagesInvitationInvitationsCreate({
invitationRequest: {
name: wizardState.invitationName!,
expires: wizardState.invitationExpires
? new Date(wizardState.invitationExpires)
: undefined,
fixedData: wizardState.invitationFixedData,
singleUse: wizardState.invitationSingleUse,
flow: flowPk || null,
},
});
wizardState.createdInvitationPk = invitation.pk;
wizardState.createdInvitation = invitation;
} catch (err) {
return this.#fail(msg("Creating invitation"), err);
}
return true;
};
override reset(): void {
this.invitationName = "";
this.invitationExpires = dateTimeLocal(new Date(Date.now() + 48 * 60 * 60 * 1000));
this.fixedDataRaw = "{}";
this.singleUse = true;
}
render(): TemplateResult {
const wizardState = this.host.state as unknown as InvitationWizardState;
const flowDisplay =
wizardState.flowMode === "existing"
? wizardState.selectedFlowSlug
: wizardState.newFlowSlug;
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Name")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.invitationName}
@input=${(ev: InputEvent) => {
const target = ev.target as HTMLInputElement;
this.invitationName = target.value.replace(/[^a-z0-9-]/g, "");
target.value = this.invitationName;
this.validate();
}}
/>
<p class="pf-c-form__helper-text">
${msg(
"The name of an invitation must be a slug: only lower case letters, numbers, and the hyphen are permitted here.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Expires")} required>
<input
type="datetime-local"
data-type="datetime-local"
class="pf-c-form-control"
required
.value=${this.invitationExpires}
@input=${(ev: InputEvent) => {
this.invitationExpires = (ev.target as HTMLInputElement).value;
this.validate();
}}
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Flow")}>
<input
type="text"
class="pf-c-form-control"
readonly
disabled
.value=${flowDisplay || ""}
/>
<p class="pf-c-form__helper-text">
${msg(
"The flow selected in the previous step. The invitation will be bound to this flow.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Custom attributes")}>
<ak-codemirror
mode="yaml"
.value=${this.fixedDataRaw}
@change=${(ev: CustomEvent) => {
this.fixedDataRaw = ev.detail.value;
this.validate();
}}
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg(
"Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.",
)}
</p>
</ak-form-element-horizontal>
<ak-switch-input
label=${msg("Single use")}
?checked=${this.singleUse}
@change=${(ev: Event) => {
this.singleUse = (ev.target as HTMLInputElement).checked;
}}
help=${msg("When enabled, the invitation will be deleted after usage.")}
></ak-switch-input>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-details-step": InvitationWizardDetailsStep;
}
}

View File

@@ -0,0 +1,217 @@
import "#components/ak-textarea-input";
import "#elements/forms/HorizontalFormElement";
import type { InvitationWizardState } from "./types";
import { DEFAULT_CONFIG } from "#common/api/config";
import {
parseAPIResponseError,
pluckErrorDetail,
pluckFallbackFieldErrors,
} from "#common/errors/network";
import { AKRefreshEvent } from "#common/events";
import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { SlottedTemplateResult } from "#elements/types";
import { WizardPage } from "#elements/wizard/WizardPage";
import { StagesApi, TypeCreate } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-invitation-wizard-email-step")
export class InvitationWizardEmailStep extends WizardPage {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
@state()
toAddresses = "";
@state()
ccAddresses = "";
@state()
bccAddresses = "";
@state()
template = "email/invitation.html";
@state()
availableTemplates: TypeCreate[] = [];
override formatNextLabel(): SlottedTemplateResult {
return html`${msg("Send")}
<span class="pf-c-button__icon pf-m-end">
<i class="fas fa-paper-plane" aria-hidden="true"></i>
</span>`;
}
activeCallback = async (): Promise<void> => {
this.host.valid = this.toAddresses.trim().length > 0;
try {
this.availableTemplates = await new StagesApi(
DEFAULT_CONFIG,
).stagesEmailTemplatesList();
} catch {
this.availableTemplates = [];
}
};
parseEmailAddresses(raw: string): string[] {
return raw
.split(/[\n,;]/)
.map((value) => value.trim())
.filter((value) => value.length > 0);
}
validate(): void {
this.host.valid = this.parseEmailAddresses(this.toAddresses).length > 0;
}
nextCallback = async (): Promise<boolean> => {
const wizardState = this.host.state as unknown as InvitationWizardState;
const invitationPk = wizardState.createdInvitationPk;
if (!invitationPk) {
showMessage({
level: MessageLevel.error,
message: msg("No invitation available to send"),
});
return false;
}
const to = this.parseEmailAddresses(this.toAddresses);
if (to.length === 0) {
showMessage({
level: MessageLevel.error,
message: msg("Please enter at least one email address"),
});
return false;
}
const cc = this.parseEmailAddresses(this.ccAddresses);
const bcc = this.parseEmailAddresses(this.bccAddresses);
try {
await new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsSendEmailCreate({
inviteUuid: invitationPk,
invitationSendEmailRequest: {
emailAddresses: to,
ccAddresses: cc.length > 0 ? cc : undefined,
bccAddresses: bcc.length > 0 ? bcc : undefined,
template: this.template,
},
});
} catch (err) {
const parsed = await parseAPIResponseError(err);
const fieldErrors = pluckFallbackFieldErrors(parsed);
const detail =
fieldErrors.length > 0 ? fieldErrors.join(" ") : pluckErrorDetail(parsed);
showMessage({
level: MessageLevel.error,
message: msg("Failed to queue invitation emails"),
description: detail,
});
return false;
}
showMessage({
level: MessageLevel.success,
message: msg(
str`Invitation emails queued for sending to ${to.length} recipient(s). Check the System Tasks for more information.`,
),
});
this.dispatchEvent(new AKRefreshEvent());
return true;
};
override reset(): void {
this.toAddresses = "";
this.ccAddresses = "";
this.bccAddresses = "";
this.template = "email/invitation.html";
}
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("To")} required>
<textarea
class="pf-c-form-control"
required
rows="3"
.value=${this.toAddresses}
@input=${(ev: InputEvent) => {
this.toAddresses = (ev.target as HTMLTextAreaElement).value;
this.validate();
}}
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"One email address per line, or comma/semicolon separated. Each recipient will receive a separate email with an invitation link.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("CC")}>
<textarea
class="pf-c-form-control"
rows="2"
.value=${this.ccAddresses}
@input=${(ev: InputEvent) => {
this.ccAddresses = (ev.target as HTMLTextAreaElement).value;
}}
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"A comma-separated list of addresses to receive copies of the invitation. Recipients will receive the full list of other addresses in this list.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("BCC")}>
<textarea
class="pf-c-form-control"
rows="2"
.value=${this.bccAddresses}
@input=${(ev: InputEvent) => {
this.bccAddresses = (ev.target as HTMLTextAreaElement).value;
}}
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"A comma-separated list of addresses to receive copies of the invitation. Recipients will not receive the addresses of other recipients.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Template")} required>
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
this.template = (ev.target as HTMLSelectElement).value;
}}
>
${this.availableTemplates.map(
(template) =>
html`<option
value=${template.name}
?selected=${template.name === this.template}
>
${template.description}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${msg("Select the email template to use for sending invitations.")}
</p>
</ak-form-element-horizontal>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-email-step": InvitationWizardEmailStep;
}
}

View File

@@ -0,0 +1,347 @@
import "#components/ak-radio-input";
import "#components/ak-switch-input";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import type { InvitationWizardState } from "./types";
import { DEFAULT_CONFIG } from "#common/api/config";
import { WizardPage } from "#elements/wizard/WizardPage";
import {
FlowDesignationEnum,
type FlowSet,
type InvitationStage,
StagesApi,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
interface EnrollmentFlow {
slug: string;
pk: string;
name: string;
}
@customElement("ak-invitation-wizard-flow-step")
export class InvitationWizardFlowStep extends WizardPage {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl, PFButton, PFAlert];
@property({ type: String })
public mode: "existing" | "create" = "existing";
@state()
enrollmentFlows: EnrollmentFlow[] = [];
@state()
loading = true;
@state()
selectedFlowSlug?: string;
@state()
selectedFlowPk?: string;
@state()
newFlowName = "Enrollment with invitation";
@state()
newFlowSlug = "enrollment-with-invitation";
@state()
newStageName = "invitation-stage";
@state()
newUserType: "external" | "internal" = "external";
@state()
continueFlowWithoutInvitation = true;
activeCallback = async (): Promise<void> => {
this.host.valid = false;
if (this.mode === "create") {
this.loading = false;
this.validate();
return;
}
this.loading = true;
try {
const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({
noFlows: false,
});
const flowMap = new Map<string, EnrollmentFlow>();
stages.results.forEach((stage: InvitationStage) => {
(stage.flowSet || [])
.filter((flow: FlowSet) => flow.designation === FlowDesignationEnum.Enrollment)
.forEach((flow: FlowSet) => {
if (!flowMap.has(flow.slug)) {
flowMap.set(flow.slug, {
slug: flow.slug,
pk: flow.pk,
name: flow.name,
});
}
});
});
this.enrollmentFlows = Array.from(flowMap.values());
if (this.enrollmentFlows.length > 0) {
this.selectedFlowSlug = this.enrollmentFlows[0].slug;
this.selectedFlowPk = this.enrollmentFlows[0].pk;
this.host.valid = true;
}
} catch {
this.enrollmentFlows = [];
}
this.loading = false;
// If there's exactly one eligible flow, skip this step so the user goes
// straight to the invitation details. Drop ourselves from the step list
// so the back button from the next step doesn't bounce back here.
if (this.mode === "existing" && this.enrollmentFlows.length === 1) {
const currentSlot = this.slot;
const advanced = await this.host.navigateNext();
if (advanced) {
this.host.steps = this.host.steps.filter((s) => s !== currentSlot);
}
}
};
validate(): void {
if (this.mode === "existing") {
this.host.valid = !!this.selectedFlowSlug;
} else {
this.host.valid =
this.newFlowName.length > 0 &&
this.newFlowSlug.length > 0 &&
this.newStageName.length > 0;
}
}
nextCallback = async (): Promise<boolean> => {
const state = this.host.state as unknown as InvitationWizardState;
state.flowMode = this.mode;
if (this.mode === "existing") {
if (!this.selectedFlowSlug) return false;
state.selectedFlowSlug = this.selectedFlowSlug;
state.selectedFlowPk = this.selectedFlowPk;
state.needsFlow = false;
state.needsStage = false;
state.needsBinding = false;
} else {
if (!this.newFlowName || !this.newFlowSlug || !this.newStageName) return false;
state.newFlowName = this.newFlowName;
state.newFlowSlug = this.newFlowSlug;
state.newStageName = this.newStageName;
state.newUserType = this.newUserType;
state.continueFlowWithoutInvitation = this.continueFlowWithoutInvitation;
state.needsFlow = true;
state.needsStage = true;
state.needsBinding = true;
}
return true;
};
override reset(): void {
this.enrollmentFlows = [];
this.loading = true;
this.selectedFlowSlug = undefined;
this.selectedFlowPk = undefined;
this.newFlowName = "Enrollment with invitation";
this.newFlowSlug = "enrollment-with-invitation";
this.newStageName = "invitation-stage";
this.newUserType = "external";
this.continueFlowWithoutInvitation = true;
}
slugify(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
renderExistingFlowSelector(): TemplateResult {
if (this.enrollmentFlows.length === 0) {
return html`
<div class="pf-c-alert pf-m-warning pf-m-inline">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">
${msg("No enrollment flows with invitation stages found")}
</h4>
<div class="pf-c-alert__description">
<p>
${msg(
"You can create a new enrollment flow and invitation stage right here, or cancel and bind an invitation stage to an existing flow manually.",
)}
</p>
<button
type="button"
class="pf-c-button pf-m-primary"
@click=${() => {
this.mode = "create";
this.validate();
}}
>
${msg("Create a new enrollment flow")}
</button>
</div>
</div>
`;
}
return html`
<ak-form-element-horizontal label=${msg("Enrollment flow")} required>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<EnrollmentFlow[]> => {
if (!query) return this.enrollmentFlows;
const needle = query.toLowerCase();
return this.enrollmentFlows.filter(
(flow) =>
flow.name.toLowerCase().includes(needle) ||
flow.slug.toLowerCase().includes(needle),
);
}}
.renderElement=${(flow: EnrollmentFlow): string => flow.name}
.renderDescription=${(flow: EnrollmentFlow): TemplateResult =>
html`${flow.slug}`}
.value=${(flow: EnrollmentFlow | undefined): string | undefined => flow?.pk}
.selected=${(flow: EnrollmentFlow): boolean => flow.pk === this.selectedFlowPk}
@ak-change=${(ev: CustomEvent<{ value: EnrollmentFlow | null }>) => {
const flow = ev.detail.value;
this.selectedFlowSlug = flow?.slug;
this.selectedFlowPk = flow?.pk;
this.validate();
}}
style="display: block; width: 100%;"
></ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Only enrollment flows that have an invitation stage bound to them are listed here.",
)}
</p>
</ak-form-element-horizontal>
`;
}
renderCreateForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Flow name")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.newFlowName}
@input=${(ev: InputEvent) => {
const target = ev.target as HTMLInputElement;
this.newFlowName = target.value;
this.newFlowSlug = this.slugify(target.value);
this.validate();
}}
/>
<p class="pf-c-form__helper-text">${msg("Name for the new enrollment flow.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Flow slug")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.newFlowSlug}
@input=${(ev: InputEvent) => {
const target = ev.target as HTMLInputElement;
this.newFlowSlug = target.value.replace(/[^a-z0-9-]/g, "");
this.validate();
}}
/>
<p class="pf-c-form__helper-text">${msg("Visible in the URL.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Invitation stage name")} required>
<input
type="text"
class="pf-c-form-control"
required
.value=${this.newStageName}
@input=${(ev: InputEvent) => {
this.newStageName = (ev.target as HTMLInputElement).value;
this.validate();
}}
/>
<p class="pf-c-form__helper-text">${msg("Name for the new invitation stage.")}</p>
</ak-form-element-horizontal>
<ak-radio-input
label=${msg("User type")}
.value=${this.newUserType}
.options=${[
{
label: msg("External"),
value: "external",
description: html`${msg(
"Enrolled users are created as external (e.g. customers, guests). New users will be placed under users/external.",
)}`,
},
{
label: msg("Internal"),
value: "internal",
description: html`${msg(
"Enrolled users are created as internal (e.g. employees). New users will be placed under users/internal.",
)}`,
},
]}
@input=${(ev: CustomEvent<{ value: "external" | "internal" }>) => {
this.newUserType = ev.detail.value;
}}
></ak-radio-input>
<ak-switch-input
label=${msg("Continue flow without invitation")}
?checked=${this.continueFlowWithoutInvitation}
@change=${(ev: Event) => {
this.continueFlowWithoutInvitation = (ev.target as HTMLInputElement).checked;
}}
help=${msg(
"If enabled, the stage will jump to the next stage when no invitation is given. If disabled, the flow will be cancelled without a valid invitation.",
)}
></ak-switch-input>
`;
}
render(): TemplateResult {
if (this.loading) {
return html`<div class="pf-c-form">
<p>${msg("Loading...")}</p>
</div>`;
}
return html`<form class="pf-c-form pf-m-horizontal">
${this.mode === "existing"
? this.renderExistingFlowSelector()
: this.renderCreateForm()}
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-flow-step": InvitationWizardFlowStep;
}
}

View File

@@ -0,0 +1,102 @@
import "#admin/stages/invitation/InvitationListLink";
import type { InvitationWizardState } from "./types";
import { AKRefreshEvent } from "#common/events";
import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { WizardPage } from "#elements/wizard/WizardPage";
import { Invitation } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-invitation-wizard-success-step")
export class InvitationWizardSuccessStep extends WizardPage {
static styles: CSSResult[] = [
PFBase,
PFForm,
PFAlert,
css`
:host {
display: block;
width: 100%;
}
ak-stage-invitation-list-link {
display: block;
width: 100%;
}
`,
];
@state()
invitation?: Invitation;
#notified = false;
activeCallback = async (): Promise<void> => {
const wizardState = this.host.state as unknown as InvitationWizardState;
this.invitation = wizardState.createdInvitation;
this.host.valid = true;
if (this.invitation && !this.#notified) {
showMessage({
level: MessageLevel.success,
message: msg("Successfully created invitation."),
});
this.#notified = true;
}
};
nextCallback = async (): Promise<boolean> => {
this.dispatchEvent(new AKRefreshEvent());
return true;
};
override reset(): void {
this.invitation = undefined;
this.#notified = false;
}
render(): TemplateResult {
const invitation = this.invitation;
if (!invitation) {
return html`<div class="pf-c-alert pf-m-warning pf-m-inline">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">${msg("No invitation was created.")}</h4>
</div>`;
}
return html`
<ak-stage-invitation-list-link
.invitation=${invitation}
?inline-send-email=${true}
@ak-invitation-send-email-inline=${this.onSendViaEmail}
></ak-stage-invitation-list-link>
`;
}
onSendViaEmail = async (): Promise<void> => {
const steps = this.host.steps;
if (!steps.includes("email-step")) {
this.host.steps = [...steps, "email-step"];
}
await this.host.navigateNext();
};
}
declare global {
interface HTMLElementTagNameMap {
"ak-invitation-wizard-success-step": InvitationWizardSuccessStep;
}
}

View File

@@ -0,0 +1,31 @@
import type { Invitation } from "@goauthentik/api";
export interface InvitationWizardState {
// Step 1: Flow selection
flowMode: "existing" | "create";
selectedFlowSlug?: string;
selectedFlowPk?: string;
newFlowName?: string;
newFlowSlug?: string;
newStageName?: string;
newUserType?: "external" | "internal";
continueFlowWithoutInvitation: boolean;
// Flags for which API calls to make
needsFlow: boolean;
needsStage: boolean;
needsBinding: boolean;
// Step 2: Invitation details
invitationName?: string;
invitationExpires?: string;
invitationFixedData?: Record<string, unknown>;
invitationSingleUse: boolean;
// Results from API calls
createdStagePk?: string;
createdFlowPk?: string;
createdFlowSlug?: string;
createdInvitationPk?: string;
createdInvitation?: Invitation;
}

View File

@@ -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

View File

@@ -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 () => {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -74,6 +74,10 @@ svg[id^="mermaid-svg-"] {
}
}
ak-alert + :is(h2, p) {
padding-top: var(--pf-global--spacer--md);
}
/* #region Dark Theme */
:host([theme="dark"]) {

View File

@@ -211,9 +211,11 @@ export class SimpleTable
);
return html`<tr role="presentation">
<td role="presentation" colspan=${columnCount}>
<td role="presentation" colspan=${columnCount + 1}>
<div class="pf-l-bullseye">
<ak-empty-state><span>${message}</span></ak-empty-state>
<slot name="empty-table">
<ak-empty-state><span>${message}</span></ak-empty-state>
</slot>
</div>
</td>
</tr>`;

View File

@@ -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);

View File

@@ -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)}

View File

@@ -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;

View File

@@ -3,6 +3,7 @@ import { checkObjectShallowEquality } from "#common/collections";
import { AKElement } from "#elements/Base";
import { asInvoker, type ModalTemplate } from "#elements/dialogs/invokers";
import type { DialogInit, TransclusionElementConstructor } from "#elements/dialogs/shared";
import { ElementConstructorBoundary } from "#elements/errors/boundaries";
import type { LitPropertyRecord } from "#elements/types";
import { isAKElementConstructor, StrictUnsafe } from "#elements/utils/unsafe";
@@ -159,10 +160,22 @@ export function lookupElementConstructor<T extends CustomElementConstructor>(
tagName: string,
registry: CustomElementRegistry = window.customElements,
): T {
if (!tagName) {
// eslint-disable-next-line no-console
console.trace(
"No tag name provided for lookup. Did this value come from a different version of authentik?",
);
return ElementConstructorBoundary as unknown as T;
}
const ElementConstructor = registry.get(tagName);
if (!ElementConstructor) {
throw new TypeError(`No custom element defined for tag name: ${tagName}`);
// eslint-disable-next-line no-console
console.trace(`No custom element defined for tag name: ${tagName}`);
return ElementConstructorBoundary as unknown as T;
}
return ElementConstructor as unknown as T;

View File

@@ -0,0 +1,41 @@
import { globalAK } from "#common/global";
import { AKElement } from "#elements/Base";
import { SlottedTemplateResult } from "#elements/types";
import { CapabilitiesEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit-html";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
/**
* A fallback element to render when a custom element fails to load, either due to a missing import,
* or a version mismatch between the element's definition and its usage.
*/
@customElement("ak-element-missing")
export class ElementConstructorBoundary extends AKElement {
public styles = [PFAlert];
protected override render(): SlottedTemplateResult {
const debug = globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug);
const description = debug
? msg(
"The element could not be loaded. This may be due to a missing import or a version mismatch.",
)
: msg(
"An element could not be loaded. Please try refreshing the page or clearing your cache.",
);
return html`<div class="pf-c-alert pf-m-danger" role="alert">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">${msg("Failed to load element")}</h4>
<div class="pf-c-alert__description">${description}</div>
</div>`;
}
}

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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"

View File

@@ -20,6 +20,14 @@
}
}
/**
* P5 puts a line separating the entries, but this looks odd in our stacked usage. Specifying
* `.pf-m-stack` here also raises the specificity above the P4 default.
*/
.pf-m-stack label.pf-c-radio:not(:last-child) {
--pf-c-radio--BoxShadowColor: transparent;
}
.pf-c-radio__description {
text-wrap: balance;
text-wrap: pretty;

View File

@@ -996,7 +996,9 @@ export abstract class Table<T extends object, D = T>
* A simple pagination display, shown at both the top and bottom of the page.
*/
protected renderTablePagination(): SlottedTemplateResult {
if (!this.paginated) return nothing;
if (!this.paginated || !this.data || this.data?.pagination.totalPages < 2) {
return nothing;
}
const handler = (page: number) => {
this.page = page;

View File

@@ -17,6 +17,7 @@
.empty-state-primary {
display: flex;
justify-content: center;
gap: var(--pf-global--spacer--sm);
justify-content: center;
}

View File

@@ -182,8 +182,31 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
/**
* Actions to display at the end of the wizard.
*/
private _actions: WizardAction[] = [];
@property({ attribute: false })
public actions: WizardAction[] = [];
public get actions(): WizardAction[] {
return this._actions;
}
public set actions(value: WizardAction[]) {
const oldValue = this._actions;
this._actions = value;
if (this._actions.length > 0) {
if (!this.querySelector(`[slot="ak-wizard-page-action"]`)) {
const actionPage = document.createElement("ak-wizard-page-action");
actionPage.slot = "ak-wizard-page-action";
actionPage.dataset.wizardmanaged = "true";
this.appendChild(actionPage);
}
if (!this.steps.includes("ak-wizard-page-action")) {
this.steps = [...this.steps, "ak-wizard-page-action"];
}
}
this.requestUpdate("actions", oldValue);
}
@property({ attribute: false })
public finalHandler?: () => Promise<void>;
@@ -530,12 +553,14 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
return guard(
[activeStepIndex, lastPage, canBack, cancelable, valid, childElementCount],
() => {
const customLabel = this.activeStepElement?.formatNextLabel();
const nextLabel =
lastPage && activeStepIndex > 0
customLabel ??
(lastPage && activeStepIndex > 0
? this.cancelable
? ButtonKindLabelRecord.create()
: ButtonKindLabelRecord.finish()
: ButtonKindLabelRecord.next();
: ButtonKindLabelRecord.next());
return [
cancelable

View File

@@ -70,6 +70,16 @@ export abstract class WizardPage<S = WizardPageState> extends AKElement {
return html`<div part="sidebar-label-headline">${this.headline ?? msg("UNNAMED")}</div>`;
}
/**
* Optional override for the wizard's next-button label while this page is active.
*
* Return `null` (the default) to keep the wizard's default labeling
* (Next/Finish/Create).
*/
public formatNextLabel(): SlottedTemplateResult | null {
return null;
}
/**
* Called when the `next` button on the wizard is pressed. For forms, results in the submission
* of the current form to the back-end before being allowed to proceed to the next page. This is

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);

View File

@@ -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>`;

View File

@@ -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")}

View File

@@ -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>`

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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">

View File

@@ -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")}

View File

@@ -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")}

View File

@@ -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";

View File

@@ -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);
}
}
}

View File

@@ -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;

View 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);
}
}

View File

@@ -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 */

View File

@@ -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: "";

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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;

View File

@@ -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}

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More