Compare commits

..

15 Commits

Author SHA1 Message Date
dependabot[bot]
9aabe91119 website: bump the build group in /website with 6 updates (#22075)
Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.15.32` | `1.15.33` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.15.32` | `1.15.33` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.15.32` | `1.15.33` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.15.32` | `1.15.33` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.15.32` | `1.15.33` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.15.32` | `1.15.33` |

Updates `@swc/core-darwin-arm64` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-arm64-gnu` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-x64-gnu` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/html-darwin-arm64` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/html-linux-arm64-gnu` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/html-linux-x64-gnu` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

---
updated-dependencies:
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

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>
2026-05-06 14:16:14 +02:00
dependabot[bot]
77fae18259 web: bump ip-address from 10.1.0 to 10.2.0 in /web (#22082)
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.1.0 to 10.2.0.
- [Commits](https://github.com/beaugunderson/ip-address/commits)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.2.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-06 13:06:44 +02:00
dependabot[bot]
f6ef4d5479 web: bump the swc group across 1 directory with 11 updates (#22078)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core).


Updates `@swc/core` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/commits/v1.15.33/packages/core)

Updates `@swc/core-darwin-arm64` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-darwin-x64` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-arm-gnueabihf` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-arm64-gnu` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-arm64-musl` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-x64-gnu` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-linux-x64-musl` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-win32-arm64-msvc` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-win32-ia32-msvc` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

Updates `@swc/core-win32-x64-msvc` from 1.15.32 to 1.15.33
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.32...v1.15.33)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.15.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 12:49:05 +02:00
dependabot[bot]
b3ac4f9c4e ci: bump taiki-e/install-action from 2.75.29 to 2.75.30 in /.github/actions/setup (#22077)
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.29 to 2.75.30.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](b5fddbb536...db5fb34fa7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.30
  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-06 12:48:52 +02:00
dependabot[bot]
86658f6f03 web: bump country-flag-icons from 1.6.16 to 1.6.17 in /web (#22079)
Bumps [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons) from 1.6.16 to 1.6.17.
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.6.16...v1.6.17)

---
updated-dependencies:
- dependency-name: country-flag-icons
  dependency-version: 1.6.17
  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-06 12:48:32 +02:00
dependabot[bot]
548ab05628 web: bump yaml from 2.8.3 to 2.8.4 in /web (#22080)
Bumps [yaml](https://github.com/eemeli/yaml) from 2.8.3 to 2.8.4.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.8.3...v2.8.4)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 2.8.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-06 12:48:23 +02:00
dependabot[bot]
459fa8e219 core: bump sentry from 0.47.0 to 0.48.0 (#22081)
Bumps [sentry](https://github.com/getsentry/sentry-rust) from 0.47.0 to 0.48.0.
- [Release notes](https://github.com/getsentry/sentry-rust/releases)
- [Changelog](https://github.com/getsentry/sentry-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-rust/compare/0.47.0...0.48.0)

---
updated-dependencies:
- dependency-name: sentry
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 12:48:13 +02:00
Teffen Ellis
e40187179d packages/client-ts: Fix TypeScript config, ESBuild warnings (#21863)
* packages/client-ts: drop composite/incremental from tsconfig template

Sync with goauthentik/client-ts#13. The flags are the mechanism of
the missing-dist release bug upstream; harmless in the monorepo (no
publishing) but pointless for a single-package, no-project-references
setup. Keeping the two trees aligned avoids drift.

Co-Authored-By: Agent (authentik-m-sync-packages-final-concrete-buff) <279763771+playpen-agent@users.noreply.github.com>

* Fix package not building.

---------

Co-authored-by: Agent (authentik-m-sync-packages-final-concrete-buff) <279763771+playpen-agent@users.noreply.github.com>
2026-05-06 12:29:46 +02:00
Dominic R
f6024a23ef web: fix identification stage OUIA attributes (#22049)
* web: fix identification stage OUIA attributes

* tests/e2e: update OUIA selectors for identification stage

Match the rename of ouiaId to data-ouia-component-id in
IdentificationStage.ts so the enroll and recovery flow tests can
locate the links again.
2026-05-06 02:31:17 +02: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
57 changed files with 1834 additions and 1098 deletions

View File

@@ -64,7 +64,7 @@ runs:
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (web)

110
Cargo.lock generated
View File

@@ -176,12 +176,10 @@ dependencies = [
"arc-swap",
"argh",
"authentik-axum",
"authentik-client",
"authentik-common",
"axum",
"color-eyre",
"eyre",
"futures",
"hyper-unix-socket",
"hyper-util",
"metrics",
@@ -189,18 +187,9 @@ dependencies = [
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"rand 0.10.1",
"serde",
"serde_json",
"serde_repr",
"sqlx",
"time",
"tokio",
"tokio-retry2",
"tokio-tungstenite",
"tower",
"tracing",
"url",
"uuid",
"which",
]
@@ -553,17 +542,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -801,15 +779,6 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
@@ -1322,7 +1291,6 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -2089,7 +2057,7 @@ dependencies = [
"hashbrown 0.16.1",
"metrics",
"quanta",
"rand 0.9.2",
"rand 0.9.4",
"rand_xoshiro",
"sketches-ddsketch",
]
@@ -2776,7 +2744,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls",
@@ -2836,25 +2804,14 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -2893,12 +2850,6 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
@@ -3252,9 +3203,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "sentry"
version = "0.47.0"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb25f439f97d26fea01d717fa626167ceffcd981addaa670001e70505b72acbb"
checksum = "e8ac94aab850a23d7507307cc505332ed2bafd36c65930dfc5c43610f9e9b477"
dependencies = [
"cfg_aliases",
"httpdate",
@@ -3273,9 +3224,9 @@ dependencies = [
[[package]]
name = "sentry-backtrace"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a8c2c1bd5c1f735e84f28b48e7d72efcaafc362b7541bc8253e60e8fcdffc6"
checksum = "dc84c325ace9ca2388e510fe7d6672b5d60cd8b3bd0eb4bb4ee8314c323cd686"
dependencies = [
"backtrace",
"regex",
@@ -3284,9 +3235,9 @@ dependencies = [
[[package]]
name = "sentry-contexts"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b88a90baa654d7f0e1f4b667f6b434293d9f72c71bef16b197c76af5b7d5803"
checksum = "896c1ab62dbfe1746fb262bbf72e6feb2fb9dfb2c14709077bf71beb532e44b2"
dependencies = [
"hostname",
"libc",
@@ -3298,11 +3249,11 @@ dependencies = [
[[package]]
name = "sentry-core"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ac170a5bba8bec6e3339c90432569d89641fa7a3d3e4f44987d24f0762e6adf"
checksum = "d5f5abf20c42cb1593ec1638976e2647da55f79bccac956444c1707b6cce259a"
dependencies = [
"rand 0.9.2",
"rand 0.9.4",
"sentry-types",
"serde",
"serde_json",
@@ -3311,9 +3262,9 @@ dependencies = [
[[package]]
name = "sentry-debug-images"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd9646a972b57896d4a92ed200cf76139f8e30b3cfd03b6662ae59926d26633c"
checksum = "4b88bbe6a760d5724bb40689827e82e8db1e275947df2c59abe171bfc30bb671"
dependencies = [
"findshlibs",
"sentry-core",
@@ -3321,9 +3272,9 @@ dependencies = [
[[package]]
name = "sentry-panic"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6127d3d304ba5ce0409401e85aae538e303a569f8dbb031bf64f9ba0f7174346"
checksum = "0260dcb52562b6a79ae7702312a26dba94b79fb5baee7301087529e5ca4e872e"
dependencies = [
"sentry-backtrace",
"sentry-core",
@@ -3331,9 +3282,9 @@ dependencies = [
[[package]]
name = "sentry-tower"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5253dc4ad89863a866b93aeaaac1c9d60f2f774663b5024afe2d57e0a101c"
checksum = "d669616d5d5279b5712febfc80c343acc3695e499de0d101ed70fceacadf37f2"
dependencies = [
"sentry-core",
"tower-layer",
@@ -3342,9 +3293,9 @@ dependencies = [
[[package]]
name = "sentry-tracing"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27701acc51e68db5281802b709010395bfcbcb128b1d0a4e5873680d3b47ff0c"
checksum = "a1c035f3a0a8671ae1a231c5b457abb68b71acba2bf3054dab2a09a9d4ea487e"
dependencies = [
"bitflags 2.11.0",
"sentry-backtrace",
@@ -3355,13 +3306,13 @@ dependencies = [
[[package]]
name = "sentry-types"
version = "0.47.0"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56780cb5597d676bf22e6c11d1f062eb4def46390ea3bfb047bcbcf7dfd19bdb"
checksum = "82d8e81058ec155992191f61c7b29bfa7b2cf12012131e7cdc0678020898a7c9"
dependencies = [
"debugid",
"hex",
"rand 0.9.2",
"rand 0.9.4",
"serde",
"serde_json",
"thiserror 2.0.18",
@@ -3468,7 +3419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"digest",
]
@@ -3479,7 +3430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"digest",
]
@@ -4049,12 +4000,8 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@@ -4267,12 +4214,9 @@ dependencies = [
"http",
"httparse",
"log",
"rand 0.9.2",
"rustls",
"rustls-pki-types",
"rand 0.9.4",
"sha1",
"thiserror 2.0.18",
"url",
]
[[package]]

View File

@@ -50,7 +50,6 @@ notify = "= 8.2.0"
pin-project-lite = "= 0.2.17"
pyo3 = "= 0.28.3"
pyo3-build-config = "= 0.28.3"
rand = "= 0.10.1"
regex = "= 1.12.3"
reqwest = { version = "= 0.13.3", features = [
"form",
@@ -68,7 +67,7 @@ reqwest-middleware = { version = "= 0.5.1", features = [
"rustls",
] }
rustls = { version = "= 0.23.40", features = ["fips"] }
sentry = { version = "= 0.47.0", default-features = false, features = [
sentry = { version = "= 0.48.0", default-features = false, features = [
"backtrace",
"contexts",
"debug-images",
@@ -101,10 +100,6 @@ time = { version = "= 0.3.47", features = ["macros"] }
tokio = { version = "= 1.52.1", features = ["full", "tracing"] }
tokio-retry2 = "= 0.9.1"
tokio-rustls = "= 0.26.4"
tokio-tungstenite = { version = "= 0.29.0", features = [
"rustls-tls-webpki-roots",
"url",
] }
tokio-util = { version = "= 0.7.18", features = ["full"] }
tower = "= 0.5.3"
tower-http = { version = "= 0.6.8", features = ["timeout"] }
@@ -265,39 +260,28 @@ publish.workspace = true
[features]
default = ["core", "proxy"]
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
proxy = ["ak-common/proxy", "dep:ak-client"]
proxy = ["ak-common/proxy"]
[build-dependencies]
pyo3-build-config.workspace = true
[dependencies]
ak-axum.workspace = true
ak-client = { workspace = true, optional = true }
ak-common.workspace = true
arc-swap.workspace = true
argh.workspace = true
axum.workspace = true
color-eyre.workspace = true
eyre.workspace = true
futures.workspace = true
hyper-unix-socket.workspace = true
hyper-util.workspace = true
metrics-exporter-prometheus.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
nix.workspace = true
pyo3 = { workspace = true, optional = true }
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_repr.workspace = true
sqlx = { workspace = true, optional = true }
time.workspace = true
tokio-retry2.workspace = true
tokio-tungstenite.workspace = true
tokio.workspace = true
tower.workspace = true
tracing.workspace = true
url.workspace = true
uuid.workspace = true
which.workspace = true

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

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

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

View File

@@ -101,6 +101,8 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
rustc --version && \
cargo --version
RUN cat /root/.rustup/settings.toml
# Stage: Download uv
FROM ghcr.io/astral-sh/uv:0.11.5@sha256:555ac94f9a22e656fc5f2ce5dfee13b04e94d099e46bb8dd3a73ec7263f2e484 AS uv
# Stage: Base python image

View File

@@ -21,45 +21,33 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.2-trixie@sha256:4a7137ea573f79c86ae451ff05817ed762ef5597fcf732259e97abeb3108d873 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ENV PATH="/root/.cargo/bin:$PATH"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
apt-get update && \
# Required for installing pip packages
apt-get install -y --no-install-recommends \
# Build essentials
build-essential \
# aws-lc deps
cmake clang golang && \
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain none && \
rustup install && \
rustup default "$(sed -n 's/channel = "\(.*\)"/\1/p' rust-toolchain.toml)" && \
rustc --version && \
cargo --version
# See https://github.com/aws/aws-lc-rs/issues/569
ENV AWS_LC_FIPS_SYS_CC=clang
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
--mount=type=bind,target=.cargo/,src=.cargo/ \
--mount=type=bind,target=src/,src=src/ \
--mount=type=bind,target=packages/,src=packages/ \
--mount=type=bind,target=authentik/lib/default.yml,src=authentik/lib/default.yml \
# Required otherwise workspace discovery fails
--mount=type=bind,target=website/scripts/docsmg/,src=website/scripts/docsmg/ \
--mount=type=cache,id=cargo-git-db-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/git/db/ \
--mount=type=cache,id=cargo-registry-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/registry/ \
--mount=type=cache,id=rust-target-$TARGETARCH$TARGETVARIANT,target=/build/target/ \
cargo build --package authentik --no-default-features --features proxy --locked --release && \
cp ./target/release/authentik /bin/authentik
WORKDIR /go/src/goauthentik.io
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
dpkg --add-architecture arm64 && \
apt-get update && \
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
--mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/proxy ./cmd/proxy
# Stage 3: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341
@@ -84,13 +72,13 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/*
COPY --from=builder /bin/authentik /
COPY --from=builder /go/proxy /
COPY --from=web-builder /static/robots.txt /web/robots.txt
COPY --from=web-builder /static/security.txt /web/security.txt
COPY --from=web-builder /static/dist/ /web/dist/
COPY --from=web-builder /static/authentik/ /web/authentik/
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/authentik", "healthcheck" ]
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/proxy", "healthcheck" ]
EXPOSE 9000 9300 9443
@@ -99,4 +87,4 @@ USER 1000
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/authentik", "proxy"]
ENTRYPOINT ["/proxy"]

View File

@@ -1,6 +1,6 @@
//! Utilities for working with the authentik API client.
use ak_client::{apis::configuration::Configuration, models::Pagination};
use ak_client::apis::configuration::Configuration;
use eyre::{Result, eyre};
use url::Url;
@@ -60,42 +60,6 @@ pub fn make_config() -> Result<Configuration> {
})
}
/// Fetch all pages from a paginated API endpoint, returning all results combined.
///
/// - `fetch`: a function that takes a page number and returns a future resolving to a paginated
/// response.
/// - `get_pagination`: a function that extracts the [`Pagination`] metadata from a response.
/// - `get_results`: a function that extracts the result items from a response.
pub async fn fetch_all<T, R, E, F, Fut>(
fetch: F,
get_pagination: impl Fn(&R) -> &Pagination,
get_results: impl Fn(R) -> Vec<T>,
) -> std::result::Result<Vec<T>, E>
where
F: Fn(i32) -> Fut,
Fut: Future<Output = std::result::Result<R, E>>,
{
let mut page = 1;
let mut results = Vec::with_capacity(0);
loop {
let response = fetch(page).await?;
let next = get_pagination(&response).next;
if page == 1 {
let count = get_pagination(&response).count as usize;
results.reserve(count);
}
results.extend(get_results(response));
if next > 0.0 {
page += 1;
} else {
break;
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use serde_json::json;

View File

@@ -30,12 +30,12 @@ pub fn install() -> Result<()> {
}
if config.debug {
// let console_layer = console_subscriber::ConsoleLayer::builder()
// .server_addr(config.listen.debug_tokio)
// .spawn();
let console_layer = console_subscriber::ConsoleLayer::builder()
.server_addr(config.listen.debug_tokio)
.spawn();
tracing_subscriber::registry()
.with(ErrorLayer::default())
// .with(console_layer)
.with(console_layer)
.with(
fmt::layer()
.compact()
@@ -187,10 +187,13 @@ pub mod sentry {
environment: config.environment,
send_pii: config.send_pii,
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "This is fine, we'll never get big values here."
)]
#[expect(
clippy::as_conversions,
reason = "This is fine, we'll never get big values here."
)]
sample_rate: config.traces_sample_rate as f32,
})
}

View File

@@ -8,7 +8,8 @@
"url": "https://github.com/goauthentik/authentik.git"
},
"scripts": {
"build": "tsc && tsc -p tsconfig.esm.json",
"clean": "tsc -b --clean tsconfig.json tsconfig.esm.json",
"build": "npm run clean && tsc -b tsconfig.json tsconfig.esm.json",
"prepare": "npm run build"
},
"main": "./dist/index.js",

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

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

@@ -1,9 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"isolatedModules": true,
"incremental": true,
"rootDir": "src",
"strict": true,
"newLine": "lf",

View File

@@ -1,9 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"isolatedModules": true,
"incremental": true,
"rootDir": "src",
"strict": true,
"newLine": "lf",

View File

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

View File

@@ -36050,6 +36050,8 @@ components:
path:
type: string
minLength: 1
context:
type: string
Brand:
type: object
description: Brand Serializer
@@ -37189,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
@@ -51199,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
@@ -55981,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
@@ -56069,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

View File

@@ -8,8 +8,6 @@ use eyre::{Result, eyre};
use tracing::{error, info, trace};
mod metrics;
#[cfg(feature = "proxy")]
mod outpost;
#[cfg(feature = "core")]
mod server;
#[cfg(feature = "core")]
@@ -31,8 +29,6 @@ enum Command {
Server(server::Cli),
#[cfg(feature = "core")]
Worker(worker::Cli),
#[cfg(feature = "proxy")]
Proxy(outpost::proxy::Cli),
}
#[derive(Debug, FromArgs, PartialEq)]
@@ -57,8 +53,6 @@ fn main() -> Result<()> {
Command::Server(_) => Mode::set(Mode::Server)?,
#[cfg(feature = "core")]
Command::Worker(_) => Mode::set(Mode::Worker)?,
#[cfg(feature = "proxy")]
Command::Proxy(_) => Mode::set(Mode::Proxy)?,
}
trace!("installing error formatting");
@@ -114,10 +108,6 @@ fn main() -> Result<()> {
let workers = worker::start(args, &mut tasks)?;
metrics.workers.store(Some(workers));
}
#[cfg(feature = "proxy")]
Command::Proxy(args) => {
outpost::start::<outpost::proxy::ProxyOutpost>(args, &mut tasks).await?;
}
}
let errors = tasks.run().await;

View File

@@ -1,312 +0,0 @@
use std::{fmt::Display, sync::Arc};
use ak_common::{Arbiter, Tasks, VERSION, api, arbiter, authentik_build_hash};
use axum::http::{HeaderValue, header::AUTHORIZATION};
use eyre::{Result, eyre};
use futures::{SinkExt as _, StreamExt as _};
use nix::unistd::gethostname;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use time::UtcDateTime;
use tokio::{
signal::unix::SignalKind,
time::{Duration, interval, sleep},
};
use tokio_tungstenite::tungstenite::{Message, client::IntoClientRequest as _};
use tracing::{debug, info, instrument, trace, warn};
use url::Url;
use crate::outpost::{Outpost, OutpostController};
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, Copy, Eq)]
#[repr(u8)]
enum EventKind {
/// Code used to acknowledge a previous message.
Ack = 0,
/// Code used to send a healthcheck keepalive.
Hello = 1,
/// Code received to trigger a config update.
TriggerUpdate = 2,
/// Code received to trigger some provider specific function.
ProviderSpecific = 3,
/// Code received to identify the end of a session.
SessionEnd = 4,
}
impl Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ack => write!(f, "Ack"),
Self::Hello => write!(f, "Hello"),
Self::TriggerUpdate => write!(f, "TriggerUpdate"),
Self::ProviderSpecific => write!(f, "ProviderSpecific"),
Self::SessionEnd => write!(f, "SessionEnd"),
}
}
}
#[derive(Serialize, Deserialize)]
struct Event {
instruction: EventKind,
args: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EventSessionEnd {
session_id: String,
}
fn build_ws_url(mut url: Url, outpost_pk: &str, instance_uuid: &str, attempt: u32) -> Result<Url> {
let ws_scheme = match url.scheme() {
"https" => "wss",
"http" => "ws",
other => return Err(eyre!("Unsupported scheme for WebSocket URL: {other}")),
};
url.set_scheme(ws_scheme)
.map_err(|()| eyre!("Failed to set URL scheme to {ws_scheme}"))?;
url.set_path(&format!("{}ws/outpost/{outpost_pk}/", url.path()));
url.query_pairs_mut()
.append_pair("instance_uuid", instance_uuid)
.append_pair("attempt", &attempt.to_string());
Ok(url)
}
fn hello_args(instance_uuid: &str) -> serde_json::Value {
let raw_hostname = gethostname().unwrap_or_default();
let hostname = raw_hostname.to_string_lossy();
serde_json::json!({
"version": VERSION,
"buildHash": authentik_build_hash(None),
"uuid": instance_uuid,
// TODO: rust version and AWS-LC versions
"hostname": hostname,
})
}
#[instrument(skip_all)]
async fn handle_event<O: Outpost>(
controller: Arc<OutpostController>,
outpost: Arc<O>,
event: Event,
) -> Result<()> {
match event.instruction {
EventKind::Ack | EventKind::Hello => {}
EventKind::TriggerUpdate => {
info!("received update trigger, refreshing outpost");
sleep(controller.reload_offset).await;
controller.refresh().await?;
debug!("outpost controller has been refreshed");
outpost.refresh().await?;
debug!("outpost has been refreshed");
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "This is fine, we'll never get big values here."
)]
controller
.m_last_update
.set(UtcDateTime::now().unix_timestamp() as f64);
}
EventKind::SessionEnd => {
let event: EventSessionEnd = serde_json::from_value(event.args)?;
outpost.end_session(event).await?;
}
#[expect(
clippy::unimplemented,
reason = "this is only relevant for the RAC provider"
)]
EventKind::ProviderSpecific => unimplemented!(),
}
Ok(())
}
async fn watch_events_inner<O: Outpost>(
arbiter: Arbiter,
controller: Arc<OutpostController>,
outpost: Arc<O>,
attempt: u32,
) -> Result<()> {
let server_config = api::ServerConfig::new()?;
let ws_url = build_ws_url(
server_config.host,
&controller.outpost.load().pk.to_string(),
&controller.instance_uuid.to_string(),
attempt,
)?;
debug!(url = %ws_url, "connecting to websocket");
let mut request = ws_url.into_client_request()?;
let token = controller
.api_config
.bearer_access_token
.as_deref()
.unwrap_or("");
request.headers_mut().insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))?,
);
let (ws_stream, _response) = tokio_tungstenite::connect_async(request).await?;
let (mut ws_write, mut ws_read) = ws_stream.split();
info!(
outpost = %controller.outpost.load().pk,
"connected to websocket"
);
controller.m_connection.set(1_u8);
let get_refresh_interval = || {
let mut interval = controller.outpost.load().refresh_interval_s;
// Ensure timer interval is not negative or 0.
// If it is, we default to 5 minutes.
if interval <= 0_i32 {
interval = 60_i32 * 5_i32;
}
// Clamp interval to be at least 30 seconds.
if interval < 30_i32 {
interval = 30_i32;
}
// infallible because we bound it to be positive above
Duration::from_secs(interval.try_into().expect("infallible"))
};
let mut refresh_interval = interval(get_refresh_interval());
let mut heartbeat_interval = interval(Duration::from_secs(10));
let mut events_rx = arbiter.events_subscribe();
loop {
tokio::select! {
_ = refresh_interval.tick() => {
info!("refreshing outpost on interval");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
Event {
instruction: EventKind::TriggerUpdate,
args: serde_json::Value::Null
}
).await {
warn!(?err, "failed to refresh");
}
refresh_interval = interval(get_refresh_interval());
// Since we re-create the interval, we need to make it tick instantly to avoid
// ending up in a never-ending tick-loop.
refresh_interval.tick().await;
},
_ = heartbeat_interval.tick() => {
let ping = Event {
instruction: EventKind::Hello,
args: hello_args(&controller.instance_uuid.to_string()),
};
ws_write.send(Message::text(serde_json::to_string(&ping)?)).await?;
trace!("sent websocket hello (heartbeat)");
},
Ok(arbiter::Event::Signal(signal)) = events_rx.recv() => {
if signal == SignalKind::user_defined1() {
info!("refreshing outpost on signal");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
Event {
instruction: EventKind::TriggerUpdate,
args: serde_json::Value::Null
}
).await {
warn!(?err, "failed to refresh");
}
}
},
msg = ws_read.next() => {
let Some(msg) = msg else {
break;
};
let msg = msg?;
match msg {
Message::Text(text) => {
let Ok(event): Result<Event, _> = serde_json::from_str(&text) else {
warn!(data = text.as_str(), "failed to parse event");
continue;
};
trace!(event = %event.instruction, "received websocket event");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
event,
).await {
warn!(?err, "failed to handle event");
}
},
Message::Ping(data) => {
ws_write.send(Message::Pong(data)).await?;
},
Message::Close(_) => {
break;
},
_ => {},
}
},
() = arbiter.shutdown() => break,
}
}
Ok(())
}
async fn watch_events<O: Outpost>(
arbiter: Arbiter,
controller: Arc<OutpostController>,
outpost: Arc<O>,
) -> Result<()> {
const MAX_BACKOFF: Duration = Duration::from_mins(5);
let mut backoff = Duration::from_secs(1);
let mut attempt: u32 = 0;
loop {
tokio::select! {
() = arbiter.shutdown() => break,
res = watch_events_inner(
arbiter.clone(),
Arc::clone(&controller),
Arc::clone(&outpost),
attempt
) => {
controller.m_connection.set(0_u8);
match res {
Ok(()) => debug!("websocket disconnected cleanly"),
Err(err) => warn!(?err, attempt, "websocket error"),
}
info!(attempt, delay = backoff.as_secs(), "reconnecting websocket in {}s...", backoff.as_secs());
tokio::select! {
() = arbiter.shutdown() => break,
() = sleep(backoff) => {}
}
backoff = (backoff * 2).min(MAX_BACKOFF);
attempt += 1;
}
}
}
info!("stopping event watcher");
Ok(())
}
pub(crate) fn start<O: Outpost + 'static>(
tasks: &mut Tasks,
controller: Arc<OutpostController>,
outpost: Arc<O>,
) -> Result<()> {
let arbiter = tasks.arbiter();
tasks
.build_task()
.name(&format!("{}::watch_events", module_path!()))
.spawn(watch_events(arbiter, controller, outpost))?;
Ok(())
}

View File

@@ -1,123 +0,0 @@
use std::{sync::Arc, time::Duration};
use ak_client::{
apis::{configuration::Configuration, outposts_api::outposts_instances_list},
models::Outpost as OutpostModel,
};
use ak_common::{Tasks, VERSION, api, authentik_build_hash};
use arc_swap::ArcSwap;
use eyre::{Result, eyre};
use tracing::{debug, info, instrument};
use uuid::Uuid;
pub(crate) mod event;
#[cfg(feature = "proxy")]
pub(crate) mod proxy;
pub(crate) trait Outpost: Send + Sync + Sized {
const OUTPOST_TYPE: &'static str;
type Cli: Send + Sync;
async fn new(controller: Arc<OutpostController>) -> Result<Self>;
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()>;
fn refresh(&self) -> impl Future<Output = Result<()>> + Send;
fn end_session(&self, event: event::EventSessionEnd)
-> impl Future<Output = Result<()>> + Send;
}
#[derive(Debug)]
pub(crate) struct OutpostController {
api_config: Configuration,
outpost: ArcSwap<OutpostModel>,
instance_uuid: Uuid,
reload_offset: Duration,
m_info: metrics::Gauge,
m_last_update: metrics::Gauge,
m_connection: metrics::Gauge,
}
impl OutpostController {
#[instrument(skip_all)]
async fn get_outpost(api_config: &Configuration) -> Result<OutpostModel> {
let outposts = outposts_instances_list(
api_config, None, None, None, None, None, None, None, None, None, None, None, None,
)
.await?;
let Some(outpost) = outposts.results.into_iter().next() else {
return Err(eyre!(
"No outposts found with given token, ensure the given token corresponds to an \
authentik Outpost"
));
};
debug!(name = outpost.name, "fetched outpost configuration");
Ok(outpost)
}
#[instrument(skip_all)]
async fn new<O: Outpost>() -> Result<Self> {
let api_config = api::make_config()?;
let outpost = Self::get_outpost(&api_config).await?;
let instance_uuid = Uuid::new_v4();
let m_labels = [
("outpost_name", outpost.name.clone()),
("outpost_type", O::OUTPOST_TYPE.to_owned()),
("uuid", instance_uuid.to_string()),
("version", VERSION.to_owned()),
("build", authentik_build_hash(None)),
];
metrics::describe_gauge!("authentik_outpost_info", "Outpost info");
let m_info = metrics::gauge!("authentik_outpost_info", &m_labels);
metrics::describe_gauge!("authentik_outpost_last_update", "Time of last update");
let m_last_update = metrics::gauge!("authentik_outpost_last_update", &m_labels);
metrics::describe_gauge!("authentik_outpost_connection", "Connection status");
let m_connection = metrics::gauge!("authentik_outpost_connection", &m_labels);
let reload_offset = Duration::from_secs(rand::random_range(0..10));
let controller = Self {
api_config,
outpost: ArcSwap::from_pointee(outpost),
instance_uuid,
reload_offset,
m_info,
m_last_update,
m_connection,
};
info!(embedded = controller.is_embedded(), "outpost mode");
debug!(?reload_offset, "HA Reload offset");
Ok(controller)
}
fn is_embedded(&self) -> bool {
self.outpost
.load()
.managed
.as_ref()
.and_then(|m| m.as_deref())
.is_some_and(|m| m == "goauthentik.io/outposts/embedded")
}
async fn refresh(&self) -> Result<()> {
let outpost = Self::get_outpost(&self.api_config).await?;
self.outpost.swap(Arc::new(outpost));
Ok(())
}
}
#[instrument(skip_all)]
pub(crate) async fn start<O: Outpost + 'static>(_cli: O::Cli, tasks: &mut Tasks) -> Result<()> {
let controller = Arc::new(OutpostController::new::<O>().await?);
let outpost = Arc::new(O::new(Arc::clone(&controller)).await?);
event::start(tasks, Arc::clone(&controller), Arc::clone(&outpost))?;
outpost.start(tasks)?;
controller.m_info.set(1_u8);
Ok(())
}

View File

@@ -1,41 +0,0 @@
use ak_client::models::ProxyOutpostConfig;
use axum::Router;
use eyre::{Result, eyre};
use tracing::instrument;
use url::Url;
use crate::outpost::proxy::ProxyOutpost;
const REDIRECT_PARAM: &str = "rd";
const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback";
const LOGOUT_SIGNATURE: &str = "X-authentik-logout";
#[derive(Debug)]
pub(super) struct Application {
pub(super) host: String,
pub(super) provider: ProxyOutpostConfig,
pub(super) router: Router,
}
impl Application {
#[instrument(skip_all)]
pub(super) fn new(_existing_apps: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result<Self> {
let external_url = Url::parse(&provider.external_host)?;
if !external_url.has_authority() {
return Err(eyre!("no host in external host"));
}
let external_host = external_url.authority();
let _redirect_url = {
let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?;
redirect_url.set_query(Some(&format!("{CALLBACK_SIGNATURE}=true")));
redirect_url
};
Ok(Self {
host: external_host.to_owned(),
provider,
router: Router::new(),
})
}
}

View File

@@ -1,76 +0,0 @@
use std::sync::Arc;
use tower::util::ServiceExt as _;
use ak_axum::{error::Result, extract::host::Host};
use axum::{
extract::{Request, State},
http::{Method, StatusCode, header::CONTENT_TYPE},
response::{IntoResponse as _, Response},
};
use metrics::histogram;
use serde_json::json;
use tokio::time::Instant;
use tracing::{Instrument as _, field, info_span, instrument, trace, warn};
use crate::outpost::proxy::ProxyOutpost;
#[instrument(skip_all)]
pub(super) async fn handle_ping(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
) -> Response {
let start = Instant::now();
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
"outpost_name" => outpost.controller.outpost.load().name.clone(),
"method" => method.to_string(),
"host" => host,
"type" => "ping",
)
.record(start.elapsed().as_secs_f64());
StatusCode::NO_CONTENT.into_response()
}
#[instrument(skip_all)]
pub(super) async fn default(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
request: Request,
) -> Result<Response> {
let span = info_span!("proxy outpost request", user = field::Empty);
let start = Instant::now();
let Some(app) = outpost.lookup_app(&host) else {
trace!(headers = ?request.headers(), "tracing headers for no hostname match");
warn!("no app for hostname");
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(CONTENT_TYPE, "application/json")
.body(
json!({
"message": "no app for hostname",
"host": host,
"detail": format!("check the outpost settings and make sure '{host}' is included."),
})
.to_string()
.into(),
)
.expect("infallible"));
};
trace!("passing to application");
let response = app.router.clone().oneshot(request).instrument(span).await?;
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
"outpost_name" => outpost.controller.outpost.load().name.clone(),
"method" => method.to_string(),
"host" => host,
"type" => "app",
)
.record(start.elapsed().as_secs_f64());
Ok(response)
}

View File

@@ -1,187 +0,0 @@
use std::{collections::HashMap, sync::Arc};
use ak_axum::router::wrap_router;
use ak_client::{apis::outposts_api::outposts_proxy_list, models::ProxyMode};
use ak_common::{Tasks, api::fetch_all, config};
use arc_swap::ArcSwap;
use argh::FromArgs;
use axum::Router;
use eyre::Result;
use tracing::{debug, error, info, instrument, warn};
use crate::outpost::{Outpost, OutpostController, proxy::application::Application};
mod application;
mod handlers;
#[derive(Debug, Default, FromArgs, PartialEq, Eq)]
/// Run the authentik proxy outpost.
#[argh(subcommand, name = "proxy")]
#[expect(
clippy::empty_structs_with_brackets,
reason = "argh doesn't support unit structs"
)]
pub(crate) struct Cli {}
pub(crate) struct ProxyOutpost {
controller: Arc<OutpostController>,
apps: ArcSwap<HashMap<String, Arc<Application>>>,
}
impl Outpost for ProxyOutpost {
type Cli = Cli;
const OUTPOST_TYPE: &'static str = "proxy";
#[instrument(skip_all)]
async fn new(controller: Arc<OutpostController>) -> Result<Self> {
Ok(Self {
controller,
apps: ArcSwap::from_pointee(HashMap::with_capacity(0)),
})
}
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()> {
let router = build_router(self);
for addr in config::get().listen.http.iter().copied() {
ak_axum::server::start_plain(tasks, "proxy-outpost", router.clone(), addr, false)?;
}
Ok(())
}
#[instrument(skip_all)]
async fn refresh(&self) -> Result<()> {
debug!(
outpost_pk = %self.controller.outpost.load().pk,
"requesting providers for outpost"
);
let providers = fetch_all(
|page| {
outposts_proxy_list(
&self.controller.api_config,
None,
None,
Some(page),
Some(100_i32),
None,
)
},
|r| &r.pagination,
|r| r.results,
)
.await
.inspect_err(|err| error!(?err, "failed to fetch providers"))?;
debug!(count = providers.len(), "fetched providers");
if providers.is_empty() && !self.controller.is_embedded() {
warn!(
"no providers assigned to this outpost, check outpost configuration in authentik"
);
}
for (i, provider) in providers.iter().enumerate() {
debug!(
index = i,
name = provider.name,
external_host = provider.external_host,
assigned_to_app = provider.assigned_application_name,
"provider details"
);
}
let mut apps = HashMap::with_capacity(providers.len());
for provider in providers {
let name = provider.name.clone();
let Ok(application) = Application::new(self, provider)
.inspect_err(|err| warn!(?err, "failed to setup application, skipping provider"))
else {
continue;
};
info!(name, host = application.host, "loaded application");
apps.insert(application.host.clone(), Arc::new(application));
}
self.apps.store(Arc::new(apps));
Ok(())
}
async fn end_session(&self, _event: super::event::EventSessionEnd) -> Result<()> {
// todo!()
warn!(?_event, "removing session");
Ok(())
}
}
impl ProxyOutpost {
#[instrument(skip(self))]
fn lookup_app(&self, host: &str) -> Option<Arc<Application>> {
let apps = self.apps.load();
// If we only have a single app, host name switching doesn't matter.
if apps.len() == 1
&& let Some(app) = apps.values().next()
{
debug!(app = app.provider.name, "found a single app, using it");
return Some(Arc::clone(app));
}
if let Some(app) = apps.get(host) {
debug!(app = app.provider.name, "found app based direct host match");
return Some(Arc::clone(app));
}
// For forward_auth_domain, we don't have a direct app to domain relationship.
// Check through all apps, and check how much of their cookie domain matches the host.
// Return the application that has the longest match.
let mut longest_match = None;
let mut longest_len = 0_usize;
for app in apps.values() {
if app.provider.mode != Some(ProxyMode::ForwardDomain) {
continue;
}
if let Some(cookie_domain) = app.provider.cookie_domain.as_deref() {
// Check if the cookie domain has a leading period for a wildcard.
// This will decrease the weight of a wildcard domain, but a request to example.com
// with the cookie domain set to example.com will still be routed correctly.
let domain = cookie_domain.trim_start_matches('.');
if host.ends_with(domain) && domain.len() > longest_len {
longest_len = domain.len();
longest_match = Some(Arc::clone(app));
}
// For forward_auth_domain, we need to response on the external domain too.
if app.provider.external_host == host {
debug!(app = app.provider.name, "found app based on external_host");
return Some(Arc::clone(app));
}
}
}
if let Some(app) = &longest_match {
debug!(app = app.provider.name, "found app based on cookie domain");
}
longest_match
}
}
fn build_router(outpost: Arc<ProxyOutpost>) -> Router {
wrap_router(
Router::new()
.nest(
"/outpost.goauthentik.io/ping",
Router::new().fallback(handlers::handle_ping),
)
.fallback(handlers::default)
.with_state(outpost),
true,
)
}

View File

@@ -111,8 +111,12 @@ class TestFlowsEnroll(SeleniumTestCase):
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[ouiaId='enroll']")))
identification_stage.find_element(By.CSS_SELECTOR, "a[ouiaId='enroll']").click()
wait.until(
ec.presence_of_element_located((By.CSS_SELECTOR, "a[data-ouia-component-id='enroll']"))
)
identification_stage.find_element(
By.CSS_SELECTOR, "a[data-ouia-component-id='enroll']"
).click()
# First prompt stage
flow_executor = self.get_shadow_root("ak-flow-executor")

View File

@@ -27,8 +27,14 @@ class TestFlowsRecovery(SeleniumTestCase):
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[ouiaId='recovery']")))
identification_stage.find_element(By.CSS_SELECTOR, "a[ouiaId='recovery']").click()
wait.until(
ec.presence_of_element_located(
(By.CSS_SELECTOR, "a[data-ouia-component-id='recovery']")
)
)
identification_stage.find_element(
By.CSS_SELECTOR, "a[data-ouia-component-id='recovery']"
).click()
# First prompt stage
flow_executor = self.get_shadow_root("ak-flow-executor")

8
uv.lock generated
View File

@@ -322,7 +322,7 @@ requires-dist = [
{ 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" },
@@ -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]]

126
web/package-lock.json generated
View File

@@ -66,7 +66,7 @@
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.2",
"core-js": "^3.49.0",
"country-flag-icons": "^1.6.16",
"country-flag-icons": "^1.6.17",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.4.2",
@@ -118,7 +118,7 @@
"vitest": "^4.1.1",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",
"yaml": "^2.8.3"
"yaml": "^2.8.4"
},
"engines": {
"node": ">=24",
@@ -4577,9 +4577,9 @@
}
},
"node_modules/@swc/core": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.32.tgz",
"integrity": "sha512-/eWL0n43D64QWEUHLtTE+jDqjkJhyidjkDhv6f0uJohOUAhywxQ9wXYp845DNNds0JpCdI4Uo0a9bl+vbXf+ew==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz",
"integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -4594,18 +4594,18 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.32",
"@swc/core-darwin-x64": "1.15.32",
"@swc/core-linux-arm-gnueabihf": "1.15.32",
"@swc/core-linux-arm64-gnu": "1.15.32",
"@swc/core-linux-arm64-musl": "1.15.32",
"@swc/core-linux-ppc64-gnu": "1.15.32",
"@swc/core-linux-s390x-gnu": "1.15.32",
"@swc/core-linux-x64-gnu": "1.15.32",
"@swc/core-linux-x64-musl": "1.15.32",
"@swc/core-win32-arm64-msvc": "1.15.32",
"@swc/core-win32-ia32-msvc": "1.15.32",
"@swc/core-win32-x64-msvc": "1.15.32"
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-darwin-x64": "1.15.33",
"@swc/core-linux-arm-gnueabihf": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-arm64-musl": "1.15.33",
"@swc/core-linux-ppc64-gnu": "1.15.33",
"@swc/core-linux-s390x-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/core-linux-x64-musl": "1.15.33",
"@swc/core-win32-arm64-msvc": "1.15.33",
"@swc/core-win32-ia32-msvc": "1.15.33",
"@swc/core-win32-x64-msvc": "1.15.33"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
@@ -4617,9 +4617,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.32.tgz",
"integrity": "sha512-/YWMvJDPu+AAwuUsM2G+DNQ/7zhodURGzdQyewEqcvgklAdDHs3LwQmLLnyn6SJl8DT8UOxkbzK+D1PmPeelRg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz",
"integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==",
"cpu": [
"arm64"
],
@@ -4633,9 +4633,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.32.tgz",
"integrity": "sha512-KOTXJXdAhWL+hZ77MYP3z+4pcMFaQhQ74yqyN1uz093q0YnbxpqMtYpPISbYvMHzVRNNx5kN+9RZAXEaadhWVA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz",
"integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==",
"cpu": [
"x64"
],
@@ -4649,9 +4649,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.32.tgz",
"integrity": "sha512-oOoxLweljlc0A4X8ybsgxV7cVaYTwBOg2iMDJcFR3Sr48C+lsv9VzSmqdK/IVIXF4W4GjLc3VqTAdSMXlfVLuQ==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz",
"integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==",
"cpu": [
"arm"
],
@@ -4665,9 +4665,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.32.tgz",
"integrity": "sha512-oDzEkdl6D6BAWdMtU5KGO7y3HR5fJcvByNLyEk9+ugj8nP5Ovb7P4kBcStBXc4MPExFGQryehiINMlmY8HlclA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz",
"integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==",
"cpu": [
"arm64"
],
@@ -4681,9 +4681,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.32.tgz",
"integrity": "sha512-omcqjoZP/b8D8PuczVoRwJieC6ibj7qIxTftNYokz4/aSmKFHvsd7nIFfPk5ZvtzncbH4AY7+Dkr/Lp2gWxYeA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz",
"integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==",
"cpu": [
"arm64"
],
@@ -4697,9 +4697,9 @@
}
},
"node_modules/@swc/core-linux-ppc64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.32.tgz",
"integrity": "sha512-KGkTMyz/Tbn3PBNu0AVZ4GTDFKnICrYcTiNPZq8DrvK42pnFsf3GNDrIG9E5AtQlTmC0YigkWKmu0eMcfTrmgA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz",
"integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==",
"cpu": [
"ppc64"
],
@@ -4713,9 +4713,9 @@
}
},
"node_modules/@swc/core-linux-s390x-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.32.tgz",
"integrity": "sha512-G3Aa4tVS/3OGZBkoNIwUF9F6RAy+Osb4GOlo62SinLmDiErz/ykmM7KH0wkz6l9kM8jJq1HyAM6atJTUEbBk7g==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz",
"integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==",
"cpu": [
"s390x"
],
@@ -4729,9 +4729,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.32.tgz",
"integrity": "sha512-ERsjfGcj6CBmj3vJnGDO8m8rTvw6RqMcWo1dogOtNx3/+/0+NNpJiXDobJrr1GwInI/BHAEkvSFIH6d2LqPcUQ==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz",
"integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==",
"cpu": [
"x64"
],
@@ -4745,9 +4745,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.32.tgz",
"integrity": "sha512-N4Ggahe/8SUbTX50P6EdhbW9YWcgbZVb52R4cq6MK+zsoMjRq7rGvV5ztA05QnbaCYqMYx8rTY7KAIA3Crdo4Q==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz",
"integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==",
"cpu": [
"x64"
],
@@ -4761,9 +4761,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.32.tgz",
"integrity": "sha512-01yN0o9jvo8xBTP12aPK2wW8b41jmOlGbDDlAnoynotc4pO6xA0zby9f1z6j++qXDpGBttLySq1omgVrlQKYcw==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz",
"integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==",
"cpu": [
"arm64"
],
@@ -4777,9 +4777,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.32.tgz",
"integrity": "sha512-fLagI9XZYNpTcmlqAcp3KBtmj7E19WCmYD80Jxj1Kn5tGNa7yxNLd3NNdWxuZGUPl5iC0/KqZru7g08gF6Fsrw==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz",
"integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==",
"cpu": [
"ia32"
],
@@ -4793,9 +4793,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.32.tgz",
"integrity": "sha512-gbc2bQ/T2CiR+w0OvcVKwLOFAcPZBvmWmolbwpg1E8UrpeC03DGtyMUApOHNXNYWA3SHFrYXCQtosrcMza1YFg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz",
"integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==",
"cpu": [
"x64"
],
@@ -7475,9 +7475,9 @@
}
},
"node_modules/country-flag-icons": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.16.tgz",
"integrity": "sha512-HxJVoE/aaZGcUMx1vK/u9430uKGB3ODZDDZJJOqVJQzoHk5v42c0fSp1rk4tDfyr1dVOJjwxRiaBPliBMo2Liw==",
"version": "1.6.17",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.17.tgz",
"integrity": "sha512-Nmik0289ZVZSI3c7mJR/amg6DyY7Z59b0sTFSKayeX72mHfPzCPJygwJs2pYgQULzuAyWeCUgwAJ+Dq8OR+JFw==",
"license": "MIT"
},
"node_modules/crelt": {
@@ -10748,9 +10748,9 @@
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"optional": true,
"engines": {
@@ -19093,9 +19093,9 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz",
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -19280,7 +19280,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.15.32",
"@swc/core": "^1.15.33",
"@webcomponents/template": "^1.5.1",
"base64-js": "^1.5.1",
"core-js": "^3.49.0",

View File

@@ -142,7 +142,7 @@
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.2",
"core-js": "^3.49.0",
"country-flag-icons": "^1.6.16",
"country-flag-icons": "^1.6.17",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.4.2",
@@ -194,7 +194,7 @@
"vitest": "^4.1.1",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",
"yaml": "^2.8.3"
"yaml": "^2.8.4"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.28.0",

View File

@@ -20,7 +20,7 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.8.1",
"@swc/core": "^1.15.32",
"@swc/core": "^1.15.33",
"@webcomponents/template": "^1.5.1",
"base64-js": "^1.5.1",
"core-js": "^3.49.0",

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

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

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

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

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

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

@@ -386,7 +386,7 @@ export class IdentificationStage extends BaseStage<
return html`<a
href=${url}
class="pf-c-button pf-m-secondary pf-m-block"
ouiaId="passwordless"
data-ouia-component-id="passwordless"
>
${msg("Use a security key")}
</a> `;
@@ -475,12 +475,12 @@ export class IdentificationStage extends BaseStage<
${enrollUrl
? html`<div class="pf-c-login__main-footer-band-item">
${msg("Need an account?")}
<a href="${enrollUrl}" ouiaId="enroll">${msg("Sign up.")}</a>
<a href="${enrollUrl}" data-ouia-component-id="enroll">${msg("Sign up.")}</a>
</div>`
: nothing}
${recoveryUrl
? html`<div class="pf-c-login__main-footer-band-item">
<a href="${recoveryUrl}" ouiaId="recovery"
<a href="${recoveryUrl}" data-ouia-component-id="recovery"
>${msg("Forgot username or password?")}</a
>
</div>`

View File

@@ -51,12 +51,12 @@
"@rspack/binding-darwin-arm64": "1.7.11",
"@rspack/binding-linux-arm64-gnu": "1.7.11",
"@rspack/binding-linux-x64-gnu": "1.7.11",
"@swc/core-darwin-arm64": "1.15.32",
"@swc/core-linux-arm64-gnu": "1.15.32",
"@swc/core-linux-x64-gnu": "1.15.32",
"@swc/html-darwin-arm64": "1.15.32",
"@swc/html-linux-arm64-gnu": "1.15.32",
"@swc/html-linux-x64-gnu": "1.15.32",
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/html-darwin-arm64": "1.15.33",
"@swc/html-linux-arm64-gnu": "1.15.33",
"@swc/html-linux-x64-gnu": "1.15.33",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0"

View File

@@ -41,12 +41,12 @@
"@rspack/binding-darwin-arm64": "1.7.11",
"@rspack/binding-linux-arm64-gnu": "1.7.11",
"@rspack/binding-linux-x64-gnu": "1.7.11",
"@swc/core-darwin-arm64": "1.15.32",
"@swc/core-linux-arm64-gnu": "1.15.32",
"@swc/core-linux-x64-gnu": "1.15.32",
"@swc/html-darwin-arm64": "1.15.32",
"@swc/html-linux-arm64-gnu": "1.15.32",
"@swc/html-linux-x64-gnu": "1.15.32",
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/html-darwin-arm64": "1.15.33",
"@swc/html-linux-arm64-gnu": "1.15.33",
"@swc/html-linux-x64-gnu": "1.15.33",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0"
@@ -163,12 +163,12 @@
"@rspack/binding-darwin-arm64": "1.7.11",
"@rspack/binding-linux-arm64-gnu": "1.7.11",
"@rspack/binding-linux-x64-gnu": "1.7.11",
"@swc/core-darwin-arm64": "1.15.32",
"@swc/core-linux-arm64-gnu": "1.15.32",
"@swc/core-linux-x64-gnu": "1.15.32",
"@swc/html-darwin-arm64": "1.15.32",
"@swc/html-linux-arm64-gnu": "1.15.32",
"@swc/html-linux-x64-gnu": "1.15.32",
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/html-darwin-arm64": "1.15.33",
"@swc/html-linux-arm64-gnu": "1.15.33",
"@swc/html-linux-x64-gnu": "1.15.33",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0"
@@ -6849,9 +6849,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.32.tgz",
"integrity": "sha512-/YWMvJDPu+AAwuUsM2G+DNQ/7zhodURGzdQyewEqcvgklAdDHs3LwQmLLnyn6SJl8DT8UOxkbzK+D1PmPeelRg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz",
"integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==",
"cpu": [
"arm64"
],
@@ -6897,9 +6897,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.32.tgz",
"integrity": "sha512-oDzEkdl6D6BAWdMtU5KGO7y3HR5fJcvByNLyEk9+ugj8nP5Ovb7P4kBcStBXc4MPExFGQryehiINMlmY8HlclA==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz",
"integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==",
"cpu": [
"arm64"
],
@@ -6964,9 +6964,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.32.tgz",
"integrity": "sha512-ERsjfGcj6CBmj3vJnGDO8m8rTvw6RqMcWo1dogOtNx3/+/0+NNpJiXDobJrr1GwInI/BHAEkvSFIH6d2LqPcUQ==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz",
"integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==",
"cpu": [
"x64"
],
@@ -7127,9 +7127,9 @@
}
},
"node_modules/@swc/html-darwin-arm64": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.15.32.tgz",
"integrity": "sha512-WgY386nwyz24cTJ+Nztd4cKvfPJexLYAzurSYDmuYxS3HihWoTFZWMDomTfM8yr2UCi8SwW+zTNAWxJxUaKESg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.15.33.tgz",
"integrity": "sha512-zyO6uMBfLyCh55wundAxKX+8P/f98ecuyir4VX6nTmn6y7x37ndB8f01LUrd9Tiq6eEAvDXLiqEUvuGjEc7Pmg==",
"cpu": [
"arm64"
],
@@ -7175,9 +7175,9 @@
}
},
"node_modules/@swc/html-linux-arm64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.15.32.tgz",
"integrity": "sha512-gvlByySjNDWX2FUIGVBWOhd00rySz0AOydQpuXCK0ldYbFVMby9oXbp2JVmE5UsB6J4YZqZh4ajmmqCGvFHi4Q==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.15.33.tgz",
"integrity": "sha512-7tZ0IgmUslI9Extu/TpxJS0GjJoDx0j9zeq2cIidPdM/njSBpyRB7n4B292Q5WFVh7PcZl7WXqqqMczibQ27aA==",
"cpu": [
"arm64"
],
@@ -7242,9 +7242,9 @@
}
},
"node_modules/@swc/html-linux-x64-gnu": {
"version": "1.15.32",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.15.32.tgz",
"integrity": "sha512-IveuScZfAwDZEBs6pTvdG/MwGyMPuxp74l9ngp2PbUboVBIfUS894kATBaBuSBYXajZ4v4wqv01PGM81rUhGQg==",
"version": "1.15.33",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.15.33.tgz",
"integrity": "sha512-JDNb4Uq+7g+23QuOtwWnP0/EqztWIHFFdQdeBIS5zx83YBG2dYRMdPAjnHJWh2YRZxdepd8q6S9MUIxpSrouAg==",
"cpu": [
"x64"
],

View File

@@ -39,12 +39,12 @@
"@rspack/binding-darwin-arm64": "1.7.11",
"@rspack/binding-linux-arm64-gnu": "1.7.11",
"@rspack/binding-linux-x64-gnu": "1.7.11",
"@swc/core-darwin-arm64": "1.15.32",
"@swc/core-linux-arm64-gnu": "1.15.32",
"@swc/core-linux-x64-gnu": "1.15.32",
"@swc/html-darwin-arm64": "1.15.32",
"@swc/html-linux-arm64-gnu": "1.15.32",
"@swc/html-linux-x64-gnu": "1.15.32",
"@swc/core-darwin-arm64": "1.15.33",
"@swc/core-linux-arm64-gnu": "1.15.33",
"@swc/core-linux-x64-gnu": "1.15.33",
"@swc/html-darwin-arm64": "1.15.33",
"@swc/html-linux-arm64-gnu": "1.15.33",
"@swc/html-linux-x64-gnu": "1.15.33",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0"