Compare commits

..

43 Commits

Author SHA1 Message Date
d8607ab324 core: remove session migration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-05-19 13:27:02 +02:00
3748781368 sources/kerberos: resolve logger warnings (#14540)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-05-18 01:31:41 +02:00
99b559893b core, web: update translations (#14530)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-05-16 16:03:21 +02:00
8014088c3a core: bump astral-sh/uv from 0.7.3 to 0.7.4 (#14531)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.7.3 to 0.7.4.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.7.3...0.7.4)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.7.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>
2025-05-16 16:03:08 +02:00
3ee353126f core: bump github.com/getsentry/sentry-go from 0.32.0 to 0.33.0 (#14532)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  dependency-version: 0.33.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>
2025-05-16 16:02:57 +02:00
db76c5d9e2 core: bump goauthentik.io/api/v3 from 3.2025040.1 to 3.2025041.1 (#14533)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025040.1 to 3.2025041.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2025040.1...v3.2025041.1)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-version: 3.2025041.1
  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>
2025-05-16 16:02:50 +02:00
61bff69b7d core: bump django-pglock from 1.7.1 to 1.7.2 (#14534)
Bumps [django-pglock](https://github.com/AmbitionEng/django-pglock) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/AmbitionEng/django-pglock/releases)
- [Changelog](https://github.com/AmbitionEng/django-pglock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AmbitionEng/django-pglock/compare/1.7.1...1.7.2)

---
updated-dependencies:
- dependency-name: django-pglock
  dependency-version: 1.7.2
  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>
2025-05-16 16:02:42 +02:00
69651323e3 web: bump API Client version (#14528)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-05-15 20:19:16 +02:00
75a0ac9588 release: 2025.4.1 (#14527)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	package.json
2025-05-15 20:12:41 +02:00
941a697397 website/docs: release notes for 2025.4.1 (#14526)
* website/docs: release notes for 2025.4.1

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-15 19:26:01 +02:00
4a74db17a1 web: bump undici from 6.21.1 to 6.21.3 in /web (#14524)
Bumps [undici](https://github.com/nodejs/undici) from 6.21.1 to 6.21.3.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.1...v6.21.3)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.21.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-15 16:58:28 +02:00
0cf6bff93c tests/e2e: add test for authentication flow in compatibility mode (#14392)
* tests/e2e: add test for authentication flow in compatibility mode

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

* web: Add prefix class to CSS for easier debugging of constructed stylesheets.

- Use CSS variables for highlighter.

* web: Fix issue where MDX components apply styles out of order.

* web: Fix hover color.

* web: Fix CSS module types. Clean up globals.

* web: Fix issues surrounding availability of shadow root in compatibility mode.

* web: Fix typo.

* web: Partial fixes for storybook dark theme.

* web: Fix overflow.

* web: Fix issues surrounding competing interfaces attempting to apply styles.

* fix padding in ak-alert in. markdown

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

* web: Minimize use of sub-module exports.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Teffen Ellis <teffen@sister.software>
2025-05-15 16:51:11 +02:00
814e438422 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#14513)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-05-15 16:25:28 +02:00
2db77a37dd lifecycle/aws: bump aws-cdk from 2.1014.0 to 2.1015.0 in /lifecycle/aws (#14516)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1014.0 to 2.1015.0.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1015.0/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1015.0
  dependency-type: direct:development
  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>
2025-05-15 15:09:53 +02:00
e40c5ac617 web/admin: Dual select state management, custom event dispatching. (#14490)
* web/admin: Fix issues surrounding dual select state management.

* web: Fix nested path.

* web: Use PatternFly variable.
2025-05-15 14:47:47 +02:00
7440900dac core: fix unable to create group if no enable_group_superuser permission is given (#14510)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-15 14:41:26 +02:00
ca96b27825 web/admin: Fix sidebar toggle synchronization. (#14487)
* web: Fix issue where resizing from tablet or smaller viewport desyncs the sidebar.

* web: Fix issue where focus style overrides hover state style.
2025-05-14 17:19:22 +02:00
ad4a765a80 website: bump the build group in /website with 6 updates (#14502)
Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@rspack/binding-darwin-arm64](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.3.9` | `1.3.10` |
| [@rspack/binding-linux-arm64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.3.9` | `1.3.10` |
| [@rspack/binding-linux-x64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.3.9` | `1.3.10` |
| [lightningcss-darwin-arm64](https://github.com/parcel-bundler/lightningcss) | `1.30.0` | `1.30.1` |
| [lightningcss-linux-arm64-gnu](https://github.com/parcel-bundler/lightningcss) | `1.30.0` | `1.30.1` |
| [lightningcss-linux-x64-gnu](https://github.com/parcel-bundler/lightningcss) | `1.30.0` | `1.30.1` |


Updates `@rspack/binding-darwin-arm64` from 1.3.9 to 1.3.10
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.3.10/packages/rspack)

Updates `@rspack/binding-linux-arm64-gnu` from 1.3.9 to 1.3.10
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.3.10/packages/rspack)

Updates `@rspack/binding-linux-x64-gnu` from 1.3.9 to 1.3.10
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.3.10/packages/rspack)

Updates `lightningcss-darwin-arm64` from 1.30.0 to 1.30.1
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.30.0...v1.30.1)

Updates `lightningcss-linux-arm64-gnu` from 1.30.0 to 1.30.1
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.30.0...v1.30.1)

Updates `lightningcss-linux-x64-gnu` from 1.30.0 to 1.30.1
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.30.0...v1.30.1)

---
updated-dependencies:
- dependency-name: "@rspack/binding-darwin-arm64"
  dependency-version: 1.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-arm64-gnu"
  dependency-version: 1.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-x64-gnu"
  dependency-version: 1.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: lightningcss-darwin-arm64
  dependency-version: 1.30.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: lightningcss-linux-arm64-gnu
  dependency-version: 1.30.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: lightningcss-linux-x64-gnu
  dependency-version: 1.30.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 16:11:52 +02:00
4dcd481010 core: remove OldAuthenticatedSession content type (#14507)
* core: remove `OldAuthenticatedSession` content type

This was left out from https://github.com/goauthentik/authentik/pull/9736

* remove stale content types in `repair_permissions`

Co-authored-by: Jens Langhammer <jens@goauthentik.io>

* run `remove_stale_contenttypes` for each tenant

---------

Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-05-14 15:02:29 +02:00
d0dc14d84d core: bump msgraph-sdk from 1.29.0 to 1.30.0 (#14503)
Bumps [msgraph-sdk](https://github.com/microsoftgraph/msgraph-sdk-python) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/microsoftgraph/msgraph-sdk-python/releases)
- [Changelog](https://github.com/microsoftgraph/msgraph-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoftgraph/msgraph-sdk-python/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: msgraph-sdk
  dependency-version: 1.30.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>
2025-05-14 12:22:41 +02:00
7bf960352b core: bump twilio from 9.6.0 to 9.6.1 (#14505)
Bumps [twilio](https://github.com/twilio/twilio-python) from 9.6.0 to 9.6.1.
- [Release notes](https://github.com/twilio/twilio-python/releases)
- [Changelog](https://github.com/twilio/twilio-python/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-python/compare/9.6.0...9.6.1)

---
updated-dependencies:
- dependency-name: twilio
  dependency-version: 9.6.1
  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>
2025-05-14 12:22:27 +02:00
c07d01661b core: bump psycopg[c,pool] from 3.2.8 to 3.2.9 (#14504)
Bumps [psycopg[c,pool]](https://github.com/psycopg/psycopg) from 3.2.8 to 3.2.9.
- [Changelog](https://github.com/psycopg/psycopg/blob/3.2.9/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.2.8...3.2.9)

---
updated-dependencies:
- dependency-name: psycopg[c,pool]
  dependency-version: 3.2.9
  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>
2025-05-14 12:19:42 +02:00
427597ec14 enterprise: fix expired license's users being counted (#14451)
* enterprise: fix expired license's users being counted

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

* tests to the rescue

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

* hmm

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-13 15:59:17 +02:00
7cc77bd387 website/integrations: fix missing closing brace for semaphore (#14467)
Update index.mdx

Added missing closing bracket

Signed-off-by: ericgu08 <79233593+ericgu08@users.noreply.github.com>
2025-05-13 15:26:10 +02:00
381a1a2c49 tests/e2e: Add E2E tests for Flow SFE (#14484)
* add e2e test for SFE login

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

* add helper text in SFE on password stage

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

* build sfe for e2e

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

* fix ci e2e cache key not considering sfe

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

* fix sfe missing from docker build

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

* sigh I forgot npm

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-13 12:03:12 +02:00
08f8222224 website: bump semver from 7.7.1 to 7.7.2 in /website (#14491)
Bumps [semver](https://github.com/npm/node-semver) from 7.7.1 to 7.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.7.1...v7.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.7.2
  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>
2025-05-13 12:01:13 +02:00
1211c34a18 core: bump django from 5.1.8 to 5.1.9 (#14483)
* build(deps): bump django from 5.1.8 to 5.1.9

Bumps [django](https://github.com/django/django) from 5.1.8 to 5.1.9.
- [Commits](https://github.com/django/django/compare/5.1.8...5.1.9)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.1.9
  dependency-type: direct:production
...

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

* bump lock

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-05-12 21:23:10 +02:00
22efb57369 core: bump psycopg[c,pool] from 3.2.7 to 3.2.8 (#14481)
Bumps [psycopg[c,pool]](https://github.com/psycopg/psycopg) from 3.2.7 to 3.2.8.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.2.7...3.2.8)

---
updated-dependencies:
- dependency-name: psycopg[c,pool]
  dependency-version: 3.2.8
  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>
2025-05-12 20:55:37 +02:00
3eeda53be6 core: bump sentry-sdk from 2.27.0 to 2.28.0 (#14482)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.27.0 to 2.28.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.27.0...2.28.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.28.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>
2025-05-12 20:55:34 +02:00
82ace18703 root: pin package version in pyproject for dependabot (#14469)
* root: pin package version in pyproject for dependabot

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

* use exact as we know that works now

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-12 19:32:48 +02:00
8589079252 core: fix session migration when old session can't be loaded (#14466)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-12 15:51:49 +02:00
ae2af6e58e root: temporarily deactivate database pool option (#14443)
* root: temporarily deactivate database pool option

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

* format

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

* deactivate tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-12 14:48:01 +02:00
86a7f98ff6 website: bump the build group in /website with 3 updates (#14475)
Bumps the build group in /website with 3 updates: [lightningcss-darwin-arm64](https://github.com/parcel-bundler/lightningcss), [lightningcss-linux-arm64-gnu](https://github.com/parcel-bundler/lightningcss) and [lightningcss-linux-x64-gnu](https://github.com/parcel-bundler/lightningcss).


Updates `lightningcss-darwin-arm64` from 1.29.3 to 1.30.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.29.3...v1.30.0)

Updates `lightningcss-linux-arm64-gnu` from 1.29.3 to 1.30.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.29.3...v1.30.0)

Updates `lightningcss-linux-x64-gnu` from 1.29.3 to 1.30.0
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.29.3...v1.30.0)

---
updated-dependencies:
- dependency-name: lightningcss-darwin-arm64
  dependency-version: 1.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: lightningcss-linux-arm64-gnu
  dependency-version: 1.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: lightningcss-linux-x64-gnu
  dependency-version: 1.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 14:39:18 +02:00
3af45371d3 website/docs: stages: fix-typo (#14477)
Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-05-12 14:12:33 +02:00
b01ffd934f website/docs: Update Kubernetes Bootstrap Instructions (#14471)
* website/docs: update envFrom block for automated install

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-05-11 15:13:14 +02:00
f11ba94603 root: improve sentry distributed tracing (#14468)
* core: include all sentry headers

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

* remove spotlight patch we dont need anymore

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

* always trace in debug

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

* init sentry earlier

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

* re-add light interface

https://github.com/goauthentik/authentik/pull/14331

removes 2 unneeded API calls

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

* sentry integrated router

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

* use new Sentry middleware to propagate headers

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

* fix missing baggage

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

* cleanup logs

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

* use sanitized URLs for logging/tracing

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-11 02:40:31 +02:00
7d2aa43364 Revert "web/admin: fix enterprise menu display" (#14458)
Revert "web/admin: fix enterprise menu display (#14447)"

This reverts commit 0611eea0e7.
2025-05-10 18:26:07 +02:00
f1351a7577 website/docs: update outdated custom CSS docs (#14441)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-10 11:15:47 -05:00
0611eea0e7 web/admin: fix enterprise menu display (#14447)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-10 00:54:33 +02:00
d0b46fcf9c core: bump msgraph-sdk from 1.28.0 to v1.29.0 (#14454) 2025-05-10 00:51:32 +02:00
dcbdc37d31 core: bump opentelemetry-api from 1.32.1 to v1.33.0 (#14455) 2025-05-10 00:51:29 +02:00
d07f396379 core: bump platformdirs from 4.3.7 to v4.3.8 (#14456) 2025-05-10 00:51:27 +02:00
0972103b83 core: bump ruff from 0.11.8 to v0.11.9 (#14457) 2025-05-10 00:51:24 +02:00
128 changed files with 3278 additions and 3888 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2025.4.0 current_version = 2025.4.1
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -200,7 +200,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: web/dist path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }} key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
- name: prepare web ui - name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true' if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web working-directory: web
@ -208,6 +208,7 @@ jobs:
npm ci npm ci
make -C .. gen-client-ts make -C .. gen-client-ts
npm run build npm run build
npm run build:sfe
- name: run e2e - name: run e2e
run: | run: |
uv run coverage run manage.py test ${{ matrix.job.glob }} uv run coverage run manage.py test ${{ matrix.job.glob }}

View File

@ -40,7 +40,8 @@ COPY ./web /work/web/
COPY ./website /work/website/ COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build RUN npm run build && \
npm run build:sfe
# Stage 3: Build go proxy # Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
@ -93,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv # Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.3 AS uv FROM ghcr.io/astral-sh/uv:0.7.4 AS uv
# Stage 6: Base python image # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2025.4.0" __version__ = "2025.4.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -5,10 +5,10 @@ from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from sentry_sdk import get_current_span
from authentik import get_full_version from authentik import get_full_version
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.lib.sentry import get_http_meta
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
@ -32,13 +32,9 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template""" """Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND) brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant()) tenant = getattr(request, "tenant", Tenant())
trace = ""
span = get_current_span()
if span:
trace = span.to_traceparent()
return { return {
"brand": brand, "brand": brand,
"footer_links": tenant.footer_links, "footer_links": tenant.footer_links,
"sentry_trace": trace, "html_meta": {**get_http_meta()},
"version": get_full_version(), "version": get_full_version(),
} }

View File

@ -99,18 +99,17 @@ class GroupSerializer(ModelSerializer):
if superuser if superuser
else "authentik_core.disable_group_superuser" else "authentik_core.disable_group_superuser"
) )
has_perm = user.has_perm(perm) if self.instance or superuser:
if self.instance and not has_perm: has_perm = user.has_perm(perm) or user.has_perm(perm, self.instance)
has_perm = user.has_perm(perm, self.instance) if not has_perm:
if not has_perm: raise ValidationError(
raise ValidationError( _(
_( (
( "User does not have permission to set "
"User does not have permission to set " "superuser status to {superuser_status}."
"superuser status to {superuser_status}." ).format_map({"superuser_status": superuser})
).format_map({"superuser_status": superuser}) )
) )
)
return superuser return superuser
class Meta: class Meta:

View File

@ -2,6 +2,7 @@
from django.apps import apps from django.apps import apps
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
from django.core.management import call_command
from django.core.management.base import BaseCommand, no_translations from django.core.management.base import BaseCommand, no_translations
from guardian.management import create_anonymous_user from guardian.management import create_anonymous_user
@ -16,6 +17,10 @@ class Command(BaseCommand):
"""Check permissions for all apps""" """Check permissions for all apps"""
for tenant in Tenant.objects.filter(ready=True): for tenant in Tenant.objects.filter(ready=True):
with tenant: with tenant:
# See https://code.djangoproject.com/ticket/28417
# Remove potential lingering old permissions
call_command("remove_stale_contenttypes", "--no-input")
for app in apps.get_app_configs(): for app in apps.get_app_configs():
self.stdout.write(f"Checking app {app.name} ({app.label})\n") self.stdout.write(f"Checking app {app.name} ({app.label})\n")
create_permissions(app, verbosity=0) create_permissions(app, verbosity=0)

View File

@ -1,123 +1,9 @@
# Generated by Django 5.0.11 on 2025-01-27 12:58 # Generated by Django 5.0.11 on 2025-01-27 12:58
import uuid import uuid
import pickle # nosec
from django.core import signing
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.utils.timezone import now, timedelta
from authentik.lib.migrations import progress_bar
from authentik.root.middleware import ClientIPMiddleware
SESSION_CACHE_ALIAS = "default"
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps()/loads() and
cache backends.
"""
def __init__(self, protocol=None):
self.protocol = pickle.HIGHEST_PROTOCOL if protocol is None else protocol
def dumps(self, obj):
"""Pickle data to be stored in redis"""
return pickle.dumps(obj, self.protocol)
def loads(self, data):
"""Unpickle data to be loaded from redis"""
return pickle.loads(data) # nosec
def _migrate_session(
apps,
db_alias,
session_key,
session_data,
expires,
):
Session = apps.get_model("authentik_core", "Session")
OldAuthenticatedSession = apps.get_model("authentik_core", "OldAuthenticatedSession")
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
old_auth_session = (
OldAuthenticatedSession.objects.using(db_alias).filter(session_key=session_key).first()
)
args = {
"session_key": session_key,
"expires": expires,
"last_ip": ClientIPMiddleware.default_ip,
"last_user_agent": "",
"session_data": {},
}
for k, v in session_data.items():
if k == "authentik/stages/user_login/last_ip":
args["last_ip"] = v
elif k in ["last_user_agent", "last_used"]:
args[k] = v
elif args in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY]:
pass
else:
args["session_data"][k] = v
if old_auth_session:
args["last_user_agent"] = old_auth_session.last_user_agent
args["last_used"] = old_auth_session.last_used
args["session_data"] = pickle.dumps(args["session_data"])
session = Session.objects.using(db_alias).create(**args)
if old_auth_session:
AuthenticatedSession.objects.using(db_alias).create(
session=session,
user=old_auth_session.user,
)
def migrate_redis_sessions(apps, schema_editor):
from django.core.cache import caches
db_alias = schema_editor.connection.alias
cache = caches[SESSION_CACHE_ALIAS]
# Not a redis cache, skipping
if not hasattr(cache, "keys"):
return
print("\nMigrating Redis sessions to database, this might take a couple of minutes...")
for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()):
_migrate_session(
apps=apps,
db_alias=db_alias,
session_key=key.removeprefix(KEY_PREFIX),
session_data=session_data,
expires=now() + timedelta(seconds=cache.ttl(key)),
)
def migrate_database_sessions(apps, schema_editor):
DjangoSession = apps.get_model("sessions", "Session")
db_alias = schema_editor.connection.alias
print("\nMigration database sessions, this might take a couple of minutes...")
for django_session in progress_bar(DjangoSession.objects.using(db_alias).all()):
session_data = signing.loads(
django_session.session_data,
salt="django.contrib.sessions.SessionStore",
serializer=PickleSerializer,
)
_migrate_session(
apps=apps,
db_alias=db_alias,
session_key=django_session.session_key,
session_data=session_data,
expires=django_session.expire_date,
)
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -227,12 +113,4 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Authenticated Sessions", "verbose_name_plural": "Authenticated Sessions",
}, },
), ),
migrations.RunPython(
code=migrate_redis_sessions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython(
code=migrate_database_sessions,
reverse_code=migrations.RunPython.noop,
),
] ]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.1.9 on 2025-05-14 11:15
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
db_alias = schema_editor.connection.alias
ContentType = apps.get_model("contenttypes", "ContentType")
ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0047_delete_oldauthenticatedsession"),
]
operations = [
migrations.RunPython(
code=remove_old_authenticated_session_content_type,
),
]

View File

@ -21,7 +21,9 @@
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" /> {% for key, value in html_meta.items %}
<meta name="{{key}}" content="{{ value }}" />
{% endfor %}
</head> </head>
<body> <body>
{% block body %} {% block body %}

View File

@ -124,6 +124,16 @@ class TestGroupsAPI(APITestCase):
{"is_superuser": ["User does not have permission to set superuser status to True."]}, {"is_superuser": ["User does not have permission to set superuser status to True."]},
) )
def test_superuser_no_perm_no_superuser(self):
"""Test creating a group without permission and without superuser flag"""
assign_perm("authentik_core.add_group", self.login_user)
self.client.force_login(self.login_user)
res = self.client.post(
reverse("authentik_api:group-list"),
data={"name": generate_id(), "is_superuser": False},
)
self.assertEqual(res.status_code, 201)
def test_superuser_update_no_perm(self): def test_superuser_update_no_perm(self):
"""Test updating a superuser group without permission""" """Test updating a superuser group without permission"""
group = Group.objects.create(name=generate_id(), is_superuser=True) group = Group.objects.create(name=generate_id(), is_superuser=True)

View File

@ -132,13 +132,14 @@ class LicenseKey:
"""Get a summarized version of all (not expired) licenses""" """Get a summarized version of all (not expired) licenses"""
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0) total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in License.objects.all(): for lic in License.objects.all():
total.internal_users += lic.internal_users if lic.is_valid:
total.external_users += lic.external_users total.internal_users += lic.internal_users
total.external_users += lic.external_users
total.license_flags.extend(lic.status.license_flags)
exp_ts = int(mktime(lic.expiry.timetuple())) exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0: if total.exp == 0:
total.exp = exp_ts total.exp = exp_ts
total.exp = max(total.exp, exp_ts) total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags)
return total return total
@staticmethod @staticmethod

View File

@ -39,6 +39,10 @@ class License(SerializerModel):
internal_users = models.BigIntegerField() internal_users = models.BigIntegerField()
external_users = models.BigIntegerField() external_users = models.BigIntegerField()
@property
def is_valid(self) -> bool:
return self.expiry >= now()
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.api import LicenseSerializer from authentik.enterprise.api import LicenseSerializer

View File

@ -8,6 +8,7 @@ from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.enterprise.license import LicenseKey from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import ( from authentik.enterprise.models import (
THRESHOLD_READ_ONLY_WEEKS, THRESHOLD_READ_ONLY_WEEKS,
@ -71,9 +72,9 @@ class TestEnterpriseLicense(TestCase):
) )
def test_valid_multiple(self): def test_valid_multiple(self):
"""Check license verification""" """Check license verification"""
lic = License.objects.create(key=generate_id()) lic = License.objects.create(key=generate_id(), expiry=expiry_valid)
self.assertTrue(lic.status.status().is_valid) self.assertTrue(lic.status.status().is_valid)
lic2 = License.objects.create(key=generate_id()) lic2 = License.objects.create(key=generate_id(), expiry=expiry_valid)
self.assertTrue(lic2.status.status().is_valid) self.assertTrue(lic2.status.status().is_valid)
total = LicenseKey.get_total() total = LicenseKey.get_total()
self.assertEqual(total.internal_users, 200) self.assertEqual(total.internal_users, 200)
@ -232,7 +233,9 @@ class TestEnterpriseLicense(TestCase):
) )
def test_expiry_expired(self): def test_expiry_expired(self):
"""Check license verification""" """Check license verification"""
License.objects.create(key=generate_id()) User.objects.all().delete()
License.objects.all().delete()
License.objects.create(key=generate_id(), expiry=expiry_expired)
self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED) self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED)
@patch( @patch(

View File

@ -7,7 +7,7 @@
{{ block.super }} {{ block.super }}
<link rel="prefetch" href="{{ flow_background_url }}" /> <link rel="prefetch" href="{{ flow_background_url }}" />
{% if flow.compatibility_mode and not inspector %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script> <script>ShadyDOM = { force: true };</script>
{% endif %} {% endif %}
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
<script> <script>

View File

@ -363,6 +363,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True) pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
if not pool_options: if not pool_options:
pool_options = True pool_options = True
# FIXME: Temporarily force pool to be deactivated.
# See https://github.com/goauthentik/authentik/issues/14320
pool_options = False
db = { db = {
"default": { "default": {

View File

@ -17,7 +17,7 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport from sentry_sdk import HttpTransport, get_current_scope
from sentry_sdk import init as sentry_sdk_init from sentry_sdk import init as sentry_sdk_init
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from sentry_sdk.integrations.argv import ArgvIntegration from sentry_sdk.integrations.argv import ArgvIntegration
@ -27,6 +27,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.socket import SocketIntegration from sentry_sdk.integrations.socket import SocketIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
@ -95,6 +96,8 @@ def traces_sampler(sampling_context: dict) -> float:
return 0 return 0
if _type == "websocket": if _type == "websocket":
return 0 return 0
if CONFIG.get_bool("debug"):
return 1
return float(CONFIG.get("error_reporting.sample_rate", 0.1)) return float(CONFIG.get("error_reporting.sample_rate", 0.1))
@ -167,3 +170,14 @@ def before_send(event: dict, hint: dict) -> dict | None:
if settings.DEBUG: if settings.DEBUG:
return None return None
return event return event
def get_http_meta():
"""Get sentry-related meta key-values"""
scope = get_current_scope()
meta = {
SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
}
if bag := scope.get_baggage():
meta[BAGGAGE_HEADER_NAME] = bag.serialize()
return meta

View File

@ -494,86 +494,88 @@ class TestConfig(TestCase):
}, },
) )
def test_db_pool(self): # FIXME: Temporarily force pool to be deactivated.
"""Test DB Config with pool""" # See https://github.com/goauthentik/authentik/issues/14320
config = ConfigLoader() # def test_db_pool(self):
config.set("postgresql.host", "foo") # """Test DB Config with pool"""
config.set("postgresql.name", "foo") # config = ConfigLoader()
config.set("postgresql.user", "foo") # config.set("postgresql.host", "foo")
config.set("postgresql.password", "foo") # config.set("postgresql.name", "foo")
config.set("postgresql.port", "foo") # config.set("postgresql.user", "foo")
config.set("postgresql.test.name", "foo") # config.set("postgresql.password", "foo")
config.set("postgresql.use_pool", True) # config.set("postgresql.port", "foo")
conf = django_db_config(config) # config.set("postgresql.test.name", "foo")
self.assertEqual( # config.set("postgresql.use_pool", True)
conf, # conf = django_db_config(config)
{ # self.assertEqual(
"default": { # conf,
"ENGINE": "authentik.root.db", # {
"HOST": "foo", # "default": {
"NAME": "foo", # "ENGINE": "authentik.root.db",
"OPTIONS": { # "HOST": "foo",
"pool": True, # "NAME": "foo",
"sslcert": None, # "OPTIONS": {
"sslkey": None, # "pool": True,
"sslmode": None, # "sslcert": None,
"sslrootcert": None, # "sslkey": None,
}, # "sslmode": None,
"PASSWORD": "foo", # "sslrootcert": None,
"PORT": "foo", # },
"TEST": {"NAME": "foo"}, # "PASSWORD": "foo",
"USER": "foo", # "PORT": "foo",
"CONN_MAX_AGE": 0, # "TEST": {"NAME": "foo"},
"CONN_HEALTH_CHECKS": False, # "USER": "foo",
"DISABLE_SERVER_SIDE_CURSORS": False, # "CONN_MAX_AGE": 0,
} # "CONN_HEALTH_CHECKS": False,
}, # "DISABLE_SERVER_SIDE_CURSORS": False,
) # }
# },
# )
def test_db_pool_options(self): # def test_db_pool_options(self):
"""Test DB Config with pool""" # """Test DB Config with pool"""
config = ConfigLoader() # config = ConfigLoader()
config.set("postgresql.host", "foo") # config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo") # config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo") # config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo") # config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo") # config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo") # config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True) # config.set("postgresql.use_pool", True)
config.set( # config.set(
"postgresql.pool_options", # "postgresql.pool_options",
base64.b64encode( # base64.b64encode(
dumps( # dumps(
{ # {
"max_size": 15, # "max_size": 15,
} # }
).encode() # ).encode()
).decode(), # ).decode(),
) # )
conf = django_db_config(config) # conf = django_db_config(config)
self.assertEqual( # self.assertEqual(
conf, # conf,
{ # {
"default": { # "default": {
"ENGINE": "authentik.root.db", # "ENGINE": "authentik.root.db",
"HOST": "foo", # "HOST": "foo",
"NAME": "foo", # "NAME": "foo",
"OPTIONS": { # "OPTIONS": {
"pool": { # "pool": {
"max_size": 15, # "max_size": 15,
}, # },
"sslcert": None, # "sslcert": None,
"sslkey": None, # "sslkey": None,
"sslmode": None, # "sslmode": None,
"sslrootcert": None, # "sslrootcert": None,
}, # },
"PASSWORD": "foo", # "PASSWORD": "foo",
"PORT": "foo", # "PORT": "foo",
"TEST": {"NAME": "foo"}, # "TEST": {"NAME": "foo"},
"USER": "foo", # "USER": "foo",
"CONN_MAX_AGE": 0, # "CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False, # "CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False, # "DISABLE_SERVER_SIDE_CURSORS": False,
} # }
}, # },
) # )

View File

@ -317,7 +317,7 @@ class KerberosSource(Source):
usage="accept", name=name, store=self.get_gssapi_store() usage="accept", name=name, store=self.get_gssapi_store()
) )
except gssapi.exceptions.GSSError as exc: except gssapi.exceptions.GSSError as exc:
LOGGER.warn("GSSAPI credentials failure", exc=exc) LOGGER.warning("GSSAPI credentials failure", exc=exc)
return None return None

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2025.4.0 Blueprint schema", "title": "authentik 2025.4.1 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -55,7 +55,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

4
go.mod
View File

@ -5,7 +5,7 @@ go 1.24.0
require ( require (
beryju.io/ldap v0.1.0 beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-oidc/v3 v3.14.1
github.com/getsentry/sentry-go v0.32.0 github.com/getsentry/sentry-go v0.33.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/runtime v0.28.0
@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025040.1 goauthentik.io/api/v3 v3.2025041.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.14.0

8
go.sum
View File

@ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg=
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025040.1 h1:rQEcMNpz84/LPX8LVFteOJuserrd4PnU4k1Iu/wWqhs= goauthentik.io/api/v3 v3.2025041.1 h1:GAN6AoTmfnCGgx1SyM07jP4/LR/T3rkTEyShSBd3Co8=
goauthentik.io/api/v3 v3.2025040.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025041.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2025.4.0" const VERSION = "2025.4.1"

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1014.0", "aws-cdk": "^2.1015.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
}, },
"engines": { "engines": {
@ -17,9 +17,9 @@
} }
}, },
"node_modules/aws-cdk": { "node_modules/aws-cdk": {
"version": "2.1014.0", "version": "2.1015.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1014.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1015.0.tgz",
"integrity": "sha512-es101rtRAClix9BncNL54iW90MiOyRv4iCC5tv/firGDnidS6pPinuK0IIFt0RO6w0+3heRxWBXg8HY+f9877w==", "integrity": "sha512-txd+yMVVybtLfiwT409+fahbP0SkiwhmQvQf6PVVYnWzDPSknxYlUNJHisHV4tJEcbHWn1QPsLmqqMT0bw8hBg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@ -10,7 +10,7 @@
"node": ">=20" "node": ">=20"
}, },
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1014.0", "aws-cdk": "^2.1015.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
} }
} }

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image Description: authentik Docker image
AuthentikVersion: AuthentikVersion:
Type: String Type: String
Default: 2025.4.0 Default: 2025.4.1
Description: authentik Docker image tag Description: authentik Docker image tag
AuthentikServerCPU: AuthentikServerCPU:
Type: Number Type: Number

View File

@ -1,6 +1,6 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.4.0", "version": "2025.4.1",
"private": true, "private": true,
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {

View File

@ -1,104 +1,104 @@
[project] [project]
name = "authentik" name = "authentik"
version = "2025.4.0" version = "2025.4.1"
description = "" description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*" requires-python = "==3.13.*"
dependencies = [ dependencies = [
"argon2-cffi", "argon2-cffi==23.1.0",
"celery", "celery==5.5.2",
"channels", "channels==4.2.2",
"channels-redis", "channels-redis==4.2.1",
"cryptography", "cryptography==44.0.3",
"dacite", "dacite==1.9.2",
"deepmerge", "deepmerge==2.0",
"defusedxml", "defusedxml==0.7.1",
"django", "django==5.1.9",
"django-countries", "django-countries==7.6.1",
"django-cte", "django-cte==1.3.3",
"django-filter", "django-filter==25.1",
"django-guardian", "django-guardian<3.0.0",
"django-model-utils", "django-model-utils==5.0.0",
"django-pglock", "django-pglock==1.7.2",
"django-prometheus", "django-prometheus==2.3.1",
"django-redis", "django-redis==5.4.0",
"django-storages[s3]", "django-storages[s3]==1.14.6",
"django-tenants", "django-tenants==3.7.0",
"djangorestframework", "djangorestframework==3.16.0",
"djangorestframework-guardian", "djangorestframework-guardian==0.3.0",
"docker", "docker==7.1.0",
"drf-orjson-renderer", "drf-orjson-renderer==1.7.3",
"drf-spectacular", "drf-spectacular==0.28.0",
"dumb-init", "dumb-init==1.2.5.post1",
"duo-client", "duo-client==5.5.0",
"fido2", "fido2==1.2.0",
"flower", "flower==2.0.1",
"geoip2", "geoip2==5.1.0",
"geopy", "geopy==2.4.1",
"google-api-python-client", "google-api-python-client==2.169.0",
"gssapi", "gssapi==1.9.0",
"gunicorn", "gunicorn==23.0.0",
"jsonpatch", "jsonpatch==1.33",
"jwcrypto", "jwcrypto==1.5.6",
"kubernetes", "kubernetes==32.0.1",
"ldap3", "ldap3==2.9.1",
"lxml", "lxml==5.4.0",
"msgraph-sdk", "msgraph-sdk==1.30.0",
"opencontainers", "opencontainers==0.0.14",
"packaging", "packaging==25.0",
"paramiko", "paramiko==3.5.1",
"psycopg[c, pool]", "psycopg[c,pool]==3.2.9",
"pydantic", "pydantic==2.11.4",
"pydantic-scim", "pydantic-scim==0.0.8",
"pyjwt", "pyjwt==2.10.1",
"pyrad", "pyrad==2.4",
"python-kadmin-rs", "python-kadmin-rs==0.6.0",
"pyyaml", "pyyaml==6.0.2",
"requests-oauthlib", "requests-oauthlib==2.0.0",
"scim2-filter-parser", "scim2-filter-parser==0.7.0",
"sentry-sdk", "sentry-sdk==2.28.0",
"service_identity", "service-identity==24.2.0",
"setproctitle", "setproctitle==1.3.6",
"structlog", "structlog==25.3.0",
"swagger-spec-validator", "swagger-spec-validator==3.0.4",
"tenant-schemas-celery", "tenant-schemas-celery==4.0.1",
"twilio", "twilio==9.6.1",
"ua-parser", "ua-parser==1.0.1",
"unidecode", "unidecode==1.4.0",
"urllib3 <3", "urllib3<3",
"uvicorn[standard]", "uvicorn[standard]==0.34.2",
"watchdog", "watchdog==6.0.0",
"webauthn", "webauthn==2.5.2",
"wsproto", "wsproto==1.2.0",
"xmlsec", "xmlsec==1.3.15",
"zxcvbn", "zxcvbn==4.5.0",
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"aws-cdk-lib", "aws-cdk-lib==2.188.0",
"bandit", "bandit==1.8.3",
"black", "black==25.1.0",
"bump2version", "bump2version==1.0.1",
"channels[daphne]", "channels[daphne]==4.2.2",
"codespell", "codespell==2.4.1",
"colorama", "colorama==0.4.6",
"constructs", "constructs==10.4.2",
"coverage[toml]", "coverage[toml]==7.8.0",
"debugpy", "debugpy==1.8.14",
"drf-jsonschema-serializer", "drf-jsonschema-serializer==3.0.0",
"freezegun", "freezegun==1.5.1",
"importlib-metadata", "importlib-metadata==8.6.1",
"k5test", "k5test==0.10.4",
"pdoc", "pdoc==15.0.3",
"pytest", "pytest==8.3.5",
"pytest-django", "pytest-django==4.11.1",
"pytest-github-actions-annotate-failures", "pytest-github-actions-annotate-failures==0.3.0",
"pytest-randomly", "pytest-randomly==3.16.0",
"pytest-timeout", "pytest-timeout==2.4.0",
"requests-mock", "requests-mock==1.12.1",
"ruff", "ruff==0.11.9",
"selenium", "selenium==4.32.0",
] ]
[tool.uv] [tool.uv]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2025.4.0 version: 2025.4.1
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io

View File

@ -1,12 +1,19 @@
"""test default login flow""" """test default login flow"""
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.flows.models import Flow
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsLogin(SeleniumTestCase): class TestFlowsLogin(SeleniumTestCase):
"""test default login flow""" """test default login flow"""
def tearDown(self):
# Reset authentication flow's compatibility mode; we need to do this as its
# not specified in the blueprint
Flow.objects.filter(slug="default-authentication-flow").update(compatibility_mode=False)
return super().tearDown()
@retry() @retry()
@apply_blueprint( @apply_blueprint(
"default/flow-default-authentication-flow.yaml", "default/flow-default-authentication-flow.yaml",
@ -23,3 +30,21 @@ class TestFlowsLogin(SeleniumTestCase):
self.login() self.login()
self.wait_for_url(self.if_user_url("/library")) self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user) self.assert_user(self.user)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
def test_login_compatibility_mode(self):
"""test default login flow with compatibility mode enabled"""
Flow.objects.filter(slug="default-authentication-flow").update(compatibility_mode=True)
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
)
)
self.login(shadow_dom=False)
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -0,0 +1,51 @@
"""test default login (using SFE interface) flow"""
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from authentik.blueprints.tests import apply_blueprint
from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsLoginSFE(SeleniumTestCase):
"""test default login flow"""
def login(self):
"""Do entire login flow adjusted for SFE"""
flow_executor = self.driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
self.user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username
)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
def test_login(self):
"""test default login flow"""
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
query={"sfe": True},
)
)
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -29,6 +29,7 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.command import Command from selenium.webdriver.remote.command import Command
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -37,8 +38,8 @@ from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
RETRIES = int(environ.get("RETRIES", "3"))
IS_CI = "CI" in environ IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
def get_docker_tag() -> str: def get_docker_tag() -> str:
@ -240,10 +241,30 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root) element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root)
return element return element
def login(self): def shady_dom(self) -> WebElement:
"""Do entire login flow and check user afterwards""" class wrapper:
flow_executor = self.get_shadow_root("ak-flow-executor") def __init__(self, container: WebDriver):
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor) self.container = container
def find_element(self, by: str, selector: str) -> WebElement:
return self.container.execute_script(
"return document.__shady_native_querySelector(arguments[0])", selector
)
return wrapper(self.driver)
def login(self, shadow_dom=True):
"""Do entire login flow"""
if shadow_dom:
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
else:
flow_executor = self.shady_dom()
identification_stage = self.shady_dom()
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=uidField]")))
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").click() identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys( identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys(
@ -253,8 +274,16 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
Keys.ENTER Keys.ENTER
) )
flow_executor = self.get_shadow_root("ak-flow-executor") if shadow_dom:
password_stage = self.get_shadow_root("ak-stage-password", flow_executor) flow_executor = self.get_shadow_root("ak-flow-executor")
password_stage = self.get_shadow_root("ak-stage-password", flow_executor)
else:
flow_executor = self.shady_dom()
password_stage = self.shady_dom()
wait = WebDriverWait(password_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=password]")))
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys( password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username self.user.username
) )

321
uv.lock generated
View File

@ -164,7 +164,7 @@ wheels = [
[[package]] [[package]]
name = "authentik" name = "authentik"
version = "2025.4.0" version = "2025.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "argon2-cffi" }, { name = "argon2-cffi" },
@ -265,100 +265,100 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "argon2-cffi" }, { name = "argon2-cffi", specifier = "==23.1.0" },
{ name = "celery" }, { name = "celery", specifier = "==5.5.2" },
{ name = "channels" }, { name = "channels", specifier = "==4.2.2" },
{ name = "channels-redis" }, { name = "channels-redis", specifier = "==4.2.1" },
{ name = "cryptography" }, { name = "cryptography", specifier = "==44.0.3" },
{ name = "dacite" }, { name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge" }, { name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml" }, { name = "defusedxml", specifier = "==0.7.1" },
{ name = "django" }, { name = "django", specifier = "==5.1.9" },
{ name = "django-countries" }, { name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte" }, { name = "django-cte", specifier = "==1.3.3" },
{ name = "django-filter" }, { name = "django-filter", specifier = "==25.1" },
{ name = "django-guardian" }, { name = "django-guardian", specifier = "<3.0.0" },
{ name = "django-model-utils" }, { name = "django-model-utils", specifier = "==5.0.0" },
{ name = "django-pglock" }, { name = "django-pglock", specifier = "==1.7.2" },
{ name = "django-prometheus" }, { name = "django-prometheus", specifier = "==2.3.1" },
{ name = "django-redis" }, { name = "django-redis", specifier = "==5.4.0" },
{ name = "django-storages", extras = ["s3"] }, { name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ name = "django-tenants", git = "https://github.com/rissson/django-tenants.git?branch=authentik-fixes" }, { name = "django-tenants", git = "https://github.com/rissson/django-tenants.git?branch=authentik-fixes" },
{ name = "djangorestframework", git = "https://github.com/authentik-community/django-rest-framework?rev=896722bab969fabc74a08b827da59409cf9f1a4e" }, { name = "djangorestframework", git = "https://github.com/authentik-community/django-rest-framework?rev=896722bab969fabc74a08b827da59409cf9f1a4e" },
{ name = "djangorestframework-guardian" }, { name = "djangorestframework-guardian", specifier = "==0.3.0" },
{ name = "docker" }, { name = "docker", specifier = "==7.1.0" },
{ name = "drf-orjson-renderer" }, { name = "drf-orjson-renderer", specifier = "==1.7.3" },
{ name = "drf-spectacular" }, { name = "drf-spectacular", specifier = "==0.28.0" },
{ name = "dumb-init" }, { name = "dumb-init", specifier = "==1.2.5.post1" },
{ name = "duo-client" }, { name = "duo-client", specifier = "==5.5.0" },
{ name = "fido2" }, { name = "fido2", specifier = "==1.2.0" },
{ name = "flower" }, { name = "flower", specifier = "==2.0.1" },
{ name = "geoip2" }, { name = "geoip2", specifier = "==5.1.0" },
{ name = "geopy" }, { name = "geopy", specifier = "==2.4.1" },
{ name = "google-api-python-client" }, { name = "google-api-python-client", specifier = "==2.169.0" },
{ name = "gssapi" }, { name = "gssapi", specifier = "==1.9.0" },
{ name = "gunicorn" }, { name = "gunicorn", specifier = "==23.0.0" },
{ name = "jsonpatch" }, { name = "jsonpatch", specifier = "==1.33" },
{ name = "jwcrypto" }, { name = "jwcrypto", specifier = "==1.5.6" },
{ name = "kubernetes" }, { name = "kubernetes", specifier = "==32.0.1" },
{ name = "ldap3" }, { name = "ldap3", specifier = "==2.9.1" },
{ name = "lxml" }, { name = "lxml", specifier = "==5.4.0" },
{ name = "msgraph-sdk" }, { name = "msgraph-sdk", specifier = "==1.30.0" },
{ name = "opencontainers", git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8" }, { name = "opencontainers", git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8" },
{ name = "packaging" }, { name = "packaging", specifier = "==25.0" },
{ name = "paramiko" }, { name = "paramiko", specifier = "==3.5.1" },
{ name = "psycopg", extras = ["c", "pool"] }, { name = "psycopg", extras = ["c", "pool"], specifier = "==3.2.9" },
{ name = "pydantic" }, { name = "pydantic", specifier = "==2.11.4" },
{ name = "pydantic-scim" }, { name = "pydantic-scim", specifier = "==0.0.8" },
{ name = "pyjwt" }, { name = "pyjwt", specifier = "==2.10.1" },
{ name = "pyrad" }, { name = "pyrad", specifier = "==2.4" },
{ name = "python-kadmin-rs" }, { name = "python-kadmin-rs", specifier = "==0.6.0" },
{ name = "pyyaml" }, { name = "pyyaml", specifier = "==6.0.2" },
{ name = "requests-oauthlib" }, { name = "requests-oauthlib", specifier = "==2.0.0" },
{ name = "scim2-filter-parser" }, { name = "scim2-filter-parser", specifier = "==0.7.0" },
{ name = "sentry-sdk" }, { name = "sentry-sdk", specifier = "==2.28.0" },
{ name = "service-identity" }, { name = "service-identity", specifier = "==24.2.0" },
{ name = "setproctitle" }, { name = "setproctitle", specifier = "==1.3.6" },
{ name = "structlog" }, { name = "structlog", specifier = "==25.3.0" },
{ name = "swagger-spec-validator" }, { name = "swagger-spec-validator", specifier = "==3.0.4" },
{ name = "tenant-schemas-celery" }, { name = "tenant-schemas-celery", specifier = "==4.0.1" },
{ name = "twilio" }, { name = "twilio", specifier = "==9.6.1" },
{ name = "ua-parser" }, { name = "ua-parser", specifier = "==1.0.1" },
{ name = "unidecode" }, { name = "unidecode", specifier = "==1.4.0" },
{ name = "urllib3", specifier = "<3" }, { name = "urllib3", specifier = "<3" },
{ name = "uvicorn", extras = ["standard"] }, { name = "uvicorn", extras = ["standard"], specifier = "==0.34.2" },
{ name = "watchdog" }, { name = "watchdog", specifier = "==6.0.0" },
{ name = "webauthn" }, { name = "webauthn", specifier = "==2.5.2" },
{ name = "wsproto" }, { name = "wsproto", specifier = "==1.2.0" },
{ name = "xmlsec" }, { name = "xmlsec", specifier = "==1.3.15" },
{ name = "zxcvbn" }, { name = "zxcvbn", specifier = "==4.5.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "aws-cdk-lib" }, { name = "aws-cdk-lib", specifier = "==2.188.0" },
{ name = "bandit" }, { name = "bandit", specifier = "==1.8.3" },
{ name = "black" }, { name = "black", specifier = "==25.1.0" },
{ name = "bump2version" }, { name = "bump2version", specifier = "==1.0.1" },
{ name = "channels", extras = ["daphne"] }, { name = "channels", extras = ["daphne"], specifier = "==4.2.2" },
{ name = "codespell" }, { name = "codespell", specifier = "==2.4.1" },
{ name = "colorama" }, { name = "colorama", specifier = "==0.4.6" },
{ name = "constructs" }, { name = "constructs", specifier = "==10.4.2" },
{ name = "coverage", extras = ["toml"] }, { name = "coverage", extras = ["toml"], specifier = "==7.8.0" },
{ name = "debugpy" }, { name = "debugpy", specifier = "==1.8.14" },
{ name = "drf-jsonschema-serializer" }, { name = "drf-jsonschema-serializer", specifier = "==3.0.0" },
{ name = "freezegun" }, { name = "freezegun", specifier = "==1.5.1" },
{ name = "importlib-metadata" }, { name = "importlib-metadata", specifier = "==8.6.1" },
{ name = "k5test" }, { name = "k5test", specifier = "==0.10.4" },
{ name = "pdoc" }, { name = "pdoc", specifier = "==15.0.3" },
{ name = "pytest" }, { name = "pytest", specifier = "==8.3.5" },
{ name = "pytest-django" }, { name = "pytest-django", specifier = "==4.11.1" },
{ name = "pytest-github-actions-annotate-failures" }, { name = "pytest-github-actions-annotate-failures", specifier = "==0.3.0" },
{ name = "pytest-randomly" }, { name = "pytest-randomly", specifier = "==3.16.0" },
{ name = "pytest-timeout" }, { name = "pytest-timeout", specifier = "==2.4.0" },
{ name = "requests-mock" }, { name = "requests-mock", specifier = "==1.12.1" },
{ name = "ruff" }, { name = "ruff", specifier = "==0.11.9" },
{ name = "selenium" }, { name = "selenium", specifier = "==4.32.0" },
] ]
[[package]] [[package]]
@ -387,16 +387,16 @@ wheels = [
[[package]] [[package]]
name = "aws-cdk-asset-awscli-v1" name = "aws-cdk-asset-awscli-v1"
version = "2.2.231" version = "2.2.235"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jsii" }, { name = "jsii" },
{ name = "publication" }, { name = "publication" },
{ name = "typeguard" }, { name = "typeguard" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/01/b2/4a142d1d8093691c1b54b7b35f463f6defa1d0a8a08b7be2277eae73c726/aws_cdk_asset_awscli_v1-2.2.231.tar.gz", hash = "sha256:859d99e0fcdc2f6ada44090ad9f921743da3ca3a6d9f39ab06836d4c8e0fbc23", size = 17960944, upload-time = "2025-04-07T16:48:17.423Z" } sdist = { url = "https://files.pythonhosted.org/packages/20/e8/6706ee98e9ba436aa07ca3a65d79cf40c50005f4f760f139bec0f6c3606a/aws_cdk_asset_awscli_v1-2.2.235.tar.gz", hash = "sha256:0a2023f9d32158ae86d43dfeac2ba7679e8a050cb99b7565b26192e60e57a91c", size = 19130124, upload-time = "2025-05-05T15:24:02.938Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/2d/dae06874ab3a66ad898d9c2d792c863b8b8249b203a1d8e3b36dfca44a93/aws_cdk_asset_awscli_v1-2.2.231-py3-none-any.whl", hash = "sha256:06d6b1d9e52272c315b944320f7039b47c6a6058f063fa33ab0ec06fea17bfbe", size = 17959325, upload-time = "2025-04-07T16:48:14.477Z" }, { url = "https://files.pythonhosted.org/packages/97/27/b167173d7fb784848563d596085dc8e95cabbe7b01f8a5c0ac1ed6a80c36/aws_cdk_asset_awscli_v1-2.2.235-py3-none-any.whl", hash = "sha256:701a47a97419b917ce73cf9c922a26c2895943b4b18b191e1285572b8584ae1e", size = 19128489, upload-time = "2025-05-05T15:23:59.87Z" },
] ]
[[package]] [[package]]
@ -571,30 +571,30 @@ wheels = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.38.12" version = "1.38.13"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "botocore" }, { name = "botocore" },
{ name = "jmespath" }, { name = "jmespath" },
{ name = "s3transfer" }, { name = "s3transfer" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/73/14f9b57b764d9a8d998a4127bdc1f35adfb9d625f0cbe8814eb0d6bd6ff2/boto3-1.38.12.tar.gz", hash = "sha256:ca06315fdb20821fc1084a7b08557556eed97cb917a30ff19d8524b495383889", size = 111823, upload-time = "2025-05-08T19:28:07.83Z" } sdist = { url = "https://files.pythonhosted.org/packages/c7/89/a47f62b3f81a2e3484d2a2b8dd4906c5b6e57da0af0bd59d36f99ba20baf/boto3-1.38.13.tar.gz", hash = "sha256:6633bce2b73284acce1453ca85834c7c5a59e0dbcce1170be461cc079bdcdfcf", size = 111812, upload-time = "2025-05-09T19:33:02.962Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/49/eca817a49ce08394cf2bc710d077e130f7553957991da9b6feff2a7ac19a/boto3-1.38.12-py3-none-any.whl", hash = "sha256:9939b65b0bf04781f531245f110dd0ada6825f06cf9b95350efb830b9f69d214", size = 139936, upload-time = "2025-05-08T19:28:04.895Z" }, { url = "https://files.pythonhosted.org/packages/72/25/79e219648f10d060d152542fcf3be0093120471774b99c1a7f41ceaeca9b/boto3-1.38.13-py3-none-any.whl", hash = "sha256:668400d13889d2d2fcd66ce785cc0b0fc040681f58a9c7f67daa9149a52b6c63", size = 139934, upload-time = "2025-05-09T19:33:00.855Z" },
] ]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.38.12" version = "1.38.13"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jmespath" }, { name = "jmespath" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/41/5a/37274d8510d4ad86bc8349e716d62c6b95c20e48403de3b34bc53cd7708c/botocore-1.38.12.tar.gz", hash = "sha256:86c459de3e39b418f4eb81e88c23fba02995496141db73816e7f65cb8b04408b", size = 13883975, upload-time = "2025-05-08T19:27:53.822Z" } sdist = { url = "https://files.pythonhosted.org/packages/de/36/5b0faba074684744244e1e030e73fd5612bc2c38f557eec0a7f1a3d7ddd2/botocore-1.38.13.tar.gz", hash = "sha256:22feee15753cd3f9f7179d041604078a1024701497d27b22be7c6707e8d13ccb", size = 13882010, upload-time = "2025-05-09T19:32:51.172Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/6c/0d519204c2d7fe715f589878cacc3bc5265ddcff10a2bd95f159419b9ebd/botocore-1.38.12-py3-none-any.whl", hash = "sha256:bcea44f3fe3a5bc18030656b8d32013d8b2d76b54433f591500a14bcac2e94ee", size = 13544024, upload-time = "2025-05-08T19:27:48.228Z" }, { url = "https://files.pythonhosted.org/packages/94/df/a7a8097471d5a3bc7d408850222292d874ffc190aef7e1cacf9af770339e/botocore-1.38.13-py3-none-any.whl", hash = "sha256:de29fee43a1f02787fb5b3756ec09917d5661ed95b2b2d64797ab04196f69e14", size = 13544507, upload-time = "2025-05-09T19:32:37.727Z" },
] ]
[[package]] [[package]]
@ -750,14 +750,14 @@ wheels = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" },
] ]
[[package]] [[package]]
@ -979,16 +979,16 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.8" version = "5.1.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
{ name = "sqlparse" }, { name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/00/40/45adc1b93435d1b418654a734b68351bb6ce0a0e5e37b2f0e9aeb1a2e233/Django-5.1.8.tar.gz", hash = "sha256:42e92a1dd2810072bcc40a39a212b693f94406d0ba0749e68eb642f31dc770b4", size = 10723602, upload-time = "2025-04-02T11:19:56.028Z" } sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/0d/e6dd0ed898b920fec35c6eeeb9acbeb831fff19ad21c5e684744df1d4a36/Django-5.1.8-py3-none-any.whl", hash = "sha256:11b28fa4b00e59d0def004e9ee012fefbb1065a5beb39ee838983fd24493ad4f", size = 8277130, upload-time = "2025-04-02T11:19:51.591Z" }, { url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" },
] ]
[[package]] [[package]]
@ -1063,15 +1063,15 @@ wheels = [
[[package]] [[package]]
name = "django-pglock" name = "django-pglock"
version = "1.7.1" version = "1.7.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "django-pgactivity" }, { name = "django-pgactivity" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ca/9f/3b4b2f7021b626b3981646254f04fcc2db681d7ba7b24be563552368be70/django_pglock-1.7.1.tar.gz", hash = "sha256:69050bdb522fd34585d49bb8a4798dbfbab9ec4754dd1927b1b9eef2ec0edadf", size = 16907, upload-time = "2024-12-16T01:53:47.29Z" } sdist = { url = "https://files.pythonhosted.org/packages/9e/1a/6c552b75d0a6b380215c919a667072e4949a3123f275506c6e6ff82f6b76/django_pglock-1.7.2.tar.gz", hash = "sha256:d1d8521b382a5819e8d14978d0e8e63ab2763cb784f5a6bfdbe5de807da4a61a", size = 17287, upload-time = "2025-05-15T22:07:23.744Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/bf/6fc72033801279b4ae2003c5b93cc22dfe0814ca1f56432f7cd06975381d/django_pglock-1.7.1-py3-none-any.whl", hash = "sha256:15db418fb56bee37fc8707038495b5085af9b8c203ebfa300202572127bdb3f0", size = 17343, upload-time = "2024-12-16T01:53:45.243Z" }, { url = "https://files.pythonhosted.org/packages/fb/d2/21f19531945f03021460d40654bc2fc3b0c474b57b279d5f5a1c34be7f1b/django_pglock-1.7.2-py3-none-any.whl", hash = "sha256:2f9335527779445fe86507b37e26cfde485a32b91d982a8f80039d3bcd25d596", size = 17674, upload-time = "2025-05-15T22:07:22.618Z" },
] ]
[[package]] [[package]]
@ -2065,7 +2065,7 @@ wheels = [
[[package]] [[package]]
name = "msgraph-sdk" name = "msgraph-sdk"
version = "1.28.0" version = "1.30.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "azure-identity" }, { name = "azure-identity" },
@ -2075,9 +2075,9 @@ dependencies = [
{ name = "microsoft-kiota-serialization-text" }, { name = "microsoft-kiota-serialization-text" },
{ name = "msgraph-core" }, { name = "msgraph-core" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b9/41/40bb3c630ca026182aefd79a9862ef4a1917b1161c83690c858d714788f5/msgraph_sdk-1.28.0.tar.gz", hash = "sha256:b2d64b7bd711ad285fc2c090dd524853a026848732e1c83874fe34561805350d", size = 6121069, upload-time = "2025-04-15T11:39:08.184Z" } sdist = { url = "https://files.pythonhosted.org/packages/e9/4a/4ff19671f6ea06f98fb2405f73a90350e4719ccc692e85e9e0c2fa066826/msgraph_sdk-1.30.0.tar.gz", hash = "sha256:59e30af6d7244c9009146d620c331e169701b651317746b16f561e2e2452e73f", size = 6608744, upload-time = "2025-05-13T13:09:12.594Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/58/d8e9593ea81779d503831b5b06c8d9881d5affefe3df99ca20112c969e6f/msgraph_sdk-1.28.0-py3-none-any.whl", hash = "sha256:bd33b186371dfa8ed6375dfda92eef0931485633e69b06c001ce3c2fd3658f18", size = 25091309, upload-time = "2025-04-15T11:39:04.968Z" }, { url = "https://files.pythonhosted.org/packages/70/95/451ec4db8a924274a1f7260809ea03fe9c2b446d84dc5238e92e49a1b522/msgraph_sdk-1.30.0-py3-none-any.whl", hash = "sha256:6748f5cdb5ddbcff9e4f3fb073dd0a604cb00e1cf285dd0fea6969c93ba8282f", size = 27140767, upload-time = "2025-05-13T13:09:07.718Z" },
] ]
[[package]] [[package]]
@ -2157,42 +2157,42 @@ source = { git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd679573
[[package]] [[package]]
name = "opentelemetry-api" name = "opentelemetry-api"
version = "1.32.1" version = "1.33.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "deprecated" }, { name = "deprecated" },
{ name = "importlib-metadata" }, { name = "importlib-metadata" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/40/2359245cd33641c2736a0136a50813352d72f3fc209de28fb226950db4a1/opentelemetry_api-1.32.1.tar.gz", hash = "sha256:a5be71591694a4d9195caf6776b055aa702e964d961051a0715d05f8632c32fb", size = 64138, upload-time = "2025-04-15T16:02:13.97Z" } sdist = { url = "https://files.pythonhosted.org/packages/70/ca/920a73b4a11cd271ba1c62f34dba27d7783996a6a7ac0bac7c83b230736d/opentelemetry_api-1.33.0.tar.gz", hash = "sha256:cc4380fd2e6da7dcb52a828ea81844ed1f4f2eb638ca3c816775109d93d58ced", size = 65000, upload-time = "2025-05-09T14:56:00.967Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl", hash = "sha256:bbd19f14ab9f15f0e85e43e6a958aa4cb1f36870ee62b7fd205783a112012724", size = 65287, upload-time = "2025-04-15T16:01:49.747Z" }, { url = "https://files.pythonhosted.org/packages/e6/c4/26c7ec8e51c19632f42503dbabed286c261fb06f8f61ffd348690e36958a/opentelemetry_api-1.33.0-py3-none-any.whl", hash = "sha256:158df154f628e6615b65fdf6e59f99afabea7213e72c5809dd4adf06c0d997cd", size = 65772, upload-time = "2025-05-09T14:55:38.395Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-sdk" name = "opentelemetry-sdk"
version = "1.32.1" version = "1.33.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a3/65/2069caef9257fae234ca0040d945c741aa7afbd83a7298ee70fc0bc6b6f4/opentelemetry_sdk-1.32.1.tar.gz", hash = "sha256:8ef373d490961848f525255a42b193430a0637e064dd132fd2a014d94792a092", size = 161044, upload-time = "2025-04-15T16:02:28.905Z" } sdist = { url = "https://files.pythonhosted.org/packages/37/0a/b7ae406175a2798a767e12db223e842911d9c398eea100c41c989afd2aa8/opentelemetry_sdk-1.33.0.tar.gz", hash = "sha256:a7fc56d1e07b218fcc316b24d21b59d3f1967b2ca22c217b05da3a26b797cc68", size = 161381, upload-time = "2025-05-09T14:56:12.347Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/00/d3976cdcb98027aaf16f1e980e54935eb820872792f0eaedd4fd7abb5964/opentelemetry_sdk-1.32.1-py3-none-any.whl", hash = "sha256:bba37b70a08038613247bc42beee5a81b0ddca422c7d7f1b097b32bf1c7e2f17", size = 118989, upload-time = "2025-04-15T16:02:08.814Z" }, { url = "https://files.pythonhosted.org/packages/b4/34/831f5d9ae9375c9ba2446cb3cc0be79d8d73b78f813c9567e1615c2624f6/opentelemetry_sdk-1.33.0-py3-none-any.whl", hash = "sha256:bed376b6d37fbf00688bb65edfee817dd01d48b8559212831437529a6066049a", size = 118861, upload-time = "2025-05-09T14:55:56.956Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-semantic-conventions" name = "opentelemetry-semantic-conventions"
version = "0.53b1" version = "0.54b0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "deprecated" }, { name = "deprecated" },
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5e/b6/3c56e22e9b51bcb89edab30d54830958f049760bbd9ab0a759cece7bca88/opentelemetry_semantic_conventions-0.53b1.tar.gz", hash = "sha256:4c5a6fede9de61211b2e9fc1e02e8acacce882204cd770177342b6a3be682992", size = 114350, upload-time = "2025-04-15T16:02:29.793Z" } sdist = { url = "https://files.pythonhosted.org/packages/92/8c/bc970d1599ff40b7913c953a95195addf11c81a27cc85d5ed568e9f8c57f/opentelemetry_semantic_conventions-0.54b0.tar.gz", hash = "sha256:467b739977bdcb079af1af69f73632535cdb51099d5e3c5709a35d10fe02a9c9", size = 118646, upload-time = "2025-05-09T14:56:13.596Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/6b/a8fb94760ef8da5ec283e488eb43235eac3ae7514385a51b6accf881e671/opentelemetry_semantic_conventions-0.53b1-py3-none-any.whl", hash = "sha256:21df3ed13f035f8f3ea42d07cbebae37020367a53b47f1ebee3b10a381a00208", size = 188443, upload-time = "2025-04-15T16:02:10.095Z" }, { url = "https://files.pythonhosted.org/packages/c8/aa/f7c46c19aee189e0123ef7209eaafc417e242b2073485dfb40523d6d8612/opentelemetry_semantic_conventions-0.54b0-py3-none-any.whl", hash = "sha256:fad7c1cf8908fd449eb5cf9fbbeefb301acf4bc995101f85277899cec125d823", size = 194937, upload-time = "2025-05-09T14:55:58.562Z" },
] ]
[[package]] [[package]]
@ -2290,11 +2290,11 @@ wheels = [
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.7" version = "4.3.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
] ]
[[package]] [[package]]
@ -2396,14 +2396,14 @@ wheels = [
[[package]] [[package]]
name = "psycopg" name = "psycopg"
version = "3.2.7" version = "3.2.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/fe/16/ca27b38762a630b70243f51eb6a728f903a17cddc4961626fa540577aba6/psycopg-3.2.7.tar.gz", hash = "sha256:9afa609c7ebf139827a38c0bf61be9c024a3ed743f56443de9d38e1efc260bf3", size = 157238, upload-time = "2025-04-30T13:05:22.867Z" } sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload-time = "2025-05-13T16:11:15.533Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/eb/6e32d259437125a17b0bc2624e06c86149c618501da1dcbc8539b2684f6f/psycopg-3.2.7-py3-none-any.whl", hash = "sha256:d39747d2d5b9658b69fa462ad21d31f1ba4a5722ad1d0cb952552bc0b4125451", size = 200028, upload-time = "2025-04-30T12:59:32.435Z" }, { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -2416,9 +2416,9 @@ pool = [
[[package]] [[package]]
name = "psycopg-c" name = "psycopg-c"
version = "3.2.7" version = "3.2.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/13/74e41e5195e6a0a02b9f1e3560bc714021b725e89a40f5879df58d4189c6/psycopg_c-3.2.7.tar.gz", hash = "sha256:14455cf71ed29fdfa725c550f8c58056a852bb27b55eb59e3a0f127ca92751a3", size = 609707, upload-time = "2025-04-30T13:05:24.834Z" } sdist = { url = "https://files.pythonhosted.org/packages/83/7f/6147cb842081b0b32692bf5a0fdf58e9ac95418ebac1184d4431ec44b85f/psycopg_c-3.2.9.tar.gz", hash = "sha256:8c9f654f20c6c56bddc4543a3caab236741ee94b6732ab7090b95605502210e2", size = 609538, upload-time = "2025-05-13T16:11:19.856Z" }
[[package]] [[package]]
name = "psycopg-pool" name = "psycopg-pool"
@ -2874,27 +2874,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.11.8" version = "0.11.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399, upload-time = "2025-05-01T14:53:24.459Z" } sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134, upload-time = "2025-05-09T16:19:41.511Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473, upload-time = "2025-05-01T14:52:37.252Z" }, { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453, upload-time = "2025-05-09T16:18:58.2Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862, upload-time = "2025-05-01T14:52:41.022Z" }, { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566, upload-time = "2025-05-09T16:19:01.432Z" },
{ url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273, upload-time = "2025-05-01T14:52:43.551Z" }, { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020, upload-time = "2025-05-09T16:19:03.897Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330, upload-time = "2025-05-01T14:52:45.48Z" }, { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935, upload-time = "2025-05-09T16:19:06.455Z" },
{ url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223, upload-time = "2025-05-01T14:52:47.675Z" }, { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971, upload-time = "2025-05-09T16:19:10.261Z" },
{ url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353, upload-time = "2025-05-01T14:52:50.264Z" }, { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631, upload-time = "2025-05-09T16:19:12.307Z" },
{ url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936, upload-time = "2025-05-01T14:52:52.394Z" }, { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236, upload-time = "2025-05-09T16:19:15.006Z" },
{ url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083, upload-time = "2025-05-01T14:52:55.424Z" }, { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436, upload-time = "2025-05-09T16:19:17.063Z" },
{ url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834, upload-time = "2025-05-01T14:52:58.056Z" }, { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759, upload-time = "2025-05-09T16:19:19.693Z" },
{ url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713, upload-time = "2025-05-01T14:53:01.244Z" }, { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985, upload-time = "2025-05-09T16:19:21.831Z" },
{ url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182, upload-time = "2025-05-01T14:53:03.726Z" }, { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775, upload-time = "2025-05-09T16:19:24.401Z" },
{ url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027, upload-time = "2025-05-01T14:53:06.555Z" }, { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957, upload-time = "2025-05-09T16:19:27.08Z" },
{ url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298, upload-time = "2025-05-01T14:53:08.825Z" }, { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307, upload-time = "2025-05-09T16:19:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884, upload-time = "2025-05-01T14:53:11.626Z" }, { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026, upload-time = "2025-05-09T16:19:31.569Z" },
{ url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102, upload-time = "2025-05-01T14:53:14.303Z" }, { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627, upload-time = "2025-05-09T16:19:33.657Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410, upload-time = "2025-05-01T14:53:16.571Z" }, { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340, upload-time = "2025-05-09T16:19:35.815Z" },
{ url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129, upload-time = "2025-05-01T14:53:22.27Z" }, { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" },
] ]
[[package]] [[package]]
@ -2940,15 +2940,15 @@ wheels = [
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.27.0" version = "2.28.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/cf/b6/a92ae6fa6d7e6e536bc586776b1669b84fb724dfe21b8ff08297f2d7c969/sentry_sdk-2.27.0.tar.gz", hash = "sha256:90f4f883f9eff294aff59af3d58c2d1b64e3927b28d5ada2b9b41f5aeda47daf", size = 323556, upload-time = "2025-04-24T10:09:37.927Z" } sdist = { url = "https://files.pythonhosted.org/packages/5e/bb/6a41b2e0e9121bed4d2ec68d50568ab95c49f4744156a9bbb789c866c66d/sentry_sdk-2.28.0.tar.gz", hash = "sha256:14d2b73bc93afaf2a9412490329099e6217761cbab13b6ee8bc0e82927e1504e", size = 325052, upload-time = "2025-05-12T07:53:12.785Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/8b/fb496a45854e37930b57564a20fb8e90dd0f8b6add0491527c00f2163b00/sentry_sdk-2.27.0-py2.py3-none-any.whl", hash = "sha256:c58935bfff8af6a0856d37e8adebdbc7b3281c2b632ec823ef03cd108d216ff0", size = 340786, upload-time = "2025-04-24T10:09:35.897Z" }, { url = "https://files.pythonhosted.org/packages/9b/4e/b1575833094c088dfdef63fbca794518860fcbc8002aadf51ebe8b6a387f/sentry_sdk-2.28.0-py2.py3-none-any.whl", hash = "sha256:51496e6cb3cb625b99c8e08907c67a9112360259b0ef08470e532c3ab184a232", size = 341693, upload-time = "2025-05-12T07:53:10.882Z" },
] ]
[[package]] [[package]]
@ -3000,11 +3000,11 @@ wheels = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.3.1" version = "80.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/70/dc/3976b322de9d2e87ed0007cf04cc7553969b6c7b3f48a565d0333748fbcd/setuptools-80.3.1.tar.gz", hash = "sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927", size = 1315082, upload-time = "2025-05-04T18:47:04.397Z" } sdist = { url = "https://files.pythonhosted.org/packages/95/32/0cc40fe41fd2adb80a2f388987f4f8db3c866c69e33e0b4c8b093fdf700e/setuptools-80.4.0.tar.gz", hash = "sha256:5a78f61820bc088c8e4add52932ae6b8cf423da2aff268c23f813cfbb13b4006", size = 1315008, upload-time = "2025-05-09T20:42:27.972Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/7e/5d8af3317ddbf9519b687bd1c39d8737fde07d97f54df65553faca5cffb1/setuptools-80.3.1-py3-none-any.whl", hash = "sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537", size = 1201172, upload-time = "2025-05-04T18:47:02.575Z" }, { url = "https://files.pythonhosted.org/packages/b1/93/dba5ed08c2e31ec7cdc2ce75705a484ef0be1a2fecac8a58272489349de8/setuptools-80.4.0-py3-none-any.whl", hash = "sha256:6cdc8cb9a7d590b237dbe4493614a9b75d0559b888047c1f67d49ba50fc3edb2", size = 1200812, upload-time = "2025-05-09T20:42:25.325Z" },
] ]
[[package]] [[package]]
@ -3099,14 +3099,14 @@ wheels = [
[[package]] [[package]]
name = "tenant-schemas-celery" name = "tenant-schemas-celery"
version = "3.0.0" version = "4.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d0/fe/cfe19eb7cc3ad8e39d7df7b7c44414bf665b6ac6660c998eb498f89d16c6/tenant_schemas_celery-3.0.0.tar.gz", hash = "sha256:6be3ae1a5826f262f0f3dd343c6a85a34a1c59b89e04ae37de018f36562fed55", size = 15954, upload-time = "2024-05-19T11:16:41.837Z" } sdist = { url = "https://files.pythonhosted.org/packages/19/f8/cf055bf171b5d83d6fe96f1840fba90d3d274be2b5c35cd21b873302b128/tenant_schemas_celery-4.0.1.tar.gz", hash = "sha256:8b8f055fcd82aa53274c09faf88653a935241518d93b86ab2d43a3df3b70c7f8", size = 18870, upload-time = "2025-04-22T18:23:51.061Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/2c/376e1e641ad08b374c75d896468a7be2e6906ce3621fd0c9f9dc09ff1963/tenant_schemas_celery-3.0.0-py3-none-any.whl", hash = "sha256:ca0f69e78ef698eb4813468231df5a0ab6a660c08e657b65f5ac92e16887eec8", size = 18108, upload-time = "2024-05-19T11:16:39.92Z" }, { url = "https://files.pythonhosted.org/packages/e9/a8/fd663c461550d6fedfb24e987acc1557ae5b6615ca08fc6c70dbaaa88aa5/tenant_schemas_celery-4.0.1-py3-none-any.whl", hash = "sha256:d06a3ff6956db3a95168ce2051b7bff2765f9ce0d070e14df92f07a2b60ae0a0", size = 21364, upload-time = "2025-04-22T18:23:49.899Z" },
] ]
[[package]] [[package]]
@ -3160,7 +3160,7 @@ wheels = [
[[package]] [[package]]
name = "twilio" name = "twilio"
version = "9.6.0" version = "9.6.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
@ -3168,9 +3168,9 @@ dependencies = [
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "requests" }, { name = "requests" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/91/e9/ffc6e52465ffc16fad31fa64aea4e10e06cb4803447310c539c6fd66e859/twilio-9.6.0.tar.gz", hash = "sha256:bcb6cbc7f1dad09717d48d3e610573b6a55fa4a1f6fd1006f5b59cf6878b5562", size = 986499, upload-time = "2025-05-05T10:48:17.921Z" } sdist = { url = "https://files.pythonhosted.org/packages/95/78/453ff0d35442c53490c22d077f580684a2352846c721d3e01f4c6dfa85bd/twilio-9.6.1.tar.gz", hash = "sha256:bb80b31d4d9e55c33872efef7fb99373149ed4093f21c56cf582797da45862f5", size = 987002, upload-time = "2025-05-13T09:56:55.183Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/04/1d9f452b1089c634bd6d64b40b9002c935b8214e9b08a7cbbfef204c8186/twilio-9.6.0-py2.py3-none-any.whl", hash = "sha256:19e8554c56324186973dcb3121de34626755db15331767e3021a2e23f80c6a3b", size = 1859151, upload-time = "2025-05-05T10:48:15.394Z" }, { url = "https://files.pythonhosted.org/packages/02/f4/36fe2566a3ad7f71a89fd28ea2ebb6b2aa05c3a4d5a55b3ca6c358768c6b/twilio-9.6.1-py2.py3-none-any.whl", hash = "sha256:441fdab61b9a204eef770368380b962cbf08dc0fe9f757fe4b1d63ced37ddeed", size = 1859407, upload-time = "2025-05-13T09:56:53.094Z" },
] ]
[[package]] [[package]]
@ -3487,12 +3487,17 @@ wheels = [
[[package]] [[package]]
name = "xmlsec" name = "xmlsec"
version = "1.3.14" version = "1.3.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "lxml" }, { name = "lxml" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/25/5b/244459b51dfe91211c1d9ec68fb5307dfc51e014698f52de575d25f753e0/xmlsec-1.3.14.tar.gz", hash = "sha256:934f804f2f895bcdb86f1eaee236b661013560ee69ec108d29cdd6e5f292a2d9", size = 68854, upload-time = "2024-04-17T19:34:29.388Z" } sdist = { url = "https://files.pythonhosted.org/packages/6b/0b/d851367799b865500efd0b255c39fc5d30892ea28c1569ca185a76d19576/xmlsec-1.3.15.tar.gz", hash = "sha256:baa856b83d0012e278e6f6cbec96ac8128de667ca9fa9a2eeb02c752e816f6d8", size = 114117, upload-time = "2025-03-11T22:37:00.567Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/17/0a272e6087ddb24bec96528acf061341845f458671e2a5cb35ff867a7c89/xmlsec-1.3.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6ac2154311d32a6571e22f224ed16356029e59bd5ca76edeb3922a809adfe89c", size = 3746315, upload-time = "2025-03-11T22:36:43.675Z" },
{ url = "https://files.pythonhosted.org/packages/b7/91/7ce9317e3a2a03e3811e62be52e091c1e661da2d59b5c7f60ec1840a1e6b/xmlsec-1.3.15-cp313-cp313-win32.whl", hash = "sha256:5ed218129f89b0592926ad2be42c017bece469db9b7380dc41bc09b01ca26d5d", size = 2146158, upload-time = "2025-03-11T22:36:44.887Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/93311b9eedc11055ba667e666dc6ca1e2cc59c2356e91b73c3d5a6738fbf/xmlsec-1.3.15-cp313-cp313-win_amd64.whl", hash = "sha256:5fc29e69b064323317b3862751a3a8107670e0a17510ca4517bbdc1939a90b1a", size = 2442027, upload-time = "2025-03-11T22:36:46.431Z" },
]
[[package]] [[package]]
name = "yarl" name = "yarl"

View File

@ -1,11 +0,0 @@
import { create } from "@storybook/theming/create";
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
export default create({
base: isDarkMode ? "dark" : "light",
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
});

69
web/.storybook/main.js Normal file
View File

@ -0,0 +1,69 @@
/**
* @file Storybook configuration.
* @import { StorybookConfig } from "@storybook/web-components-vite";
* @import { InlineConfig, Plugin } from "vite";
*/
import { cwd } from "process";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
const NODE_ENV = process.env.NODE_ENV || "development";
const CSSImportPattern = /import [\w\$]+ from .+\.(css)/g;
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
/**
* @satisfies {Plugin<never>}
*/
const inlineCSSPlugin = {
name: "inline-css-plugin",
transform: (source, id) => {
if (!JavaScriptFilePattern.test(id)) return;
const code = source.replace(CSSImportPattern, (match) => {
return `${match}?inline`;
});
return {
code,
};
},
};
/**
* @satisfies {StorybookConfig}
*/
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
viteFinal({ plugins = [], ...config }) {
/**
* @satisfies {InlineConfig}
*/
const mergedConfig = {
...config,
define: {
"process.env.NODE_ENV": JSON.stringify(NODE_ENV),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(process.env.AK_API_BASE_PATH || ""),
},
plugins: [inlineCSSPlugin, ...plugins, postcssLit(), tsconfigPaths()],
};
return mergedConfig;
},
};
export default config;

View File

@ -1,81 +0,0 @@
import replace from "@rollup/plugin-replace";
import type { StorybookConfig } from "@storybook/web-components-vite";
import { cwd } from "process";
import modify from "rollup-plugin-modify";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
export const isProdBuild = process.env.NODE_ENV === "production";
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
const importInlinePatterns = [
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
'import styles from "\\./LibraryPageImpl\\.css',
];
const importInlineRegexp = new RegExp(importInlinePatterns.map((a) => `(${a})`).join("|"));
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
staticDirs: [
{
from: "../node_modules/@patternfly/patternfly/patternfly-base.css",
to: "@patternfly/patternfly/patternfly-base.css",
},
{
from: "../src/common/styles/authentik.css",
to: "@goauthentik/common/styles/authentik.css",
},
{
from: "../src/common/styles/theme-dark.css",
to: "@goauthentik/common/styles/theme-dark.css",
},
{
from: "../src/common/styles/one-dark.css",
to: "@goauthentik/common/styles/one-dark.css",
},
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
return {
...config,
plugins: [
modify({
find: importInlineRegexp,
replace: (match: RegExpMatchArray) => {
return `${match}?inline`;
},
}),
replace({
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development",
),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
"preventAssignment": true,
}),
...config.plugins,
postcssLit(),
tsconfigPaths(),
],
};
},
};
export default config;

38
web/.storybook/manager.js Normal file
View File

@ -0,0 +1,38 @@
/**
* @file Storybook manager configuration.
*
* @import { ThemeVarsPartial } from "storybook/internal/theming";
*/
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
import { addons } from "@storybook/manager-api";
import { create } from "@storybook/theming/create";
/**
* @satisfies {Partial<ThemeVarsPartial>}
*/
const baseTheme = {
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
};
const uiTheme = resolveUITheme();
addons.setConfig({
theme: create({
...baseTheme,
base: uiTheme,
}),
enableShortcuts: false,
});
createUIThemeEffect((nextUITheme) => {
addons.setConfig({
theme: create({
...baseTheme,
base: nextUITheme,
}),
enableShortcuts: false,
});
});

View File

@ -1,9 +0,0 @@
// .storybook/manager.js
import { addons } from "@storybook/manager-api";
import authentikTheme from "./authentikTheme";
addons.setConfig({
theme: authentikTheme,
enableShortcuts: false,
});

View File

@ -1,5 +1,3 @@
<link rel="stylesheet" href="@patternfly/patternfly/patternfly-base.css" />
<link rel="stylesheet" href="@goauthentik/common/styles/authentik.css" />
<style> <style>
body { body {
overflow-y: scroll; overflow-y: scroll;

32
web/.storybook/preview.js Normal file
View File

@ -0,0 +1,32 @@
/// <reference types="../types/css.js" />
/**
* @file Storybook manager configuration.
*
* @import { Preview } from "@storybook/web-components";
*/
import { applyDocumentTheme } from "@goauthentik/web/common/theme.ts";
applyDocumentTheme();
/**
* @satisfies {Preview}
*/
const preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@ -1,30 +0,0 @@
import type { Preview } from "@storybook/web-components";
import "@goauthentik/common/styles/authentik.css";
// import "@goauthentik/common/styles/theme-dark.css";
import "@patternfly/patternfly/components/Brand/brand.css";
import "@patternfly/patternfly/components/Page/page.css";
// .storybook/preview.js
import "@patternfly/patternfly/patternfly-base.css";
const preview: Preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
cssUserPrefs: {
"prefers-color-scheme": "light",
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

2772
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@
"lint:precommit": "wireit", "lint:precommit": "wireit",
"lint:types": "wireit", "lint:types": "wireit",
"lit-analyse": "wireit", "lit-analyse": "wireit",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "wireit", "precommit": "wireit",
"prettier": "wireit", "prettier": "wireit",
"prettier-check": "wireit", "prettier-check": "wireit",
@ -37,7 +36,14 @@
"exports": { "exports": {
"./package.json": "./package.json", "./package.json": "./package.json",
"./paths": "./paths.js", "./paths": "./paths.js",
"./scripts/*": "./scripts/*.mjs" "./scripts/*": "./scripts/*.mjs",
"./elements/*": "./src/elements/*",
"./common/*": "./src/common/*",
"./components/*": "./src/components/*",
"./flow/*": "./src/flow/*",
"./locales/*": "./src/locales/*",
"./user/*": "./src/user/*",
"./admin/*": "./src/admin/*"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
@ -50,7 +56,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.0-1746018955", "@goauthentik/api": "^2025.4.1-1747332783",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -106,14 +112,14 @@
"@hcaptcha/types": "^1.0.4", "@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0", "@lit/localize-tools": "^0.8.0",
"@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-replace": "^6.0.1",
"@storybook/addon-essentials": "^8.3.4", "@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-links": "^8.3.4", "@storybook/addon-links": "^8.6.12",
"@storybook/api": "^7.6.17", "@storybook/blocks": "^8.6.12",
"@storybook/blocks": "^8.3.4", "@storybook/experimental-addon-test": "^8.6.12",
"@storybook/builder-vite": "^8.3.4", "@storybook/manager-api": "^8.6.12",
"@storybook/manager-api": "^8.3.4", "@storybook/test": "^8.6.12",
"@storybook/web-components": "^8.3.4", "@storybook/web-components": "^8.6.12",
"@storybook/web-components-vite": "^8.3.4", "@storybook/web-components-vite": "^8.6.12",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41", "@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15", "@types/codemirror": "^5.60.15",
@ -145,9 +151,8 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pseudolocale": "^2.1.0", "pseudolocale": "^2.1.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0", "rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.3.4", "storybook": "^8.6.12",
"storybook-addon-mock": "^5.0.0", "storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3", "turnstile-types": "^1.2.3",
"typescript": "^5.6.2", "typescript": "^5.6.2",

View File

@ -6,7 +6,7 @@
* @import { Message as ESBuildMessage } from "esbuild"; * @import { Message as ESBuildMessage } from "esbuild";
*/ */
const logPrefix = "👷 [ESBuild]"; const logPrefix = "authentik/dev/web: ";
const log = console.debug.bind(console, logPrefix); const log = console.debug.bind(console, logPrefix);
/** /**
@ -76,7 +76,7 @@ export class ESBuildObserver extends EventSource {
*/ */
#startListener = () => { #startListener = () => {
this.#trackActivity(); this.#trackActivity();
log("⏰ Build started..."); log("⏰ Build started...");
}; };
#internalErrorListener = () => { #internalErrorListener = () => {
@ -86,7 +86,7 @@ export class ESBuildObserver extends EventSource {
clearTimeout(this.#keepAliveInterval); clearTimeout(this.#keepAliveInterval);
this.close(); this.close();
log("⛔️ Closing connection"); log("⛔️ Closing connection");
} }
}; };
@ -126,13 +126,13 @@ export class ESBuildObserver extends EventSource {
this.#trackActivity(); this.#trackActivity();
if (!this.online) { if (!this.online) {
log("🚫 Build finished while offline."); log("🚫 Build finished while offline.");
this.deferredReload = true; this.deferredReload = true;
return; return;
} }
log("🛎️ Build completed! Reloading..."); log("🛎️ Build completed! Reloading...");
// We use an animation frame to keep the reload from happening before the // We use an animation frame to keep the reload from happening before the
// event loop has a chance to process the message. // event loop has a chance to process the message.
@ -189,13 +189,13 @@ export class ESBuildObserver extends EventSource {
if (!this.deferredReload) return; if (!this.deferredReload) return;
log("🛎️ Reloading after offline build..."); log("🛎️ Reloading after offline build...");
this.deferredReload = false; this.deferredReload = false;
window.location.reload(); window.location.reload();
}); });
log("🛎️ Listening for build changes..."); log("🛎️ Listening for build changes...");
this.#keepAliveInterval = setInterval(() => { this.#keepAliveInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
@ -203,7 +203,7 @@ export class ESBuildObserver extends EventSource {
if (now - this.lastUpdatedAt < 10_000) return; if (now - this.lastUpdatedAt < 10_000) return;
this.alive = false; this.alive = false;
log("👋 Waiting for build to start..."); log("👋 Waiting for build to start...");
}, 15_000); }, 15_000);
} }

View File

@ -210,6 +210,9 @@ class PasswordStage extends Stage<PasswordChallenge> {
<form id="password-form"> <form id="password-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt=""> <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1> <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3">
<input type="text" readonly class="form-control-plaintext" value="Welcome, ${this.challenge?.pendingUser}.">
</div>
<div class="form-label-group my-3 has-validation"> <div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password"> <input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")} ${this.renderInputError("password")}

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bash
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
>>>>>>> main
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
}
);
}
-const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m));
+const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m)), QX2 = () => {};
function Gp({
data: n,
- onUpdateData: a = () => {
- },
+ onUpdateData: a = QX2,
editingEnabled: s = !1,
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -4,13 +4,13 @@ import { ROUTES } from "@goauthentik/admin/Routes";
import { import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import { SidebarToggleEventDetail } from "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
@ -26,7 +26,7 @@ import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem"; import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, eventOptions, property, query } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -52,28 +52,33 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Properties //#region Properties
@property({ type: Boolean }) @property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); public notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@property({ type: Boolean }) @property({ type: Boolean })
apiDrawerOpen = getURLParam("apiDrawerOpen", false); public apiDrawerOpen = getURLParam("apiDrawerOpen", false);
ws: WebsocketClient; protected readonly ws: WebsocketClient;
@state() @property({
user?: SessionUser; type: Object,
attribute: false,
reflect: false,
})
public user?: SessionUser;
@query("ak-about-modal") @query("ak-about-modal")
aboutModal?: AboutModal; public aboutModal?: AboutModal;
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
public sidebarOpen: boolean; public sidebarOpen: boolean;
#toggleSidebar = () => { @eventOptions({ passive: true })
this.sidebarOpen = !this.sidebarOpen; protected sidebarListener(event: CustomEvent<SidebarToggleEventDetail>) {
}; this.sidebarOpen = !!event.detail.open;
}
#sidebarMatcher: MediaQueryList; #sidebarMatcher: MediaQueryList;
#sidebarListener = (event: MediaQueryListEvent) => { #sidebarMediaQueryListener = (event: MediaQueryListEvent) => {
this.sidebarOpen = event.matches; this.sidebarOpen = event.matches;
}; };
@ -81,59 +86,57 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Styles //#region Styles
static get styles(): CSSResult[] { static styles: CSSResult[] = [
return [ PFBase,
PFBase, PFPage,
PFPage, PFButton,
PFButton, PFDrawer,
PFDrawer, PFNav,
PFNav, css`
css` .pf-c-page__main,
.pf-c-page__main, .pf-c-drawer__content,
.pf-c-drawer__content, .pf-c-page__drawer {
.pf-c-page__drawer { z-index: auto !important;
z-index: auto !important; background-color: transparent;
background-color: transparent; }
}
.display-none { .display-none {
display: none; display: none;
} }
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
:host([theme="dark"]) {
/* Global page background colour */
.pf-c-page { .pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important; --pf-c-page--BackgroundColor: var(--ak-dark-background);
} }
}
:host([theme="dark"]) { ak-page-navbar {
/* Global page background colour */ grid-area: header;
.pf-c-page { }
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
}
ak-page-navbar { .ak-sidebar {
grid-area: header; grid-area: nav;
} }
.ak-sidebar { .pf-c-drawer__panel {
grid-area: nav; z-index: var(--pf-global--ZIndex--xl);
} }
`,
.pf-c-drawer__panel { ];
z-index: var(--pf-global--ZIndex--xl);
}
`,
];
}
//#endregion //#endregion
//#region Lifecycle //#region Lifecycle
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)"); this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches; this.sidebarOpen = this.#sidebarMatcher.matches;
} }
@ -141,8 +144,6 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen; this.notificationDrawerOpen = !this.notificationDrawerOpen;
updateURLParams({ updateURLParams({
@ -157,17 +158,17 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
}); });
}); });
this.#sidebarMatcher.addEventListener("change", this.#sidebarListener); this.#sidebarMatcher.addEventListener("change", this.#sidebarMediaQueryListener, {
passive: true,
});
} }
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); this.#sidebarMatcher.removeEventListener("change", this.#sidebarMediaQueryListener);
this.#sidebarMatcher.removeEventListener("change", this.#sidebarListener);
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry(true);
this.user = await me(); this.user = await me();
const canAccessAdmin = const canAccessAdmin =
@ -197,7 +198,7 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
return html` <ak-locale-context> return html` <ak-locale-context>
<div class="pf-c-page"> <div class="pf-c-page">
<ak-page-navbar> <ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}>
<ak-version-banner></ak-version-banner> <ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status> <ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar> </ak-page-navbar>

View File

@ -21,7 +21,7 @@ import { type LocalTypeCreate } from "./ProviderChoices.js";
@customElement("ak-application-wizard-provider-choice-step") @customElement("ak-application-wizard-provider-choice-step")
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) { export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
label = msg("Choose A Provider"); label = msg("Choose a Provider");
@state() @state()
failureMessage = ""; failureMessage = "";

View File

@ -45,9 +45,9 @@ const providerListArgs = (page: number, search = "") => ({
}); });
const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => { const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => {
const label = item.assignedBackchannelApplicationName const label =
? item.assignedBackchannelApplicationName item.assignedBackchannelApplicationName || item.assignedApplicationName || item.name;
: item.assignedApplicationName;
return [ return [
`${item.pk}`, `${item.pk}`,
html`<div class="selection-main">${label}</div> html`<div class="selection-main">${label}</div>

View File

@ -15,7 +15,7 @@ import { DetailedCountry, GeoIPPolicy, PoliciesApi } from "@goauthentik/api";
import { countryCache } from "./CountryCache"; import { countryCache } from "./CountryCache";
function countryToPair(country: DetailedCountry): DualSelectPair { function countryToPair(country: DetailedCountry): DualSelectPair {
return [country.code, country.name]; return [country.code, country.name, country.name];
} }
@customElement("ak-policy-geoip-form") @customElement("ak-policy-geoip-form")
@ -210,17 +210,16 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
.getCountries() .getCountries()
.then((results) => { .then((results) => {
if (!search) return results; if (!search) return results;
return results.filter((result) => return results.filter((result) =>
result.name result.name
.toLowerCase() .toLowerCase()
.includes(search.toLowerCase()), .includes(search.toLowerCase()),
); );
}) })
.then((results) => { .then((results) => ({
return { options: results.map(countryToPair),
options: results.map(countryToPair), }));
};
});
}} }}
.selected=${(this.instance?.countriesObj ?? []).map(countryToPair)} .selected=${(this.instance?.countriesObj ?? []).map(countryToPair)}
available-label="${msg("Available Countries")}" available-label="${msg("Available Countries")}"

View File

@ -1,18 +1,14 @@
import { import {
CSRFHeaderName,
CSRFMiddleware, CSRFMiddleware,
EventMiddleware, EventMiddleware,
LoggingMiddleware, LoggingMiddleware,
} from "@goauthentik/common/api/middleware"; } from "@goauthentik/common/api/middleware.js";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants.js";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global.js";
import { SentryMiddleware } from "@goauthentik/common/sentry";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
// HACK: Workaround for ESBuild not being able to hoist import statement across entrypoints.
// This can be removed after ESBuild uses a single build context for all entrypoints.
export { CSRFHeaderName };
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config); let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
export function config(): Promise<Config> { export function config(): Promise<Config> {
if (!globalConfigPromise) { if (!globalConfigPromise) {
@ -66,21 +62,13 @@ export function brand(): Promise<CurrentBrand> {
return globalBrandPromise; return globalBrandPromise;
} }
export function getMetaContent(key: string): string {
const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`);
if (!metaEl) return "";
return metaEl.content;
}
export const DEFAULT_CONFIG = new Configuration({ export const DEFAULT_CONFIG = new Configuration({
basePath: `${globalAK().api.base}api/v3`, basePath: `${globalAK().api.base}api/v3`,
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
},
middleware: [ middleware: [
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(globalAK().brand), new LoggingMiddleware(globalAK().brand),
new SentryMiddleware(),
], ],
}); });

View File

@ -1,5 +1,5 @@
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants"; import { EVENT_REQUEST_POST } from "@goauthentik/common/constants.js";
import { getCookie } from "@goauthentik/common/utils"; import { getCookie } from "@goauthentik/common/utils.js";
import { import {
CurrentBrand, CurrentBrand,

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2025.4.0"; export const VERSION = "2025.4.1";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";
@ -11,7 +11,6 @@ export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle"; export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle"; export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle";
export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle"; export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle";
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
export const EVENT_WS_MESSAGE = "ak-ws-message"; export const EVENT_WS_MESSAGE = "ak-ws-message";
export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
export const EVENT_LOCALE_CHANGE = "ak-locale-change"; export const EVENT_LOCALE_CHANGE = "ak-locale-change";

View File

@ -1,5 +1,5 @@
import { config } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants"; import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils"; import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import { import {
@ -10,8 +10,16 @@ import {
setTag, setTag,
setUser, setUser,
} from "@sentry/browser"; } from "@sentry/browser";
import { getTraceData } from "@sentry/core";
import * as Spotlight from "@spotlightjs/spotlight";
import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api"; import {
CapabilitiesEnum,
FetchParams,
Middleware,
RequestContext,
ResponseError,
} from "@goauthentik/api";
/** /**
* A generic error that can be thrown without triggering Sentry's reporting. * A generic error that can be thrown without triggering Sentry's reporting.
@ -21,69 +29,94 @@ export class SentryIgnoredError extends Error {}
export const TAG_SENTRY_COMPONENT = "authentik.component"; export const TAG_SENTRY_COMPONENT = "authentik.component";
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities"; export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
export async function configureSentry(canDoPpi = false): Promise<Config> { let _sentryConfigured = false;
const cfg = await config();
if (cfg.errorReporting.enabled) { export function configureSentry(canDoPpi = false) {
init({ const cfg = globalAK().config;
dsn: cfg.errorReporting.sentryDsn, const debug = cfg.capabilities.includes(CapabilitiesEnum.CanDebug);
ignoreErrors: [ if (!cfg.errorReporting.enabled && !debug) {
/network/gi, return cfg;
/fetch/gi, }
/module/gi, init({
// Error on edge on ios, dsn: cfg.errorReporting.sentryDsn,
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight ignoreErrors: [
/instantSearchSDKJSBridgeClearHighlight/gi, /network/gi,
// Seems to be an issue in Safari and Firefox /fetch/gi,
/MutationObserver.observe/gi, /module/gi,
/NS_ERROR_FAILURE/gi, // Error on edge on ios,
], // https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
release: `authentik@${VERSION}`, /instantSearchSDKJSBridgeClearHighlight/gi,
// Seems to be an issue in Safari and Firefox
/MutationObserver.observe/gi,
/NS_ERROR_FAILURE/gi,
],
release: `authentik@${VERSION}`,
integrations: [
browserTracingIntegration({
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
instrumentNavigation: false,
instrumentPageLoad: false,
traceFetch: false,
}),
],
tracePropagationTargets: [window.location.origin],
tracesSampleRate: debug ? 1.0 : cfg.errorReporting.tracesSampleRate,
environment: cfg.errorReporting.environment,
beforeSend: (
event: ErrorEvent,
hint: EventHint,
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
if (
hint.originalException instanceof ResponseError ||
hint.originalException instanceof DOMException
) {
return null;
}
return event;
},
});
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
if (window.location.pathname.includes("if/")) {
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
}
if (debug) {
Spotlight.init({
injectImmediately: true,
integrations: [ integrations: [
browserTracingIntegration({ Spotlight.sentry({
shouldCreateSpanForRequest: (url: string) => { injectIntoSDK: true,
return url.startsWith(window.location.host);
},
}), }),
], ],
tracesSampleRate: cfg.errorReporting.tracesSampleRate,
environment: cfg.errorReporting.environment,
beforeSend: (
event: ErrorEvent,
hint: EventHint,
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
if (
hint.originalException instanceof ResponseError ||
hint.originalException instanceof DOMException
) {
return null;
}
return event;
},
}); });
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(",")); console.debug("authentik/config: Enabled Sentry Spotlight");
if (window.location.pathname.includes("if/")) { }
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`); if (cfg.errorReporting.sendPii && canDoPpi) {
} me().then((user) => {
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) { setUser({ email: user.user.email });
const Spotlight = await import("@spotlightjs/spotlight"); console.debug("authentik/config: Sentry with PII enabled.");
});
Spotlight.init({ injectImmediately: true }); } else {
} console.debug("authentik/config: Sentry enabled.");
if (cfg.errorReporting.sendPii && canDoPpi) { }
me().then((user) => { _sentryConfigured = true;
setUser({ email: user.user.email }); }
console.debug("authentik/config: Sentry with PII enabled.");
}); export class SentryMiddleware implements Middleware {
} else { pre?(context: RequestContext): Promise<FetchParams | void> {
console.debug("authentik/config: Sentry enabled."); if (!_sentryConfigured) {
} return Promise.resolve(context);
}
const traceData = getTraceData();
// @ts-ignore
context.init.headers["baggage"] = traceData["baggage"];
// @ts-ignore
context.init.headers["sentry-trace"] = traceData["sentry-trace"];
return Promise.resolve(context);
} }
return cfg;
} }

View File

@ -1,3 +1,12 @@
/**
* @file authentik base UI theme.
*/
/* Defined to better identify the base theme when debugging constructed stylesheets. */
.__AK_UI_BASE__ {
--__AK_UI_BASE__: 1;
}
/* #region Global */ /* #region Global */
:root { :root {

View File

@ -1,42 +1,48 @@
/* /**
* @file Atom One Dark syntax highlighting theme.
*
* @see https://github.com/atom/one-dark-syntax
*/
Atom One Dark by Daniel Gamage /* Defined to better identify the One Dark theme when debugging constructed stylesheets. */
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax .__HIGHLIGHT_THEME_ONE_DARK__ {
--__HIGHLIGHT_THEME_ONE_DARK__: 1;
}
base: #282c34 :root {
mono-1: #abb2bf --one-dark-base: #282c34;
mono-2: #818896 --one-dark-mono-1: #abb2bf;
mono-3: #5c6370 --one-dark-mono-2: #818896;
hue-1: #56b6c2 --one-dark-mono-3: #5c6370;
hue-2: #61aeee --one-dark-hue-1: #56b6c2;
hue-3: #c678dd --one-dark-hue-2: #61aeee;
hue-4: #98c379 --one-dark-hue-3: #c678dd;
hue-5: #e06c75 --one-dark-hue-4: #98c379;
hue-5-2: #be5046 --one-dark-hue-5: #e06c75;
hue-6: #d19a66 --one-dark-hue-5-2: #be5046;
hue-6-2: #e6c07b --one-dark-hue-6: #d19a66;
--one-dark-hue-6-2: #e6c07b;
*/ }
.hljs { .hljs {
color: #abb2bf; color: var(--one-dark-mono-1);
background: #282c34; background: var(--one-dark-base);
} }
pre:has(.hljs) { pre:has(.hljs) {
background: #282c34; background: var(--one-dark-base);
} }
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #5c6370; color: var(--one-dark-mono-3);
font-style: italic; font-style: italic;
} }
.hljs-doctag, .hljs-doctag,
.hljs-keyword, .hljs-keyword,
.hljs-formula { .hljs-formula {
color: #c678dd; color: var(--one-dark-hue-3);
} }
.hljs-section, .hljs-section,
@ -44,11 +50,11 @@ pre:has(.hljs) {
.hljs-selector-tag, .hljs-selector-tag,
.hljs-deletion, .hljs-deletion,
.hljs-subst { .hljs-subst {
color: #e06c75; color: var(--one-dark-hue-5);
} }
.hljs-literal { .hljs-literal {
color: #56b6c2; color: var(--one-dark-hue-1);
} }
.hljs-string, .hljs-string,
@ -56,7 +62,7 @@ pre:has(.hljs) {
.hljs-addition, .hljs-addition,
.hljs-attribute, .hljs-attribute,
.hljs-meta .hljs-string { .hljs-meta .hljs-string {
color: #98c379; color: var(--one-dark-hue-4);
} }
.hljs-attr, .hljs-attr,
@ -67,7 +73,7 @@ pre:has(.hljs) {
.hljs-selector-attr, .hljs-selector-attr,
.hljs-selector-pseudo, .hljs-selector-pseudo,
.hljs-number { .hljs-number {
color: #d19a66; color: var(--one-dark-hue-6);
} }
.hljs-symbol, .hljs-symbol,
@ -76,13 +82,13 @@ pre:has(.hljs) {
.hljs-meta, .hljs-meta,
.hljs-selector-id, .hljs-selector-id,
.hljs-title { .hljs-title {
color: #61aeee; color: var(--one-dark-hue-2);
} }
.hljs-built_in, .hljs-built_in,
.hljs-title.class_, .hljs-title.class_,
.hljs-class .hljs-title { .hljs-class .hljs-title {
color: #e6c07b; color: var(--one-dark-hue-6-2);
} }
.hljs-emphasis { .hljs-emphasis {

View File

@ -1,3 +1,12 @@
/**
* @file authentik dark UI theme.
*/
/* Defined to better identify the dark theme when debugging constructed stylesheets. */
.__AK_UI_DARK__ {
--__AK_UI_DARK__: 1;
}
/* #region Global */ /* #region Global */
:root { :root {
@ -5,9 +14,6 @@
--ak-global--Color--100: var(--ak-dark-foreground) !important; --ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker); --pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important; --pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
--ak-mermaid-message-text: var(--ak-dark-foreground) !important;
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter) !important;
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
} }
body { body {
@ -256,8 +262,13 @@ input[type="date"]::-webkit-calendar-picker-indicator {
color: var(--ak-dark-background-lighter); color: var(--ak-dark-background-lighter);
} }
.pf-c-button.pf-m-plain:hover { .pf-c-button.pf-m-plain {
color: var(--ak-dark-foreground); --pf-c-button--m-plain--focus--Color: var(--pf-global--Color--200);
--pf-c-button--m-plain--hover--Color: var(--ak-dark-foreground);
&:focus:hover {
color: var(--pf-c-button--m-plain--hover--Color);
}
} }
.pf-c-button.pf-m-control { .pf-c-button.pf-m-control {

View File

@ -1,17 +1,27 @@
/** /**
* @file Stylesheet utilities. * @file Stylesheet utilities.
*/ */
import { CSSResult, CSSResultOrNative, ReactiveElement, css } from "lit"; import { CSSResultOrNative, ReactiveElement, adoptStyles as adoptStyleSheetsShim, css } from "lit";
/** /**
* Elements containing adoptable stylesheets. * Element-like objects containing adoptable stylesheets.
*
* Note that while these all possess the `adoptedStyleSheets` property,
* browser differences and polyfills may make them not actually adoptable.
*
* This type exists to normalize the different ways of accessing the property.
*/ */
export type StyleSheetParent = Pick<DocumentOrShadowRoot, "adoptedStyleSheets">; export type StyleRoot =
| Document
| ShadowRoot
| DocumentFragment
| HTMLElement
| DocumentOrShadowRoot;
/** /**
* Type-predicate to determine if a given object has adoptable stylesheets. * Type-predicate to determine if a given object has adoptable stylesheets.
*/ */
export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheetParent { export function isStyleRoot(input: StyleRoot): input is ShadowRoot {
// Sanity check - Does the input have the right shape? // Sanity check - Does the input have the right shape?
if (!input || typeof input !== "object") return false; if (!input || typeof input !== "object") return false;
@ -25,39 +35,12 @@ export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheet
// All we care about is that it's shaped like an array. // All we care about is that it's shaped like an array.
if (!("length" in input.adoptedStyleSheets)) return false; if (!("length" in input.adoptedStyleSheets)) return false;
if (typeof input.adoptedStyleSheets.length !== "number") return false; return typeof input.adoptedStyleSheets.length === "number";
// Finally is the array mutable?
return "push" in input.adoptedStyleSheets;
} }
/** /**
* Assert that the given input can adopt stylesheets. * Create a lazy-loaded `CSSResult` compatible with Lit's
*/ * element lifecycle.
export function assertAdoptableStyleSheetParent<T>(
input: T,
): asserts input is T & StyleSheetParent {
if (isAdoptableStyleSheetParent(input)) return;
console.debug("Given input missing `adoptedStyleSheets`", input);
throw new TypeError("Assertion failed: `adoptedStyleSheets` missing in given input");
}
export function resolveStyleSheetParent<T extends HTMLElement | DocumentFragment | Document>(
renderRoot: T,
) {
const styleRoot = "ShadyDOM" in window ? document : renderRoot;
assertAdoptableStyleSheetParent(styleRoot);
return styleRoot;
}
export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
/**
* Given a source of CSS, create a `CSSStyleSheet`.
* *
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet` * @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
* *
@ -68,8 +51,12 @@ export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
* *
* It works well when Storybook is running in `dev`, but in `build` it fails. * It works well when Storybook is running in `dev`, but in `build` it fails.
* Storied components will have to map their textual CSS imports. * Storied components will have to map their textual CSS imports.
*
* @see {@linkcode createStyleSheetUnsafe} to create a `CSSStyleSheet` from the given input.
*/ */
export function createStyleSheet(input: string): CSSResult { export function createCSSResult(input: string | CSSModule | CSSResultOrNative): CSSResultOrNative {
if (typeof input !== "string") return input;
const inputTemplate = [input] as unknown as TemplateStringsArray; const inputTemplate = [input] as unknown as TemplateStringsArray;
const result = css(inputTemplate, []); const result = css(inputTemplate, []);
@ -78,74 +65,91 @@ export function createStyleSheet(input: string): CSSResult {
} }
/** /**
* Given a source of CSS, create a `CSSStyleSheet`. * Create a `CSSStyleSheet` from the given input, if it is not already a `CSSStyleSheet`.
* *
* @see {@linkcode createStyleSheet} * @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
*
* @see {@linkcode createCSSResult} for the lazy-loaded `CSSResult` normalization.
*/ */
export function normalizeCSSSource(css: string): CSSStyleSheet; export function createStyleSheetUnsafe(
export function normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet; input: string | CSSModule | CSSResultOrNative,
export function normalizeCSSSource(cssResult: CSSResult): CSSResult; ): CSSStyleSheet {
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative; const result = typeof input === "string" ? createCSSResult(input) : input;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative {
if (typeof input === "string") return createStyleSheet(input);
return input;
}
/**
* Create a `CSSStyleSheet` from the given input.
*/
export function createStyleSheetUnsafe(input: StyleSheetInit): CSSStyleSheet {
const result = normalizeCSSSource(input);
if (result instanceof CSSStyleSheet) return result; if (result instanceof CSSStyleSheet) return result;
if (!result.styleSheet) { if (result.styleSheet) return result.styleSheet;
console.debug(
"authentik/common/stylesheets: CSSResult missing styleSheet, returning empty",
{ result, input },
);
throw new TypeError("Expected a CSSStyleSheet"); const styleSheet = new CSSStyleSheet();
}
return result.styleSheet; styleSheet.replaceSync(result.cssText);
return styleSheet;
} }
export type StyleSheetsAction =
| Iterable<CSSStyleSheet>
| ((currentStyleSheets: CSSStyleSheet[]) => Iterable<CSSStyleSheet>);
/** /**
* Append stylesheet(s) to the given roots. * Set the adopted stylesheets of a given style parent.
* *
* @see {@linkcode removeStyleSheet} to remove a stylesheet from a given roots. * ```ts
* setAdoptedStyleSheets(document.body, (currentStyleSheets) => [
* ...currentStyleSheets,
* myStyleSheet,
* ]);
* ```
*
* @remarks
* Replacing `adoptedStyleSheets` more than once in the same frame may result in
* the `currentStyleSheets` parameter being out of sync with the actual sheets.
*
* A style root's `adoptedStyleSheets` is a proxy object that only updates when
* DOM is repainted. We can't easily cache the previous entries since the style root
* may polyfilled via ShadyDOM.
*
* Short of using {@linkcode requestAnimationFrame} to sequence the adoption,
* and a visibility toggle to avoid a flash of styles between renders,
* we can't reliably cache the previous entries.
*
* In the meantime, we should try to apply all the sheets in a single frame.
*/ */
export function appendStyleSheet( export function setAdoptedStyleSheets(styleRoot: StyleRoot, styleSheets: StyleSheetsAction): void {
styleParent: StyleSheetParent, let changed = false;
...insertions: CSSStyleSheet[]
): void {
insertions = Array.isArray(insertions) ? insertions : [insertions];
for (const styleSheetInsertion of insertions) { const currentAdoptedStyleSheets = isStyleRoot(styleRoot)
if (styleParent.adoptedStyleSheets.includes(styleSheetInsertion)) return; ? [...styleRoot.adoptedStyleSheets]
: [];
styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, styleSheetInsertion]; const result =
typeof styleSheets === "function" ? styleSheets(currentAdoptedStyleSheets) : styleSheets;
const nextAdoptedStyleSheets: CSSStyleSheet[] = [];
for (const [idx, styleSheet] of Array.from(result).entries()) {
const previousStyleSheet = currentAdoptedStyleSheets[idx];
changed ||= previousStyleSheet !== styleSheet;
if (nextAdoptedStyleSheets.includes(styleSheet)) continue;
nextAdoptedStyleSheets.push(styleSheet);
} }
changed ||= nextAdoptedStyleSheets.length !== currentAdoptedStyleSheets.length;
if (!changed) return;
if (styleRoot === document) {
document.adoptedStyleSheets = nextAdoptedStyleSheets;
return;
}
adoptStyleSheetsShim(styleRoot as unknown as ShadowRoot, nextAdoptedStyleSheets);
} }
/** //#region Debugging
* Remove a stylesheet from the given roots, matching by referential equality.
*
* @see {@linkcode appendStyleSheet} to append a stylesheet to a given roots.
*/
export function removeStyleSheet(
styleParent: StyleSheetParent,
...removals: CSSStyleSheet[]
): void {
const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter(
(styleSheet) => !removals.includes(styleSheet),
);
if (nextAdoptedStyleSheets.length === styleParent.adoptedStyleSheets.length) return;
styleParent.adoptedStyleSheets = nextAdoptedStyleSheets;
}
/** /**
* Serialize a stylesheet to a string. * Serialize a stylesheet to a string.
@ -159,8 +163,8 @@ export function serializeStyleSheet(stylesheet: CSSStyleSheet): string {
/** /**
* Inspect the adopted stylesheets of a given style parent, serializing them to strings. * Inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/ */
export function inspectStyleSheets(styleParent: StyleSheetParent): string[] { export function inspectStyleSheets(styleRoot: ShadowRoot): string[] {
return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet)); return styleRoot.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
} }
interface InspectedStyleSheetEntry { interface InspectedStyleSheetEntry {
@ -174,8 +178,11 @@ interface InspectedStyleSheetEntry {
* Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings. * Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/ */
export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry { export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry {
const styleParent = resolveStyleSheetParent(element.renderRoot); if (!isStyleRoot(element.renderRoot)) {
const styles = inspectStyleSheets(styleParent); throw new TypeError("Cannot inspect a render root that doesn't have adoptable stylesheets");
}
const styles = inspectStyleSheets(element.renderRoot);
const tagName = element.tagName.toLowerCase(); const tagName = element.tagName.toLowerCase();
const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, { const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, {
@ -186,12 +193,14 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
return NodeFilter.FILTER_SKIP; return NodeFilter.FILTER_SKIP;
}, },
}); });
const children: InspectedStyleSheetEntry[] = []; const children: InspectedStyleSheetEntry[] = [];
let currentNode: Node | null = treewalker.nextNode(); let currentNode: Node | null = treewalker.nextNode();
while (currentNode) { while (currentNode) {
const childElement = currentNode as ReactiveElement; const childElement = currentNode as ReactiveElement;
if (!isAdoptableStyleSheetParent(childElement.renderRoot)) { if (!isStyleRoot(childElement.renderRoot)) {
currentNode = treewalker.nextNode(); currentNode = treewalker.nextNode();
continue; continue;
} }
@ -221,3 +230,5 @@ if (process.env.NODE_ENV === "development") {
inspectStyleSheets, inspectStyleSheets,
}); });
} }
//#endregion

View File

@ -1,10 +1,47 @@
/** /**
* @file Theme utilities. * @file Theme utilities.
*/ */
import { UIConfig } from "@goauthentik/common/ui/config"; import {
type StyleRoot,
createStyleSheetUnsafe,
setAdoptedStyleSheets,
} from "@goauthentik/common/stylesheets.js";
import { UIConfig } from "@goauthentik/common/ui/config.js";
import AKBase from "@goauthentik/common/styles/authentik.css";
import AKBaseDark from "@goauthentik/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
//#region Stylesheet Exports
/**
* A global style sheet for the Patternfly base styles.
*
* @remarks
*
* While a component *may* import its own instance of the PFBase style sheet,
* this instance ensures referential identity.
*/
export const $PFBase = createStyleSheetUnsafe(PFBase);
/**
* A global style sheet for the authentik base styles.
*
* @see {@linkcode $PFBase} for details.
*/
export const $AKBase = createStyleSheetUnsafe(AKBase);
/**
* A global style sheet for the authentik dark theme.
*
* @see {@linkcode $PFBase} for details.
*/
export const $AKBaseDark = createStyleSheetUnsafe(AKBaseDark);
//#endregion
//#region Scheme Types //#region Scheme Types
/** /**
@ -134,15 +171,21 @@ export function resolveUITheme(
* Effect listener invoked when the color scheme changes. * Effect listener invoked when the color scheme changes.
*/ */
export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void; export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void;
/** /**
* Create an effect that runs * Effect destructor invoked when cleanup is required.
*/
export type UIThemeDestructor = () => void;
/**
* Create an effect that runs UI theme changes.
* *
* @returns A cleanup function that removes the effect. * @returns A cleanup function that removes the effect.
*/ */
export function createUIThemeEffect( export function createUIThemeEffect(
effect: UIThemeListener, effect: UIThemeListener,
listenerOptions?: AddEventListenerOptions, listenerOptions?: AddEventListenerOptions,
): () => void { ): UIThemeDestructor {
const colorSchemeTarget = resolveUITheme(); const colorSchemeTarget = resolveUITheme();
const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget]; const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget];
@ -174,6 +217,8 @@ export function createUIThemeEffect(
mediaQueryList.removeEventListener("change", changeListener); mediaQueryList.removeEventListener("change", changeListener);
}; };
listenerOptions?.signal?.addEventListener("abort", cleanup);
return cleanup; return cleanup;
} }
@ -181,16 +226,96 @@ export function createUIThemeEffect(
//#region Theme Element //#region Theme Element
/**
* Applies the current UI theme to the given style root.
*
* @param styleRoot The style root to apply the theme to.
* @param currentUITheme The current UI theme to apply.
* @param additionalStyleSheets Additional style sheets to apply, in addition to the theme's base sheets.
* @category CSS
*
* @see {@linkcode setAdoptedStyleSheets} for caveats.
*/
export function applyUITheme(
styleRoot: StyleRoot,
currentUITheme: ResolvedUITheme = resolveUITheme(),
...additionalStyleSheets: Array<CSSStyleSheet | undefined | null>
): void {
setAdoptedStyleSheets(styleRoot, (currentStyleSheets) => {
const appendedSheets = additionalStyleSheets.filter(Boolean) as CSSStyleSheet[];
if (currentUITheme === UiThemeEnum.Dark) {
return [...currentStyleSheets, $AKBaseDark, ...appendedSheets];
}
return [
...currentStyleSheets.filter((styleSheet) => styleSheet !== $AKBaseDark),
...appendedSheets,
];
});
}
/**
* Applies the given theme to the document, i.e. the `<html>` element.
*
* @param hint The color scheme hint to use.
*/
export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "auto"): void {
const preferredColorScheme = formatColorScheme(hint);
const applyStyleSheets: UIThemeListener = (currentUITheme) => {
console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`);
setAdoptedStyleSheets(document, (currentStyleSheets) => {
if (currentUITheme === "dark") {
return [...currentStyleSheets, $PFBase, $AKBase, $AKBaseDark];
}
return [
...currentStyleSheets.filter((styleSheet) => styleSheet !== $AKBaseDark),
$PFBase,
$AKBase,
];
});
document.documentElement.dataset.theme = currentUITheme;
};
if (preferredColorScheme === "auto") {
createUIThemeEffect(applyStyleSheets);
return;
}
applyStyleSheets(preferredColorScheme);
}
/** /**
* An element that can be themed. * An element that can be themed.
*/ */
export interface ThemedElement extends HTMLElement { export interface ThemedElement extends HTMLElement {
brand?: CurrentBrand; /**
uiConfig?: UIConfig; * The brand information for the current theme.
config?: Config; */
readonly brand?: CurrentBrand;
/**
* The UI configuration for the current theme,
* typically injected through a Lit Mixin.
*
* @see {@linkcode UIConfig} for details.
*/
readonly uiConfig?: UIConfig;
/**
* An authentik configuration initially provided by the server.
*/
readonly config?: Config;
activeTheme: ResolvedUITheme; activeTheme: ResolvedUITheme;
} }
/**
* Returns the root interface element of the page.
*
* @todo Can this be handled with a Lit Mixin?
*/
export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null { export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null {
const element = document.body.querySelector<T>("[data-ak-interface-root]"); const element = document.body.querySelector<T>("[data-ak-interface-root]");

View File

@ -1,5 +1,5 @@
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users.js";
import { isUserRoute } from "@goauthentik/elements/router/utils"; import { isUserRoute } from "@goauthentik/elements/router/utils.js";
import { UiThemeEnum, UserSelf } from "@goauthentik/api"; import { UiThemeEnum, UserSelf } from "@goauthentik/api";
import { CurrentBrand } from "@goauthentik/api"; import { CurrentBrand } from "@goauthentik/api";

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config.js";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants.js";
import { isResponseErrorLike } from "@goauthentik/common/errors/network"; import { isResponseErrorLike } from "@goauthentik/common/errors/network.js";
import { CoreApi, SessionUser } from "@goauthentik/api"; import { CoreApi, SessionUser } from "@goauthentik/api";

View File

@ -31,18 +31,19 @@ const container = (testItem: TemplateResult) =>
li { li {
display: block; display: block;
} }
p {
color: black; ak-hint {
margin-top: 1em; --ak-hint--Color: var(--pf-global--Color--dark-100);
} }
* { @media (prefers-color-scheme: dark) {
--ak-hint--Color: black !important; ak-hint {
--ak-hint--Color: var(--pf-global--Color--light-100);
}
} }
ak-hint-title::part(ak-hint-title),
ak-hint-footer::part(ak-hint-footer), p {
slotted::(*) { margin-top: 1em;
color: black;
} }
</style> </style>

View File

@ -2,7 +2,7 @@ import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types"; import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
import { html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
@ -64,7 +64,15 @@ export class Alert extends AKElement implements IAlert {
icon = "fa-exclamation-circle"; icon = "fa-exclamation-circle";
static get styles() { static get styles() {
return [PFBase, PFAlert]; return [
PFBase,
PFAlert,
css`
p {
margin: 0;
}
`,
];
} }
get classmap() { get classmap() {

View File

@ -1,30 +1,24 @@
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global.js";
import { import {
StyleSheetInit, StyleRoot,
StyleSheetParent, createCSSResult,
appendStyleSheet,
createStyleSheetUnsafe, createStyleSheetUnsafe,
removeStyleSheet, } from "@goauthentik/common/stylesheets.js";
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import { import {
$AKBase,
CSSColorSchemeValue, CSSColorSchemeValue,
ResolvedUITheme, ResolvedUITheme,
UIThemeListener, ThemedElement,
applyUITheme,
createUIThemeEffect, createUIThemeEffect,
formatColorScheme, formatColorScheme,
resolveUITheme, resolveUITheme,
} from "@goauthentik/common/theme"; } from "@goauthentik/common/theme.js";
import { type ThemedElement } from "@goauthentik/common/theme";
import { localized } from "@lit/localize"; import { localized } from "@lit/localize";
import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit"; import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
// Re-export the theme helpers // Re-export the theme helpers
@ -32,6 +26,58 @@ export { rootInterface } from "@goauthentik/common/theme";
@localized() @localized()
export class AKElement extends LitElement implements ThemedElement { export class AKElement extends LitElement implements ThemedElement {
//#region Static Properties
public static styles?: Array<CSSResult | CSSModule>;
protected static override finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
if (!styles) return [$AKBase];
if (!Array.isArray(styles)) return [createCSSResult(styles), $AKBase];
return [
// ---
...(styles.flat() as CSSResultOrNative[]).map(createCSSResult),
$AKBase,
];
}
//#endregion
//#region Lifecycle
constructor() {
super();
const { brand } = globalAK();
this.preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
public override disconnectedCallback(): void {
this.#themeAbortController?.abort();
super.disconnectedCallback();
}
/**
* Returns the node into which the element should render.
*
* @see {LitElement.createRenderRoot} for more information.
*/
protected override createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
this.styleRoot ??= renderRoot;
return renderRoot;
}
//#endregion
//#region Properties //#region Properties
/** /**
@ -53,87 +99,54 @@ export class AKElement extends LitElement implements ThemedElement {
//#region Private Properties //#region Private Properties
readonly #preferredColorScheme: CSSColorSchemeValue; /**
* The preferred color scheme used to look up the UI theme.
*/
protected readonly preferredColorScheme: CSSColorSchemeValue;
#customCSSStyleSheet: CSSStyleSheet | null; /**
#darkThemeStyleSheet: CSSStyleSheet | null = null; * A custom CSS style sheet to apply to the element.
*/
readonly #customCSSStyleSheet: CSSStyleSheet | null;
/**
* A controller to abort theme updates, such as when the element is disconnected.
*/
#themeAbortController: AbortController | null = null; #themeAbortController: AbortController | null = null;
/**
* The style root to which the theme is applied.
*/
#styleRoot?: StyleRoot;
//#endregion protected set styleRoot(nextStyleRoot: StyleRoot | undefined) {
//#region Lifecycle
protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
// Ensure all style sheets being passed are really style sheets.
const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark];
if (!styles) return baseStyles.map(createStyleSheetUnsafe);
if (Array.isArray(styles)) {
return [
//---
...(styles as unknown as CSSResultOrNative[]),
...baseStyles,
].flatMap(createStyleSheetUnsafe);
}
return [styles, ...baseStyles].map(createStyleSheetUnsafe);
}
constructor() {
super();
const { brand } = globalAK();
this.#preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this.#themeAbortController?.abort(); this.#themeAbortController?.abort();
}
#styleRoot?: StyleSheetParent; this.#styleRoot = nextStyleRoot;
#dispatchTheme: UIThemeListener = (nextUITheme) => { if (!nextStyleRoot) return;
if (!this.#styleRoot) return;
if (nextUITheme === UiThemeEnum.Dark) {
this.#darkThemeStyleSheet ||= createStyleSheetUnsafe(ThemeDark);
appendStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.activeTheme = UiThemeEnum.Dark;
} else if (this.#darkThemeStyleSheet) {
removeStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.#darkThemeStyleSheet = null;
this.activeTheme = UiThemeEnum.Light;
}
};
protected createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
this.#styleRoot = resolveStyleSheetParent(renderRoot);
if (this.#customCSSStyleSheet) {
console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`);
appendStyleSheet(this.#styleRoot, this.#customCSSStyleSheet);
}
this.#themeAbortController = new AbortController(); this.#themeAbortController = new AbortController();
if (this.#preferredColorScheme === "dark") { if (this.preferredColorScheme === "dark") {
this.#dispatchTheme(UiThemeEnum.Dark); applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet);
} else if (this.#preferredColorScheme === "auto") {
createUIThemeEffect(this.#dispatchTheme, {
signal: this.#themeAbortController.signal,
});
}
return renderRoot; this.activeTheme = UiThemeEnum.Dark;
} else if (this.preferredColorScheme === "auto") {
createUIThemeEffect(
(nextUITheme) => {
applyUITheme(nextStyleRoot, nextUITheme, this.#customCSSStyleSheet);
this.activeTheme = nextUITheme;
},
{
signal: this.#themeAbortController.signal,
},
);
}
}
protected get styleRoot(): StyleRoot | undefined {
return this.#styleRoot;
} }
//#endregion //#endregion

View File

@ -1,34 +1,45 @@
import { import { globalAK } from "@goauthentik/common/global.js";
appendStyleSheet, import { ThemedElement, applyDocumentTheme } from "@goauthentik/common/theme.js";
createStyleSheetUnsafe, import { UIConfig } from "@goauthentik/common/ui/config.js";
resolveStyleSheetParent, import { AKElement } from "@goauthentik/elements/Base.js";
} from "@goauthentik/common/stylesheets"; import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController.js";
import { ThemedElement } from "@goauthentik/common/theme";
import { UIConfig } from "@goauthentik/common/ui/config";
import { AKElement } from "@goauthentik/elements/Base";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { state } from "lit/decorators.js"; import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api"; import {
type Config,
type CurrentBrand,
type LicenseSummary,
type Version,
} from "@goauthentik/api";
import { BrandContextController } from "./BrandContextController"; import { BrandContextController } from "./BrandContextController.js";
import { ConfigContextController } from "./ConfigContextController"; import { ConfigContextController } from "./ConfigContextController.js";
import { EnterpriseContextController } from "./EnterpriseContextController"; import { EnterpriseContextController } from "./EnterpriseContextController.js";
const configContext = Symbol("configContext"); const configContext = Symbol("configContext");
const modalController = Symbol("modalController"); const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext"); const versionContext = Symbol("versionContext");
export abstract class Interface extends AKElement implements ThemedElement { export abstract class LightInterface extends AKElement implements ThemedElement {
protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase); constructor() {
super();
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
[configContext]: ConfigContextController; if (!document.documentElement.dataset.theme) {
applyDocumentTheme(globalAK().brand.uiTheme);
}
}
}
[modalController]: ModalOrchestrationController; export abstract class Interface extends LightInterface implements ThemedElement {
static styles = [PFBase];
protected [configContext]: ConfigContextController;
protected [modalController]: ModalOrchestrationController;
@state() @state()
public config?: Config; public config?: Config;
@ -38,11 +49,6 @@ export abstract class Interface extends AKElement implements ThemedElement {
constructor() { constructor() {
super(); super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
this.addController(new BrandContextController(this)); this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this); this[configContext] = new ConfigContextController(this);

View File

@ -1,4 +1,4 @@
import { AuthenticatedInterface, Interface } from "./Interface"; import { AuthenticatedInterface, Interface, LightInterface } from "./Interface";
export { Interface, AuthenticatedInterface }; export { Interface, AuthenticatedInterface, LightInterface };
export default Interface; export default Interface;

View File

@ -1,8 +1,4 @@
import { import { EVENT_WS_MESSAGE, TITLE_DEFAULT } from "@goauthentik/common/constants";
EVENT_SIDEBAR_TOGGLE,
EVENT_WS_MESSAGE,
TITLE_DEFAULT,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config"; import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
@ -29,6 +25,14 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api"; import { SessionUser } from "@goauthentik/api";
//#region Events
export interface SidebarToggleEventDetail {
open?: boolean;
}
//#endregion
//#region Page Navbar //#region Page Navbar
export interface PageNavbarDetails { export interface PageNavbarDetails {
@ -45,7 +49,10 @@ export interface PageNavbarDetails {
* dispatched by the `ak-page-header` component. * dispatched by the `ak-page-header` component.
*/ */
@customElement("ak-page-navbar") @customElement("ak-page-navbar")
export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails { export class AKPageNavbar
extends WithBrandConfig(AKElement)
implements PageNavbarDetails, SidebarToggleEventDetail
{
//#region Static Properties //#region Static Properties
private static elementRef: AKPageNavbar | null = null; private static elementRef: AKPageNavbar | null = null;
@ -260,29 +267,31 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
//#region Properties //#region Properties
@property({ type: String }) @state()
icon?: string; icon?: string;
@property({ type: Boolean }) @state()
iconImage = false; iconImage = false;
@property({ type: String }) @state()
header?: string; header?: string;
@property({ type: String }) @state()
description?: string; description?: string;
@property({ type: Boolean }) @state()
hasIcon = true; hasIcon = true;
@property({ type: Boolean }) @property({
open = true; type: Boolean,
})
public open?: boolean;
@state() @state()
session?: SessionUser; protected session?: SessionUser;
@state() @state()
uiConfig!: UIConfig; protected uiConfig!: UIConfig;
//#endregion //#endregion
@ -305,9 +314,10 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
this.open = !this.open; this.open = !this.open;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, { new CustomEvent<SidebarToggleEventDetail>("sidebar-toggle", {
bubbles: true, bubbles: true,
composed: true, composed: true,
detail: { open: this.open },
}), }),
); );
} }

View File

@ -12,7 +12,6 @@ import type { DualSelectPair } from "./types.js";
* A top-level component for multi-select elements have dynamically generated "selected" * A top-level component for multi-select elements have dynamically generated "selected"
* lists. * lists.
*/ */
@customElement("ak-dual-select-dynamic-selected") @customElement("ak-dual-select-dynamic-selected")
export class AkDualSelectDynamic extends AkDualSelectProvider { export class AkDualSelectDynamic extends AkDualSelectProvider {
/** /**
@ -23,20 +22,24 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
* @attr * @attr
*/ */
@property({ attribute: false }) @property({ attribute: false })
selector: (_: DualSelectPair[]) => Promise<DualSelectPair[]> = async (_) => Promise.resolve([]); selector: (_: DualSelectPair[]) => Promise<DualSelectPair[]> = () => Promise.resolve([]);
private firstUpdateHasRun = false; #didFirstUpdate = false;
willUpdate(changed: PropertyValues<this>) { willUpdate(changed: PropertyValues<this>) {
super.willUpdate(changed); super.willUpdate(changed);
// On the first update *only*, even before rendering, when the options are handed up, update // On the first update *only*, even before rendering, when the options are handed up, update
// the selected list with the contents derived from the selector. // the selected list with the contents derived from the selector.
if (!this.firstUpdateHasRun && this.options.length > 0) {
this.firstUpdateHasRun = true; if (this.#didFirstUpdate) return;
this.selector(this.options).then((selected) => { if (this.options.length === 0) return;
this.selected = selected;
}); this.#didFirstUpdate = true;
}
this.selector(this.options).then((selected) => {
this.selected = selected;
});
} }
render() { render() {

View File

@ -1,18 +1,16 @@
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import { debounce } from "@goauthentik/elements/utils/debounce"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter.js";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { PropertyValues, html } from "lit"; import { PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js"; import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import type { Pagination } from "@goauthentik/api"; import type { Pagination } from "@goauthentik/api";
import "./ak-dual-select"; import "./ak-dual-select.js";
import { AkDualSelect } from "./ak-dual-select"; import { AkDualSelect } from "./ak-dual-select.js";
import type { DataProvider, DualSelectPair } from "./types"; import { type DataProvider, DualSelectEventType, type DualSelectPair } from "./types.js";
/** /**
* @element ak-dual-select-provider * @element ak-dual-select-provider
@ -22,18 +20,19 @@ import type { DataProvider, DualSelectPair } from "./types";
* between authentik and the generic ak-dual-select component; aside from knowing that * between authentik and the generic ak-dual-select component; aside from knowing that
* the Pagination object "looks like Django," the interior components don't know anything * the Pagination object "looks like Django," the interior components don't know anything
* about authentik at all and could be dropped into Gravity unchanged.) * about authentik at all and could be dropped into Gravity unchanged.)
*
*/ */
@customElement("ak-dual-select-provider") @customElement("ak-dual-select-provider")
export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) { export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) {
/** A function that takes a page and returns the DualSelectPair[] collection with which to update //#region Properties
* the "Available" pane.
/**
* A function that takes a page and returns the {@linkcode DualSelectPair DualSelectPair[]}
* collection with which to update the "Available" pane.
* *
* @attr * @attr
*/ */
@property({ type: Object }) @property({ type: Object })
provider!: DataProvider; public provider!: DataProvider;
/** /**
* The list of selected items. This is the *complete* list, not paginated, as presented by a * The list of selected items. This is the *complete* list, not paginated, as presented by a
@ -42,7 +41,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ type: Array }) @property({ type: Array })
selected: DualSelectPair[] = []; public selected: DualSelectPair[] = [];
/** /**
* The label for the left ("available") pane * The label for the left ("available") pane
@ -50,7 +49,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ attribute: "available-label" }) @property({ attribute: "available-label" })
availableLabel = msg("Available options"); public availableLabel = msg("Available options");
/** /**
* The label for the right ("selected") pane * The label for the right ("selected") pane
@ -58,7 +57,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ attribute: "selected-label" }) @property({ attribute: "selected-label" })
selectedLabel = msg("Selected options"); public selectedLabel = msg("Selected options");
/** /**
* The debounce for the search as the user is typing in a request * The debounce for the search as the user is typing in a request
@ -66,103 +65,125 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ attribute: "search-delay", type: Number }) @property({ attribute: "search-delay", type: Number })
searchDelay = 250; public searchDelay = 250;
public get value() {
return this.dualSelector.value!.selected.map(([k, _]) => k);
}
public json() {
return this.value;
}
//#endregion
//#region State
@state() @state()
options: DualSelectPair[] = []; protected options: DualSelectPair[] = [];
protected dualSelector: Ref<AkDualSelect> = createRef(); #loading = false;
protected isLoading = false; #didFirstUpdate = false;
#selected: DualSelectPair[] = [];
private doneFirstUpdate = false; #previousSearchValue = "";
private internalSelected: DualSelectPair[] = [];
protected pagination?: Pagination; protected pagination?: Pagination;
constructor() { //#endregion
super();
setTimeout(() => this.fetch(1), 0); //#region Refs
this.onNav = this.onNav.bind(this);
this.onChange = this.onChange.bind(this); protected dualSelector = createRef<AkDualSelect>();
this.onSearch = this.onSearch.bind(this);
this.addCustomListener("ak-pagination-nav-to", this.onNav); //#endregion
this.addCustomListener("ak-dual-select-change", this.onChange);
this.addCustomListener("ak-dual-select-search", this.onSearch); //#region Lifecycle
public connectedCallback(): void {
super.connectedCallback();
this.addCustomListener(DualSelectEventType.NavigateTo, this.#navigationListener);
this.addCustomListener(DualSelectEventType.Change, this.#changeListener);
this.addCustomListener(DualSelectEventType.Search, this.#searchListener);
this.#fetch(1);
} }
willUpdate(changedProperties: PropertyValues<this>) { willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected") && !this.doneFirstUpdate) { if (changedProperties.has("selected") && !this.#didFirstUpdate) {
this.doneFirstUpdate = true; this.#didFirstUpdate = true;
this.internalSelected = this.selected; this.#selected = this.selected;
}
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(
AkDualSelectProvider.prototype.doSearch.bind(this),
this.searchDelay,
);
} }
if (changedProperties.has("provider")) { if (changedProperties.has("provider")) {
this.pagination = undefined; this.pagination = undefined;
this.fetch(); this.#previousSearchValue = "";
this.#fetch();
} }
} }
async fetch(page?: number, search = "") { //#endregion
if (this.isLoading) {
return;
}
this.isLoading = true;
const goto = page ?? this.pagination?.current ?? 1;
const data = await this.provider(goto, search);
this.pagination = data.pagination;
this.options = data.options;
this.isLoading = false;
}
onNav(event: Event) { //#region Private Methods
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`);
}
this.fetch(event.detail);
}
onChange(event: Event) { #fetch = async (page?: number, search = this.#previousSearchValue): Promise<void> => {
if (!(event instanceof CustomEvent)) { if (this.#loading) return;
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.internalSelected = event.detail.value;
this.selected = this.internalSelected;
}
onSearch(event: Event) { this.#previousSearchValue = search;
if (!(event instanceof CustomEvent)) { this.#loading = true;
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.doSearch(event.detail);
}
doSearch(search: string) { page ??= this.pagination?.current ?? 1;
this.pagination = undefined;
this.fetch(undefined, search);
}
get value() { return this.provider(page, search)
return this.dualSelector.value!.selected.map(([k, _]) => k); .then((data) => {
} this.pagination = data.pagination;
this.options = data.options;
})
.catch((error) => {
console.error(error);
})
.finally(() => {
this.#loading = false;
});
};
json() { //#endregion
return this.value;
} //#region Event Listeners
#navigationListener = (event: CustomEvent<number>) => {
this.#fetch(event.detail, this.#previousSearchValue);
};
#changeListener = (event: CustomEvent<{ value: DualSelectPair[] }>) => {
this.#selected = event.detail.value;
this.selected = this.#selected;
};
#searchListener = (event: CustomEvent<string>) => {
this.#doSearch(event.detail);
};
#searchTimeoutID?: ReturnType<typeof setTimeout>;
#doSearch = (search: string) => {
clearTimeout(this.#searchTimeoutID);
setTimeout(() => {
this.pagination = undefined;
this.#fetch(undefined, search);
}, this.searchDelay);
};
//#endregion
render() { render() {
return html`<ak-dual-select return html`<ak-dual-select
${ref(this.dualSelector)} ${ref(this.dualSelector)}
.options=${this.options} .options=${this.options}
.pages=${this.pagination} .pages=${this.pagination}
.selected=${this.internalSelected} .selected=${this.#selected}
available-label=${this.availableLabel} available-label=${this.availableLabel}
selected-label=${this.selectedLabel} selected-label=${this.selectedLabel}
></ak-dual-select>`; ></ak-dual-select>`;

View File

@ -3,6 +3,7 @@ import {
CustomEmitterElement, CustomEmitterElement,
CustomListenerElement, CustomListenerElement,
} from "@goauthentik/elements/utils/eventEmitter"; } from "@goauthentik/elements/utils/eventEmitter";
import { match } from "ts-pattern";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { PropertyValues, html, nothing } from "lit"; import { PropertyValues, html, nothing } from "lit";
@ -15,34 +16,41 @@ import { globalVariables, mainStyles } from "./components/styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import "./components/ak-dual-select-available-pane"; import "./components/ak-dual-select-available-pane.js";
import { AkDualSelectAvailablePane } from "./components/ak-dual-select-available-pane"; import { AkDualSelectAvailablePane } from "./components/ak-dual-select-available-pane.js";
import "./components/ak-dual-select-controls"; import "./components/ak-dual-select-controls.js";
import "./components/ak-dual-select-selected-pane"; import "./components/ak-dual-select-selected-pane.js";
import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane"; import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane.js";
import "./components/ak-pagination"; import "./components/ak-pagination.js";
import "./components/ak-search-bar"; import "./components/ak-search-bar.js";
import { import {
EVENT_ADD_ALL, BasePagination,
EVENT_ADD_ONE, DualSelectEventType,
EVENT_ADD_SELECTED, DualSelectPair,
EVENT_DELETE_ALL, SearchbarEventDetail,
EVENT_REMOVE_ALL, SearchbarEventSource,
EVENT_REMOVE_ONE, } from "./types.js";
EVENT_REMOVE_SELECTED,
} from "./constants";
import type { BasePagination, DualSelectPair, SearchbarEvent } from "./types";
function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) { function localeComparator(a: DualSelectPair, b: DualSelectPair) {
const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2]; const aSortBy = a[2];
return l < r ? -1 : l > r ? 1 : 0; const bSortBy = b[2];
return aSortBy.localeCompare(bSortBy);
} }
function mapDualPairs(pairs: DualSelectPair[]) { function keyfinder(key: string) {
return new Map(pairs.map(([k, v, _]) => [k, v])); return ([k]: DualSelectPair) => k === key;
} }
const styles = [PFBase, PFButton, globalVariables, mainStyles]; const DelegatedEvents = [
DualSelectEventType.AddSelected,
DualSelectEventType.RemoveSelected,
DualSelectEventType.AddAll,
DualSelectEventType.RemoveAll,
DualSelectEventType.DeleteAll,
DualSelectEventType.AddOne,
DualSelectEventType.RemoveOne,
] as const satisfies DualSelectEventType[];
/** /**
* @element ak-dual-select * @element ak-dual-select
@ -53,24 +61,25 @@ const styles = [PFBase, PFButton, globalVariables, mainStyles];
* *
* @fires ak-dual-select-change - A custom change event with the current `selected` list. * @fires ak-dual-select-change - A custom change event with the current `selected` list.
*/ */
const keyfinder =
(key: string) =>
([k]: DualSelectPair) =>
k === key;
@customElement("ak-dual-select") @customElement("ak-dual-select")
export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) { export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) {
static get styles() { static styles = [PFBase, PFButton, globalVariables, mainStyles];
return styles;
}
/* The list of options to *currently* show. Note that this is not *all* the options, only the //#region Properties
* currently shown list of options from a pagination collection. */
/**
* The list of options to *currently* show.
*
* Note that this is not *all* the options,
* only the currently shown list of options from a pagination collection.
*/
@property({ type: Array }) @property({ type: Array })
options: DualSelectPair[] = []; options: DualSelectPair[] = [];
/* The list of options selected. This is the *entire* list and will not be paginated. */ /**
* The list of options selected.
* This is the *entire* list and will not be paginated.
*/
@property({ type: Array }) @property({ type: Array })
selected: DualSelectPair[] = []; selected: DualSelectPair[] = [];
@ -83,138 +92,133 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
@property({ attribute: "selected-label" }) @property({ attribute: "selected-label" })
selectedLabel = msg("Selected options"); selectedLabel = msg("Selected options");
//#endregion
//#region State
@state() @state()
selectedFilter: string = ""; protected selectedFilter: string = "";
#selectedKeys: Set<string> = new Set();
//#endregion
//#region Refs
availablePane: Ref<AkDualSelectAvailablePane> = createRef(); availablePane: Ref<AkDualSelectAvailablePane> = createRef();
selectedPane: Ref<AkDualSelectSelectedPane> = createRef(); selectedPane: Ref<AkDualSelectSelectedPane> = createRef();
selectedKeys: Set<string> = new Set(); //#endregion
//#region Lifecycle
constructor() { constructor() {
super(); super();
this.handleMove = this.handleMove.bind(this);
this.handleSearch = this.handleSearch.bind(this); for (const eventName of DelegatedEvents) {
[ this.addCustomListener(eventName, this.#moveListener);
EVENT_ADD_ALL, }
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_SELECTED,
EVENT_ADD_ONE,
EVENT_REMOVE_ONE,
].forEach((eventName: string) => {
this.addCustomListener(eventName, (event: Event) => this.handleMove(eventName, event));
});
this.addCustomListener("ak-dual-select-move", () => { this.addCustomListener("ak-dual-select-move", () => {
this.requestUpdate(); this.requestUpdate();
}); });
this.addCustomListener("ak-search", this.handleSearch);
this.addCustomListener("ak-search", this.#searchListener);
} }
willUpdate(changedProperties: PropertyValues<this>) { willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected")) { if (changedProperties.has("selected")) {
this.selectedKeys = new Set(this.selected.map(([key, _]) => key)); this.#selectedKeys = new Set(this.selected.map(([key]) => key));
} }
// Pagination invalidates available moveables. // Pagination invalidates available moveables.
if (changedProperties.has("options") && this.availablePane.value) { if (changedProperties.has("options") && this.availablePane.value) {
this.availablePane.value.clearMove(); this.availablePane.value.clearMove();
} }
} }
handleMove(eventName: string, event: Event) { //#endregion
if (!(event instanceof CustomEvent)) {
throw new Error(`Expected move event here, got ${eventName}`);
}
switch (eventName) { //#region Event Listeners
case EVENT_ADD_SELECTED: {
this.addSelected(); #moveListener = (event: CustomEvent<string>) => {
break; match(event.type)
} .with(DualSelectEventType.AddSelected, () => this.addSelected())
case EVENT_REMOVE_SELECTED: { .with(DualSelectEventType.RemoveSelected, () => this.removeSelected())
this.removeSelected(); .with(DualSelectEventType.AddAll, () => this.addAllVisible())
break; .with(DualSelectEventType.RemoveAll, () => this.removeAllVisible())
} .with(DualSelectEventType.DeleteAll, () => this.removeAll())
case EVENT_ADD_ALL: { .with(DualSelectEventType.AddOne, () => this.addOne(event.detail))
this.addAllVisible(); .with(DualSelectEventType.RemoveOne, () => this.removeOne(event.detail))
break; .otherwise(() => {
} throw new Error(`Expected move event here, got ${event.type}`);
case EVENT_REMOVE_ALL: { });
this.removeAllVisible();
break; this.dispatchCustomEvent(DualSelectEventType.Change, { value: this.value });
}
case EVENT_DELETE_ALL: {
this.removeAll();
break;
}
case EVENT_ADD_ONE: {
this.addOne(event.detail);
break;
}
case EVENT_REMOVE_ONE: {
this.removeOne(event.detail);
break;
}
default:
throw new Error(
`AkDualSelect.handleMove received unknown event type: ${eventName}`,
);
}
this.dispatchCustomEvent("ak-dual-select-change", { value: this.value });
event.stopPropagation(); event.stopPropagation();
} };
protected addSelected() {
if (this.availablePane.value!.moveable.length === 0) return;
addSelected() {
if (this.availablePane.value!.moveable.length === 0) {
return;
}
this.selected = this.availablePane.value!.moveable.reduce( this.selected = this.availablePane.value!.moveable.reduce(
(acc, key) => { (acc, key) => {
const value = this.options.find(keyfinder(key)); const value = this.options.find(keyfinder(key));
return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc; return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc;
}, },
[...this.selected], [...this.selected],
); );
// This is where the information gets... lossy. Dammit. // This is where the information gets... lossy. Dammit.
this.availablePane.value!.clearMove(); this.availablePane.value!.clearMove();
} }
addOne(key: string) { protected addOne(key: string) {
const requested = this.options.find(keyfinder(key)); const requested = this.options.find(keyfinder(key));
if (requested && !this.selected.find(keyfinder(requested[0]))) {
this.selected = [...this.selected, requested]; if (!requested) return;
} if (this.selected.find(keyfinder(requested[0]))) return;
this.selected = [...this.selected, requested];
} }
// These are the *currently visible* options; the parent node is responsible for paginating and // These are the *currently visible* options; the parent node is responsible for paginating and
// updating the list of currently visible options; // updating the list of currently visible options;
addAllVisible() { protected addAllVisible() {
// Create a new array of all current options and selected, and de-dupe. // Create a new array of all current options and selected, and de-dupe.
const selected = mapDualPairs([...this.options, ...this.selected]); const selected = new Map<string, DualSelectPair>([
this.selected = Array.from(selected.entries()); ...this.options.map((pair) => [pair[0], pair] as const),
...this.selected.map((pair) => [pair[0], pair] as const),
]);
this.selected = Array.from(selected.values());
this.availablePane.value!.clearMove(); this.availablePane.value!.clearMove();
} }
removeSelected() { protected removeSelected() {
if (this.selectedPane.value!.moveable.length === 0) { if (this.selectedPane.value!.moveable.length === 0) return;
return;
}
const deselected = new Set(this.selectedPane.value!.moveable); const deselected = new Set(this.selectedPane.value!.moveable);
this.selected = this.selected.filter(([key]) => !deselected.has(key)); this.selected = this.selected.filter(([key]) => !deselected.has(key));
this.selectedPane.value!.clearMove(); this.selectedPane.value!.clearMove();
} }
removeOne(key: string) { protected removeOne(key: string) {
this.selected = this.selected.filter(([k]) => k !== key); this.selected = this.selected.filter(([k]) => k !== key);
} }
removeAllVisible() { protected removeAllVisible() {
// Remove all the items from selected that are in the *currently visible* options list // Remove all the items from selected that are in the *currently visible* options list
const options = new Set(this.options.map(([k, _]) => k)); const options = new Set(this.options.map(([k]) => k));
this.selected = this.selected.filter(([k]) => !options.has(k)); this.selected = this.selected.filter(([k]) => !options.has(k));
this.selectedPane.value!.clearMove(); this.selectedPane.value!.clearMove();
} }
@ -223,24 +227,25 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
this.selectedPane.value!.clearMove(); this.selectedPane.value!.clearMove();
} }
handleSearch(event: SearchbarEvent) { #searchListener = (event: CustomEvent<SearchbarEventDetail>) => {
switch (event.detail.source) { const { source, value } = event.detail;
case "ak-dual-list-available-search":
return this.handleAvailableSearch(event.detail.value); match(source)
case "ak-dual-list-selected-search": .with(SearchbarEventSource.Available, () => {
return this.handleSelectedSearch(event.detail.value); this.dispatchCustomEvent(DualSelectEventType.Search, value);
} })
.with(SearchbarEventSource.Selected, () => {
this.selectedFilter = value;
this.selectedPane.value!.clearMove();
})
.exhaustive();
event.stopPropagation(); event.stopPropagation();
} };
handleAvailableSearch(value: string) { //#endregion
this.dispatchCustomEvent("ak-dual-select-search", value);
}
handleSelectedSearch(value: string) { //#region Public Getters
this.selectedFilter = value;
this.selectedPane.value!.clearMove();
}
get value() { get value() {
return this.selected; return this.selected;
@ -251,7 +256,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
// added. // added.
const allMoved = const allMoved =
this.options.length === this.options.length ===
this.options.filter(([key, _]) => this.selectedKeys.has(key)).length; this.options.filter(([key, _]) => this.#selectedKeys.has(key)).length;
return this.options.length > 0 && !allMoved; return this.options.length > 0 && !allMoved;
} }
@ -259,7 +264,8 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
get canRemoveAll() { get canRemoveAll() {
// False if no visible option can be found in the selected list // False if no visible option can be found in the selected list
return ( return (
this.options.length > 0 && !!this.options.find(([key, _]) => this.selectedKeys.has(key)) this.options.length > 0 &&
!!this.options.find(([key, _]) => this.#selectedKeys.has(key))
); );
} }
@ -267,6 +273,10 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
return (this.pages?.next ?? 0) > 0 || (this.pages?.previous ?? 0) > 0; return (this.pages?.next ?? 0) > 0 || (this.pages?.previous ?? 0) > 0;
} }
//#endregion
//#region Render
render() { render() {
const selected = const selected =
this.selectedFilter === "" this.selectedFilter === ""
@ -282,11 +292,15 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
const availableCount = this.availablePane.value?.toMove.size ?? 0; const availableCount = this.availablePane.value?.toMove.size ?? 0;
const selectedCount = this.selectedPane.value?.toMove.size ?? 0; const selectedCount = this.selectedPane.value?.toMove.size ?? 0;
const selectedTotal = selected.length; const selectedTotal = selected.length;
const availableStatus = const availableStatus =
availableCount > 0 ? msg(str`${availableCount} item(s) marked to add.`) : "&nbsp;"; availableCount > 0 ? msg(str`${availableCount} item(s) marked to add.`) : "&nbsp;";
const selectedTotalStatus = msg(str`${selectedTotal} item(s) selected.`); const selectedTotalStatus = msg(str`${selectedTotal} item(s) selected.`);
const selectedCountStatus = const selectedCountStatus =
selectedCount > 0 ? " " + msg(str`${selectedCount} item(s) marked to remove.`) : ""; selectedCount > 0 ? " " + msg(str`${selectedCount} item(s) marked to remove.`) : "";
const selectedStatus = `${selectedTotalStatus} ${selectedCountStatus}`; const selectedStatus = `${selectedTotalStatus} ${selectedCountStatus}`;
return html` return html`
@ -310,7 +324,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
<ak-dual-select-available-pane <ak-dual-select-available-pane
${ref(this.availablePane)} ${ref(this.availablePane)}
.options=${this.options} .options=${this.options}
.selected=${this.selectedKeys} .selected=${this.#selectedKeys}
></ak-dual-select-available-pane> ></ak-dual-select-available-pane>
${this.needPagination ${this.needPagination
? html`<ak-pagination .pages=${this.pages}></ak-pagination>` ? html`<ak-pagination .pages=${this.pages}></ak-pagination>`
@ -344,12 +358,14 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
<ak-dual-select-selected-pane <ak-dual-select-selected-pane
${ref(this.selectedPane)} ${ref(this.selectedPane)}
.selected=${selected.toSorted(alphaSort)} .selected=${selected.toSorted(localeComparator)}
></ak-dual-select-selected-pane> ></ak-dual-select-selected-pane>
</div> </div>
</div> </div>
`; `;
} }
//#endregion
} }
declare global { declare global {

View File

@ -1,26 +1,24 @@
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html, nothing } from "lit"; import { PropertyValues, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js"; import { map } from "lit/directives/map.js";
import { createRef, ref } from "lit/directives/ref.js";
import { availablePaneStyles, listStyles } from "./styles.css"; import { availablePaneStyles, listStyles } from "./styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_ADD_ONE } from "../constants"; import { DualSelectEventType, DualSelectPair } from "../types.js";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles];
const hostAttributes = [ const hostAttributes = [
["aria-labelledby", "dual-list-selector-available-pane-status"], ["aria-labelledby", "dual-list-selector-available-pane-status"],
["aria-multiselectable", "true"], ["aria-multiselectable", "true"],
["role", "listbox"], ["role", "listbox"],
]; ] as const satisfies Array<[string, string]>;
/** /**
* @element ak-dual-select-available-panel * @element ak-dual-select-available-panel
@ -37,81 +35,109 @@ const hostAttributes = [
* *
* It is not expected that the `ak-dual-select-available-move-changed` event will be used; instead, * It is not expected that the `ak-dual-select-available-move-changed` event will be used; instead,
* the attribute will be read by the parent when a control is clicked. * the attribute will be read by the parent when a control is clicked.
*
*/ */
@customElement("ak-dual-select-available-pane") @customElement("ak-dual-select-available-pane")
export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEventType>(
static get styles() { AKElement,
return styles; ) {
} static styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles];
//#region Properties
/* The array of key/value pairs this pane is currently showing */ /* The array of key/value pairs this pane is currently showing */
@property({ type: Array }) @property({ type: Array })
readonly options: DualSelectPair[] = []; readonly options: DualSelectPair[] = [];
/* A set (set being easy for lookups) of keys with all the pairs selected, so that the ones /**
* currently being shown that have already been selected can be marked and their clicks ignored. * A set (set being easy for lookups) of keys with all the pairs selected,
* * so that the ones currently being shown that have already been selected
* can be marked and their clicks ignored.
*/ */
@property({ type: Object }) @property({ type: Object })
readonly selected: Set<string> = new Set(); readonly selected: Set<string> = new Set();
/* This is the only mutator for this object. It collects the list of objects the user has //#endregion
* clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent
* orchestrator for the dual-select widget can and will access it to get the list of keys to be //#region State
* moved (removed) if the user so requests.
/**
* This is the only mutator for this object.
* It collects the list of objects the user has clicked on *in this pane*.
* *
* It is explicitly marked as "public" to emphasize that the parent orchestrator
* for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests.
*/ */
@state() @state()
public toMove: Set<string> = new Set(); public toMove: Set<string> = new Set();
constructor() { //#endregion
super();
this.onClick = this.onClick.bind(this); //#region Refs
this.onMove = this.onMove.bind(this);
} protected listRef = createRef<HTMLDivElement>();
//#region Lifecycle
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
for (const [attr, value] of hostAttributes) {
if (!this.hasAttribute(attr)) { if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value); this.setAttribute(attr, value);
} }
}); }
} }
clearMove() { protected updated(changed: PropertyValues<this>) {
if (changed.has("options")) {
this.listRef.value?.scrollTo(0, 0);
}
}
//#region Public API
public clearMove() {
this.toMove = new Set(); this.toMove = new Set();
} }
onClick(key: string) {
if (this.selected.has(key)) {
return;
}
if (this.toMove.has(key)) {
this.toMove.delete(key);
} else {
this.toMove.add(key);
}
this.dispatchCustomEvent(
"ak-dual-select-available-move-changed",
Array.from(this.toMove.values()).sort(),
);
this.dispatchCustomEvent("ak-dual-select-move");
// Necessary because updating a map won't trigger a state change
this.requestUpdate();
}
onMove(key: string) {
this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_ADD_ONE, key);
this.requestUpdate();
}
get moveable() { get moveable() {
return Array.from(this.toMove.values()); return Array.from(this.toMove.values());
} }
//#endregion
//#region Event Listeners
#clickListener(key: string): void {
if (this.selected.has(key)) return;
if (this.toMove.has(key)) {
this.toMove.delete(key);
} else {
this.toMove.add(key);
}
this.dispatchCustomEvent(
DualSelectEventType.MoveChanged,
Array.from(this.toMove.values()).sort(),
);
this.dispatchCustomEvent(DualSelectEventType.Move);
// Necessary because updating a map won't trigger a state change
this.requestUpdate();
}
#moveListener(key: string): void {
this.toMove.delete(key);
this.dispatchCustomEvent(DualSelectEventType.AddOne, key);
this.requestUpdate();
}
//#region Render
// DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and // DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and
// will not re-arrange or reconstruct the list automatically if the actual sources do not // will not re-arrange or reconstruct the list automatically if the actual sources do not
// change; this allows the available pane to illustrate selected items with the checkmark // change; this allows the available pane to illustrate selected items with the checkmark
@ -119,17 +145,18 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
render() { render() {
return html` return html`
<div class="pf-c-dual-list-selector__menu"> <div ${ref(this.listRef)} class="pf-c-dual-list-selector__menu">
<ul class="pf-c-dual-list-selector__list"> <ul class="pf-c-dual-list-selector__list">
${map(this.options, ([key, label]) => { ${map(this.options, ([key, label]) => {
const selected = classMap({ const selected = classMap({
"pf-m-selected": this.toMove.has(key), "pf-m-selected": this.toMove.has(key),
}); });
return html` <li return html` <li
class="pf-c-dual-list-selector__list-item" class="pf-c-dual-list-selector__list-item"
aria-selected="false" aria-selected="false"
@click=${() => this.onClick(key)} @click=${() => this.#clickListener(key)}
@dblclick=${() => this.onMove(key)} @dblclick=${() => this.#moveListener(key)}
role="option" role="option"
data-ak-key=${key} data-ak-key=${key}
tabindex="-1" tabindex="-1"
@ -154,6 +181,8 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
</div> </div>
`; `;
} }
//#endregion
} }
export default AkDualSelectAvailablePane; export default AkDualSelectAvailablePane;

View File

@ -8,34 +8,7 @@ import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import { DualSelectEventType } from "../types.js";
EVENT_ADD_ALL,
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_SELECTED,
} from "../constants";
const styles = [
PFBase,
PFButton,
css`
:host {
align-self: center;
padding-right: var(--pf-c-dual-list-selector__controls--PaddingRight);
padding-left: var(--pf-c-dual-list-selector__controls--PaddingLeft);
}
.pf-c-dual-list-selector {
max-width: 4rem;
}
.ak-dual-list-selector__controls {
display: grid;
justify-content: center;
align-content: center;
height: 100%;
}
`,
];
/** /**
* @element ak-dual-select-controls * @element ak-dual-select-controls
@ -43,64 +16,84 @@ const styles = [
* The "control box" for a dual-list multi-select. It's controlled by the parent orchestrator as to * The "control box" for a dual-list multi-select. It's controlled by the parent orchestrator as to
* whether or not any of its controls are enabled. It sends a variety of messages to the parent * whether or not any of its controls are enabled. It sends a variety of messages to the parent
* orchestrator which will then reconcile the "available" and "selected" panes at need. * orchestrator which will then reconcile the "available" and "selected" panes at need.
*
*/ */
@customElement("ak-dual-select-controls") @customElement("ak-dual-select-controls")
export class AkDualSelectControls extends CustomEmitterElement(AKElement) { export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static get styles() { static styles = [
return styles; PFBase,
} PFButton,
css`
:host {
align-self: center;
padding-right: var(--pf-c-dual-list-selector__controls--PaddingRight);
padding-left: var(--pf-c-dual-list-selector__controls--PaddingLeft);
}
.pf-c-dual-list-selector {
max-width: calc(var(--pf-global--spacer-md, 1rem) * 4);
}
.ak-dual-list-selector__controls {
display: grid;
justify-content: center;
align-content: center;
height: 100%;
}
`,
];
/* Set to true if any *visible* elements can be added to the selected list /**
* Set to true if any *visible* elements can be added to the selected list.
*/ */
@property({ attribute: "add-active", type: Boolean }) @property({ attribute: "add-active", type: Boolean })
addActive = false; addActive = false;
/* Set to true if any elements can be removed from the selected list (essentially, /**
* Set to true if any elements can be removed from the selected list (essentially,
* if the selected list is not empty) * if the selected list is not empty)
*/ */
@property({ attribute: "remove-active", type: Boolean }) @property({ attribute: "remove-active", type: Boolean })
removeActive = false; removeActive = false;
/* Set to true if *all* the currently visible elements can be moved /**
* Set to true if *all* the currently visible elements can be moved
* into the selected list (essentially, if any visible elements are * into the selected list (essentially, if any visible elements are
* not currently selected) * not currently selected).
*/ */
@property({ attribute: "add-all-active", type: Boolean }) @property({ attribute: "add-all-active", type: Boolean })
addAllActive = false; addAllActive = false;
/* Set to true if *any* of the elements currently visible in the available /**
* Set to true if *any* of the elements currently visible in the available
* pane are available to be moved to the selected list, enabling that * pane are available to be moved to the selected list, enabling that
* all of those specific elements be moved out of the selected list * all of those specific elements be moved out of the selected list.
*/ */
@property({ attribute: "remove-all-active", type: Boolean }) @property({ attribute: "remove-all-active", type: Boolean })
removeAllActive = false; removeAllActive = false;
/* if deleteAll is enabled, set to true to show that there are elements in the /**
* if deleteAll is enabled, set to true to show that there are elements in the
* selected list that can be deleted. * selected list that can be deleted.
*/ */
@property({ attribute: "delete-all-active", type: Boolean }) @property({ attribute: "delete-all-active", type: Boolean })
enableDeleteAll = false; enableDeleteAll = false;
/* Set to true if you want the `...AllActive` buttons made available. */ /**
* Set to true if you want the `...AllActive` buttons made available.
*/
@property({ attribute: "enable-select-all", type: Boolean }) @property({ attribute: "enable-select-all", type: Boolean })
selectAll = false; selectAll = false;
/* Set to true if you want the `ClearAllSelected` button made available */ /**
* Set to true if you want the `ClearAllSelected` button made available
*/
@property({ attribute: "enable-delete-all", type: Boolean }) @property({ attribute: "enable-delete-all", type: Boolean })
deleteAll = false; deleteAll = false;
constructor() { renderButton(
super(); label: string,
this.onClick = this.onClick.bind(this); eventType: DualSelectEventType,
} active: boolean,
direction: string,
onClick(eventName: string) { ) {
this.dispatchCustomEvent(eventName);
}
renderButton(label: string, event: string, active: boolean, direction: string) {
return html` return html`
<div class="pf-c-dual-list-selector__controls-item"> <div class="pf-c-dual-list-selector__controls-item">
<button <button
@ -109,7 +102,7 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
aria-label=${label} aria-label=${label}
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
type="button" type="button"
@click=${() => this.onClick(event)} @click=${() => this.dispatchCustomEvent(eventType)}
data-ouia-component-type="AK/Button" data-ouia-component-type="AK/Button"
> >
<i class="fa ${direction}"></i> <i class="fa ${direction}"></i>
@ -123,7 +116,7 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
<div class="ak-dual-list-selector__controls"> <div class="ak-dual-list-selector__controls">
${this.renderButton( ${this.renderButton(
msg("Add"), msg("Add"),
EVENT_ADD_SELECTED, DualSelectEventType.AddSelected,
this.addActive, this.addActive,
"fa-angle-right", "fa-angle-right",
)} )}
@ -131,13 +124,13 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
? html` ? html`
${this.renderButton( ${this.renderButton(
msg("Add All Available"), msg("Add All Available"),
EVENT_ADD_ALL, DualSelectEventType.AddAll,
this.addAllActive, this.addAllActive,
"fa-angle-double-right", "fa-angle-double-right",
)} )}
${this.renderButton( ${this.renderButton(
msg("Remove All Available"), msg("Remove All Available"),
EVENT_REMOVE_ALL, DualSelectEventType.RemoveAll,
this.removeAllActive, this.removeAllActive,
"fa-angle-double-left", "fa-angle-double-left",
)} )}
@ -145,14 +138,14 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
: nothing} : nothing}
${this.renderButton( ${this.renderButton(
msg("Remove"), msg("Remove"),
EVENT_REMOVE_SELECTED, DualSelectEventType.RemoveSelected,
this.removeActive, this.removeActive,
"fa-angle-left", "fa-angle-left",
)} )}
${this.deleteAll ${this.deleteAll
? html`${this.renderButton( ? html`${this.renderButton(
msg("Remove All"), msg("Remove All"),
EVENT_DELETE_ALL, DualSelectEventType.DeleteAll,
this.enableDeleteAll, this.enableDeleteAll,
"fa-times", "fa-times",
)}` )}`

View File

@ -11,16 +11,13 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_REMOVE_ONE } from "../constants"; import { DualSelectEventType, DualSelectPair } from "../types";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
const hostAttributes = [ const hostAttributes = [
["aria-labelledby", "dual-list-selector-selected-pane-status"], ["aria-labelledby", "dual-list-selector-selected-pane-status"],
["aria-multiselectable", "true"], ["aria-multiselectable", "true"],
["role", "listbox"], ["role", "listbox"],
]; ] as const satisfies Array<[string, string]>;
/** /**
* @element ak-dual-select-available-panel * @element ak-dual-select-available-panel
@ -38,68 +35,86 @@ const hostAttributes = [
* *
*/ */
@customElement("ak-dual-select-selected-pane") @customElement("ak-dual-select-selected-pane")
export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) { export class AkDualSelectSelectedPane extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static get styles() { static styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
return styles;
}
/* The array of key/value pairs that are in the selected list. ALL of them. */ //#region Properties
/* The array of key/value pairs that are in the selected list. ALL of them. */
@property({ type: Array }) @property({ type: Array })
readonly selected: DualSelectPair[] = []; readonly selected: DualSelectPair[] = [];
/* //#endregion
* This is the only mutator for this object. It collects the list of objects the user has
* clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent //#region State
* orchestrator for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests. /**
* This is the only mutator for this object.
* It collects the list of objects the user has clicked on *in this pane*.
* *
* It is explicitly marked as "public" to emphasize that the parent orchestrator
* for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests.
*/ */
@state() @state()
public toMove: Set<string> = new Set(); public toMove: Set<string> = new Set();
constructor() { //#endregion
super();
this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
}
connectedCallback() { //#region Lifecycle
public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
for (const [attr, value] of hostAttributes) {
if (!this.hasAttribute(attr)) { if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value); this.setAttribute(attr, value);
} }
}); }
} }
clearMove() { //#endregion
//#region Public API
public clearMove() {
this.toMove = new Set(); this.toMove = new Set();
} }
onClick(key: string) { public get moveable() {
return Array.from(this.toMove.values());
}
//#endregion
//#region Event Listeners
#clickListener = (key: string): void => {
if (this.toMove.has(key)) { if (this.toMove.has(key)) {
this.toMove.delete(key); this.toMove.delete(key);
} else { } else {
this.toMove.add(key); this.toMove.add(key);
} }
this.dispatchCustomEvent( this.dispatchCustomEvent(
"ak-dual-select-selected-move-changed", DualSelectEventType.MoveChanged,
Array.from(this.toMove.values()).sort(), Array.from(this.toMove.values()).sort(),
); );
this.dispatchCustomEvent("ak-dual-select-move"); this.dispatchCustomEvent("ak-dual-select-move");
// Necessary because updating a map won't trigger a state change // Necessary because updating a map won't trigger a state change
this.requestUpdate(); this.requestUpdate();
} };
onMove(key: string) { #moveListener = (key: string): void => {
this.toMove.delete(key); this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_REMOVE_ONE, key);
this.requestUpdate();
}
get moveable() { this.dispatchCustomEvent(DualSelectEventType.RemoveOne, key);
return Array.from(this.toMove.values()); this.requestUpdate();
} };
//#endregion
//#region Render
render() { render() {
return html` return html`
@ -113,8 +128,8 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
class="pf-c-dual-list-selector__list-item" class="pf-c-dual-list-selector__list-item"
aria-selected="false" aria-selected="false"
id="dual-list-selector-basic-selected-pane-list-option-0" id="dual-list-selector-basic-selected-pane-list-option-0"
@click=${() => this.onClick(key)} @click=${() => this.#clickListener(key)}
@dblclick=${() => this.onMove(key)} @dblclick=${() => this.#moveListener(key)}
role="option" role="option"
data-ak-key=${key} data-ak-key=${key}
tabindex="-1" tabindex="-1"
@ -134,6 +149,8 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
</div> </div>
`; `;
} }
//#endregion
} }
export default AkDualSelectSelectedPane; export default AkDualSelectSelectedPane;

View File

@ -9,85 +9,77 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { BasePagination } from "../types"; import { BasePagination, DualSelectEventType } from "../types.js";
const styles = [
PFBase,
PFButton,
PFPagination,
css`
:host([theme="dark"]) .pf-c-pagination__nav-control .pf-c-button {
color: var(--pf-c-button--m-plain--disabled--Color);
--pf-c-button--disabled--Color: var(--pf-c-button--m-plain--Color);
}
:host([theme="dark"]) .pf-c-pagination__nav-control .pf-c-button:disabled {
color: var(--pf-c-button--disabled--Color);
}
`,
];
@customElement("ak-pagination") @customElement("ak-pagination")
export class AkPagination extends CustomEmitterElement(AKElement) { export class AkPagination extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static get styles() { static styles = [
return styles; PFBase,
} PFButton,
PFPagination,
css`
:host([theme="dark"]) {
.pf-c-pagination__nav-control .pf-c-button {
color: var(--pf-c-button--m-plain--disabled--Color);
--pf-c-button--disabled--Color: var(--pf-c-button--m-plain--Color);
}
.pf-c-pagination__nav-control .pf-c-button:disabled {
color: var(--pf-c-button--disabled--Color);
}
}
`,
];
@property({ attribute: false }) @property({ attribute: false })
pages?: BasePagination; pages?: BasePagination;
constructor() { #clickListener = (nav: number = 0) => {
super(); this.dispatchCustomEvent(DualSelectEventType.NavigateTo, nav);
this.onClick = this.onClick.bind(this); };
}
onClick(nav: number | undefined) {
this.dispatchCustomEvent("ak-pagination-nav-to", nav ?? 0);
}
render() { render() {
return this.pages const { pages } = this;
? html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div if (!pages) return nothing;
class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md"
> return html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-options-menu"> <div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain"> <div class="pf-c-options-menu">
<span class="pf-c-options-menu__toggle-text"> <div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
${msg( <span class="pf-c-options-menu__toggle-text">
str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`, ${msg(str`${pages.startIndex} - ${pages.endIndex} of ${pages.count}`)}
)} </span>
</span> </div>
</div> </div>
</div> <nav class="pf-c-pagination__nav" aria-label=${msg("Pagination")}>
<nav class="pf-c-pagination__nav" aria-label=${msg("Pagination")}> <div class="pf-c-pagination__nav-control pf-m-prev">
<div class="pf-c-pagination__nav-control pf-m-prev"> <button
<button class="pf-c-button pf-m-plain"
class="pf-c-button pf-m-plain" @click=${() => {
@click=${() => { this.#clickListener(pages.previous);
this.onClick(this.pages?.previous); }}
}} ?disabled="${(pages.previous ?? 0) < 1}"
?disabled="${(this.pages?.previous ?? 0) < 1}" aria-label="${msg("Go to previous page")}"
aria-label="${msg("Go to previous page")}" >
> <i class="fas fa-angle-left" aria-hidden="true"></i>
<i class="fas fa-angle-left" aria-hidden="true"></i> </button>
</button> </div>
</div> <div class="pf-c-pagination__nav-control pf-m-next">
<div class="pf-c-pagination__nav-control pf-m-next"> <button
<button class="pf-c-button pf-m-plain"
class="pf-c-button pf-m-plain" @click=${() => {
@click=${() => { this.#clickListener(pages.next);
this.onClick(this.pages?.next); }}
}} ?disabled="${(pages.next ?? 0) <= 0}"
?disabled="${(this.pages?.next ?? 0) <= 0}" aria-label="${msg("Go to next page")}"
aria-label="${msg("Go to next page")}" >
> <i class="fas fa-angle-right" aria-hidden="true"></i>
<i class="fas fa-angle-right" aria-hidden="true"></i> </button>
</button> </div>
</div> </nav>
</nav> </div>
</div> </div>`;
</div>`
: nothing;
} }
} }

View File

@ -4,47 +4,45 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit"; import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js"; import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import { globalVariables, searchStyles } from "./search.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { SearchbarEvent } from "../types"; import type { SearchbarEventDetail, SearchbarEventSource } from "../types.ts";
import { globalVariables, searchStyles } from "./search.css.js";
const styles = [PFBase, globalVariables, searchStyles];
@customElement("ak-search-bar") @customElement("ak-search-bar")
export class AkSearchbar extends CustomEmitterElement(AKElement) { export class AkSearchbar extends CustomEmitterElement(AKElement) {
static get styles() { static styles = [PFBase, globalVariables, searchStyles];
return styles;
}
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
value = ""; public value = "";
/** /**
* If you're using more than one search, this token can help listeners distinguishing between * If you're using more than one search, this token can help listeners distinguishing between
* those searches. Lit's own helpers sometimes erase the source and current targets. * those searches. Lit's own helpers sometimes erase the source and current targets.
*/ */
@property({ type: String }) @property({ type: String })
name = ""; public name?: SearchbarEventSource;
input: Ref<HTMLInputElement> = createRef(); protected inputRef = createRef<HTMLInputElement>();
constructor() { #changeListener = () => {
super(); const inputElement = this.inputRef.value;
this.onChange = this.onChange.bind(this);
}
onChange(_event: Event) { if (inputElement) {
if (this.input.value) { this.value = inputElement.value;
this.value = this.input.value.value;
} }
this.dispatchCustomEvent<SearchbarEvent>("ak-search", {
if (!this.name) {
console.warn("ak-search-bar: no name provided, event will not be dispatched");
return;
}
this.dispatchCustomEvent<SearchbarEventDetail>("ak-search", {
source: this.name, source: this.name,
value: this.value, value: this.value,
}); });
} };
render() { render() {
return html` return html`
@ -56,8 +54,8 @@ export class AkSearchbar extends CustomEmitterElement(AKElement) {
><input ><input
type="search" type="search"
class="pf-c-text-input-group__text-input" class="pf-c-text-input-group__text-input"
${ref(this.input)} ${ref(this.inputRef)}
@input=${this.onChange} @input=${this.#changeListener}
value="${this.value}" value="${this.value}"
/></span> /></span>
</div> </div>

View File

@ -1,7 +0,0 @@
export const EVENT_ADD_SELECTED = "ak-dual-select-add";
export const EVENT_REMOVE_SELECTED = "ak-dual-select-remove";
export const EVENT_ADD_ALL = "ak-dual-select-add-all";
export const EVENT_REMOVE_ALL = "ak-dual-select-remove-all";
export const EVENT_DELETE_ALL = "ak-dual-select-remove-everything";
export const EVENT_ADD_ONE = "ak-dual-select-add-one";
export const EVENT_REMOVE_ONE = "ak-dual-select-remove-one";

View File

@ -1,7 +1,7 @@
import { AkDualSelect } from "./ak-dual-select";
import "./ak-dual-select";
import { AkDualSelectProvider } from "./ak-dual-select-provider";
import "./ak-dual-select-provider"; import "./ak-dual-select-provider";
import { AkDualSelectProvider } from "./ak-dual-select-provider.js";
import "./ak-dual-select.js";
import { AkDualSelect } from "./ak-dual-select.js";
export { AkDualSelect, AkDualSelectProvider }; export { AkDualSelect, AkDualSelectProvider };
export default AkDualSelect; export default AkDualSelect;

View File

@ -9,7 +9,7 @@ import { Pagination } from "@goauthentik/api";
import "../ak-dual-select"; import "../ak-dual-select";
import { AkDualSelect } from "../ak-dual-select"; import { AkDualSelect } from "../ak-dual-select";
import type { DualSelectPair } from "../types"; import { DualSelectEventType, type DualSelectPair } from "../types";
const goodForYouRaw = ` const goodForYouRaw = `
Apple, Arrowroot, Artichoke, Arugula, Asparagus, Avocado, Bamboo, Banana, Basil, Beet Root, Apple, Arrowroot, Artichoke, Arugula, Asparagus, Avocado, Bamboo, Banana, Basil, Beet Root,
@ -24,7 +24,8 @@ Rosemary, Rutabaga, Shallot, Soybeans, Spinach, Squash, Strawberries, Sweet pota
Thyme, Tomatillo, Tomato, Turnip, Waterchestnut, Watercress, Watermelon, Yams Thyme, Tomatillo, Tomato, Turnip, Waterchestnut, Watercress, Watermelon, Yams
`; `;
const keyToPair = (key: string): DualSelectPair => [slug(key), key]; const keyToPair = (key: string): DualSelectPair => [slug(key), key, key];
const goodForYou: DualSelectPair[] = goodForYouRaw const goodForYou: DualSelectPair[] = goodForYouRaw
.split("\n") .split("\n")
.join(" ") .join(" ")
@ -83,7 +84,7 @@ export class AkSbFruity extends LitElement {
totalPages: Math.ceil(this.options.length / this.pageLength), totalPages: Math.ceil(this.options.length / this.pageLength),
}; };
this.onNavigation = this.onNavigation.bind(this); this.onNavigation = this.onNavigation.bind(this);
this.addEventListener("ak-pagination-nav-to", this.onNavigation); this.addEventListener(DualSelectEventType.NavigateTo, this.onNavigation);
} }
onNavigation(evt: Event) { onNavigation(evt: Event) {

View File

@ -1,5 +1,4 @@
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
import { debounce } from "@goauthentik/elements/utils/debounce";
import { Meta, StoryObj } from "@storybook/web-components"; import { Meta, StoryObj } from "@storybook/web-components";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
@ -45,20 +44,24 @@ const displayMessage = (result: any) => {
target!.replaceChildren(doc.firstChild!); target!.replaceChildren(doc.firstChild!);
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any const displayMessage2 = (result: string) => {
const displayMessage2 = (result: any) => {
console.debug("Huh."); console.debug("Huh.");
const doc = new DOMParser().parseFromString(`<p><i>Behavior</i>: ${result}</p>`, "text/xml"); const doc = new DOMParser().parseFromString(`<p><i>Behavior</i>: ${result}</p>`, "text/xml");
const target = document.querySelector("#action-button-message-pad-2"); const target = document.querySelector("#action-button-message-pad-2");
target!.replaceChildren(doc.firstChild!); target!.replaceChildren(doc.firstChild!);
}; };
const displayMessage2b = debounce(displayMessage2, 250); let displayMessage2bTimeoutID: ReturnType<typeof setTimeout>;
window.addEventListener("input", (event: Event) => { window.addEventListener("input", (event: Event) => {
const message = (event.target as HTMLInputElement | undefined)?.value ?? "-- undefined --"; const message = (event.target as HTMLInputElement | undefined)?.value ?? "-- undefined --";
displayMessage(message); displayMessage(message);
displayMessage2b(message);
clearTimeout(displayMessage2bTimeoutID);
displayMessage2bTimeoutID = setTimeout(() => {
displayMessage2(message);
}, 250);
}); });
type Story = StoryObj; type Story = StoryObj;

View File

@ -5,6 +5,7 @@ import { TemplateResult, html } from "lit";
import "../components/ak-pagination"; import "../components/ak-pagination";
import { AkPagination } from "../components/ak-pagination"; import { AkPagination } from "../components/ak-pagination";
import { DualSelectEventType } from "../types";
const metadata: Meta<AkPagination> = { const metadata: Meta<AkPagination> = {
title: "Elements / Dual Select / Pagination Control", title: "Elements / Dual Select / Pagination Control",
@ -54,7 +55,7 @@ const handleMoveChanged = (result: any) => {
); );
}; };
window.addEventListener("ak-pagination-nav-to", handleMoveChanged); window.addEventListener(DualSelectEventType.NavigateTo, handleMoveChanged);
type Story = StoryObj; type Story = StoryObj;

View File

@ -12,9 +12,7 @@ import { globalVariables } from "../components/styles.css";
@customElement("sb-dual-select-host-provider") @customElement("sb-dual-select-host-provider")
export class SbHostProvider extends LitElement { export class SbHostProvider extends LitElement {
static get styles() { static styles = globalVariables;
return globalVariables;
}
render() { render() {
return html`<slot></slot>`; return html`<slot></slot>`;

View File

@ -2,19 +2,44 @@ import { TemplateResult } from "lit";
import { Pagination } from "@goauthentik/api"; import { Pagination } from "@goauthentik/api";
// export const DualSelectEventType = {
// - key: string AddSelected: "ak-dual-select-add",
// - label (string or TemplateResult), RemoveSelected: "ak-dual-select-remove",
// - sortBy (optional) string to sort by. If the sort string is Search: "ak-dual-select-search",
// - localMapping: The object the key represents; used by some specific apps. API layers may use AddAll: "ak-dual-select-add-all",
// this as a way to find the preset object. RemoveAll: "ak-dual-select-remove-all",
// DeleteAll: "ak-dual-select-remove-everything",
// Note that this is a *tuple*, not a record or map! AddOne: "ak-dual-select-add-one",
RemoveOne: "ak-dual-select-remove-one",
Move: "ak-dual-select-move",
MoveChanged: "ak-dual-select-available-move-changed",
Change: "ak-dual-select-change",
NavigateTo: "ak-pagination-nav-to",
} as const satisfies Record<string, string>;
export type DualSelectPair<T = never> = [ export type DualSelectEventType = (typeof DualSelectEventType)[keyof typeof DualSelectEventType];
/**
* A tuple representing a single object in the dual select list.
*/
export type DualSelectPair<T = unknown> = [
/**
* The key used to identify the object in the API.
*/
key: string, key: string,
/**
* A human-readable label for the object.
*/
label: string | TemplateResult, label: string | TemplateResult,
sortBy?: string, /**
* A string to sort by. If not provided, the key will be used.
*/
sortBy: string,
/**
* A local mapping of the key to the object. This is used by some specific apps.
*
* API layers may use this as a way to find the preset object.
*/
localMapping?: T, localMapping?: T,
]; ];
@ -30,9 +55,14 @@ export type DataProvision = {
export type DataProvider = (page: number, search?: string) => Promise<DataProvision>; export type DataProvider = (page: number, search?: string) => Promise<DataProvision>;
export const SearchbarEventSource = {
Available: "ak-dual-list-available-search",
Selected: "ak-dual-list-selected-search",
} as const satisfies Record<string, string>;
export type SearchbarEventSource = (typeof SearchbarEventSource)[keyof typeof SearchbarEventSource];
export interface SearchbarEventDetail { export interface SearchbarEventDetail {
source: string; source: SearchbarEventSource;
value: string; value: string;
} }
export type SearchbarEvent = CustomEvent<SearchbarEventDetail>;

View File

@ -27,11 +27,14 @@ import remarkGFM from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter"; import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import remarkParse from "remark-parse"; import remarkParse from "remark-parse";
import { CSSResult, css } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css"; import PFList from "@patternfly/patternfly/components/List/list.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
@ -67,80 +70,96 @@ export class AKMDX extends AKElement {
resolvedHTML = ""; resolvedHTML = "";
static get styles(): CSSResult[] { static styles = [
return [ PFBase,
PFList, PFList,
PFContent, PFTable,
css` PFContent,
a { OneDark,
--pf-global--link--Color: var(--pf-global--link--Color--light); css`
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover); :host {
--pf-global--link--Color--visited: var(--pf-global--link--Color); --ak-mermaid-message-text: var(--pf-c-content--Color);
--ak-table-stripe-background: var(--pf-global--BackgroundColor--light-200);
}
:host([theme="dark"]) {
--ak-mermaid-message-text: var(--ak-dark-foreground);
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter);
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
}
ak-alert + p {
margin-block-start: var(--pf-global--spacer--md);
}
a {
--pf-global--link--Color: var(--pf-global--link--Color--light);
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
--pf-global--link--Color--visited: var(--pf-global--link--Color);
}
/*
Note that order of anchor pseudo-selectors must follow:
1. link
2. visited
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
}
a:visited {
color: var(--pf-global--link--Color--visited);
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
h2:first-of-type {
margin-top: 0;
}
table thead,
table tr:nth-child(2n) {
background-color: var(--ak-table-stripe-background,);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
pre {
overflow-x: auto;
}
pre:has(.hljs) {
padding: var(--pf-global--spacer--md);
}
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
} }
/* .messageText {
Note that order of anchor pseudo-selectors must follow: stroke-width: 4;
1. link fill: var(--ak-mermaid-message-text) !important;
2. visited paint-order: stroke;
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
} }
}
a:visited { `,
color: var(--pf-global--link--Color--visited); ];
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
h2:first-of-type {
margin-top: 0;
}
table thead,
table tr:nth-child(2n) {
background-color: var(
--ak-table-stripe-background,
var(--pf-global--BackgroundColor--light-200)
);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
pre:has(.hljs) {
padding: var(--pf-global--spacer--md);
}
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
}
.messageText {
stroke-width: 4;
fill: var(--ak-mermaid-message-text) !important;
paint-order: stroke;
}
}
`,
];
}
public async connectedCallback() { public async connectedCallback() {
super.connectedCallback(); super.connectedCallback();

View File

@ -16,5 +16,5 @@ export const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, frontmatter })
nextChildren.unshift(<h1 key="header-title">{title}</h1>); nextChildren.unshift(<h1 key="header-title">{title}</h1>);
} }
return <>{nextChildren}</>; return <div className="pf-c-content">{nextChildren}</div>;
}; };

View File

@ -40,7 +40,7 @@ export class FormGroup extends AKElement {
* restructured to allow for this. * restructured to allow for this.
*/ */
.pf-c-form__field-group:has(.pf-c-form__field-group-header:hover) .pf-c-button { .pf-c-form__field-group:has(.pf-c-form__field-group-header:hover) .pf-c-button {
color: var(--pf-global--Color--100) !important; color: var(--pf-c-button--m-plain--hover--Color) !important;
} }
/** /**

View File

@ -6,19 +6,35 @@ import { TemplateResult } from "lit";
export class RouteMatch { export class RouteMatch {
route: Route; route: Route;
arguments: { [key: string]: string }; arguments: { [key: string]: string };
fullUrl?: string; fullURL: string;
constructor(route: Route) { constructor(route: Route, fullUrl: string) {
this.route = route; this.route = route;
this.arguments = {}; this.arguments = {};
this.fullURL = fullUrl;
} }
render(): TemplateResult { render(): TemplateResult {
return this.route.render(this.arguments); return this.route.render(this.arguments);
} }
/**
* Convert the matched Route's URL regex to a sanitized, readable URL by replacing
* all regex values with placeholders according to the name of their regex group.
*
* @returns The sanitized URL for logging/tracing.
*/
sanitizedURL() {
let cleanedURL = this.fullURL;
for (const match of Object.keys(this.arguments)) {
const value = this.arguments[match];
cleanedURL = cleanedURL?.replace(value, `:${match}`);
}
return cleanedURL;
}
toString(): string { toString(): string {
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify( return `<RouteMatch url=${this.sanitizedURL()} route=${this.route} arguments=${JSON.stringify(
this.arguments, this.arguments,
)}>`; )}>`;
} }

View File

@ -3,8 +3,15 @@ import { AKElement } from "@goauthentik/elements/Base";
import { Route } from "@goauthentik/elements/router/Route"; import { Route } from "@goauthentik/elements/router/Route";
import { RouteMatch } from "@goauthentik/elements/router/RouteMatch"; import { RouteMatch } from "@goauthentik/elements/router/RouteMatch";
import "@goauthentik/elements/router/Router404"; import "@goauthentik/elements/router/Router404";
import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getClient,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from "@sentry/browser";
import { Client, Span } from "@sentry/types";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
// Poliyfill for hashchange.newURL, // Poliyfill for hashchange.newURL,
@ -53,6 +60,9 @@ export class RouterOutlet extends AKElement {
@property({ attribute: false }) @property({ attribute: false })
routes: Route[] = []; routes: Route[] = [];
private sentryClient?: Client;
private pageLoadSpan?: Span;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
css` css`
@ -69,6 +79,15 @@ export class RouterOutlet extends AKElement {
constructor() { constructor() {
super(); super();
window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev)); window.addEventListener("hashchange", (ev: HashChangeEvent) => this.navigate(ev));
this.sentryClient = getClient();
if (this.sentryClient) {
this.pageLoadSpan = startBrowserTracingPageLoadSpan(this.sentryClient, {
name: window.location.pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
},
});
}
} }
firstUpdated(): void { firstUpdated(): void {
@ -92,9 +111,8 @@ export class RouterOutlet extends AKElement {
this.routes.some((route) => { this.routes.some((route) => {
const match = route.url.exec(activeUrl); const match = route.url.exec(activeUrl);
if (match !== null) { if (match !== null) {
matchedRoute = new RouteMatch(route); matchedRoute = new RouteMatch(route, activeUrl);
matchedRoute.arguments = match.groups || {}; matchedRoute.arguments = match.groups || {};
matchedRoute.fullUrl = activeUrl;
console.debug("authentik/router: found match ", matchedRoute); console.debug("authentik/router: found match ", matchedRoute);
return true; return true;
} }
@ -107,13 +125,31 @@ export class RouterOutlet extends AKElement {
<ak-router-404 url=${activeUrl}></ak-router-404> <ak-router-404 url=${activeUrl}></ak-router-404>
</div>`; </div>`;
}); });
matchedRoute = new RouteMatch(route); matchedRoute = new RouteMatch(route, activeUrl);
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {}; matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
matchedRoute.fullUrl = activeUrl;
} }
this.current = matchedRoute; this.current = matchedRoute;
} }
updated(changedProperties: PropertyValues<this>): void {
if (!changedProperties.has("current") || !this.current) return;
if (!this.sentryClient) return;
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
if (this.pageLoadSpan) {
this.pageLoadSpan.updateName(this.current.sanitizedURL());
this.pageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, "route");
this.pageLoadSpan = undefined;
} else {
startBrowserTracingNavigationSpan(this.sentryClient, {
op: "navigation",
name: this.current.sanitizedURL(),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route",
},
});
}
}
render(): TemplateResult | undefined { render(): TemplateResult | undefined {
return this.current?.render(); return this.current?.render();
} }

View File

@ -1,21 +1,16 @@
import { import { applyDocumentTheme } from "@goauthentik/common/theme.js";
appendStyleSheet,
assertAdoptableStyleSheetParent,
createStyleSheetUnsafe,
} from "@goauthentik/common/stylesheets.js";
import { TemplateResult, render as litRender } from "lit"; import { TemplateResult, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css"; /**
import PFBase from "@patternfly/patternfly/patternfly-base.css"; * A special version of render that ensures our stylesheets:
*
// A special version of render that ensures our style sheets will always be available * - Will always be available to all elements under test.
// to all elements under test. Ensures they look right during testing, and that any * - Ensure they look right during testing.
// CSS-based checks for visibility will return correct values. * - CSS-based checks for visibility will return correct values.
*/
export const render = (body: TemplateResult) => { export const render = (body: TemplateResult) => {
assertAdoptableStyleSheetParent(document); applyDocumentTheme();
appendStyleSheet(document, ...[PFBase, AKGlobal].map(createStyleSheetUnsafe));
return litRender(body, document.body); return litRender(body, document.body);
}; };

View File

@ -1,6 +1,11 @@
import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit"; import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit";
import "lit"; import "lit";
/**
* Type utility to make readonly properties mutable.
*/
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
/** /**
* A custom element which may be used as a host for a ReactiveController. * A custom element which may be used as a host for a ReactiveController.
* *
@ -8,11 +13,12 @@ import "lit";
* *
* This type is derived from an internal type in Lit. * This type is derived from an internal type in Lit.
*/ */
export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & T> & HTMLElement; export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & Writeable<T>> & HTMLElement;
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement; export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement;
export type LitElementConstructor = new (...args: never[]) => LitElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LitElementConstructor = new (...args: any[]) => LitElement;
/** /**
* A constructor that has been extended with a mixin. * A constructor that has been extended with a mixin.

View File

@ -1,13 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Callback = (...args: any[]) => any;
export function debounce<F extends Callback, T extends object>(callback: F, wait: number) {
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<F>) => {
// @ts-ignore
const context: T = this satisfies object;
if (timeout !== undefined) {
clearTimeout(timeout);
}
timeout = setTimeout(() => callback.apply(context, args), wait);
};
}

View File

@ -1,18 +1,28 @@
import { createMixin } from "@goauthentik/elements/types"; import {
ConstructorWithMixin,
LitElementConstructor,
createMixin,
} from "@goauthentik/elements/types";
import { CustomEventDetail, isCustomEvent } from "@goauthentik/elements/utils/customEvents"; import { CustomEventDetail, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
export interface EmmiterElementHandler { export interface CustomEventEmitterMixin<EventType extends string = string> {
dispatchCustomEvent<T>( dispatchCustomEvent<D extends CustomEventDetail>(
eventName: string, eventType: EventType,
detail?: T extends CustomEvent<infer U> ? U : T, detail?: D,
eventInit?: EventInit, eventInit?: EventInit,
): void; ): void;
} }
export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperClass }) => { export function CustomEmitterElement<
return class EmmiterElementHandler extends SuperClass { EventType extends string = string,
T extends LitElementConstructor = LitElementConstructor,
>(SuperClass: T) {
abstract class CustomEventEmmiter
extends SuperClass
implements CustomEventEmitterMixin<EventType>
{
public dispatchCustomEvent<D extends CustomEventDetail>( public dispatchCustomEvent<D extends CustomEventDetail>(
eventName: string, eventType: string,
detail: D = {} as D, detail: D = {} as D,
eventInit: EventInit = {}, eventInit: EventInit = {},
) { ) {
@ -26,7 +36,7 @@ export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperC
} }
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(eventName, { new CustomEvent(eventType, {
composed: true, composed: true,
bubbles: true, bubbles: true,
...eventInit, ...eventInit,
@ -34,34 +44,20 @@ export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperC
}), }),
); );
} }
}; }
});
/** return CustomEventEmmiter as unknown as ConstructorWithMixin<
* Mixin that enables Lit Elements to handle custom events in a more straightforward manner. T,
* CustomEventEmitterMixin<EventType>
*/ >;
// This is a neat trick: this static "class" is just a namespace for these unique symbols. Because
// of all the constraints on them, they're legal field names in Typescript objects! Which means that
// we can use them as identifiers for internal references in a Typescript class with absolutely no
// risk that a future user who wants a name like 'addHandler' or 'removeHandler' will override any
// of those, either in this mixin or in any class that this is mixed into, past or present along the
// chain of inheritance.
class HK {
public static readonly listenHandlers: unique symbol = Symbol();
public static readonly addHandler: unique symbol = Symbol();
public static readonly removeHandler: unique symbol = Symbol();
public static readonly getHandler: unique symbol = Symbol();
} }
type EventHandler = (ev: CustomEvent) => void; type CustomEventListener<D = unknown> = (ev: CustomEvent<D>) => void;
type EventMap = WeakMap<EventHandler, EventHandler>; type EventMap<D = unknown> = WeakMap<CustomEventListener<D>, CustomEventListener<D>>;
export interface CustomEventTarget { export interface CustomEventTarget<EventType extends string = string> {
addCustomListener(eventName: string, handler: EventHandler): void; addCustomListener<D = unknown>(eventType: EventType, handler: CustomEventListener<D>): void;
removeCustomListener(eventName: string, handler: EventHandler): void; removeCustomListener<D = unknown>(eventType: EventType, handler: CustomEventListener<D>): void;
} }
/** /**
@ -72,11 +68,15 @@ export interface CustomEventTarget {
*/ */
export const CustomListenerElement = createMixin<CustomEventTarget>(({ SuperClass }) => { export const CustomListenerElement = createMixin<CustomEventTarget>(({ SuperClass }) => {
return class ListenerElementHandler extends SuperClass implements CustomEventTarget { return class ListenerElementHandler extends SuperClass implements CustomEventTarget {
private [HK.listenHandlers] = new Map<string, EventMap>(); #listenHandlers = new Map<string, EventMap>();
private [HK.getHandler](eventName: string, handler: EventHandler) { #getListener<D = unknown>(
const internalMap = this[HK.listenHandlers].get(eventName); eventType: string,
return internalMap ? internalMap.get(handler) : undefined; handler: CustomEventListener<D>,
): CustomEventListener<D> | undefined {
const internalMap = this.#listenHandlers.get(eventType) as EventMap<D> | undefined;
return internalMap?.get(handler);
} }
// For every event NAME, we create a WeakMap that pairs the event handler given to us by the // For every event NAME, we create a WeakMap that pairs the event handler given to us by the
@ -85,50 +85,58 @@ export const CustomListenerElement = createMixin<CustomEventTarget>(({ SuperClas
// meanwhile, this allows us to remove it from the event listeners if it's still around // meanwhile, this allows us to remove it from the event listeners if it's still around
// using the original handler's identity as the key. // using the original handler's identity as the key.
// //
private [HK.addHandler]( #addListener<D = unknown>(
eventName: string, eventType: string,
handler: EventHandler, handler: CustomEventListener<D>,
internalHandler: EventHandler, internalHandler: CustomEventListener<D>,
) { ) {
if (!this[HK.listenHandlers].has(eventName)) { let internalMap = this.#listenHandlers.get(eventType) as EventMap<D> | undefined;
this[HK.listenHandlers].set(eventName, new WeakMap());
if (!internalMap) {
internalMap = new WeakMap();
this.#listenHandlers.set(eventType, internalMap as EventMap);
} }
const internalMap = this[HK.listenHandlers].get(eventName);
internalMap.set(handler, internalHandler);
}
#removeListener<D = unknown>(eventType: string, listener: CustomEventListener<D>) {
const internalMap = this.#listenHandlers.get(eventType) as EventMap<D> | undefined;
if (internalMap) { if (internalMap) {
internalMap.set(handler, internalHandler); internalMap.delete(listener);
} }
} }
private [HK.removeHandler](eventName: string, handler: EventHandler) { addCustomListener<D = unknown>(eventType: string, listener: CustomEventListener<D>) {
const internalMap = this[HK.listenHandlers].get(eventName);
if (internalMap) {
internalMap.delete(handler);
}
}
addCustomListener(eventName: string, handler: EventHandler) {
const internalHandler = (event: Event) => { const internalHandler = (event: Event) => {
if (!isCustomEvent(event)) { if (!isCustomEvent<D>(event)) {
console.error( console.error(
`Received a standard event for custom event ${eventName}; event will not be handled.`, `Received a standard event for custom event ${eventType}; event will not be handled.`,
); );
return;
return null;
} }
handler(event);
return listener(event);
}; };
this[HK.addHandler](eventName, handler, internalHandler);
this.addEventListener(eventName, internalHandler); this.#addListener(eventType, listener, internalHandler);
this.addEventListener(eventType, internalHandler);
} }
removeCustomListener(eventName: string, handler: EventHandler) { removeCustomListener<D = unknown>(eventType: string, listener: CustomEventListener<D>) {
const realHandler = this[HK.getHandler](eventName, handler); const realHandler = this.#getListener(eventType, listener);
if (realHandler) { if (realHandler) {
this.removeEventListener( this.removeEventListener(
eventName, eventType,
realHandler as EventListenerOrEventListenerObject, realHandler as EventListenerOrEventListenerObject,
); );
} }
this[HK.removeHandler](eventName, handler);
this.#removeListener<D>(eventType, listener);
} }
}; };
}); });

View File

@ -171,6 +171,7 @@ export class FlowExecutor extends Interface implements StageHost {
} }
constructor() { constructor() {
configureSentry();
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
const inspector = new URL(window.location.toString()).searchParams.get("inspector"); const inspector = new URL(window.location.toString()).searchParams.get("inspector");
@ -237,7 +238,6 @@ export class FlowExecutor extends Interface implements StageHost {
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry();
if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) { if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) {
this.inspectorAvailable = true; this.inspectorAvailable = true;
} }

View File

@ -36,7 +36,7 @@ export const PasswordManagerPrefill: {
totp: undefined, totp: undefined,
}; };
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", { export const OR_LIST_FORMATTERS: Intl.ListFormat = new Intl.ListFormat("default", {
style: "short", style: "short",
type: "disjunction", type: "disjunction",
}); });

View File

@ -1,13 +1,13 @@
// sort-imports-ignore // sort-imports-ignore
import "rapidoc"; import "rapidoc";
import "@goauthentik/elements/ak-locale-context/index.js";
import { CSRFHeaderName } from "@goauthentik/common/api/config"; import { CSRFHeaderName } from "@goauthentik/common/api/middleware.js";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants.js";
import { getCookie } from "@goauthentik/common/utils"; import { getCookie } from "@goauthentik/common/utils.js";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface/Interface.js";
import "@goauthentik/elements/ak-locale-context"; import { DefaultBrand } from "@goauthentik/common/ui/config.js";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { themeImage } from "@goauthentik/elements/utils/images.js";
import { themeImage } from "@goauthentik/elements/utils/images";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";

View File

@ -1,7 +1,7 @@
import { Interface } from "@goauthentik/elements/Interface"; import { LightInterface } from "@goauthentik/elements/Interface";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -10,30 +10,18 @@ import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-loading") @customElement("ak-loading")
export class Loading extends Interface { export class Loading extends LightInterface {
static get styles(): CSSResult[] { static styles = [
return [ PFBase,
PFBase, PFPage,
PFPage, PFSpinner,
PFSpinner, PFEmptyState,
PFEmptyState, css`
css` :host([theme="dark"]) h1 {
:host([theme="dark"]) h1 { color: var(--ak-dark-foreground);
color: var(--ak-dark-foreground); }
} `,
`, ];
];
}
registerContexts(): void {
// Stub function to avoid making API requests for things we don't need. The `Interface` base class loads
// a bunch of data that is used globally by various things, however this is an interface that is shown
// very briefly and we don't need any of that data.
}
async _initCustomCSS(): Promise<void> {
// Stub function to avoid fetching custom CSS.
}
render(): TemplateResult { render(): TemplateResult {
return html` <section return html` <section

View File

@ -281,10 +281,10 @@ export class UserInterface extends AuthenticatedInterface {
me?: SessionUser; me?: SessionUser;
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.fetchConfigurationDetails(); this.fetchConfigurationDetails();
configureSentry(true);
this.toggleNotificationDrawer = this.toggleNotificationDrawer.bind(this); this.toggleNotificationDrawer = this.toggleNotificationDrawer.bind(this);
this.toggleApiDrawer = this.toggleApiDrawer.bind(this); this.toggleApiDrawer = this.toggleApiDrawer.bind(this);
this.fetchConfigurationDetails = this.fetchConfigurationDetails.bind(this); this.fetchConfigurationDetails = this.fetchConfigurationDetails.bind(this);

23
web/types/css.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/**
* @file ESBuild CSS module type definitions.
*/
declare module "*.css" {
import { CSSResult } from "lit";
global {
/**
* A branded type representing a CSS file imported by ESBuild.
*
* While this is a `string`, this is typed as a {@linkcode CSSResult}
* to satisfy LitElement's `static styles` property.
*/
export type CSSModule = CSSResult & { readonly __brand?: string };
}
/**
* The text content of a CSS file imported by ESBuild.
*/
const css: CSSModule;
export default css;
}

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