Compare commits

...

54 Commits

Author SHA1 Message Date
783b1b0a79 web: Clean up version constants. Tidy types 2025-05-19 00:54:28 +02:00
7c69add264 web: Fix issues surrounding Vite/ESBuild types. 2025-05-19 00:42:19 +02:00
248fcd5d7f web: Check JS files. Add types. 2025-05-19 00:38:40 +02:00
2c64e3f9ba web: Fix issue where Storybook cannot resolve styles. 2025-05-17 17:24:23 +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
b448e76db4 web/flows/sfe: fix global background image not being loaded (#14442)
* web/flows/sfe: add initial loading spinner

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

* fix brand-level default flow background not working with SFE and loading original image with full flow interface

https://github.com/goauthentik/authentik/pull/13079#issuecomment-2853357407
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-09 17:58:43 +02:00
f2937bd6dd outposts: fix tmpdir in containers not being set (#14444)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-09 17:57:25 +02:00
53c2e3e77c lifecycle: fix ak dump_config (#14445)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-09 17:56:01 +02:00
7dd62c1f55 lifecycle/aws: bump aws-cdk from 2.1013.0 to 2.1014.0 in /lifecycle/aws (#14436)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1013.0 to 2.1014.0.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1014.0/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1014.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-09 15:50:56 +02:00
33e3510fba website/integrations: update integration template (#14432)
* Updated indentation and service name formatting.

* Angle brackets for mentions of service name

* Update website/integrations/template/service.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Fixes issues with <service name> being read as a tag.

* Update website/integrations/template/service.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-05-09 09:51:11 +01:00
0e5fac2642 website/integrations: fix sonarqube badge (#14434)
Moves the badge to frontmatter.
2025-05-08 20:22:29 +02:00
c53b1fe78a website/integrations: coder: fix period (#14423) 2025-05-08 09:10:39 -05:00
838a7457b2 website: bump the build group in /website with 3 updates (#14427)
Bumps the build group in /website with 3 updates: [@rspack/binding-darwin-arm64](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack), [@rspack/binding-linux-arm64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) and [@rspack/binding-linux-x64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack).


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

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

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

---
updated-dependencies:
- dependency-name: "@rspack/binding-darwin-arm64"
  dependency-version: 1.3.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-arm64-gnu"
  dependency-version: 1.3.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-x64-gnu"
  dependency-version: 1.3.9
  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-08 15:03:06 +02:00
a3c07bc9ff core: bump astral-sh/uv from 0.7.2 to 0.7.3 (#14426)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.7.2 to 0.7.3.
- [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.2...0.7.3)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-08 15:02:00 +02:00
156 changed files with 3571 additions and 3954 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.4.0
current_version = 2025.4.1
tag = 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*))?

View File

@ -200,7 +200,7 @@ jobs:
uses: actions/cache@v4
with:
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
if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web
@ -208,6 +208,7 @@ jobs:
npm ci
make -C .. gen-client-ts
npm run build
npm run build:sfe
- name: run e2e
run: |
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 ./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
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"
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.2 AS uv
FROM ghcr.io/astral-sh/uv:0.7.4 AS uv
# Stage 6: Base python image
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
__version__ = "2025.4.0"
__version__ = "2025.4.1"
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 Value as V
from django.http.request import HttpRequest
from sentry_sdk import get_current_span
from authentik import get_full_version
from authentik.brands.models import Brand
from authentik.lib.sentry import get_http_meta
from authentik.tenants.models import Tenant
_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"""
brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant())
trace = ""
span = get_current_span()
if span:
trace = span.to_traceparent()
return {
"brand": brand,
"footer_links": tenant.footer_links,
"sentry_trace": trace,
"html_meta": {**get_http_meta()},
"version": get_full_version(),
}

View File

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

View File

@ -2,6 +2,7 @@
from django.apps import apps
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 guardian.management import create_anonymous_user
@ -16,6 +17,10 @@ class Command(BaseCommand):
"""Check permissions for all apps"""
for tenant in Tenant.objects.filter(ready=True):
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():
self.stdout.write(f"Checking app {app.name} ({app.label})\n")
create_permissions(app, verbosity=0)

View File

@ -31,7 +31,10 @@ class PickleSerializer:
def loads(self, data):
"""Unpickle data to be loaded from redis"""
return pickle.loads(data) # nosec
try:
return pickle.loads(data) # nosec
except Exception:
return {}
def _migrate_session(

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>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
{% for key, value in html_meta.items %}
<meta name="{{key}}" content="{{ value }}" />
{% endfor %}
</head>
<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."]},
)
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):
"""Test updating a superuser group without permission"""
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"""
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in License.objects.all():
total.internal_users += lic.internal_users
total.external_users += lic.external_users
if lic.is_valid:
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()))
if total.exp == 0:
total.exp = exp_ts
total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags)
return total
@staticmethod

View File

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

View File

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

View File

@ -15,6 +15,7 @@
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" />
<link rel="prefetch" href="{{ flow_background_url }}" />
{% include "base/header_js.html" %}
<style>
html,
@ -22,7 +23,7 @@
height: 100%;
}
body {
background-image: url("{{ flow.background_url }}");
background-image: url("{{ flow_background_url }}");
background-repeat: no-repeat;
background-size: cover;
}

View File

@ -5,9 +5,9 @@
{% block head_before %}
{{ block.super }}
<link rel="prefetch" href="{{ flow.background_url }}" />
<link rel="prefetch" href="{{ flow_background_url }}" />
{% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script>
<script>ShadyDOM = { force: true };</script>
{% endif %}
{% include "base/header_js.html" %}
<script>
@ -21,7 +21,7 @@ window.authentik.flow = {
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style>
:root {
--ak-flow-background: url("{{ flow.background_url }}");
--ak-flow-background: url("{{ flow_background_url }}");
}
</style>
{% endblock %}

View File

@ -13,7 +13,9 @@ class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
kwargs["flow_background_url"] = flow.background_url(self.request)
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

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)
if not pool_options:
pool_options = True
# FIXME: Temporarily force pool to be deactivated.
# See https://github.com/goauthentik/authentik/issues/14320
pool_options = False
db = {
"default": {

View File

@ -17,7 +17,7 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError
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.api import set_tag
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.stdlib import StdlibIntegration
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 websockets.exceptions import WebSocketException
@ -95,6 +96,8 @@ def traces_sampler(sampling_context: dict) -> float:
return 0
if _type == "websocket":
return 0
if CONFIG.get_bool("debug"):
return 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:
return None
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):
"""Test DB Config with pool"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": True,
"sslcert": None,
"sslkey": None,
"sslmode": None,
"sslrootcert": None,
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)
# FIXME: Temporarily force pool to be deactivated.
# See https://github.com/goauthentik/authentik/issues/14320
# def test_db_pool(self):
# """Test DB Config with pool"""
# config = ConfigLoader()
# config.set("postgresql.host", "foo")
# config.set("postgresql.name", "foo")
# config.set("postgresql.user", "foo")
# config.set("postgresql.password", "foo")
# config.set("postgresql.port", "foo")
# config.set("postgresql.test.name", "foo")
# config.set("postgresql.use_pool", True)
# conf = django_db_config(config)
# self.assertEqual(
# conf,
# {
# "default": {
# "ENGINE": "authentik.root.db",
# "HOST": "foo",
# "NAME": "foo",
# "OPTIONS": {
# "pool": True,
# "sslcert": None,
# "sslkey": None,
# "sslmode": None,
# "sslrootcert": None,
# },
# "PASSWORD": "foo",
# "PORT": "foo",
# "TEST": {"NAME": "foo"},
# "USER": "foo",
# "CONN_MAX_AGE": 0,
# "CONN_HEALTH_CHECKS": False,
# "DISABLE_SERVER_SIDE_CURSORS": False,
# }
# },
# )
def test_db_pool_options(self):
"""Test DB Config with pool"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True)
config.set(
"postgresql.pool_options",
base64.b64encode(
dumps(
{
"max_size": 15,
}
).encode()
).decode(),
)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": {
"max_size": 15,
},
"sslcert": None,
"sslkey": None,
"sslmode": None,
"sslrootcert": None,
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)
# def test_db_pool_options(self):
# """Test DB Config with pool"""
# config = ConfigLoader()
# config.set("postgresql.host", "foo")
# config.set("postgresql.name", "foo")
# config.set("postgresql.user", "foo")
# config.set("postgresql.password", "foo")
# config.set("postgresql.port", "foo")
# config.set("postgresql.test.name", "foo")
# config.set("postgresql.use_pool", True)
# config.set(
# "postgresql.pool_options",
# base64.b64encode(
# dumps(
# {
# "max_size": 15,
# }
# ).encode()
# ).decode(),
# )
# conf = django_db_config(config)
# self.assertEqual(
# conf,
# {
# "default": {
# "ENGINE": "authentik.root.db",
# "HOST": "foo",
# "NAME": "foo",
# "OPTIONS": {
# "pool": {
# "max_size": 15,
# },
# "sslcert": None,
# "sslkey": None,
# "sslmode": None,
# "sslrootcert": None,
# },
# "PASSWORD": "foo",
# "PORT": "foo",
# "TEST": {"NAME": "foo"},
# "USER": "foo",
# "CONN_MAX_AGE": 0,
# "CONN_HEALTH_CHECKS": False,
# "DISABLE_SERVER_SIDE_CURSORS": False,
# }
# },
# )

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",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2025.4.0 Blueprint schema",
"title": "authentik 2025.4.1 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
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
command: server
environment:
@ -55,7 +55,7 @@ services:
redis:
condition: service_healthy
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
command: worker
environment:

4
go.mod
View File

@ -5,7 +5,7 @@ go 1.24.0
require (
beryju.io/ldap v0.1.0
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-ldap/ldap/v3 v3.4.11
github.com/go-openapi/runtime v0.28.0
@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
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/oauth2 v0.30.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/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
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.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg=
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/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
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.2025040.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025041.1 h1:GAN6AoTmfnCGgx1SyM07jP4/LR/T3rkTEyShSBd3Co8=
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-20190510104115-cbcb75029529/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())
}
const VERSION = "2025.4.0"
const VERSION = "2025.4.1"

View File

@ -56,6 +56,7 @@ EXPOSE 3389 6636 9300
USER 1000
ENV GOFIPS=1
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/ldap"]

View File

@ -97,6 +97,7 @@ elif [[ "$1" == "test-all" ]]; then
elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE)
elif [[ "$1" == "dump_config" ]]; then
shift
exec python -m authentik.lib.config $@
elif [[ "$1" == "debug" ]]; then
exec sleep infinity

View File

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

View File

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

View File

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

View File

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

View File

@ -76,6 +76,7 @@ EXPOSE 9000 9300 9443
USER 1000
ENV GOFIPS=1
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/proxy"]

View File

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

View File

@ -56,6 +56,7 @@ HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/rac", "healthch
USER 1000
ENV GOFIPS=1
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/rac"]

View File

@ -56,6 +56,7 @@ EXPOSE 1812/udp 9300
USER 1000
ENV GOFIPS=1
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/radius"]

View File

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

View File

@ -1,12 +1,19 @@
"""test default login flow"""
from authentik.blueprints.tests import apply_blueprint
from authentik.flows.models import Flow
from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsLogin(SeleniumTestCase):
"""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()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
@ -23,3 +30,21 @@ class TestFlowsLogin(SeleniumTestCase):
self.login()
self.wait_for_url(self.if_user_url("/library"))
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.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
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.lib.generators import generate_id
RETRIES = int(environ.get("RETRIES", "3"))
IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
def get_docker_tag() -> str:
@ -240,10 +241,30 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root)
return element
def login(self):
"""Do entire login flow and check user afterwards"""
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
def shady_dom(self) -> WebElement:
class wrapper:
def __init__(self, container: WebDriver):
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]").send_keys(
@ -253,8 +274,16 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
Keys.ENTER
)
flow_executor = self.get_shadow_root("ak-flow-executor")
password_stage = self.get_shadow_root("ak-stage-password", flow_executor)
if shadow_dom:
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(
self.user.username
)

321
uv.lock generated
View File

@ -164,7 +164,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2025.4.0"
version = "2025.4.1"
source = { editable = "." }
dependencies = [
{ name = "argon2-cffi" },
@ -265,100 +265,100 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "argon2-cffi" },
{ name = "celery" },
{ name = "channels" },
{ name = "channels-redis" },
{ name = "cryptography" },
{ name = "dacite" },
{ name = "deepmerge" },
{ name = "defusedxml" },
{ name = "django" },
{ name = "django-countries" },
{ name = "django-cte" },
{ name = "django-filter" },
{ name = "django-guardian" },
{ name = "django-model-utils" },
{ name = "django-pglock" },
{ name = "django-prometheus" },
{ name = "django-redis" },
{ name = "django-storages", extras = ["s3"] },
{ name = "argon2-cffi", specifier = "==23.1.0" },
{ name = "celery", specifier = "==5.5.2" },
{ name = "channels", specifier = "==4.2.2" },
{ name = "channels-redis", specifier = "==4.2.1" },
{ name = "cryptography", specifier = "==44.0.3" },
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "django", specifier = "==5.1.9" },
{ name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==1.3.3" },
{ name = "django-filter", specifier = "==25.1" },
{ name = "django-guardian", specifier = "<3.0.0" },
{ name = "django-model-utils", specifier = "==5.0.0" },
{ name = "django-pglock", specifier = "==1.7.2" },
{ name = "django-prometheus", specifier = "==2.3.1" },
{ name = "django-redis", specifier = "==5.4.0" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ 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-guardian" },
{ name = "docker" },
{ name = "drf-orjson-renderer" },
{ name = "drf-spectacular" },
{ name = "dumb-init" },
{ name = "duo-client" },
{ name = "fido2" },
{ name = "flower" },
{ name = "geoip2" },
{ name = "geopy" },
{ name = "google-api-python-client" },
{ name = "gssapi" },
{ name = "gunicorn" },
{ name = "jsonpatch" },
{ name = "jwcrypto" },
{ name = "kubernetes" },
{ name = "ldap3" },
{ name = "lxml" },
{ name = "msgraph-sdk" },
{ name = "djangorestframework-guardian", specifier = "==0.3.0" },
{ name = "docker", specifier = "==7.1.0" },
{ name = "drf-orjson-renderer", specifier = "==1.7.3" },
{ name = "drf-spectacular", specifier = "==0.28.0" },
{ name = "dumb-init", specifier = "==1.2.5.post1" },
{ name = "duo-client", specifier = "==5.5.0" },
{ name = "fido2", specifier = "==1.2.0" },
{ name = "flower", specifier = "==2.0.1" },
{ name = "geoip2", specifier = "==5.1.0" },
{ name = "geopy", specifier = "==2.4.1" },
{ name = "google-api-python-client", specifier = "==2.169.0" },
{ name = "gssapi", specifier = "==1.9.0" },
{ name = "gunicorn", specifier = "==23.0.0" },
{ name = "jsonpatch", specifier = "==1.33" },
{ name = "jwcrypto", specifier = "==1.5.6" },
{ name = "kubernetes", specifier = "==32.0.1" },
{ name = "ldap3", specifier = "==2.9.1" },
{ name = "lxml", specifier = "==5.4.0" },
{ name = "msgraph-sdk", specifier = "==1.30.0" },
{ name = "opencontainers", git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8" },
{ name = "packaging" },
{ name = "paramiko" },
{ name = "psycopg", extras = ["c", "pool"] },
{ name = "pydantic" },
{ name = "pydantic-scim" },
{ name = "pyjwt" },
{ name = "pyrad" },
{ name = "python-kadmin-rs" },
{ name = "pyyaml" },
{ name = "requests-oauthlib" },
{ name = "scim2-filter-parser" },
{ name = "sentry-sdk" },
{ name = "service-identity" },
{ name = "setproctitle" },
{ name = "structlog" },
{ name = "swagger-spec-validator" },
{ name = "tenant-schemas-celery" },
{ name = "twilio" },
{ name = "ua-parser" },
{ name = "unidecode" },
{ name = "packaging", specifier = "==25.0" },
{ name = "paramiko", specifier = "==3.5.1" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.2.9" },
{ name = "pydantic", specifier = "==2.11.4" },
{ name = "pydantic-scim", specifier = "==0.0.8" },
{ name = "pyjwt", specifier = "==2.10.1" },
{ name = "pyrad", specifier = "==2.4" },
{ name = "python-kadmin-rs", specifier = "==0.6.0" },
{ name = "pyyaml", specifier = "==6.0.2" },
{ name = "requests-oauthlib", specifier = "==2.0.0" },
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
{ name = "sentry-sdk", specifier = "==2.28.0" },
{ name = "service-identity", specifier = "==24.2.0" },
{ name = "setproctitle", specifier = "==1.3.6" },
{ name = "structlog", specifier = "==25.3.0" },
{ name = "swagger-spec-validator", specifier = "==3.0.4" },
{ name = "tenant-schemas-celery", specifier = "==4.0.1" },
{ name = "twilio", specifier = "==9.6.1" },
{ name = "ua-parser", specifier = "==1.0.1" },
{ name = "unidecode", specifier = "==1.4.0" },
{ name = "urllib3", specifier = "<3" },
{ name = "uvicorn", extras = ["standard"] },
{ name = "watchdog" },
{ name = "webauthn" },
{ name = "wsproto" },
{ name = "xmlsec" },
{ name = "zxcvbn" },
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.2" },
{ name = "watchdog", specifier = "==6.0.0" },
{ name = "webauthn", specifier = "==2.5.2" },
{ name = "wsproto", specifier = "==1.2.0" },
{ name = "xmlsec", specifier = "==1.3.15" },
{ name = "zxcvbn", specifier = "==4.5.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "aws-cdk-lib" },
{ name = "bandit" },
{ name = "black" },
{ name = "bump2version" },
{ name = "channels", extras = ["daphne"] },
{ name = "codespell" },
{ name = "colorama" },
{ name = "constructs" },
{ name = "coverage", extras = ["toml"] },
{ name = "debugpy" },
{ name = "drf-jsonschema-serializer" },
{ name = "freezegun" },
{ name = "importlib-metadata" },
{ name = "k5test" },
{ name = "pdoc" },
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "pytest-github-actions-annotate-failures" },
{ name = "pytest-randomly" },
{ name = "pytest-timeout" },
{ name = "requests-mock" },
{ name = "ruff" },
{ name = "selenium" },
{ name = "aws-cdk-lib", specifier = "==2.188.0" },
{ name = "bandit", specifier = "==1.8.3" },
{ name = "black", specifier = "==25.1.0" },
{ name = "bump2version", specifier = "==1.0.1" },
{ name = "channels", extras = ["daphne"], specifier = "==4.2.2" },
{ name = "codespell", specifier = "==2.4.1" },
{ name = "colorama", specifier = "==0.4.6" },
{ name = "constructs", specifier = "==10.4.2" },
{ name = "coverage", extras = ["toml"], specifier = "==7.8.0" },
{ name = "debugpy", specifier = "==1.8.14" },
{ name = "drf-jsonschema-serializer", specifier = "==3.0.0" },
{ name = "freezegun", specifier = "==1.5.1" },
{ name = "importlib-metadata", specifier = "==8.6.1" },
{ name = "k5test", specifier = "==0.10.4" },
{ name = "pdoc", specifier = "==15.0.3" },
{ name = "pytest", specifier = "==8.3.5" },
{ name = "pytest-django", specifier = "==4.11.1" },
{ name = "pytest-github-actions-annotate-failures", specifier = "==0.3.0" },
{ name = "pytest-randomly", specifier = "==3.16.0" },
{ name = "pytest-timeout", specifier = "==2.4.0" },
{ name = "requests-mock", specifier = "==1.12.1" },
{ name = "ruff", specifier = "==0.11.9" },
{ name = "selenium", specifier = "==4.32.0" },
]
[[package]]
@ -387,16 +387,16 @@ wheels = [
[[package]]
name = "aws-cdk-asset-awscli-v1"
version = "2.2.231"
version = "2.2.235"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsii" },
{ name = "publication" },
{ 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 = [
{ 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]]
@ -571,30 +571,30 @@ wheels = [
[[package]]
name = "boto3"
version = "1.38.10"
version = "1.38.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/26/c4a2f1c64efb5ae6b47b94cb543282ab5770aa2c4562aba6934af628cf76/boto3-1.38.10.tar.gz", hash = "sha256:af4c78a3faa1a56cbaeb9e06cd5580772138be519fc6e740b81db586d5d1910c", size = 111837, upload-time = "2025-05-06T19:29:55.088Z" }
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 = [
{ url = "https://files.pythonhosted.org/packages/2f/fe/2b69dcdd433c32ba80b36eabfe799e8c3e0b08ff3e0fc06bc2e1cc065a19/boto3-1.38.10-py3-none-any.whl", hash = "sha256:26113a47d549bc3c46dbf56c8ab74f272c3da55df23e2c460fcf3c6c64d54dce", size = 139911, upload-time = "2025-05-06T19:29:51.823Z" },
{ 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]]
name = "botocore"
version = "1.38.10"
version = "1.38.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/18/c03b763c831e269d76a7c0fcba53802f99bf68f8d4530af672ae96a6d343/botocore-1.38.10.tar.gz", hash = "sha256:c531c13803e0fad5b395c5ccab4c11ac88acfccde71c9b998df6fa841392a8fc", size = 13881598, upload-time = "2025-05-06T19:29:41.315Z" }
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 = [
{ url = "https://files.pythonhosted.org/packages/f9/92/2c522e277c95d35b4b83bff6a3839875d91b0d835a93545828a7046013c4/botocore-1.38.10-py3-none-any.whl", hash = "sha256:5244454bb9e8fbb6510145d1554e82fd243e8583507d83077ecf4f8efb66cb46", size = 13539530, upload-time = "2025-05-06T19:29:35.384Z" },
{ 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]]
@ -750,14 +750,14 @@ wheels = [
[[package]]
name = "click"
version = "8.1.8"
version = "8.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ 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 = [
{ 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]]
@ -979,16 +979,16 @@ wheels = [
[[package]]
name = "django"
version = "5.1.8"
version = "5.1.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ 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 = [
{ 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]]
@ -1063,15 +1063,15 @@ wheels = [
[[package]]
name = "django-pglock"
version = "1.7.1"
version = "1.7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ 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 = [
{ 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]]
@ -2065,7 +2065,7 @@ wheels = [
[[package]]
name = "msgraph-sdk"
version = "1.28.0"
version = "1.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-identity" },
@ -2075,9 +2075,9 @@ dependencies = [
{ name = "microsoft-kiota-serialization-text" },
{ 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 = [
{ 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]]
@ -2157,42 +2157,42 @@ source = { git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd679573
[[package]]
name = "opentelemetry-api"
version = "1.32.1"
version = "1.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ 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 = [
{ 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]]
name = "opentelemetry-sdk"
version = "1.32.1"
version = "1.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ 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 = [
{ 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]]
name = "opentelemetry-semantic-conventions"
version = "0.53b1"
version = "0.54b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ 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 = [
{ 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]]
@ -2290,11 +2290,11 @@ wheels = [
[[package]]
name = "platformdirs"
version = "4.3.7"
version = "4.3.8"
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 = [
{ 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]]
@ -2396,14 +2396,14 @@ wheels = [
[[package]]
name = "psycopg"
version = "3.2.7"
version = "3.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ 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 = [
{ 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]
@ -2416,9 +2416,9 @@ pool = [
[[package]]
name = "psycopg-c"
version = "3.2.7"
version = "3.2.9"
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]]
name = "psycopg-pool"
@ -2874,27 +2874,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.11.8"
version = "0.11.9"
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 = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
@ -2940,15 +2940,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
version = "2.27.0"
version = "2.28.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ 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 = [
{ 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]]
@ -3000,11 +3000,11 @@ wheels = [
[[package]]
name = "setuptools"
version = "80.3.1"
version = "80.4.0"
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 = [
{ 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]]
@ -3099,14 +3099,14 @@ wheels = [
[[package]]
name = "tenant-schemas-celery"
version = "3.0.0"
version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ 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 = [
{ 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]]
@ -3160,7 +3160,7 @@ wheels = [
[[package]]
name = "twilio"
version = "9.6.0"
version = "9.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@ -3168,9 +3168,9 @@ dependencies = [
{ name = "pyjwt" },
{ 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 = [
{ 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]]
@ -3487,12 +3487,17 @@ wheels = [
[[package]]
name = "xmlsec"
version = "1.3.14"
version = "1.3.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ 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]]
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",
});

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

@ -0,0 +1,63 @@
/**
* @file Storybook configuration.
* @import { StorybookConfig } from "@storybook/web-components-vite";
* @import { InlineConfig, Plugin } from "vite";
*/
import { createBundleDefinitions } from "@goauthentik/web/scripts/esbuild/environment";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
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: createBundleDefinitions(),
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>
body {
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:types": "wireit",
"lit-analyse": "wireit",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "wireit",
"prettier": "wireit",
"prettier-check": "wireit",
@ -37,7 +36,14 @@
"exports": {
"./package.json": "./package.json",
"./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": {
"@codemirror/lang-css": "^6.3.1",
@ -50,7 +56,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.0-1746018955",
"@goauthentik/api": "^2025.4.1-1747332783",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -106,14 +112,14 @@
"@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0",
"@rollup/plugin-replace": "^6.0.1",
"@storybook/addon-essentials": "^8.3.4",
"@storybook/addon-links": "^8.3.4",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.3.4",
"@storybook/builder-vite": "^8.3.4",
"@storybook/manager-api": "^8.3.4",
"@storybook/web-components": "^8.3.4",
"@storybook/web-components-vite": "^8.3.4",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-links": "^8.6.12",
"@storybook/blocks": "^8.6.12",
"@storybook/experimental-addon-test": "^8.6.12",
"@storybook/manager-api": "^8.6.12",
"@storybook/test": "^8.6.12",
"@storybook/web-components": "^8.6.12",
"@storybook/web-components-vite": "^8.6.12",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
@ -145,9 +151,8 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.3.4",
"storybook": "^8.6.12",
"storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3",
"typescript": "^5.6.2",

View File

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

View File

@ -4,15 +4,20 @@
export {};
declare global {
/**
* Environment variables injected by ESBuild.
*/
interface ImportMetaEnv {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
readonly ESBUILD_WATCHER_URL?: string;
}
interface ImportMeta {
readonly env: {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
ESBUILD_WATCHER_URL: string;
};
readonly env: ImportMetaEnv;
}
}

View File

@ -1,16 +0,0 @@
/**
* @file Constants for JavaScript and TypeScript files.
*/
/// <reference types="../../types/global.js" />
/**
* The current Node.js environment, defaulting to "development" when not set.
*
* Note, this should only be used during the build process.
*
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
* ensure that module tree-shaking works correctly.
*
*/
export const NodeEnvironment = process.env.NODE_ENV || "development";

View File

@ -1,6 +1,20 @@
/**
* @file Utility functions for building and copying files.
* @file Utility functions for working with environment variables.
*/
/// <reference types="./types/global.js" />
//#region Constants
/**
* The current Node.js environment, defaulting to "development" when not set.
*
* Note, this should only be used during the build process.
*
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
* ensure that module tree-shaking works correctly.
*
*/
export const NodeEnvironment = process.env.NODE_ENV || "development";
/**
* A source environment variable, which can be a string, number, boolean, null, or undefined.
@ -14,19 +28,26 @@
* @typedef {T extends string ? `"${T}"` : T} JSONify
*/
//#endregion
//#region Utilities
/**
* Given an object of environment variables, returns a new object with the same keys and values, but
* with the values serialized as strings.
*
* @template {Record<string, EnvironmentVariable>} EnvRecord
* @template {string} [Prefix='process.env.']
* @template {string} [Prefix='import.meta.env.']
*
* @param {EnvRecord} input
* @param {Prefix} [prefix='process.env.']
* @param {Prefix} [prefix='import.meta.env.']
*
* @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify<EnvRecord[K]>}}
*/
export function serializeEnvironmentVars(input, prefix = /** @type {Prefix} */ ("process.env.")) {
export function serializeEnvironmentVars(
input,
prefix = /** @type {Prefix} */ ("import.meta.env."),
) {
/**
* @type {Record<string, string>}
*/
@ -40,3 +61,5 @@ export function serializeEnvironmentVars(input, prefix = /** @type {Prefix} */ (
return /** @type {any} */ (env);
}
//#endregion

View File

@ -1,7 +1,6 @@
/// <reference types="./types/global.js" />
export * from "./paths.js";
export * from "./constants.js";
export * from "./build.js";
export * from "./environment.js";
export * from "./version.js";
export * from "./scripting.js";

View File

@ -47,7 +47,16 @@ class SimpleFlowExecutor {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
loading() {
this.container.innerHTML = `<div class="d-flex justify-content-center">
<div class="spinner-border spinner-border-md" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`;
}
start() {
this.loading();
$.ajax({
type: "GET",
url: this.apiURL,
@ -201,6 +210,9 @@ class PasswordStage extends Stage<PasswordChallenge> {
<form id="password-form">
<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>
<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">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}

View File

@ -1,17 +1,32 @@
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import process from "process";
/**
* @file Lit Localize build script.
*
* @remarks
* Determines if all the Xliff translation source files are present and if the Typescript source files generated from those sources are up-to-date.
*
* If they are not, it runs the locale building script,
* intercepting the long spew of "this string is not translated" and replacing it with a
* summary of how many strings are missing with respect to the source locale.
*
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config"
*/
import { PackageRoot } from "@goauthentik/web/paths";
import { spawnSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";
import path from "node:path";
/**
* Determines if all the Xliff translation source files are present and if the Typescript source
* files generated from those sources are up-to-date. If they are not, it runs the locale building
* script, intercepting the long spew of "this string is not translated" and replacing it with a
* summary of how many strings are missing with respect to the source locale.
* @type {ConfigFile}
*/
const localizeRules = JSON.parse(
readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"),
);
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
/**
*
* @param {string} loc
* @returns {boolean}
*/
function generatedFileIsUpToDateWithXliffSource(loc) {
const xliff = path.join("./xliff", `${loc}.xlf`);
const gened = path.join("./src/locales", `${loc}.ts`);
@ -22,7 +37,7 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
// generates a unique error message and halts the build.
try {
var xlfStat = fs.statSync(xliff);
var xlfStat = statSync(xliff);
} catch (_error) {
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
process.exit(1);
@ -30,7 +45,7 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
// If the generated file doesn't exist, of course it's not up to date.
try {
var genedStat = fs.statSync(gened);
var genedStat = statSync(gened);
} catch (_error) {
return false;
}

View File

@ -1,3 +1,4 @@
/// <reference types="../types/esbuild.js" />
/**
* @file ESBuild script for building the authentik web UI.
*
@ -9,7 +10,6 @@ import {
NodeEnvironment,
readBuildIdentifier,
resolvePackage,
serializeEnvironmentVars,
} from "@goauthentik/monorepo";
import { DistDirectory, DistDirectoryName, EntryPoint, PackageRoot } from "@goauthentik/web/paths";
import { deepmerge } from "deepmerge-ts";
@ -20,15 +20,10 @@ import * as fs from "node:fs/promises";
import * as path from "node:path";
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
import { createBundleDefinitions } from "./esbuild/environment.mjs";
const logPrefix = "[Build]";
const definitions = serializeEnvironmentVars({
NODE_ENV: NodeEnvironment,
CWD: process.cwd(),
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH,
});
const patternflyPath = resolvePackage("@patternfly/patternfly");
/**
@ -86,7 +81,7 @@ const BASE_ESBUILD_OPTIONS = {
root: MonoRepoRoot,
}),
],
define: definitions,
define: createBundleDefinitions(),
format: "esm",
logOverride: {
/**

View File

@ -0,0 +1,29 @@
/**
* @file ESBuild environment utilities.
*/
import { AuthentikVersion, NodeEnvironment, serializeEnvironmentVars } from "@goauthentik/monorepo";
/**
* Creates a mapping of environment variables to their respective runtime constants.
*/
export function createBundleDefinitions() {
const SerializedNodeEnvironment = /** @type {`"development"` | `"production"`} */ (
JSON.stringify(NodeEnvironment)
);
/**
* @satisfies {Record<ESBuildImportEnvKey, string>}
*/
const envRecord = {
AK_VERSION: AuthentikVersion,
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH ?? "",
};
return {
...serializeEnvironmentVars(envRecord),
// We need to explicitly set this for NPM packages that use `process`
// to determine their environment.
"process.env.NODE_ENV": SerializedNodeEnvironment,
"import.meta.env.NODE_ENV": SerializedNodeEnvironment,
};
}

View File

@ -35,6 +35,11 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
const projectRoot = path.join(__dirname, "..");
process.chdir(projectRoot);
/**
*
* @param {string[]} flags
* @returns
*/
const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]);
const [configFile, files] = hasFlag(["-n", "--nightmare"])

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

@ -1,22 +1,36 @@
import { readFileSync } from "fs";
import path from "path";
/**
* @file Pseudo-localization script.
*
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config.js"
* @import { Config } from '@lit/localize-tools/lib/types/config.js';
* @import { ProgramMessage } from "@lit/localize-tools/src/messages.js"
* @import { Locale } from "@lit/localize-tools/src/types/locale.js"
*/
import { PackageRoot } from "@goauthentik/web/paths";
import { readFileSync } from "node:fs";
import path from "node:path";
import pseudolocale from "pseudolocale";
import { fileURLToPath } from "url";
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js";
import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const pseudoLocale = "pseudo-LOCALE";
const pseudoLocale = /** @type {Locale} */ ("pseudo-LOCALE");
const targetLocales = [pseudoLocale];
const baseConfig = JSON.parse(readFileSync(path.join(__dirname, "../lit-localize.json"), "utf-8"));
/**
* @type {ConfigFile}
*/
const baseConfig = JSON.parse(readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"));
// Need to make some internal specifications to satisfy the transformer. It doesn't actually matter
// which Localizer we use (transformer or runtime), because all of the functionality we care about
// is in their common parent class, but I had to pick one. Everything else here is just pure
// exploitation of the lit/localize-tools internals.
/**
* @satisfies {Config}
*/
const config = {
...baseConfig,
baseDir: path.join(__dirname, ".."),
@ -28,6 +42,11 @@ const config = {
resolve: (path) => path,
};
/**
*
* @param {ProgramMessage} message
* @returns
*/
const pseudoMessagify = (message) => ({
name: message.name,
contents: message.contents.map((content) =>
@ -36,7 +55,7 @@ const pseudoMessagify = (message) => ({
});
const localizer = new TransformLitLocalizer(config);
const messages = localizer.extractSourceMessages().messages;
const { messages } = localizer.extractSourceMessages();
const translations = messages.map(pseudoMessagify);
const sorted = sortProgramMessages([...messages]);
const formatter = makeFormatter(config);

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/EmptyState";
@ -45,7 +44,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
}
return [
[msg("Version"), version.versionCurrent],
[msg("UI Version"), VERSION],
[msg("UI Version"), import.meta.env.AK_VERSION],
[msg("Build"), build],
[msg("Python version"), status.runtime.pythonVersion],
[msg("Platform"), status.runtime.platform],

View File

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

View File

@ -8,7 +8,6 @@ import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart";
import "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
@ -22,8 +21,6 @@ import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { when } from "lit/directives/when.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
@ -33,21 +30,17 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api";
export function versionFamily(): string {
const parts = VERSION.split(".");
parts.pop();
return parts.join(".");
function createReleaseNotesURL(semver: string): URL {
const segments = semver.split(".");
const versionFamily = segments.slice(0, -1).join(".");
const release = `${versionFamily}#fixed-in-${segments.join("")}`;
return new URL(`/docs/releases/${release}`, "https://goauthentik.io");
}
const RELEASE = `${VERSION.split(".").slice(0, -1).join(".")}#fixed-in-${VERSION.replaceAll(
".",
"",
)}`;
const AdminOverviewBase = WithLicenseSummary(AKElement);
type Renderer = () => TemplateResult | typeof nothing;
@customElement("ak-admin-overview")
export class AdminOverviewPage extends AdminOverviewBase {
static get styles(): CSSResult[] {
@ -83,7 +76,11 @@ export class AdminOverviewPage extends AdminOverviewBase {
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
[
msg("Check the release notes"),
createReleaseNotesURL(import.meta.env.AK_VERSION).href,
true,
],
];
@state()
@ -193,45 +190,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
</div>`
: nothing} `;
}
renderActions() {
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
const quickActions: [string, string][] = [
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
];
const action = ([label, url]: [string, string]) => {
const isExternal = url.startsWith("https://");
const ex = (truecase: Renderer, falsecase: Renderer) =>
when(isExternal, truecase, falsecase);
const content = html`${label}${ex(
() => html`<i class="fas fa-external-link-alt ak-external-link"></i>`,
() => nothing,
)}`;
return html`<li>
${ex(
() =>
html`<a
href="${url}"
class="pf-u-mb-xl"
rel="noopener noreferrer"
target="_blank"
>${content}</a
>`,
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
)}
</li>`;
};
return html`${map(quickActions, action)}`;
}
}
declare global {

View File

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

View File

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

View File

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

View File

@ -1,18 +1,14 @@
import {
CSRFHeaderName,
CSRFMiddleware,
EventMiddleware,
LoggingMiddleware,
} from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
} from "@goauthentik/common/api/middleware.js";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants.js";
import { globalAK } from "@goauthentik/common/global.js";
import { SentryMiddleware } from "@goauthentik/common/sentry";
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);
export function config(): Promise<Config> {
if (!globalConfigPromise) {
@ -66,21 +62,13 @@ export function brand(): Promise<CurrentBrand> {
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({
basePath: `${globalAK().api.base}api/v3`,
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
},
middleware: [
new CSRFMiddleware(),
new EventMiddleware(),
new LoggingMiddleware(globalAK().brand),
new SentryMiddleware(),
],
});
@ -91,4 +79,6 @@ export function AndNext(url: string): string {
return `?next=${encodeURIComponent(url)}`;
}
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
console.debug(
`authentik(early): version ${import.meta.env.AK_VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`,
);

View File

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

View File

@ -1,17 +1,39 @@
/**
* @file Global constants used throughout the application.
*
* @todo Much of this content can be moved to a specific file, element, or component.
*/
/// <reference types="../../types/esbuild.js" />
//#region Patternfly
export const SECONDARY_CLASS = "pf-m-secondary";
export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2025.4.0";
//#endregion
//#region Application
export const TITLE_DEFAULT = "authentik";
/**
* The delimiter used to parse the URL for the current route.
*
* @todo Move this to the ak-router.
*/
export const ROUTE_SEPARATOR = ";";
//#endregion
//#region Events
export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-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_FLOW_ADVANCE = "ak-flow-advance";
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
@ -21,7 +43,17 @@ export const EVENT_MESSAGE = "ak-message";
export const EVENT_THEME_CHANGE = "ak-theme-change";
export const EVENT_REFRESH_ENTERPRISE = "ak-refresh-enterprise";
//#endregion
//#region WebSocket
export const WS_MSG_TYPE_MESSAGE = "message";
export const WS_MSG_TYPE_REFRESH = "refresh";
//#endregion
//#region LocalStorage
export const LOCALSTORAGE_AUTHENTIK_KEY = "authentik-local-settings";
//#endregion

View File

@ -1,4 +1,3 @@
import { VERSION } from "@goauthentik/common/constants";
import { SentryIgnoredError } from "@goauthentik/common/sentry";
export interface PlexPinResponse {
@ -19,7 +18,7 @@ export const DEFAULT_HEADERS = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Plex-Product": "authentik",
"X-Plex-Version": VERSION,
"X-Plex-Version": import.meta.env.AK_VERSION,
"X-Plex-Device-Vendor": "goauthentik.io",
};

View File

@ -1,5 +1,4 @@
import { config } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { me } from "@goauthentik/common/users";
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import {
@ -10,8 +9,16 @@ import {
setTag,
setUser,
} 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.
@ -21,69 +28,94 @@ export class SentryIgnoredError extends Error {}
export const TAG_SENTRY_COMPONENT = "authentik.component";
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
export async function configureSentry(canDoPpi = false): Promise<Config> {
const cfg = await config();
let _sentryConfigured = false;
if (cfg.errorReporting.enabled) {
init({
dsn: cfg.errorReporting.sentryDsn,
ignoreErrors: [
/network/gi,
/fetch/gi,
/module/gi,
// Error on edge on ios,
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
/instantSearchSDKJSBridgeClearHighlight/gi,
// Seems to be an issue in Safari and Firefox
/MutationObserver.observe/gi,
/NS_ERROR_FAILURE/gi,
],
release: `authentik@${VERSION}`,
export function configureSentry(canDoPpi = false) {
const cfg = globalAK().config;
const debug = cfg.capabilities.includes(CapabilitiesEnum.CanDebug);
if (!cfg.errorReporting.enabled && !debug) {
return cfg;
}
init({
dsn: cfg.errorReporting.sentryDsn,
ignoreErrors: [
/network/gi,
/fetch/gi,
/module/gi,
// Error on edge on ios,
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
/instantSearchSDKJSBridgeClearHighlight/gi,
// Seems to be an issue in Safari and Firefox
/MutationObserver.observe/gi,
/NS_ERROR_FAILURE/gi,
],
release: `authentik@${import.meta.env.AK_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: [
browserTracingIntegration({
shouldCreateSpanForRequest: (url: string) => {
return url.startsWith(window.location.host);
},
Spotlight.sentry({
injectIntoSDK: true,
}),
],
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(","));
if (window.location.pathname.includes("if/")) {
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
}
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
const Spotlight = await import("@spotlightjs/spotlight");
Spotlight.init({ injectImmediately: true });
}
if (cfg.errorReporting.sendPii && canDoPpi) {
me().then((user) => {
setUser({ email: user.user.email });
console.debug("authentik/config: Sentry with PII enabled.");
});
} else {
console.debug("authentik/config: Sentry enabled.");
}
console.debug("authentik/config: Enabled Sentry Spotlight");
}
if (cfg.errorReporting.sendPii && canDoPpi) {
me().then((user) => {
setUser({ email: user.user.email });
console.debug("authentik/config: Sentry with PII enabled.");
});
} else {
console.debug("authentik/config: Sentry enabled.");
}
_sentryConfigured = true;
}
export class SentryMiddleware implements Middleware {
pre?(context: RequestContext): Promise<FetchParams | void> {
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 */
: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
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
/* Defined to better identify the One Dark theme when debugging constructed stylesheets. */
.__HIGHLIGHT_THEME_ONE_DARK__ {
--__HIGHLIGHT_THEME_ONE_DARK__: 1;
}
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
:root {
--one-dark-base: #282c34;
--one-dark-mono-1: #abb2bf;
--one-dark-mono-2: #818896;
--one-dark-mono-3: #5c6370;
--one-dark-hue-1: #56b6c2;
--one-dark-hue-2: #61aeee;
--one-dark-hue-3: #c678dd;
--one-dark-hue-4: #98c379;
--one-dark-hue-5: #e06c75;
--one-dark-hue-5-2: #be5046;
--one-dark-hue-6: #d19a66;
--one-dark-hue-6-2: #e6c07b;
}
.hljs {
color: #abb2bf;
background: #282c34;
color: var(--one-dark-mono-1);
background: var(--one-dark-base);
}
pre:has(.hljs) {
background: #282c34;
background: var(--one-dark-base);
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
color: var(--one-dark-mono-3);
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
color: var(--one-dark-hue-3);
}
.hljs-section,
@ -44,11 +50,11 @@ pre:has(.hljs) {
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
color: var(--one-dark-hue-5);
}
.hljs-literal {
color: #56b6c2;
color: var(--one-dark-hue-1);
}
.hljs-string,
@ -56,7 +62,7 @@ pre:has(.hljs) {
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string {
color: #98c379;
color: var(--one-dark-hue-4);
}
.hljs-attr,
@ -67,7 +73,7 @@ pre:has(.hljs) {
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
color: var(--one-dark-hue-6);
}
.hljs-symbol,
@ -76,13 +82,13 @@ pre:has(.hljs) {
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
color: var(--one-dark-hue-2);
}
.hljs-built_in,
.hljs-title.class_,
.hljs-class .hljs-title {
color: #e6c07b;
color: var(--one-dark-hue-6-2);
}
.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 */
:root {
@ -5,9 +14,6 @@
--ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--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 {
@ -256,8 +262,13 @@ input[type="date"]::-webkit-calendar-picker-indicator {
color: var(--ak-dark-background-lighter);
}
.pf-c-button.pf-m-plain:hover {
color: var(--ak-dark-foreground);
.pf-c-button.pf-m-plain {
--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 {

View File

@ -1,17 +1,27 @@
/**
* @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.
*/
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?
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.
if (!("length" in input.adoptedStyleSheets)) return false;
if (typeof input.adoptedStyleSheets.length !== "number") return false;
// Finally is the array mutable?
return "push" in input.adoptedStyleSheets;
return typeof input.adoptedStyleSheets.length === "number";
}
/**
* Assert that the given input can adopt stylesheets.
*/
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`.
* Create a lazy-loaded `CSSResult` compatible with Lit's
* element lifecycle.
*
* @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.
* 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 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 normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet;
export function normalizeCSSSource(cssResult: CSSResult): CSSResult;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative {
if (typeof input === "string") return createStyleSheet(input);
export function createStyleSheetUnsafe(
input: string | CSSModule | CSSResultOrNative,
): CSSStyleSheet {
const result = typeof input === "string" ? createCSSResult(input) : 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.styleSheet) {
console.debug(
"authentik/common/stylesheets: CSSResult missing styleSheet, returning empty",
{ result, input },
);
if (result.styleSheet) return result.styleSheet;
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(
styleParent: StyleSheetParent,
...insertions: CSSStyleSheet[]
): void {
insertions = Array.isArray(insertions) ? insertions : [insertions];
export function setAdoptedStyleSheets(styleRoot: StyleRoot, styleSheets: StyleSheetsAction): void {
let changed = false;
for (const styleSheetInsertion of insertions) {
if (styleParent.adoptedStyleSheets.includes(styleSheetInsertion)) return;
const currentAdoptedStyleSheets = isStyleRoot(styleRoot)
? [...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);
}
/**
* 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;
}
//#region Debugging
/**
* 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.
*/
export function inspectStyleSheets(styleParent: StyleSheetParent): string[] {
return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
export function inspectStyleSheets(styleRoot: ShadowRoot): string[] {
return styleRoot.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
}
interface InspectedStyleSheetEntry {
@ -174,8 +178,11 @@ interface InspectedStyleSheetEntry {
* Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry {
const styleParent = resolveStyleSheetParent(element.renderRoot);
const styles = inspectStyleSheets(styleParent);
if (!isStyleRoot(element.renderRoot)) {
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 treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, {
@ -186,12 +193,14 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
return NodeFilter.FILTER_SKIP;
},
});
const children: InspectedStyleSheetEntry[] = [];
let currentNode: Node | null = treewalker.nextNode();
while (currentNode) {
const childElement = currentNode as ReactiveElement;
if (!isAdoptableStyleSheetParent(childElement.renderRoot)) {
if (!isStyleRoot(childElement.renderRoot)) {
currentNode = treewalker.nextNode();
continue;
}
@ -214,10 +223,12 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
};
}
if (process.env.NODE_ENV === "development") {
if (import.meta.env.NODE_ENV === "development") {
Object.assign(window, {
inspectStyleSheetTree,
serializeStyleSheet,
inspectStyleSheets,
});
}
//#endregion

View File

@ -1,10 +1,47 @@
/**
* @file Theme utilities.
*/
import { UIConfig } from "@goauthentik/common/ui/config";
import {
type StyleRoot,
createStyleSheetUnsafe,
setAdoptedStyleSheets,
} from "@goauthentik/web/common/stylesheets.js";
import { UIConfig } from "@goauthentik/web/common/ui/config.js";
import AKBase from "@goauthentik/web/common/styles/authentik.css";
import AKBaseDark from "@goauthentik/web/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
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
/**
@ -134,15 +171,21 @@ export function resolveUITheme(
* Effect listener invoked when the color scheme changes.
*/
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.
*/
export function createUIThemeEffect(
effect: UIThemeListener,
listenerOptions?: AddEventListenerOptions,
): () => void {
): UIThemeDestructor {
const colorSchemeTarget = resolveUITheme();
const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget];
@ -174,6 +217,8 @@ export function createUIThemeEffect(
mediaQueryList.removeEventListener("change", changeListener);
};
listenerOptions?.signal?.addEventListener("abort", cleanup);
return cleanup;
}
@ -181,16 +226,96 @@ export function createUIThemeEffect(
//#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.
*/
export interface ThemedElement extends HTMLElement {
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
/**
* The brand information for the current theme.
*/
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;
}
/**
* 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 {
const element = document.body.querySelector<T>("[data-ak-interface-root]");

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js";
import {
EventContext,
@ -76,7 +75,7 @@ ${context.message as string}
**Version and Deployment (please complete the following information):**
- authentik version: ${VERSION}
- authentik version: ${import.meta.env.AK_VERSION}
- Deployment: [e.g. docker-compose, helm]
**Additional context**

View File

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

View File

@ -2,7 +2,7 @@ import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
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 { classMap } from "lit/directives/class-map.js";
@ -64,7 +64,15 @@ export class Alert extends AKElement implements IAlert {
icon = "fa-exclamation-circle";
static get styles() {
return [PFBase, PFAlert];
return [
PFBase,
PFAlert,
css`
p {
margin: 0;
}
`,
];
}
get classmap() {

View File

@ -1,30 +1,24 @@
import { globalAK } from "@goauthentik/common/global";
import { globalAK } from "@goauthentik/common/global.js";
import {
StyleSheetInit,
StyleSheetParent,
appendStyleSheet,
StyleRoot,
createCSSResult,
createStyleSheetUnsafe,
removeStyleSheet,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
} from "@goauthentik/common/stylesheets.js";
import {
$AKBase,
CSSColorSchemeValue,
ResolvedUITheme,
UIThemeListener,
ThemedElement,
applyUITheme,
createUIThemeEffect,
formatColorScheme,
resolveUITheme,
} from "@goauthentik/common/theme";
import { type ThemedElement } from "@goauthentik/common/theme";
} from "@goauthentik/common/theme.js";
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 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";
// Re-export the theme helpers
@ -32,6 +26,58 @@ export { rootInterface } from "@goauthentik/common/theme";
@localized()
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
/**
@ -53,87 +99,54 @@ export class AKElement extends LitElement implements ThemedElement {
//#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;
/**
* The style root to which the theme is applied.
*/
#styleRoot?: StyleRoot;
//#endregion
//#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();
protected set styleRoot(nextStyleRoot: StyleRoot | undefined) {
this.#themeAbortController?.abort();
}
#styleRoot?: StyleSheetParent;
this.#styleRoot = nextStyleRoot;
#dispatchTheme: UIThemeListener = (nextUITheme) => {
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);
}
if (!nextStyleRoot) return;
this.#themeAbortController = new AbortController();
if (this.#preferredColorScheme === "dark") {
this.#dispatchTheme(UiThemeEnum.Dark);
} else if (this.#preferredColorScheme === "auto") {
createUIThemeEffect(this.#dispatchTheme, {
signal: this.#themeAbortController.signal,
});
}
if (this.preferredColorScheme === "dark") {
applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet);
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

View File

@ -1,34 +1,45 @@
import {
appendStyleSheet,
createStyleSheetUnsafe,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
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 { globalAK } from "@goauthentik/common/global.js";
import { ThemedElement, applyDocumentTheme } from "@goauthentik/common/theme.js";
import { UIConfig } from "@goauthentik/common/ui/config.js";
import { AKElement } from "@goauthentik/elements/Base.js";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController.js";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { state } from "lit/decorators.js";
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 { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController";
import { BrandContextController } from "./BrandContextController.js";
import { ConfigContextController } from "./ConfigContextController.js";
import { EnterpriseContextController } from "./EnterpriseContextController.js";
const configContext = Symbol("configContext");
const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext");
export abstract class Interface extends AKElement implements ThemedElement {
protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
export abstract class LightInterface extends AKElement implements ThemedElement {
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()
public config?: Config;
@ -38,11 +49,6 @@ export abstract class Interface extends AKElement implements ThemedElement {
constructor() {
super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
this.addController(new BrandContextController(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;

View File

@ -1,8 +1,4 @@
import {
EVENT_SIDEBAR_TOGGLE,
EVENT_WS_MESSAGE,
TITLE_DEFAULT,
} from "@goauthentik/common/constants";
import { EVENT_WS_MESSAGE, TITLE_DEFAULT } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { UIConfig, UserDisplay, getConfigForUser } 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";
//#region Events
export interface SidebarToggleEventDetail {
open?: boolean;
}
//#endregion
//#region Page Navbar
export interface PageNavbarDetails {
@ -45,7 +49,10 @@ export interface PageNavbarDetails {
* dispatched by the `ak-page-header` component.
*/
@customElement("ak-page-navbar")
export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails {
export class AKPageNavbar
extends WithBrandConfig(AKElement)
implements PageNavbarDetails, SidebarToggleEventDetail
{
//#region Static Properties
private static elementRef: AKPageNavbar | null = null;
@ -260,29 +267,31 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
//#region Properties
@property({ type: String })
@state()
icon?: string;
@property({ type: Boolean })
@state()
iconImage = false;
@property({ type: String })
@state()
header?: string;
@property({ type: String })
@state()
description?: string;
@property({ type: Boolean })
@state()
hasIcon = true;
@property({ type: Boolean })
open = true;
@property({
type: Boolean,
})
public open?: boolean;
@state()
session?: SessionUser;
protected session?: SessionUser;
@state()
uiConfig!: UIConfig;
protected uiConfig!: UIConfig;
//#endregion
@ -305,9 +314,10 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
this.open = !this.open;
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
new CustomEvent<SidebarToggleEventDetail>("sidebar-toggle", {
bubbles: 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"
* lists.
*/
@customElement("ak-dual-select-dynamic-selected")
export class AkDualSelectDynamic extends AkDualSelectProvider {
/**
@ -23,20 +22,24 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
* @attr
*/
@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>) {
super.willUpdate(changed);
// 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.
if (!this.firstUpdateHasRun && this.options.length > 0) {
this.firstUpdateHasRun = true;
this.selector(this.options).then((selected) => {
this.selected = selected;
});
}
if (this.#didFirstUpdate) return;
if (this.options.length === 0) return;
this.#didFirstUpdate = true;
this.selector(this.options).then((selected) => {
this.selected = selected;
});
}
render() {

View File

@ -1,18 +1,16 @@
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import { debounce } from "@goauthentik/elements/utils/debounce";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter.js";
import { msg } from "@lit/localize";
import { PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import type { Pagination } from "@goauthentik/api";
import "./ak-dual-select";
import { AkDualSelect } from "./ak-dual-select";
import type { DataProvider, DualSelectPair } from "./types";
import "./ak-dual-select.js";
import { AkDualSelect } from "./ak-dual-select.js";
import { type DataProvider, DualSelectEventType, type DualSelectPair } from "./types.js";
/**
* @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
* the Pagination object "looks like Django," the interior components don't know anything
* about authentik at all and could be dropped into Gravity unchanged.)
*
*/
@customElement("ak-dual-select-provider")
export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) {
/** A function that takes a page and returns the DualSelectPair[] collection with which to update
* the "Available" pane.
//#region Properties
/**
* A function that takes a page and returns the {@linkcode DualSelectPair DualSelectPair[]}
* collection with which to update the "Available" pane.
*
* @attr
*/
@property({ type: Object })
provider!: DataProvider;
public provider!: DataProvider;
/**
* 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
*/
@property({ type: Array })
selected: DualSelectPair[] = [];
public selected: DualSelectPair[] = [];
/**
* The label for the left ("available") pane
@ -50,7 +49,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr
*/
@property({ attribute: "available-label" })
availableLabel = msg("Available options");
public availableLabel = msg("Available options");
/**
* The label for the right ("selected") pane
@ -58,7 +57,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr
*/
@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
@ -66,103 +65,125 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr
*/
@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()
options: DualSelectPair[] = [];
protected options: DualSelectPair[] = [];
protected dualSelector: Ref<AkDualSelect> = createRef();
#loading = false;
protected isLoading = false;
#didFirstUpdate = false;
#selected: DualSelectPair[] = [];
private doneFirstUpdate = false;
private internalSelected: DualSelectPair[] = [];
#previousSearchValue = "";
protected pagination?: Pagination;
constructor() {
super();
setTimeout(() => this.fetch(1), 0);
this.onNav = this.onNav.bind(this);
this.onChange = this.onChange.bind(this);
this.onSearch = this.onSearch.bind(this);
this.addCustomListener("ak-pagination-nav-to", this.onNav);
this.addCustomListener("ak-dual-select-change", this.onChange);
this.addCustomListener("ak-dual-select-search", this.onSearch);
//#endregion
//#region Refs
protected dualSelector = createRef<AkDualSelect>();
//#endregion
//#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>) {
if (changedProperties.has("selected") && !this.doneFirstUpdate) {
this.doneFirstUpdate = true;
this.internalSelected = this.selected;
}
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(
AkDualSelectProvider.prototype.doSearch.bind(this),
this.searchDelay,
);
if (changedProperties.has("selected") && !this.#didFirstUpdate) {
this.#didFirstUpdate = true;
this.#selected = this.selected;
}
if (changedProperties.has("provider")) {
this.pagination = undefined;
this.fetch();
this.#previousSearchValue = "";
this.#fetch();
}
}
async fetch(page?: number, search = "") {
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;
}
//#endregion
onNav(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`);
}
this.fetch(event.detail);
}
//#region Private Methods
onChange(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.internalSelected = event.detail.value;
this.selected = this.internalSelected;
}
#fetch = async (page?: number, search = this.#previousSearchValue): Promise<void> => {
if (this.#loading) return;
onSearch(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.doSearch(event.detail);
}
this.#previousSearchValue = search;
this.#loading = true;
doSearch(search: string) {
this.pagination = undefined;
this.fetch(undefined, search);
}
page ??= this.pagination?.current ?? 1;
get value() {
return this.dualSelector.value!.selected.map(([k, _]) => k);
}
return this.provider(page, search)
.then((data) => {
this.pagination = data.pagination;
this.options = data.options;
})
.catch((error) => {
console.error(error);
})
.finally(() => {
this.#loading = false;
});
};
json() {
return this.value;
}
//#endregion
//#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() {
return html`<ak-dual-select
${ref(this.dualSelector)}
.options=${this.options}
.pages=${this.pagination}
.selected=${this.internalSelected}
.selected=${this.#selected}
available-label=${this.availableLabel}
selected-label=${this.selectedLabel}
></ak-dual-select>`;

View File

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

View File

@ -1,26 +1,24 @@
import { AKElement } from "@goauthentik/elements/Base";
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 { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { createRef, ref } from "lit/directives/ref.js";
import { availablePaneStyles, listStyles } from "./styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_ADD_ONE } from "../constants";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles];
import { DualSelectEventType, DualSelectPair } from "../types.js";
const hostAttributes = [
["aria-labelledby", "dual-list-selector-available-pane-status"],
["aria-multiselectable", "true"],
["role", "listbox"],
];
] as const satisfies Array<[string, string]>;
/**
* @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,
* the attribute will be read by the parent when a control is clicked.
*
*/
@customElement("ak-dual-select-available-pane")
export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEventType>(
AKElement,
) {
static styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles];
//#region Properties
/* The array of key/value pairs this pane is currently showing */
@property({ type: Array })
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 })
readonly selected: Set<string> = new Set();
/* 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.
//#endregion
//#region State
/**
* 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()
public toMove: Set<string> = new Set();
constructor() {
super();
this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
}
//#endregion
//#region Refs
protected listRef = createRef<HTMLDivElement>();
//#region Lifecycle
connectedCallback() {
super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
for (const [attr, value] of hostAttributes) {
if (!this.hasAttribute(attr)) {
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();
}
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() {
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
// 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
@ -119,17 +145,18 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
render() {
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">
${map(this.options, ([key, label]) => {
const selected = classMap({
"pf-m-selected": this.toMove.has(key),
});
return html` <li
class="pf-c-dual-list-selector__list-item"
aria-selected="false"
@click=${() => this.onClick(key)}
@dblclick=${() => this.onMove(key)}
@click=${() => this.#clickListener(key)}
@dblclick=${() => this.#moveListener(key)}
role="option"
data-ak-key=${key}
tabindex="-1"
@ -154,6 +181,8 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
</div>
`;
}
//#endregion
}
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 PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
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%;
}
`,
];
import { DualSelectEventType } from "../types.js";
/**
* @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
* 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.
*
*/
@customElement("ak-dual-select-controls")
export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static 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 })
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)
*/
@property({ attribute: "remove-active", type: Boolean })
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
* not currently selected)
* not currently selected).
*/
@property({ attribute: "add-all-active", type: Boolean })
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
* 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 })
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.
*/
@property({ attribute: "delete-all-active", type: Boolean })
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 })
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 })
deleteAll = false;
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick(eventName: string) {
this.dispatchCustomEvent(eventName);
}
renderButton(label: string, event: string, active: boolean, direction: string) {
renderButton(
label: string,
eventType: DualSelectEventType,
active: boolean,
direction: string,
) {
return html`
<div class="pf-c-dual-list-selector__controls-item">
<button
@ -109,7 +102,7 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
aria-label=${label}
class="pf-c-button pf-m-plain"
type="button"
@click=${() => this.onClick(event)}
@click=${() => this.dispatchCustomEvent(eventType)}
data-ouia-component-type="AK/Button"
>
<i class="fa ${direction}"></i>
@ -123,7 +116,7 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
<div class="ak-dual-list-selector__controls">
${this.renderButton(
msg("Add"),
EVENT_ADD_SELECTED,
DualSelectEventType.AddSelected,
this.addActive,
"fa-angle-right",
)}
@ -131,13 +124,13 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
? html`
${this.renderButton(
msg("Add All Available"),
EVENT_ADD_ALL,
DualSelectEventType.AddAll,
this.addAllActive,
"fa-angle-double-right",
)}
${this.renderButton(
msg("Remove All Available"),
EVENT_REMOVE_ALL,
DualSelectEventType.RemoveAll,
this.removeAllActive,
"fa-angle-double-left",
)}
@ -145,14 +138,14 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
: nothing}
${this.renderButton(
msg("Remove"),
EVENT_REMOVE_SELECTED,
DualSelectEventType.RemoveSelected,
this.removeActive,
"fa-angle-left",
)}
${this.deleteAll
? html`${this.renderButton(
msg("Remove All"),
EVENT_DELETE_ALL,
DualSelectEventType.DeleteAll,
this.enableDeleteAll,
"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 PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_REMOVE_ONE } from "../constants";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
import { DualSelectEventType, DualSelectPair } from "../types";
const hostAttributes = [
["aria-labelledby", "dual-list-selector-selected-pane-status"],
["aria-multiselectable", "true"],
["role", "listbox"],
];
] as const satisfies Array<[string, string]>;
/**
* @element ak-dual-select-available-panel
@ -38,68 +35,86 @@ const hostAttributes = [
*
*/
@customElement("ak-dual-select-selected-pane")
export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
export class AkDualSelectSelectedPane extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
/* 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 })
readonly selected: DualSelectPair[] = [];
/*
* 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.
//#endregion
//#region State
/**
* 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()
public toMove: Set<string> = new Set();
constructor() {
super();
this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
}
//#endregion
connectedCallback() {
//#region Lifecycle
public connectedCallback() {
super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
for (const [attr, value] of hostAttributes) {
if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value);
}
});
}
}
clearMove() {
//#endregion
//#region Public API
public clearMove() {
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)) {
this.toMove.delete(key);
} else {
this.toMove.add(key);
}
this.dispatchCustomEvent(
"ak-dual-select-selected-move-changed",
DualSelectEventType.MoveChanged,
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) {
#moveListener = (key: string): void => {
this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_REMOVE_ONE, key);
this.requestUpdate();
}
get moveable() {
return Array.from(this.toMove.values());
}
this.dispatchCustomEvent(DualSelectEventType.RemoveOne, key);
this.requestUpdate();
};
//#endregion
//#region Render
render() {
return html`
@ -113,8 +128,8 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
class="pf-c-dual-list-selector__list-item"
aria-selected="false"
id="dual-list-selector-basic-selected-pane-list-option-0"
@click=${() => this.onClick(key)}
@dblclick=${() => this.onMove(key)}
@click=${() => this.#clickListener(key)}
@dblclick=${() => this.#moveListener(key)}
role="option"
data-ak-key=${key}
tabindex="-1"
@ -134,6 +149,8 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
</div>
`;
}
//#endregion
}
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 PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { BasePagination } from "../types";
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);
}
`,
];
import { BasePagination, DualSelectEventType } from "../types.js";
@customElement("ak-pagination")
export class AkPagination extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
export class AkPagination extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static 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 })
pages?: BasePagination;
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick(nav: number | undefined) {
this.dispatchCustomEvent("ak-pagination-nav-to", nav ?? 0);
}
#clickListener = (nav: number = 0) => {
this.dispatchCustomEvent(DualSelectEventType.NavigateTo, nav);
};
render() {
return this.pages
? html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<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">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
<span class="pf-c-options-menu__toggle-text">
${msg(
str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`,
)}
</span>
</div>
</div>
<nav class="pf-c-pagination__nav" aria-label=${msg("Pagination")}>
<div class="pf-c-pagination__nav-control pf-m-prev">
<button
class="pf-c-button pf-m-plain"
@click=${() => {
this.onClick(this.pages?.previous);
}}
?disabled="${(this.pages?.previous ?? 0) < 1}"
aria-label="${msg("Go to previous page")}"
>
<i class="fas fa-angle-left" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-pagination__nav-control pf-m-next">
<button
class="pf-c-button pf-m-plain"
@click=${() => {
this.onClick(this.pages?.next);
}}
?disabled="${(this.pages?.next ?? 0) <= 0}"
aria-label="${msg("Go to next page")}"
>
<i class="fas fa-angle-right" aria-hidden="true"></i>
</button>
</div>
</nav>
</div>
</div>`
: nothing;
const { pages } = this;
if (!pages) return nothing;
return html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<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">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
<span class="pf-c-options-menu__toggle-text">
${msg(str`${pages.startIndex} - ${pages.endIndex} of ${pages.count}`)}
</span>
</div>
</div>
<nav class="pf-c-pagination__nav" aria-label=${msg("Pagination")}>
<div class="pf-c-pagination__nav-control pf-m-prev">
<button
class="pf-c-button pf-m-plain"
@click=${() => {
this.#clickListener(pages.previous);
}}
?disabled="${(pages.previous ?? 0) < 1}"
aria-label="${msg("Go to previous page")}"
>
<i class="fas fa-angle-left" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-pagination__nav-control pf-m-next">
<button
class="pf-c-button pf-m-plain"
@click=${() => {
this.#clickListener(pages.next);
}}
?disabled="${(pages.next ?? 0) <= 0}"
aria-label="${msg("Go to next page")}"
>
<i class="fas fa-angle-right" aria-hidden="true"></i>
</button>
</div>
</nav>
</div>
</div>`;
}
}

View File

@ -4,47 +4,45 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit";
import { customElement, property } from "lit/decorators.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 type { SearchbarEvent } from "../types";
const styles = [PFBase, globalVariables, searchStyles];
import type { SearchbarEventDetail, SearchbarEventSource } from "../types.ts";
import { globalVariables, searchStyles } from "./search.css.js";
@customElement("ak-search-bar")
export class AkSearchbar extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
static styles = [PFBase, globalVariables, searchStyles];
@property({ type: String, reflect: true })
value = "";
public value = "";
/**
* 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.
*/
@property({ type: String })
name = "";
public name?: SearchbarEventSource;
input: Ref<HTMLInputElement> = createRef();
protected inputRef = createRef<HTMLInputElement>();
constructor() {
super();
this.onChange = this.onChange.bind(this);
}
#changeListener = () => {
const inputElement = this.inputRef.value;
onChange(_event: Event) {
if (this.input.value) {
this.value = this.input.value.value;
if (inputElement) {
this.value = inputElement.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,
value: this.value,
});
}
};
render() {
return html`
@ -56,8 +54,8 @@ export class AkSearchbar extends CustomEmitterElement(AKElement) {
><input
type="search"
class="pf-c-text-input-group__text-input"
${ref(this.input)}
@input=${this.onChange}
${ref(this.inputRef)}
@input=${this.#changeListener}
value="${this.value}"
/></span>
</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 { AkDualSelectProvider } from "./ak-dual-select-provider.js";
import "./ak-dual-select.js";
import { AkDualSelect } from "./ak-dual-select.js";
export { AkDualSelect, AkDualSelectProvider };
export default AkDualSelect;

View File

@ -9,7 +9,7 @@ import { Pagination } from "@goauthentik/api";
import "../ak-dual-select";
import { AkDualSelect } from "../ak-dual-select";
import type { DualSelectPair } from "../types";
import { DualSelectEventType, type DualSelectPair } from "../types";
const goodForYouRaw = `
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
`;
const keyToPair = (key: string): DualSelectPair => [slug(key), key];
const keyToPair = (key: string): DualSelectPair => [slug(key), key, key];
const goodForYou: DualSelectPair[] = goodForYouRaw
.split("\n")
.join(" ")
@ -83,7 +84,7 @@ export class AkSbFruity extends LitElement {
totalPages: Math.ceil(this.options.length / this.pageLength),
};
this.onNavigation = this.onNavigation.bind(this);
this.addEventListener("ak-pagination-nav-to", this.onNavigation);
this.addEventListener(DualSelectEventType.NavigateTo, this.onNavigation);
}
onNavigation(evt: Event) {

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