Compare commits

..

4 Commits

Author SHA1 Message Date
Tana M Berry
b1cb339f3a tweak 2025-08-04 15:13:41 -05:00
Tana M Berry
e124e21119 Merge branch 'main' into docs-remove-phrase 2025-08-04 15:06:03 -05:00
Tana M Berry
dc0c7a858a tweak to bump build 2025-08-04 13:53:50 -05:00
Tana M Berry
3fddbb918e removed phrase 2025-08-04 13:32:08 -05:00
157 changed files with 2159 additions and 5149 deletions

View File

@@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
GITHUB_OUTPUT=/dev/stdout \
GITHUB_REF=ref \
GITHUB_SHA=sha \
IMAGE_NAME=ghcr.io/goauthentik/server,authentik/server \
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
GITHUB_REPOSITORY=goauthentik/authentik \
python $SCRIPT_DIR/push_vars.py
@@ -12,7 +12,7 @@ GITHUB_OUTPUT=/dev/stdout \
GITHUB_OUTPUT=/dev/stdout \
GITHUB_REF=ref \
GITHUB_SHA=sha \
IMAGE_NAME=ghcr.io/goauthentik/server,authentik/server \
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
GITHUB_REPOSITORY=goauthentik/authentik \
DOCKER_USERNAME=foo \
python $SCRIPT_DIR/push_vars.py

View File

@@ -1,6 +1,5 @@
---
# Re-usable workflow for a single-architecture build
name: Reusable - Single-arch Container build
name: Single-arch Container build
on:
workflow_call:

View File

@@ -1,6 +1,5 @@
---
# Re-usable workflow for a multi-architecture build
name: Reusable - Multi-arch container build
name: Multi-arch container build
on:
workflow_call:

View File

@@ -1,13 +1,10 @@
---
name: API - Publish Python client
name: authentik-api-py-publish
on:
push:
branches: [main]
paths:
- "schema.yml"
workflow_dispatch:
jobs:
build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}

View File

@@ -1,13 +1,10 @@
---
name: API - Publish Typescript client
name: authentik-api-ts-publish
on:
push:
branches: [main]
paths:
- "schema.yml"
workflow_dispatch:
jobs:
build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}

View File

@@ -1,5 +1,4 @@
---
name: CI - API Docs
name: authentik-ci-api-docs
on:
push:
@@ -67,7 +66,7 @@ jobs:
- build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: api-docs
path: website/api/build

View File

@@ -1,5 +1,4 @@
---
name: CI - AWS cfn
name: authentik-ci-aws-cfn
on:
push:

View File

@@ -1,5 +1,4 @@
---
name: CI - Docs
name: authentik-ci-docs
on:
push:

View File

@@ -1,5 +1,5 @@
---
name: CI - Main daily
name: authentik-ci-main-daily
on:
workflow_dispatch:

View File

@@ -1,5 +1,5 @@
---
name: CI - Main
name: authentik-ci-main
on:
push:
@@ -17,12 +17,6 @@ env:
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
permissions:
# Needed for checkout
contents: read
# Needed for codecov OIDC token
id-token: write
jobs:
lint:
strategy:
@@ -142,13 +136,13 @@ jobs:
uses: codecov/codecov-action@v5
with:
flags: unit
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: unit
file: unittest.xml
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
test-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
@@ -166,13 +160,13 @@ jobs:
uses: codecov/codecov-action@v5
with:
flags: integration
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: integration
file: unittest.xml
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
test-e2e:
name: test-e2e (${{ matrix.job.name }})
runs-on: ubuntu-latest
@@ -225,13 +219,13 @@ jobs:
uses: codecov/codecov-action@v5
with:
flags: e2e
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: e2e
file: unittest.xml
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
ci-core-mark:
if: always()
needs:

View File

@@ -1,5 +1,5 @@
---
name: CI - Outpost
name: authentik-ci-outpost
on:
push:

View File

@@ -1,5 +1,4 @@
---
name: CI - Web
name: authentik-ci-web
on:
push:

View File

@@ -1,5 +1,4 @@
---
name: QA - CodeQL
name: "CodeQL"
on:
push:

View File

@@ -1,6 +1,4 @@
---
name: Gen - Webauthn MDS
name: authentik-gen-update-webauthn-mds
on:
workflow_dispatch:
schedule:

View File

@@ -1,7 +1,6 @@
---
# See https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: GH - Cleanup actions cache after PR is closed
name: Cleanup cache after PR is closed
on:
pull_request:
types:

View File

@@ -1,5 +1,4 @@
---
name: GH - GHCR retention
name: ghcr-retention
on:
# schedule:

View File

@@ -1,5 +1,5 @@
---
name: Gen - Compress images
name: authentik-compress-images
on:
push:

View File

@@ -1,6 +1,4 @@
---
name: Packages - Publish NPM packages
name: authentik-packages-npm-publish
on:
push:
branches: [main]
@@ -11,7 +9,6 @@ on:
- packages/tsconfig/**
- packages/esbuild-plugin-live-reload/**
workflow_dispatch:
jobs:
publish:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}

View File

@@ -1,5 +1,4 @@
---
name: CI - Source code docs
name: authentik-publish-source-docs
on:
push:

View File

@@ -1,5 +1,4 @@
---
name: Release - Update next branch
name: authentik-on-release-next-branch
on:
schedule:

View File

@@ -1,5 +1,5 @@
---
name: Release - On publish
name: authentik-on-release
on:
release:
@@ -16,7 +16,7 @@ jobs:
id-token: write
attestations: write
with:
image_name: ghcr.io/goauthentik/server,authentik/server
image_name: ghcr.io/goauthentik/server,beryju/authentik
release: true
registry_dockerhub: true
registry_ghcr: true
@@ -38,7 +38,7 @@ jobs:
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/docs
- name: Login to GitHub Container Registry
@@ -92,9 +92,9 @@ jobs:
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
- name: make empty clients
run: |
mkdir -p ./gen-ts-api
@@ -102,8 +102,8 @@ jobs:
- name: Docker Login Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -220,7 +220,7 @@ jobs:
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/server
- name: Get static files from docker image

View File

@@ -1,5 +1,5 @@
---
name: Release - On tag
name: authentik-on-tag
on:
push:

View File

@@ -1,5 +1,4 @@
---
name: Repo - Cleanup internal mirror
name: "authentik-repo-mirror-cleanup"
on:
workflow_dispatch:

View File

@@ -1,5 +1,4 @@
---
name: Repo - Mirror to internal
name: "authentik-repo-mirror"
on: [push, delete]

View File

@@ -1,5 +1,4 @@
---
name: Repo - Mark and close stale issues
name: "authentik-repo-stale"
on:
schedule:

View File

@@ -1,6 +1,4 @@
---
name: QA - Semgrep
name: authentik-semgrep
on:
workflow_dispatch: {}
pull_request: {}
@@ -9,11 +7,10 @@ on:
- main
- master
paths:
- .github/workflows/qa-semgrep.yml
- .github/workflows/semgrep.yml
schedule:
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
- cron: '12 15 * * *'
jobs:
semgrep:
name: semgrep/ci

View File

@@ -1,5 +1,4 @@
---
name: Translation - Post advice
name: authentik-translation-advice
on:
pull_request:

View File

@@ -1,6 +1,5 @@
---
name: Translation - Extract and compile
name: authentik-translate-extract-compile
on:
schedule:
- cron: "0 0 * * *" # every day at midnight

View File

@@ -1,7 +1,6 @@
---
# Rename transifex pull requests to have a correct naming
# Also enables auto squash-merge
name: Translation - Auto-rename Transifex PRs
name: authentik-translation-transifex-rename
on:
pull_request:

View File

@@ -76,7 +76,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 4: Download uv
FROM ghcr.io/astral-sh/uv:0.8.6 AS uv
FROM ghcr.io/astral-sh/uv:0.8.4 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base

View File

@@ -9,8 +9,8 @@
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-outpost.yml?branch=main&label=outpost%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-web.yml?branch=main&label=web%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
[![Code Coverage](https://img.shields.io/codecov/c/gh/goauthentik/authentik?style=for-the-badge)](https://codecov.io/gh/goauthentik/authentik)
![Docker pulls](https://img.shields.io/docker/pulls/authentik/server.svg?style=for-the-badge)
![Latest version](https://img.shields.io/docker/v/authentik/server?sort=semver&style=for-the-badge)
![Docker pulls](https://img.shields.io/docker/pulls/beryju/authentik.svg?style=for-the-badge)
![Latest version](https://img.shields.io/docker/v/beryju/authentik?sort=semver&style=for-the-badge)
[![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://www.transifex.com/authentik/authentik/)
## What is authentik?

View File

@@ -8,6 +8,8 @@ API Browser - {{ brand.branding_title }}
{% block head %}
<script src="{% versioned_script 'dist/standalone/api-browser/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
{% endblock %}
{% block body %}

View File

@@ -154,8 +154,7 @@ class UserSerializer(ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False, allow_null=True)
self.fields["permissions"] = ListField(
required=False,
child=ChoiceField(choices=get_permission_choices()),
required=False, child=ChoiceField(choices=get_permission_choices())
)
def create(self, validated_data: dict) -> User:
@@ -270,10 +269,7 @@ class UserSelfSerializer(ModelSerializer):
ListSerializer(
child=inline_serializer(
"UserSelfGroups",
{
"name": CharField(read_only=True),
"pk": CharField(read_only=True),
},
{"name": CharField(read_only=True), "pk": CharField(read_only=True)},
)
)
)
@@ -321,8 +317,7 @@ class UserSelfSerializer(ModelSerializer):
class SessionUserSerializer(PassiveSerializer):
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
and, if this user is being impersonated, the original user in the `original` property.
"""
and, if this user is being impersonated, the original user in the `original` property."""
user = UserSelfSerializer()
original = UserSelfSerializer(required=False)
@@ -410,15 +405,21 @@ class UserViewSet(UsedByMixin, ModelViewSet):
ordering = ["username", "date_joined", "last_updated"]
serializer_class = UserSerializer
filterset_class = UsersFilter
search_fields = ["email", "name", "uuid", "username"]
search_fields = [
"username",
"name",
"is_active",
"email",
"uuid",
"attributes",
"date_joined",
"last_updated",
]
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField
from authentik.enterprise.search.fields import (
ChoiceSearchField,
JSONSearchField,
)
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
return [
StrField(User, "username"),
@@ -513,12 +514,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
)
},
)
@action(
detail=False,
methods=["POST"],
pagination_class=None,
filter_backends=[],
)
@action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[])
def service_account(self, request: Request) -> Response:
"""Create a new user account that is marked as a service account"""
username = request.data.get("name")
@@ -562,13 +558,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
return Response(data={"non_field_errors": [str(exc)]}, status=400)
@extend_schema(responses={200: SessionUserSerializer(many=False)})
@action(
url_path="me",
url_name="me",
detail=False,
pagination_class=None,
filter_backends=[],
)
@action(url_path="me", url_name="me", detail=False, pagination_class=None, filter_backends=[])
def user_me(self, request: Request) -> Response:
"""Get information about current user"""
context = {"request": request}
@@ -694,18 +684,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not request.user.has_perm(
"authentik_core.impersonate", user_to_be
) and not request.user.has_perm("authentik_core.impersonate"):
LOGGER.debug(
"User attempted to impersonate without permissions",
user=request.user,
)
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401)
if not reason and request.tenant.impersonation_require_reason:
LOGGER.debug(
"User attempted to impersonate without providing a reason",
user=request.user,
"User attempted to impersonate without providing a reason", user=request.user
)
return Response(status=401)
@@ -744,8 +730,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
responses={
200: inline_serializer(
"UserPathSerializer",
{"paths": ListField(child=CharField(), read_only=True)},
"UserPathSerializer", {"paths": ListField(child=CharField(), read_only=True)}
)
},
parameters=[

View File

@@ -15,11 +15,7 @@
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
{% include "base/theme.html" %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>

View File

@@ -1,11 +0,0 @@
{% if ui_theme == "dark" %}
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#18191a">
{% elif ui_theme == "light" %}
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#ffffff">
{% else %}
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
{% endif %}

View File

@@ -4,6 +4,8 @@
{% block head %}
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
{% include "base/header_js.html" %}
{% endblock %}

View File

@@ -4,6 +4,8 @@
{% block head %}
<script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
{% endblock %}

View File

@@ -46,10 +46,8 @@ class InterfaceView(TemplateView):
"""Base interface view"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
brand = CurrentBrandSerializer(self.request.brand)
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
kwargs["ui_theme"] = brand.data["ui_theme"]
kwargs["brand_json"] = dumps(brand.data)
kwargs["brand_json"] = dumps(CurrentBrandSerializer(self.request.brand).data)
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash()

View File

@@ -301,7 +301,6 @@ class SessionEndStage(ChallengeStageView):
"flow_slug": self.request.brand.flow_invalidation.slug,
},
)
return SessionEndChallenge(data=data)
# This can never be reached since this challenge is created on demand and only the

View File

@@ -70,7 +70,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"signing_key",
"encryption_key",
"redirect_uris",
"backchannel_logout_uri",
"sub_mode",
"property_mappings",
"issuer_mode",

View File

@@ -1,8 +1,5 @@
"""OAuth/OpenID Constants"""
from django.db import models
from django.utils.translation import gettext_lazy as _
GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
GRANT_TYPE_IMPLICIT = "implicit"
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec
@@ -54,23 +51,3 @@ AMR_MFA = "mfa"
AMR_OTP = "otp"
AMR_WEBAUTHN = "user"
AMR_SMART_CARD = "sc"
class SubModes(models.TextChoices):
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
USER_ID = "user_id", _("Based on user ID")
USER_UUID = "user_uuid", _("Based on user UUID")
USER_USERNAME = "user_username", _("Based on the username")
USER_EMAIL = (
"user_email",
_("Based on the User's Email. This is recommended over the UPN method."),
)
USER_UPN = (
"user_upn",
_(
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
"Use this method only if you have different UPN and Mail domains."
),
)

View File

@@ -4,8 +4,10 @@ from dataclasses import asdict, dataclass, field
from hashlib import sha256
from typing import TYPE_CHECKING, Any
from django.db import models
from django.http import HttpRequest
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from authentik.core.models import default_token_duration
from authentik.events.signals import get_login_event
@@ -16,7 +18,6 @@ from authentik.providers.oauth2.constants import (
AMR_PASSWORD,
AMR_SMART_CARD,
AMR_WEBAUTHN,
SubModes,
)
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
@@ -29,6 +30,26 @@ def hash_session_key(session_key: str) -> str:
return sha256(session_key.encode("ascii")).hexdigest()
class SubModes(models.TextChoices):
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
USER_ID = "user_id", _("Based on user ID")
USER_UUID = "user_uuid", _("Based on user UUID")
USER_USERNAME = "user_username", _("Based on the username")
USER_EMAIL = (
"user_email",
_("Based on the User's Email. This is recommended over the UPN method."),
)
USER_UPN = (
"user_upn",
_(
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
"Use this method only if you have different UPN and Mail domains."
),
)
@dataclass(slots=True)
class IDToken:
"""The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be

View File

@@ -1,28 +0,0 @@
# Generated by Django 5.1.11 on 2025-07-04 03:23
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0028_migrate_session"),
]
operations = [
migrations.AddField(
model_name="oauth2provider",
name="backchannel_logout_uri",
field=models.TextField(
blank=True,
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="Back-Channel Logout URI",
),
),
migrations.AlterField(
model_name="oauth2provider",
name="_redirect_uris",
field=models.JSONField(default=list, verbose_name="Redirect URIs"),
),
]

View File

@@ -6,7 +6,7 @@ import json
from dataclasses import asdict, dataclass
from functools import cached_property
from hashlib import sha256
from typing import TYPE_CHECKING, Any
from typing import Any
from urllib.parse import urlparse, urlunparse
from cryptography.hazmat.primitives.asymmetric.ec import (
@@ -42,14 +42,11 @@ from authentik.core.models import (
)
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.providers.oauth2.constants import SubModes
from authentik.providers.oauth2.id_token import IDToken, SubModes
from authentik.sources.oauth.models import OAuthSource
if TYPE_CHECKING:
from authentik.providers.oauth2.id_token import IDToken
LOGGER = get_logger()
@@ -196,14 +193,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
default=generate_client_secret,
)
_redirect_uris = models.JSONField(
default=list,
default=dict,
verbose_name=_("Redirect URIs"),
)
backchannel_logout_uri = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
verbose_name=_("Back-Channel Logout URI"),
blank=True,
)
include_claims_in_id_token = models.BooleanField(
default=True,
@@ -488,15 +480,13 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
return f"Access Token for {self.provider_id} for user {self.user_id}"
@property
def id_token(self) -> "IDToken":
def id_token(self) -> IDToken:
"""Load ID Token from json"""
from authentik.providers.oauth2.id_token import IDToken
raw_token = json.loads(self._id_token)
return from_dict(IDToken, raw_token)
@id_token.setter
def id_token(self, value: "IDToken"):
def id_token(self, value: IDToken):
self.token = value.to_access_token(self.provider)
self._id_token = json.dumps(asdict(value))
@@ -541,15 +531,13 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
return f"Refresh Token for {self.provider_id} for user {self.user_id}"
@property
def id_token(self) -> "IDToken":
def id_token(self) -> IDToken:
"""Load ID Token from json"""
from authentik.providers.oauth2.id_token import IDToken
raw_token = json.loads(self._id_token)
return from_dict(IDToken, raw_token)
@id_token.setter
def id_token(self, value: "IDToken"):
def id_token(self, value: IDToken):
self._id_token = json.dumps(asdict(value))
@property

View File

@@ -1,34 +1,17 @@
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
from authentik.providers.oauth2.tasks import backchannel_logout_notification_dispatch
LOGGER = get_logger()
@receiver(pre_delete, sender=AuthenticatedSession)
def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
sender, instance: AuthenticatedSession, **_
):
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
"""Revoke tokens upon user logout"""
LOGGER.debug("Sending back-channel logout notifications signal!", session=instance)
access_tokens = AccessToken.objects.filter(
AccessToken.objects.filter(
user=instance.user,
session__session__session_key=instance.session.session_key,
)
backchannel_logout_notification_dispatch.send(
revocations=[
(token.provider_id, token.id_token.iss, token.session.user.uid)
for token in access_tokens
],
)
access_tokens.delete()
).delete()
@receiver(post_save, sender=User)

View File

@@ -1,68 +0,0 @@
"""OAuth2 Provider Tasks"""
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.lib.utils.http import get_http_session
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.utils import create_logout_token
from authentik.tasks.models import Task
LOGGER = get_logger()
@actor(description=_("Send a back-channel logout request to the registered client"))
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
"""Send a back-channel logout request to the registered client
Args:
provider_pk: The OAuth2 provider's primary key
iss: The issuer URL for the logout token
sub: The subject identifier to include in the logout token
Returns:
bool: True if the request was sent successfully, False otherwise
"""
self: Task = CurrentTask.get_task()
LOGGER.debug("Sending back-channel logout request", provider_pk=provider_pk, sub=sub)
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
if provider is None:
return
# Generate the logout token
logout_token = create_logout_token(iss, provider, None, sub)
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
backchannel_logout_uri = provider.backchannel_logout_uri
if not backchannel_logout_uri:
self.info("No back-channel logout URI found for provider")
return
# Send the back-channel logout request
response = get_http_session().post(
backchannel_logout_uri,
data={"logout_token": logout_token},
headers={"Content-Type": "application/x-www-form-urlencoded"},
allow_redirects=True,
)
response.raise_for_status()
self.info("Back-channel logout successful", sub=sub)
return True
@actor(description=_("Handle backchannel logout notifications dispatched via signal"))
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
"""Handle backchannel logout notifications dispatched via signal"""
for revocation in revocations:
provider_pk, iss, sub = revocation
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
send_backchannel_logout_request.send_with_options(
args=(provider_pk, iss, sub),
rel_obj=provider,
)

View File

@@ -81,46 +81,4 @@ class TestAPI(APITestCase):
},
)
self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]})
def test_backchannel_logout_uri_validation(self):
"""Test backchannel_logout_uri API validation"""
response = self.client.post(
reverse("authentik_api:oauth2provider-list"),
data={
"name": generate_id(),
"authorization_flow": create_test_flow().pk,
"invalidation_flow": create_test_flow().pk,
"redirect_uris": [
{"matching_mode": "strict", "url": "http://goauthentik.io"},
],
"backchannel_logout_uri": "invalid-url",
},
)
self.assertEqual(response.status_code, 400)
def test_backchannel_logout_uri_create_and_retrieve(self):
"""Test creating and retrieving backchannel logout URI"""
response = self.client.post(
reverse("authentik_api:oauth2provider-list"),
data={
"name": generate_id(),
"authorization_flow": create_test_flow().pk,
"invalidation_flow": create_test_flow().pk,
"redirect_uris": [
{"matching_mode": "strict", "url": "http://goauthentik.io"},
],
"backchannel_logout_uri": "http://goauthentik.io/logout",
},
)
self.assertEqual(response.status_code, 201)
provider_data = response.json()
self.assertEqual(provider_data["backchannel_logout_uri"], "http://goauthentik.io/logout")
# Test retrieving the provider
provider_pk = provider_data["pk"]
response = self.client.get(
reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider_pk})
)
self.assertEqual(response.status_code, 200)
retrieved_data = response.json()
self.assertEqual(retrieved_data["backchannel_logout_uri"], "http://goauthentik.io/logout")

View File

@@ -1,223 +0,0 @@
"""Test OAuth2 Back-Channel Logout implementation"""
from unittest.mock import Mock, patch
import jwt
from django.test import RequestFactory
from django.utils import timezone
from dramatiq.results.errors import ResultFailure
from requests import Response
from requests.exceptions import HTTPError, Timeout
from authentik.core.models import Application, AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import hash_session_key
from authentik.providers.oauth2.models import (
AccessToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,
RefreshToken,
)
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.utils import create_logout_token
class TestBackChannelLogout(OAuthTestCase):
"""Test Back-Channel Logout functionality"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.user = create_test_admin_user()
self.app = Application.objects.create(name=generate_id(), slug="test-app")
self.provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
redirect_uris=[
RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver/callback"),
],
signing_key=self.keypair,
)
self.app.provider = self.provider
self.app.save()
def _create_session(self, session_key=None):
"""Create a session with the given key or a generated one"""
session_key = session_key or f"session-{generate_id()}"
session = Session.objects.create(
session_key=session_key,
expires=timezone.now() + timezone.timedelta(hours=1),
last_ip="255.255.255.255",
)
auth_session = AuthenticatedSession.objects.create(
session=session,
user=self.user,
)
return auth_session
def _create_token(
self, provider, user, session=None, token_type="access", token_id=None
): # nosec
"""Create a token of the specified type"""
token_id = token_id or f"{token_type}-token-{generate_id()}"
kwargs = {
"provider": provider,
"user": user,
"session": session,
"token": token_id,
"_id_token": "{}",
"auth_time": timezone.now(),
}
if token_type == "access": # nosec
return AccessToken.objects.create(**kwargs)
else: # refresh
return RefreshToken.objects.create(**kwargs)
def _create_provider(self, name=None):
"""Create an OAuth2 provider"""
name = name or f"provider-{generate_id()}"
provider = OAuth2Provider.objects.create(
name=name,
authorization_flow=create_test_flow(),
redirect_uris=[
RedirectURI(RedirectURIMatchingMode.STRICT, f"http://{name}/callback"),
],
signing_key=self.keypair,
)
return provider
def _create_logout_token(
self,
provider: OAuth2Provider | None = None,
session_id: str | None = None,
sub: str | None = None,
):
"""Create a logout token with the given parameters"""
provider = provider or self.provider
# Create a token with the same issuer that the view will expect
# Use the same request object that will be used in the test
request = self.factory.post("/backchannel_logout")
return create_logout_token(
iss=provider.get_issuer(request),
provider=provider,
session_key=session_id,
sub=sub,
)
def _decode_token(self, token, provider=None):
"""Helper to decode and validate a JWT token"""
provider = provider or self.provider
key, alg = provider.jwt_key
if alg != "HS256":
key = provider.signing_key.public_key
return jwt.decode(
token, key, algorithms=[alg], options={"verify_exp": False, "verify_aud": False}
)
def test_create_logout_token_variants(self):
"""Test creating logout tokens with different combinations of parameters"""
# Test case 1: With session_id only
session_id = "test-session-123"
token1 = self._create_logout_token(session_id=session_id)
decoded1 = self._decode_token(token1)
self.assertIn("iss", decoded1)
self.assertEqual(decoded1["aud"], self.provider.client_id)
self.assertIn("iat", decoded1)
self.assertIn("jti", decoded1)
self.assertEqual(decoded1["sid"], hash_session_key(session_id))
self.assertIn("events", decoded1)
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded1["events"])
self.assertNotIn("sub", decoded1)
# Test case 2: With sub only
sub = "user-123"
token2 = self._create_logout_token(sub=sub)
decoded2 = self._decode_token(token2)
self.assertEqual(decoded2["sub"], sub)
self.assertIn("events", decoded2)
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded2["events"])
self.assertNotIn("sid", decoded2)
# Test case 3: With both session_id and sub
token3 = self._create_logout_token(session_id=session_id, sub=sub)
decoded3 = self._decode_token(token3)
self.assertEqual(decoded3["sid"], hash_session_key(session_id))
self.assertEqual(decoded3["sub"], sub)
self.assertIn("events", decoded3)
@patch("authentik.providers.oauth2.tasks.get_http_session")
def test_send_backchannel_logout_request_scenarios(self, mock_get_session):
"""Test various scenarios for backchannel logout request task"""
# Setup provider with backchannel logout URI
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
self.provider.save()
# Setup mock session and response
mock_session = Mock()
mock_get_session.return_value = mock_session
mock_response = Mock(spec=Response)
mock_response.status_code = 200
mock_response.raise_for_status.return_value = None # No exception for successful request
mock_session.post.return_value = mock_response
result = send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
)
self.assertTrue(result)
mock_session.post.assert_called_once()
call_args = mock_session.post.call_args
self.assertIn("logout_token", call_args[1]["data"])
self.assertEqual(
call_args[1]["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Scenario 2: Failed request (400 response) - should raise exception
mock_session.post.reset_mock()
error_response = Mock(spec=Response)
error_response.status_code = 400
error_response.raise_for_status.side_effect = HTTPError("HTTP 400")
mock_session.post.return_value = error_response
with self.assertRaises(ResultFailure):
send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
).get_result()
# Scenario 3: No URI configured
mock_session.post.reset_mock()
self.provider.backchannel_logout_uri = ""
self.provider.save()
result = send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
).get_result()
self.assertIsNone(result)
mock_session.post.assert_not_called()
# Scenario 4: No sub provided - should fail
result = send_backchannel_logout_request.send(
self.provider.pk, "http://testserver"
).get_result()
self.assertIsNone(result)
# Scenario 5: Non-existent provider
result = send_backchannel_logout_request.send(
99999, "http://testserver", sub="test-user-uid"
).get_result()
self.assertIsNone(result)
# Scenario 6: Request timeout
mock_session.post.side_effect = Timeout("Request timed out")
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
self.provider.save()
with self.assertRaises(ResultFailure):
send_backchannel_logout_request.send(
self.provider.pk, "http://testserver", sub="test-user-uid"
).get_result()

View File

@@ -11,9 +11,9 @@ from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import (
AccessToken,
IDToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,

View File

@@ -10,11 +10,11 @@ from django.utils import timezone
from authentik.core.models import Application, AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import (
AccessToken,
ClientTypes,
DeviceToken,
IDToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,

View File

@@ -11,9 +11,9 @@ from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import (
AccessToken,
IDToken,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,

View File

@@ -1,10 +1,8 @@
"""OAuth2/OpenID Utils"""
import re
import uuid
from base64 import b64decode
from binascii import Error
from time import time
from typing import Any
from urllib.parse import urlparse
@@ -16,7 +14,6 @@ from structlog.stdlib import get_logger
from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER
from authentik.events.models import Event, EventAction
from authentik.providers.oauth2.errors import BearerTokenError
from authentik.providers.oauth2.id_token import hash_session_key
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
LOGGER = get_logger()
@@ -214,36 +211,3 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
) -> None:
self.allowed_schemes = allowed_schemes or ["http", "https", "ftp"]
super().__init__(redirect_to, *args, **kwargs)
def create_logout_token(
iss: str,
provider: OAuth2Provider,
session_key: str | None = None,
sub: str | None = None,
) -> str:
"""Create a logout token for Back-Channel Logout
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
"""
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
# Create the logout token payload
payload = {
"iss": str(iss),
"aud": provider.client_id,
"iat": int(time()),
"jti": str(uuid.uuid4()),
"events": {
"http://schemas.openid.net/event/backchannel-logout": {},
},
}
# Add either sub or sid (or both)
if sub:
payload["sub"] = sub
if session_key:
payload["sid"] = hash_session_key(session_key)
# Encode the token
return provider.encode(payload)

View File

@@ -9,8 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.providers.oauth2.errors import TokenIntrospectionError
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.utils import TokenResponse, authenticate_provider
LOGGER = get_logger()

View File

@@ -72,8 +72,6 @@ class ProviderInfoView(View):
"device_authorization_endpoint": self.request.build_absolute_uri(
reverse("authentik_providers_oauth2:device")
),
"backchannel_logout_supported": True,
"backchannel_logout_session_supported": True,
"response_types_supported": [
ResponseTypes.CODE,
ResponseTypes.ID_TOKEN,

View File

@@ -4,6 +4,8 @@
{% block head %}
<script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}">
{% include "base/header_js.html" %}

View File

@@ -44,8 +44,6 @@ class EmailStageSerializer(StageSerializer):
"subject",
"template",
"activate_user_on_success",
"recovery_max_attempts",
"recovery_cache_timeout",
]
extra_kwargs = {"password": {"write_only": True}}

View File

@@ -1,28 +0,0 @@
# Generated by Django 5.1.11 on 2025-07-23 11:26
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_email", "0005_alter_emailstage_token_expiry"),
]
operations = [
migrations.AddField(
model_name="emailstage",
name="recovery_cache_timeout",
field=models.TextField(
default="minutes=5",
help_text="The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
migrations.AddField(
model_name="emailstage",
name="recovery_max_attempts",
field=models.PositiveIntegerField(default=5),
),
]

View File

@@ -16,8 +16,6 @@ from authentik.flows.models import Stage
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_string_validator
EMAIL_RECOVERY_MAX_ATTEMPTS = 5
LOGGER = get_logger()
@@ -72,17 +70,6 @@ class EmailStage(Stage):
use_ssl = models.BooleanField(default=False)
timeout = models.IntegerField(default=10)
from_address = models.EmailField(default="system@authentik.local")
recovery_max_attempts = models.PositiveIntegerField(default=EMAIL_RECOVERY_MAX_ATTEMPTS)
recovery_cache_timeout = models.TextField(
default="minutes=5",
validators=[timedelta_string_validator],
help_text=_(
"The time window used to count recent account recovery attempts. "
"If the number of attempts exceed recovery_max_attempts within "
"this period, further attempts will be rate-limited. "
"(Format: hours=1;minutes=2;seconds=3)."
),
)
activate_user_on_success = models.BooleanField(
default=False, help_text=_("Activate users upon completion of stage.")

View File

@@ -1,12 +1,9 @@
"""authentik multi-stage authentication engine"""
import math
from datetime import UTC, datetime, timedelta
from hashlib import sha256
from datetime import timedelta
from uuid import uuid4
from django.contrib import messages
from django.core.cache import cache
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.template.exceptions import TemplateSyntaxError
@@ -30,8 +27,6 @@ from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
EMAIL_RECOVERY_CACHE_KEY = "goauthentik.io/stages/email/stage/"
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
@@ -175,66 +170,10 @@ class EmailStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return super().challenge_invalid(response)
def _get_cache_key(self) -> str:
"""Return the cache key used for rate limiting email recovery attempts."""
user = self.get_pending_user()
user_email_hashed = sha256(user.email.lower().encode("utf-8")).hexdigest()
return EMAIL_RECOVERY_CACHE_KEY + user_email_hashed
def _is_rate_limited(self) -> int | None:
"""Check whether the email recovery attempt should be rate limited.
If the request should be rate limited, update the cache and return the
remaining time in minutes before the user is allowed to try again.
Otherwise, return None."""
cache_key = self._get_cache_key()
attempts = cache.get(cache_key, [])
stage = self.executor.current_stage
stage.refresh_from_db()
max_attempts = stage.recovery_max_attempts
cache_timeout_delta = timedelta_from_string(stage.recovery_cache_timeout)
_now = now()
start_window = _now - cache_timeout_delta
# Convert unix timestamps to datetime objects for comparison
recent_attempts_in_window = [
datetime.fromtimestamp(attempt, UTC)
for attempt in attempts
if datetime.fromtimestamp(attempt, UTC) > start_window
]
if len(recent_attempts_in_window) >= max_attempts:
retry_after = (min(recent_attempts_in_window) + cache_timeout_delta) - _now
minutes_left = max(1, math.ceil(retry_after.total_seconds() / 60))
return minutes_left
recent_attempts_in_window.append(_now)
# Convert datetime objects back to unix timestamps to update cache
recent_attempts_in_window = [attempt.timestamp() for attempt in recent_attempts_in_window]
cache.set(
cache_key,
recent_attempts_in_window,
int(cache_timeout_delta.total_seconds()),
)
return None
def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse:
if minutes_left := self._is_rate_limited():
error = _(
"Too many account verification attempts. Please try again after {minutes} minutes."
).format(minutes=minutes_left)
messages.error(self.request, error)
return super().challenge_invalid(response)
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
messages.error(self.request, _("No pending user."))
return super().challenge_invalid(response)
self.send_email()
messages.success(self.request, _("Email Successfully sent."))
# We can't call stage_ok yet, as we're still waiting

View File

@@ -1,9 +1,7 @@
"""email tests"""
from hashlib import sha256
from unittest.mock import MagicMock, PropertyMock, patch
from django.contrib import messages
from django.core import mail
from django.core.mail.backends.locmem import EmailBackend
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
@@ -11,7 +9,6 @@ from django.test import RequestFactory
from django.urls import reverse
from django.utils.http import urlencode
from authentik.brands.models import Brand
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
@@ -20,7 +17,6 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
@@ -295,173 +291,3 @@ class TestEmailStage(FlowTestCase):
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
)
def test_get_cache_key(self):
"""Test to ensure that the correct cache key is returned."""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = self.factory.post(url)
request.user = self.user
request.session = session
executor = FlowExecutorView(request=request, flow=self.flow)
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
cache_key = stage_view._get_cache_key()
expected_hash = sha256(self.user.email.lower().encode("utf-8")).hexdigest()
expected_cache_key = "goauthentik.io/stages/email/stage/" + expected_hash
self.assertEqual(cache_key, expected_cache_key)
def test_is_rate_limited_returns_none(self):
"""Test to ensure None is returned if the request shouldn't be rate limited."""
self.stage.recovery_max_attempts = 2
self.stage.recovery_cache_timeout = "minutes=10"
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = self.factory.post(url)
request.user = self.user
request.session = session
executor = FlowExecutorView(request=request, flow=self.flow)
executor.current_stage = self.stage
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
result = stage_view._is_rate_limited()
self.assertIsNone(result)
def test_is_rate_limited_returns_remaining_time(self):
"""Test to ensure the remaining time is returned if the request
should be rate limited."""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = self.factory.post(url)
request.user = self.user
request.session = session
executor = FlowExecutorView(request=request, flow=self.flow)
executor.current_stage = self.stage
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
test_cases = [
# 2 attempts within 2 minutes
(2, "seconds=120", 2),
# 4 attempts within 5 minutes
(4, "minutes=5", 5),
# 6 attempts within 5 minutes. Although 299 seconds is less than
# 5 minutes, the user is intentionally shown "5 minutes". This is
# because an initial rate limiting message like "Try again after 4 minutes"
# can be confusing.
(6, "seconds=299", 5),
]
for test_case in test_cases:
max_attempts, cache_timeout, minutes_remaining = test_case
with self.subTest(
f"Test recovery with {max_attempts} max attempts and "
f"{cache_timeout} cache timeout seconds"
):
self.stage.recovery_max_attempts = max_attempts
self.stage.recovery_cache_timeout = cache_timeout
self.stage.save()
# Simulate multiple requests
for _ in range(max_attempts):
stage_view._is_rate_limited()
# The following request should be rate-limited
result = stage_view._is_rate_limited()
self.assertEqual(result, minutes_remaining)
def _challenge_invalid_helper(self):
"""Helper to test the challenge_invalid() method."""
self.stage.recovery_max_attempts = 1
self.stage.recovery_cache_timeout = "seconds=300"
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
request = get_request(url, user=self.user)
request.session = session
request.brand = Brand.objects.create(domain="foo-domain.com", default=True)
executor = FlowExecutorView(request=request, flow=self.flow)
executor.current_stage = self.stage
executor.plan = plan
stage_view = EmailStageView(executor, request=request)
challenge_response = stage_view.get_response_instance(data={})
challenge_response.is_valid()
return challenge_response, stage_view, request
def test_challenge_invalid_not_rate_limited(self):
"""Tests that the request is not rate limited and email is sent."""
challenge_response, stage_view, request = self._challenge_invalid_helper()
with patch.object(stage_view, "send_email") as mock_send_email:
result = stage_view.challenge_invalid(challenge_response)
self.assertEqual(result.status_code, 200)
mock_send_email.assert_called_once()
message_list = list(messages.get_messages(request))
self.assertEqual(len(message_list), 1)
self.assertEqual(
"Email Successfully sent.",
message_list[-1].message,
)
def test_challenge_invalid_returns_error_if_rate_limited(self):
"""Tests that an error is returned if the request is rate limited. Ensure
that an email is not sent."""
challenge_response, stage_view, request = self._challenge_invalid_helper()
# Initial request that shouldn't be rate limited
stage_view.challenge_invalid(challenge_response)
with patch.object(stage_view, "send_email") as mock_send_email:
# This next request should be rate limited
result = stage_view.challenge_invalid(challenge_response)
self.assertEqual(result.status_code, 200)
mock_send_email.assert_not_called()
message_list = list(messages.get_messages(request))
self.assertEqual(len(message_list), 2)
self.assertEqual(
"Too many account verification attempts. Please try again after 5 minutes.",
message_list[-1].message,
)

View File

@@ -61,8 +61,6 @@ entries:
subject: authentik
template: email/password_reset.html
activate_user_on_success: true
recovery_max_attempts: 5
recovery_cache_timeout: minutes=5
- identifiers:
name: default-recovery-user-write
id: default-recovery-user-write

View File

@@ -8473,10 +8473,6 @@
},
"title": "Redirect uris"
},
"backchannel_logout_uri": {
"type": "string",
"title": "Back-Channel Logout URI"
},
"sub_mode": {
"type": "string",
"enum": [
@@ -14336,18 +14332,6 @@
"type": "boolean",
"title": "Activate user on success",
"description": "Activate users upon completion of stage."
},
"recovery_max_attempts": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"title": "Recovery max attempts"
},
"recovery_cache_timeout": {
"type": "string",
"minLength": 1,
"title": "Recovery cache timeout",
"description": "The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3)."
}
},
"required": []

2
go.mod
View File

@@ -29,7 +29,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.2025064.7
goauthentik.io/api/v3 v3.2025064.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.16.0

4
go.sum
View File

@@ -185,8 +185,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.2025064.7 h1:nh7Uh9K/XsHEz6hmPvZCK+aQC9uqwOto5hTCvtdvXPc=
goauthentik.io/api/v3 v3.2025064.7/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
goauthentik.io/api/v3 v3.2025064.3 h1:REfDBEjswP2id2WRRDUajRxX+6u+XZ7e/smYq7jw5Z0=
goauthentik.io/api/v3 v3.2025064.3/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=

View File

@@ -20,7 +20,6 @@ import (
"goauthentik.io/internal/config"
"goauthentik.io/internal/outpost/proxyv2/codecs"
"goauthentik.io/internal/outpost/proxyv2/constants"
"goauthentik.io/internal/outpost/proxyv2/filesystemstore"
"goauthentik.io/internal/outpost/proxyv2/redisstore"
"goauthentik.io/internal/utils"
)
@@ -91,10 +90,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
return rs, nil
}
dir := os.TempDir()
cs, err := filesystemstore.GetPersistentStore(dir)
if err != nil {
return nil, err
}
cs := sessions.NewFilesystemStore(dir)
cs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret))
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
@@ -127,7 +123,7 @@ func (a *Application) getAllCodecs() []securecookie.Codec {
}
func (a *Application) Logout(ctx context.Context, filter func(c Claims) bool) error {
if _, ok := a.sessions.(*filesystemstore.Store); ok {
if _, ok := a.sessions.(*sessions.FilesystemStore); ok {
files, err := os.ReadDir(os.TempDir())
if err != nil {
return err

View File

@@ -1,226 +0,0 @@
package filesystemstore
import (
"context"
"errors"
"os"
"path"
"strings"
"sync"
"syscall"
"time"
"github.com/gorilla/sessions"
log "github.com/sirupsen/logrus"
)
const (
SessionCleanupInterval = 5 * time.Minute
SessionCleanupLockFileName = "session-cleanup.lock"
SessionFilePrefix = "session_"
SessionTestFile = SessionFilePrefix + "write_test"
)
var (
ErrSessionCleanupAlreadyRunning = errors.New("session cleanup is already running by another instance")
ErrSessionStoreNoPermission = errors.New("path is not writable")
ErrSessionStorePathNotExist = errors.New("path does not exist")
)
type Store struct {
*sessions.FilesystemStore
storePath string
log *log.Entry
}
// NewStore checks if the specified store path exists, is writable and creates a new filesystem session store.
func NewStore(storePath string, keyPairs ...[]byte) (*Store, error) {
if storePath == "" {
storePath = os.TempDir()
}
// check if path exists
_, err := os.ReadDir(storePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrSessionStorePathNotExist
}
return nil, err
}
// check if path is writable
testPath := path.Join(storePath, SessionTestFile)
testFile, err := os.OpenFile(testPath, os.O_CREATE, 0600)
if err != nil {
if errors.Is(err, os.ErrPermission) {
return nil, ErrSessionStoreNoPermission
}
return nil, err
}
if err = testFile.Close(); err != nil {
return nil, err
}
if err = os.Remove(testPath); err != nil {
return nil, err
}
return &Store{
FilesystemStore: sessions.NewFilesystemStore(storePath, keyPairs...),
storePath: storePath,
log: log.WithField("logger", "authentik.outpost.proxyv2.filesystemstore"),
}, nil
}
// SessionCleanup acquires a file lock to ensure only one instance runs at a time,
// then checks and deletes expired session files from the filesystem session store.
// It supports context-based cancellation to allow graceful shutdowns or timeouts.
func (s *Store) SessionCleanup(ctx context.Context) error {
s.log.Info("Starting session cleanup")
lockPath := path.Join(s.storePath, SessionCleanupLockFileName)
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return err
}
defer func() {
if closeErr := lockFile.Close(); closeErr != nil {
s.log.WithError(closeErr).Warn("failed to close lock file")
}
}()
err = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EWOULDBLOCK {
return ErrSessionCleanupAlreadyRunning
}
return err
}
defer func() {
if flockErr := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN); flockErr != nil {
s.log.WithError(flockErr).Warn("failed to unlock file")
}
if removeErr := os.Remove(lockPath); removeErr != nil {
s.log.WithError(removeErr).Warn("failed to remove lock file")
}
}()
return s.sessionCleanup(ctx)
}
// sessionCleanup checks the modification time of all session files and removes them
// when they reach the configured maximum age in the session store.
// Since the FilesystemStore from Gorilla does not have a session cleanup function,
// it is only necessary for the filesystem session store.
func (s *Store) sessionCleanup(ctx context.Context) error {
files, err := os.ReadDir(s.storePath)
if err != nil {
return err
}
var errs []error
for _, file := range files {
select {
case <-ctx.Done():
s.log.Warn("session cleanup interrupted during file processing")
return ctx.Err()
default:
}
if !strings.HasPrefix(file.Name(), SessionFilePrefix) {
continue
}
fullPath := path.Join(s.storePath, file.Name())
stat, err := os.Lstat(fullPath)
if err != nil {
s.log.WithError(err).WithField("path", fullPath).Warning("failed to read stats from file")
errs = append(errs, err)
continue
}
modTime := stat.ModTime()
if time.Since(modTime) <= time.Duration(s.Options.MaxAge)*time.Second {
s.log.WithField("max-age", s.Options.MaxAge).WithField("modified", modTime.String()).Debug("session still valid")
continue
}
s.log.WithField("path", fullPath).WithField("modified", modTime.String()).Info("cleanup expired session")
if err = os.Remove(fullPath); err != nil {
s.log.WithError(err).WithField("path", fullPath).Warn("failed to delete session")
errs = append(errs, err)
continue
}
}
return errors.Join(errs...)
}
var (
cancelCleanup context.CancelFunc
doneCleanup chan struct{}
globalStore *Store
mu sync.Mutex
)
// GetPersistentStore creates a new filesystem store if it is the first time the function has been called,
// or if the path string has changed. It then stores this in the globalStore variable.
// If the function is called multiple times, the store from the variable is returned to ensure that only one instance is running.
func GetPersistentStore(path string) (*Store, error) {
mu.Lock()
defer mu.Unlock()
if globalStore == nil || globalStore.storePath != path {
if cancelCleanup != nil {
cancelCleanup()
if doneCleanup != nil {
<-doneCleanup
}
}
store, err := NewStore(path)
if err != nil {
return nil, err
}
globalStore = store
ctx, cancel := context.WithCancel(context.Background())
cancelCleanup = cancel
doneCleanup = make(chan struct{})
go func() {
defer close(doneCleanup)
globalStore.log.Info("Scheduling session cleanup job")
ticker := time.NewTicker(SessionCleanupInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
err := globalStore.SessionCleanup(ctx)
if err == nil {
continue
}
if errors.Is(err, ErrSessionCleanupAlreadyRunning) {
globalStore.log.WithError(err).Warn("Session cleanup is locked by another job")
continue
}
globalStore.log.WithError(err).Warn("Session cleanup returned error")
}
}
}()
}
return globalStore, nil
}
// StopPersistentStore stops the cleanup background job and clears the globalStore variable.
func StopPersistentStore() {
mu.Lock()
defer mu.Unlock()
if cancelCleanup != nil {
cancelCleanup()
if doneCleanup != nil {
<-doneCleanup
}
}
cancelCleanup = nil
doneCleanup = nil
globalStore = nil
}

View File

@@ -1,146 +0,0 @@
package filesystemstore
import (
"context"
"os"
"path"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createTempSessionFile(t *testing.T, dir string, modTime time.Time) string {
t.Helper()
path := filepath.Join(dir, "session_test")
err := os.WriteFile(path, []byte("session data"), 0600)
require.NoError(t, err)
err = os.Chtimes(path, modTime, modTime)
require.NoError(t, err)
return path
}
func TestNewStore_PathNotExist(t *testing.T) {
_, err := NewStore("/invalid_path")
assert.ErrorIs(t, err, ErrSessionStorePathNotExist)
}
func TestNewStore_PathNotWritable(t *testing.T) {
storePath := path.Join(os.TempDir(), "test")
err := os.Mkdir(storePath, 0400)
require.NoError(t, err)
_, err = NewStore(storePath)
assert.ErrorIs(t, err, ErrSessionStoreNoPermission)
_ = os.RemoveAll(storePath)
}
func TestNewStore(t *testing.T) {
tmpDir := t.TempDir()
store, err := NewStore(tmpDir)
assert.NoError(t, err)
assert.NotEmpty(t, store)
}
func TestSessionCleanup_RemovesExpired(t *testing.T) {
tmpDir := t.TempDir()
store, err := NewStore(tmpDir)
require.NoError(t, err)
store.Options.MaxAge = 1 // 1 second
// Create an expired session file
oldTime := time.Now().Add(-10 * time.Second)
createTempSessionFile(t, tmpDir, oldTime)
ctx := context.Background()
err = store.SessionCleanup(ctx)
assert.NoError(t, err)
// File should be deleted
files, _ := os.ReadDir(tmpDir)
assert.Empty(t, files)
}
func TestSessionCleanup_PreservesValid(t *testing.T) {
tmpDir := t.TempDir()
store, err := NewStore(tmpDir)
require.NoError(t, err)
store.Options.MaxAge = 3600 // 1 hour
// Create a valid (non-expired) session file
modTime := time.Now().Add(-10 * time.Second)
createTempSessionFile(t, tmpDir, modTime)
ctx := context.Background()
err = store.SessionCleanup(ctx)
assert.NoError(t, err)
// File should still exist
files, _ := os.ReadDir(tmpDir)
assert.Len(t, files, 1)
}
func TestSessionCleanup_ContextCancel(t *testing.T) {
tmpDir := t.TempDir()
store, err := NewStore(tmpDir)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
err = store.SessionCleanup(ctx)
assert.ErrorIs(t, err, context.Canceled)
}
func TestSessionCleanup_AlreadyRunning(t *testing.T) {
tmpDir := t.TempDir()
store, err := NewStore(tmpDir)
require.NoError(t, err)
// Manually acquire the lock before calling SessionCleanup
lockPath := path.Join(tmpDir, SessionCleanupLockFileName)
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
require.NoError(t, err, "failed to create lock file")
err = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
require.NoError(t, err, "failed to acquire lock for test")
// Run SessionCleanup while lock is held
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err = store.SessionCleanup(ctx)
assert.ErrorIs(t, err, ErrSessionCleanupAlreadyRunning)
// Unlock and clean up
_ = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
_ = lockFile.Close()
_ = os.Remove(lockPath)
}
func TestPersistentStore_ReusesStore(t *testing.T) {
tmpDir := t.TempDir()
store1, err := GetPersistentStore(tmpDir)
require.NoError(t, err)
assert.NotNil(t, store1)
store2, err := GetPersistentStore(tmpDir)
require.NoError(t, err)
assert.Equal(t, store1, store2)
StopPersistentStore()
}
func TestStopPersistentStore(t *testing.T) {
tmpDir := t.TempDir()
_, err := GetPersistentStore(tmpDir)
require.NoError(t, err)
StopPersistentStore()
// call again should not panic
StopPersistentStore()
}

View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1024.0",
"aws-cdk": "^2.1023.0",
"cross-env": "^10.0.0"
},
"engines": {
@@ -24,9 +24,9 @@
"license": "MIT"
},
"node_modules/aws-cdk": {
"version": "2.1024.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1024.0.tgz",
"integrity": "sha512-hY0iVT2gPX/QOQXL7RSP2sqIRI/4BYU27vSmbhZxLEj//c3pkMkd9QpIHj7gOhyWC2gf6n5JuYPw27Dgw8FEdA==",
"version": "2.1023.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1023.0.tgz",
"integrity": "sha512-DWMA+IrAsBUNF2RvH7ujpDp7wSJkqTkRL8yfK4AYpEjoGY1KMaKIfxz3M3+Nk3ogM7VhZiW3OGWEOgyDF47HOQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -10,7 +10,7 @@
"node": ">=20"
},
"devDependencies": {
"aws-cdk": "^2.1024.0",
"aws-cdk": "^2.1023.0",
"cross-env": "^10.0.0"
}
}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-07 00:12+0000\n"
"POT-Creation-Date: 2025-07-28 16:09+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -1483,27 +1483,27 @@ msgstr ""
msgid "Invalid Regex Pattern: {url}"
msgstr ""
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on the Hashed User ID"
msgstr ""
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on user ID"
msgstr ""
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on user UUID"
msgstr ""
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on the username"
msgstr ""
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on the User's Email. This is recommended over the UPN method."
msgstr ""
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid ""
"Based on the User's UPN, only works if user has a 'upn' attribute set. Use "
"this method only if you have different UPN and Mail domains."
@@ -1617,10 +1617,6 @@ msgstr ""
msgid "Redirect URIs"
msgstr ""
#: authentik/providers/oauth2/models.py
msgid "Back-Channel Logout URI"
msgstr ""
#: authentik/providers/oauth2/models.py
msgid "Include claims in id_token"
msgstr ""
@@ -1736,14 +1732,6 @@ msgstr ""
msgid "Device Tokens"
msgstr ""
#: authentik/providers/oauth2/tasks.py
msgid "Send a back-channel logout request to the registered client"
msgstr ""
#: authentik/providers/oauth2/tasks.py
msgid "Handle backchannel logout notifications dispatched via signal"
msgstr ""
#: authentik/providers/oauth2/views/authorize.py
#: authentik/providers/saml/views/flows.py
#, python-brace-format
@@ -3250,13 +3238,6 @@ msgstr ""
msgid "Account Confirmation"
msgstr ""
#: authentik/stages/email/models.py
msgid ""
"The time window used to count recent account recovery attempts. If the "
"number of attempts exceed recovery_max_attempts within this period, further "
"attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3)."
msgstr ""
#: authentik/stages/email/models.py
msgid "Activate users upon completion of stage."
msgstr ""
@@ -3281,13 +3262,6 @@ msgstr ""
msgid "Email sent."
msgstr ""
#: authentik/stages/email/stage.py
#, python-brace-format
msgid ""
"Too many account verification attempts. Please try again after {minutes} "
"minutes."
msgstr ""
#: authentik/stages/email/stage.py
msgid "Email Successfully sent."
msgstr ""

Binary file not shown.

View File

@@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-07 00:12+0000\n"
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@@ -33,10 +33,6 @@ msgstr ""
msgid "Version history"
msgstr "Historique des versions"
#: authentik/admin/tasks.py
msgid "Update latest version info."
msgstr "Mettre à jour les dernières informations de version."
#: authentik/admin/tasks.py
#, python-brace-format
msgid "New version {version} available!"
@@ -92,25 +88,10 @@ msgstr "Instances du plan"
msgid "authentik Export - {date}"
msgstr "Export authentik - {date}"
#: authentik/blueprints/v1/tasks.py
msgid "Find blueprints as `blueprints_find` does, but return a safe dict."
msgstr ""
"Cherche les plans comme le fait `blueprints_find`, mais renvoie un safe "
"dict."
#: authentik/blueprints/v1/tasks.py
msgid "Find blueprints and check if they need to be created in the database."
msgstr ""
"Cherche les plans et vérifie s'ils doivent être créés dans la base de "
"données."
#: authentik/blueprints/v1/tasks.py
msgid "Apply single blueprint."
msgstr "Applique un seul plan."
#: authentik/blueprints/v1/tasks.py
msgid "Remove blueprints which couldn't be fetched."
msgstr "Supprime les plans qui n'ont pas pu être récupérés."
#: authentik/blueprints/v1/tasks.py authentik/crypto/tasks.py
#, python-brace-format
msgid "Successfully imported {count} files."
msgstr "{count} fichiers importés avec succès."
#: authentik/brands/models.py
msgid ""
@@ -148,6 +129,10 @@ msgstr "Marques"
msgid "User does not have access to application."
msgstr "L'utilisateur n'a pas accès à l'application."
#: authentik/core/api/devices.py
msgid "Extra description not available"
msgstr "Description supplémentaire indisponible"
#: authentik/core/api/groups.py
msgid "Cannot set group as parent of itself."
msgstr "Impossible de définir le groupe en tant que parent de lui-même."
@@ -394,10 +379,6 @@ msgstr "Jetons"
msgid "View token's key"
msgstr "Voir la clé du jeton"
#: authentik/core/models.py
msgid "Set a token's key"
msgstr "Définir la clé d'un jeton"
#: authentik/core/models.py
msgid "Property Mapping"
msgstr "Mappage de propriété"
@@ -453,14 +434,6 @@ msgstr "{source} liée avec succès !"
msgid "Source is not configured for enrollment."
msgstr "La source n'est pas configurée pour l'inscription."
#: authentik/core/tasks.py
msgid "Remove expired objects."
msgstr "Supprime les objets expirés"
#: authentik/core/tasks.py
msgid "Remove temporary users created by SAML Sources."
msgstr "Supprime les utilisateurs temporaires créés par les sources SAML."
#: authentik/core/templates/if/error.html
msgid "Go home"
msgstr "Retourner à l'accueil"
@@ -513,12 +486,6 @@ msgstr "Paire de clé/certificat"
msgid "Certificate-Key Pairs"
msgstr "Paires de clé/certificat"
#: authentik/crypto/tasks.py
msgid "Discover, import and update certificates from the filesystem."
msgstr ""
"Découvre, importe et met à jour les certificats depuis le système de "
"fichiers."
#: authentik/enterprise/api.py
msgid "Enterprise is required to create/update this object."
msgstr "Entreprise est requis pour créer/mettre à jour cet objet."
@@ -571,18 +538,6 @@ msgstr "Politiques d'unicité des mots de passe"
msgid "User Password History"
msgstr "Historique des mots de passe utilisateur"
#: authentik/enterprise/policies/unique_password/tasks.py
msgid ""
"Check if any UniquePasswordPolicy exists, and if not, purge the password "
"history table."
msgstr ""
"Vérifie si une politique de mot de passe unique existe et, si ce n'est pas "
"le cas, purge la table de l'historique des mots de passe."
#: authentik/enterprise/policies/unique_password/tasks.py
msgid "Remove user password history that are too old."
msgstr "Supprime l'historique des mots de passe utilisateur trop anciens."
#: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature."
msgstr "Entreprise est requis pour accéder à cette fonctionnalité."
@@ -631,42 +586,6 @@ msgstr "Mappage de propriété Google Workspace"
msgid "Google Workspace Provider Mappings"
msgstr "Mappages de propriété Google Workspace"
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Sync Google Workspace provider objects."
msgstr "Synchronise les objets du fournisseur Google Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Full sync for Google Workspace provider."
msgstr "Synchronisation complète pour le fournisseur Google Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Sync a direct object (user, group) for Google Workspace provider."
msgstr ""
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur Google"
" Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid ""
"Dispatch syncs for a direct object (user, group) for Google Workspace "
"providers."
msgstr ""
"Déclenche des synchronisations pour un objet direct (utilisateur, groupe) "
"pour les fournisseurs Google Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid "Sync a related object (memberships) for Google Workspace provider."
msgstr ""
"Synchronise un objet lié (appartenances) pour le fournisseur Google "
"Workspace."
#: authentik/enterprise/providers/google_workspace/tasks.py
msgid ""
"Dispatch syncs for a related object (memberships) for Google Workspace "
"providers."
msgstr ""
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
"fournisseurs Google Workspace."
#: authentik/enterprise/providers/microsoft_entra/models.py
msgid "Microsoft Entra Provider User"
msgstr "Utilisateur du fournisseur Microsoft Entra"
@@ -695,42 +614,6 @@ msgstr "Mappage de propriété Microsoft Entra"
msgid "Microsoft Entra Provider Mappings"
msgstr "Mappages de propriété Microsoft Entra"
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Sync Microsoft Entra provider objects."
msgstr "Synchronise les objets du fournisseur Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Full sync for Microsoft Entra provider."
msgstr "Synchronisation complète pour le fournisseur Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Sync a direct object (user, group) for Microsoft Entra provider."
msgstr ""
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur "
"Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid ""
"Dispatch syncs for a direct object (user, group) for Microsoft Entra "
"providers."
msgstr ""
"Déclenche les synchronisations pour un objet direct (utilisateur, groupe) "
"pour les fournisseurs Microsoft Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid "Sync a related object (memberships) for Microsoft Entra provider."
msgstr ""
"Synchronise un objet lié (appartenances) pour le fournisseur Microsoft "
"Entra."
#: authentik/enterprise/providers/microsoft_entra/tasks.py
msgid ""
"Dispatch syncs for a related object (memberships) for Microsoft Entra "
"providers."
msgstr ""
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
"fournisseurs Microsoft Entra."
#: authentik/enterprise/providers/ssf/models.py
#: authentik/providers/oauth2/models.py
msgid "Signing Key"
@@ -769,12 +652,8 @@ msgid "SSF Stream Events"
msgstr "Évènements du flux SSF"
#: authentik/enterprise/providers/ssf/tasks.py
msgid "Dispatch SSF events."
msgstr "Distribue les événements SSF."
#: authentik/enterprise/providers/ssf/tasks.py
msgid "Send an SSF event."
msgstr "Envoye un événement SSF."
msgid "Failed to send request"
msgstr "Échec de l'envoi de la requête"
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
@@ -846,9 +725,10 @@ msgstr "Étape Source"
msgid "Source Stages"
msgstr "Étapes Source"
#: authentik/enterprise/tasks.py
msgid "Update enterprise license status."
msgstr "Mettre à jour le statut de licence entreprise."
#: authentik/events/api/tasks.py
#, python-brace-format
msgid "Successfully started task {name}."
msgstr "La tâche {name} a été démarrée avec succès."
#: authentik/events/models.py
msgid "Event"
@@ -960,15 +840,6 @@ msgstr ""
"Définir à quel groupe d'utilisateur cette notification doit être envoyée et "
"affichée. Si laissé vide, les notifications ne seront pas envoyées."
#: authentik/events/models.py
msgid ""
"When enabled, notification will be sent to user the user that triggered the "
"event.When destination_group is configured, notification is sent to both."
msgstr ""
"Lorsque cette option est activée, une notification est envoyée à "
"l'utilisateur qui a déclenché l'événement. Si destination_group est "
"configuré, la notification est envoyée aux deux."
#: authentik/events/models.py
msgid "Notification Rule"
msgstr "Règle de Notification"
@@ -985,6 +856,10 @@ msgstr "Mappage de Webhook"
msgid "Webhook Mappings"
msgstr "Mappages de Webhook"
#: authentik/events/models.py
msgid "Run task"
msgstr "Lancer la tâche"
#: authentik/events/models.py
msgid "System Task"
msgstr "Tâches du système"
@@ -993,31 +868,9 @@ msgstr "Tâches du système"
msgid "System Tasks"
msgstr "Tâches du système"
#: authentik/events/tasks.py
msgid "Dispatch new event notifications."
msgstr "Envoye les notifications d'un nouvel événement."
#: authentik/events/tasks.py
msgid ""
"Check if policies attached to NotificationRule match event and dispatch "
"notification tasks."
msgstr ""
"Vérifier si les politiques attachées à une règle de notifications "
"correspondent à l'événement et déclenche les tâches de notification."
#: authentik/events/tasks.py
msgid "Send notification."
msgstr "Envoye une notification."
#: authentik/events/tasks.py
msgid "Cleanup events for GDPR compliance."
msgstr "Nettoye les événements pour la conformité au RGPD."
#: authentik/events/tasks.py
msgid "Cleanup seen notifications and notifications whose event expired."
msgstr ""
"Nettoye les notifications vues et les notifications dont l'événement a "
"expiré."
#: authentik/events/system_tasks.py
msgid "Task has not been run yet."
msgstr "Tâche pas encore exécutée."
#: authentik/flows/api/flows.py
#, python-brace-format
@@ -1198,6 +1051,32 @@ msgstr ""
"Si activé, le fournisseur ne changera ou ne créera pas d'objets auprès du "
"système distant."
#: authentik/lib/sync/outgoing/tasks.py
msgid "Starting full provider sync"
msgstr "Démarrage d'une synchronisation complète du fournisseur"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing users"
msgstr "Synchronisation des utilisateurs"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups"
msgstr "Synchronisation des groupes"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Syncing page {page} of {object_type}"
msgstr "Synchronisation de la page {page} de {object_type}"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Dropping mutating request due to dry run"
msgstr "Abandon de la requête de mutation en raison d'une simulation"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Stopping sync due to error: {error}"
msgstr "Arrêt de la synchronisation due à l'erreur : {error}"
#: authentik/lib/utils/time.py
#, python-format
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
@@ -1304,32 +1183,6 @@ msgstr "Avant-poste"
msgid "Outposts"
msgstr "Avant-postes"
#: authentik/outposts/tasks.py
msgid "Update cached state of service connection."
msgstr "Met à jour l'état mis en cache de la connexion de service."
#: authentik/outposts/tasks.py
msgid "Create/update/monitor/delete the deployment of an Outpost."
msgstr "Crée/met à jour/surveille/supprime le déploiement d'un avant-poste."
#: authentik/outposts/tasks.py
msgid "Ensure that all Outposts have valid Service Accounts and Tokens."
msgstr ""
"S'assure que tous les avant-postes ont des comptes de service et des jetons "
"valides."
#: authentik/outposts/tasks.py
msgid "Send update to outpost"
msgstr "Envoye une mise à jour à un avant-poste"
#: authentik/outposts/tasks.py
msgid "Checks the local environment and create Service connections."
msgstr "Vérifie l'environnement local et crée les connexions de service."
#: authentik/outposts/tasks.py
msgid "Terminate session on all outposts."
msgstr "Met fin à la session sur tous les avant-postes."
#: authentik/policies/denied.py
msgid "Access denied"
msgstr "Accès refusé"
@@ -1664,29 +1517,29 @@ msgstr "Rechercher dans l'annuaire LDAP complet"
msgid "Invalid Regex Pattern: {url}"
msgstr "Pattern de regex invalide : {url}"
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on the Hashed User ID"
msgstr "Basé sur le hash de l'ID utilisateur"
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on user ID"
msgstr "Basé sur l'ID de l'utilisateur"
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on user UUID"
msgstr "Basé sur le UUID de l'utilisateur"
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on the username"
msgstr "Basé sur le nom d'utilisateur"
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid "Based on the User's Email. This is recommended over the UPN method."
msgstr ""
"Basé sur le courriel utilisateur. Ceci est recommandé par rapport à la "
"méthode UPN."
#: authentik/providers/oauth2/constants.py
#: authentik/providers/oauth2/id_token.py
msgid ""
"Based on the User's UPN, only works if user has a 'upn' attribute set. Use "
"this method only if you have different UPN and Mail domains."
@@ -1809,10 +1662,6 @@ msgstr "Secret du client"
msgid "Redirect URIs"
msgstr "URIs de redirection"
#: authentik/providers/oauth2/models.py
msgid "Back-Channel Logout URI"
msgstr "URI de déconnexion Back-Channel"
#: authentik/providers/oauth2/models.py
msgid "Include claims in id_token"
msgstr "Include les demandes utilisateurs dans id_token"
@@ -1941,15 +1790,6 @@ msgstr "Jeton d'équipement"
msgid "Device Tokens"
msgstr "Jetons d'équipement"
#: authentik/providers/oauth2/tasks.py
msgid "Send a back-channel logout request to the registered client"
msgstr "Envoyer une requête de déconnexion Back-Channel au client enregistré"
#: authentik/providers/oauth2/tasks.py
msgid "Handle backchannel logout notifications dispatched via signal"
msgstr ""
"Gérer les notifications de déconnexion Back-Channel envoyées via un signal"
#: authentik/providers/oauth2/views/authorize.py
#: authentik/providers/saml/views/flows.py
#, python-brace-format
@@ -2061,10 +1901,6 @@ msgstr "Fournisseur Proxy"
msgid "Proxy Providers"
msgstr "Fournisseur de Proxy"
#: authentik/providers/proxy/tasks.py
msgid "Terminate session on Proxy outpost."
msgstr "Met fin à la session sur l'avant-poste Proxy."
#: authentik/providers/rac/models.py authentik/stages/user_login/models.py
msgid ""
"Determines how long a session lasts. Default of 0 means that the sessions "
@@ -2409,35 +2245,6 @@ msgstr "Mappage fournisseur SCIM"
msgid "SCIM Provider Mappings"
msgstr "Mappages fournisseur SCIM"
#: authentik/providers/scim/tasks.py
msgid "Sync SCIM provider objects."
msgstr "Synchronise les objets du fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Full sync for SCIM provider."
msgstr "Synchronisation complète pour le fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Sync a direct object (user, group) for SCIM provider."
msgstr ""
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Dispatch syncs for a direct object (user, group) for SCIM providers."
msgstr ""
"Déclenche les synchronisations pour un objet direct (utilisateur, groupe) "
"pour les fournisseurs SCIM."
#: authentik/providers/scim/tasks.py
msgid "Sync a related object (memberships) for SCIM provider."
msgstr "Synchronise un objet lié (appartenances) pour le fournisseur SCIM."
#: authentik/providers/scim/tasks.py
msgid "Dispatch syncs for a related object (memberships) for SCIM providers."
msgstr ""
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
"fournisseurs SCIM."
#: authentik/rbac/models.py
msgid "Role"
msgstr "Rôle"
@@ -2592,14 +2399,6 @@ msgstr "Connexion du groupe à la source Kerberos"
msgid "Group Kerberos Source Connections"
msgstr "Connexions du groupe à la source Kerberos"
#: authentik/sources/kerberos/tasks.py
msgid "Check connectivity for Kerberos sources."
msgstr "Vérifie la connectivité des sources Kerberos."
#: authentik/sources/kerberos/tasks.py
msgid "Sync Kerberos source."
msgstr "Synchronise la source Kerberos."
#: authentik/sources/kerberos/views.py
msgid "SPNEGO authentication required"
msgstr "Authentification SPNEGO requise"
@@ -2767,18 +2566,6 @@ msgstr "Connexions du groupe à la source LDAP"
msgid "Password does not match Active Directory Complexity."
msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory."
#: authentik/sources/ldap/tasks.py
msgid "Check connectivity for LDAP source."
msgstr "Vérifie la connectivité des sources LDAP."
#: authentik/sources/ldap/tasks.py
msgid "Sync LDAP source."
msgstr "Synchronise la source LDAP."
#: authentik/sources/ldap/tasks.py
msgid "Sync page for LDAP source."
msgstr "Synchronise une page pour la source LDAP."
#: authentik/sources/oauth/clients/oauth2.py
msgid "No token received."
msgstr "Pas de jeton reçu."
@@ -2928,14 +2715,6 @@ msgstr "Source d'OAuth Azure AD"
msgid "Azure AD OAuth Sources"
msgstr "Source d'OAuth Azure AD"
#: authentik/sources/oauth/models.py
msgid "Entra ID OAuth Source"
msgstr "Source d'OAuth Entra ID"
#: authentik/sources/oauth/models.py
msgid "Entra ID OAuth Sources"
msgstr "Sources d'OAuth Entra ID"
#: authentik/sources/oauth/models.py
msgid "OpenID OAuth Source"
msgstr "Source d'OAuth OpenID"
@@ -2992,14 +2771,6 @@ msgstr "Connexion du groupe à la source OAuth"
msgid "Group OAuth Source Connections"
msgstr "Connexions du groupe à la source OAuth"
#: authentik/sources/oauth/tasks.py
msgid ""
"Update OAuth sources' config from well_known, and JWKS info from the "
"configured URL."
msgstr ""
"Met à jour la configuration des sources OAuth à partir de well_known, et les"
" informations JWKS à partir de l'URL configurée."
#: authentik/sources/oauth/views/callback.py
#, python-brace-format
msgid "Authentication failed: {reason}"
@@ -3058,10 +2829,6 @@ msgstr "Connexion du groupe à la source Plex"
msgid "Group Plex Source Connections"
msgstr "Connexions du groupe à la source OAuth"
#: authentik/sources/plex/tasks.py
msgid "Check the validity of a Plex source."
msgstr "Vérifie la validité d'une source Plex."
#: authentik/sources/saml/models.py
msgid "Redirect Binding"
msgstr "Liaison de Redirection"
@@ -3506,13 +3273,6 @@ msgstr "Type d'appareil WebAuthn"
msgid "WebAuthn Device types"
msgstr "Types d'appareil WebAuthn"
#: authentik/stages/authenticator_webauthn/tasks.py
msgid ""
"Background task to import FIDO Alliance MDS blob and AAGUIDs into database."
msgstr ""
"Tâche de fond pour importer le blob MDS de la FIDO Alliance et les AAGUID "
"dans la base de données."
#: authentik/stages/captcha/models.py
msgid "Public key, acquired your captcha Provider."
msgstr "Clé publique, acquise auprès de votre fournisseur captcha."
@@ -3611,17 +3371,6 @@ msgstr "Réinitialiser le Mot de Passe"
msgid "Account Confirmation"
msgstr "Confirmation du Compte"
#: authentik/stages/email/models.py
msgid ""
"The time window used to count recent account recovery attempts. If the "
"number of attempts exceed recovery_max_attempts within this period, further "
"attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3)."
msgstr ""
"La fenêtre de temps utilisée pour compter les tentatives récentes de "
"récupération de compte. Si le nombre de tentatives dépasse "
"recovery_max_attempts au cours de cette période, les tentatives "
"supplémentaires seront limitées. (Format : hours=1;minutes=2;seconds=3)."
#: authentik/stages/email/models.py
msgid "Activate users upon completion of stage."
msgstr "Activer les utilisateurs à la complétion de l'étape."
@@ -3646,23 +3395,10 @@ msgstr "Pas d'utilisateurs en attente."
msgid "Email sent."
msgstr "Email envoyé."
#: authentik/stages/email/stage.py
#, python-brace-format
msgid ""
"Too many account verification attempts. Please try again after {minutes} "
"minutes."
msgstr ""
"Trop de tentatives de vérification de compte. Veuillez réessayer après "
"{minutes} minutes."
#: authentik/stages/email/stage.py
msgid "Email Successfully sent."
msgstr "Couriel envoyé avec succès."
#: authentik/stages/email/tasks.py
msgid "Send email."
msgstr "Envoye un courriel."
#: authentik/stages/email/templates/email/account_confirmation.html
#: authentik/stages/email/templates/email/account_confirmation.txt
msgid "Welcome!"
@@ -4131,16 +3867,6 @@ msgstr ""
"souvenir de moi ne sera pas proposée. (Format: "
"hours=-1;minutes=-2;seconds=-3)"
#: authentik/stages/user_login/models.py
msgid ""
"When set to a non-zero value, authentik will save a cookie with a longer "
"expiry,to remember the device the user is logging in from. (Format: "
"hours=-1;minutes=-2;seconds=-3)"
msgstr ""
"Si cette valeur est différente de zéro, authentik enregistrera un cookie "
"avec une expiration plus longue, afin de se souvenir de l'appareil à partir "
"duquel l'utilisateur se connecte. (Format : hours=-1;minutes=-2;seconds=-3)"
#: authentik/stages/user_login/models.py
msgid "User Login Stage"
msgstr "Étape de connexion utlisateur"
@@ -4192,38 +3918,6 @@ msgid "Failed to update user. Please try again later."
msgstr ""
"Échec de mise à jour de l'utilisateur. Merci de réessayer ultérieurement,"
#: authentik/tasks/models.py
msgid "Tenant this task belongs to"
msgstr "Tenant auquel cette tâche appartient"
#: authentik/tasks/models.py
msgid "Retry failed task"
msgstr "Relancer la tâche échouée"
#: authentik/tasks/models.py
msgid "Worker status"
msgstr "État du worker"
#: authentik/tasks/models.py
msgid "Worker statuses"
msgstr "États du worker"
#: authentik/tasks/schedules/models.py
msgid "Unique schedule identifier"
msgstr "Identifiant unique des planifications"
#: authentik/tasks/schedules/models.py
msgid "User schedule identifier"
msgstr "Identifiant utilisateur des planifications"
#: authentik/tasks/schedules/models.py
msgid "Manually trigger a schedule"
msgstr "Déclencher manuellement une planification"
#: authentik/tasks/tasks.py
msgid "Remove old worker statuses."
msgstr "Supprime les anciens statuts des workers."
#: authentik/tenants/models.py
msgid ""
"Schema name must start with t_, only contain lowercase letters and numbers "
@@ -4316,76 +4010,3 @@ msgstr "Domaine"
#: authentik/tenants/models.py
msgid "Domains"
msgstr "Domaines"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Queue name"
msgstr "Nom de la file"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Dramatiq actor name"
msgstr "Nom de l'acteur Dramatiq"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Message body"
msgstr "Corps du message"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task status"
msgstr "État de la tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task last modified time"
msgstr "Heure de dernière modification de la tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task result"
msgstr "Résultat de la tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Result expiry time"
msgstr "Délai d'expiration du résultat"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Task"
msgstr "Tâche"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Tasks"
msgstr "Tâches"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
#, python-format
msgid "%(value)s is not a valid crontab"
msgstr "%(value)s n'est pas un crontab valide"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Dramatiq actor to call"
msgstr "Acteur Dramatiq à invoquer"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Args to send to the actor"
msgstr "Args à passer à l'acteur"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Kwargs to send to the actor"
msgstr "Kwargs à passer à l'acteur"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Options to send to the actor"
msgstr "Options à passer à l'acteur"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "When to schedule tasks"
msgstr "Quand planifier les tâches"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Pause this schedule"
msgstr "Mettre cette planification en pause"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Schedule"
msgstr "Planification"
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
msgid "Schedules"
msgstr "Planifications"

View File

@@ -10,8 +10,7 @@
"license": "MIT",
"dependencies": {
"deepmerge-ts": "^7.1.5",
"prism-react-renderer": "^2.4.1",
"react-dom": ">=18"
"prism-react-renderer": "^2.4.1"
},
"devDependencies": {
"@docusaurus/theme-common": "^3.8.1",
@@ -35,7 +34,8 @@
"@docusaurus/theme-common": "^3.8.1",
"@docusaurus/theme-search-algolia": "^3.8.1",
"@docusaurus/types": "^3.8.0",
"react": ">=18"
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"@docusaurus/theme-search-algolia": {
@@ -43,6 +43,9 @@
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
@@ -4653,9 +4656,9 @@
}
},
"node_modules/@types/react-dom": {
"version": "19.1.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"version": "19.1.6",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -15818,25 +15821,25 @@
}
},
"node_modules/react": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.1"
"react": "^19.1.0"
}
},
"node_modules/react-fast-compare": {

View File

@@ -661,16 +661,16 @@
}
},
"node_modules/@gerrit0/mini-shiki": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz",
"integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz",
"integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/engine-oniguruma": "^3.9.2",
"@shikijs/langs": "^3.9.2",
"@shikijs/themes": "^3.9.2",
"@shikijs/types": "^3.9.2",
"@shikijs/engine-oniguruma": "^3.7.0",
"@shikijs/langs": "^3.7.0",
"@shikijs/themes": "^3.7.0",
"@shikijs/types": "^3.7.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
@@ -845,40 +845,40 @@
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz",
"integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz",
"integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.9.2",
"@shikijs/types": "3.7.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz",
"integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.7.0.tgz",
"integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.9.2"
"@shikijs/types": "3.7.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz",
"integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.7.0.tgz",
"integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.9.2"
"@shikijs/types": "3.7.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz",
"integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.7.0.tgz",
"integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -904,13 +904,13 @@
}
},
"node_modules/@types/node": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
"undici-types": "~7.8.0"
}
},
"node_modules/@types/unist": {
@@ -2668,9 +2668,9 @@
}
},
"node_modules/tmp": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
"integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2704,13 +2704,13 @@
}
},
"node_modules/typedoc": {
"version": "0.28.9",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.9.tgz",
"integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==",
"version": "0.28.8",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.8.tgz",
"integrity": "sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@gerrit0/mini-shiki": "^3.9.0",
"@gerrit0/mini-shiki": "^3.7.0",
"lunr": "^2.3.9",
"markdown-it": "^14.1.0",
"minimatch": "^9.0.5",
@@ -2724,7 +2724,7 @@
"pnpm": ">= 10"
},
"peerDependencies": {
"typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x"
"typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x"
}
},
"node_modules/typedoc-plugin-markdown": {
@@ -2741,9 +2741,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -2762,9 +2762,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true,
"license": "MIT"
},

View File

@@ -500,102 +500,15 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz",
"integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/type-utils": "8.39.0",
"@typescript-eslint/utils": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.39.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz",
"integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
"integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.0",
"@typescript-eslint/types": "^8.39.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
"integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0"
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -605,52 +518,10 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
"integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz",
"integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/utils": "8.39.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -661,106 +532,14 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
"integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.0",
"@typescript-eslint/tsconfig-utils": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz",
"integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
"integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/types": "8.38.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -4708,16 +4487,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz",
"integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz",
"integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/utils": "8.39.0"
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4728,7 +4507,228 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/type-utils": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.38.0",
"@typescript-eslint/tsconfig-utils": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.38.0",
"@typescript-eslint/types": "^8.38.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/typescript-eslint/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/typescript-eslint/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/typescript-eslint/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/typescript-eslint/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/unbox-primitive": {

View File

@@ -73,48 +73,48 @@
}
},
"node_modules/@dozerg/condition": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@dozerg/condition/-/condition-1.0.11.tgz",
"integrity": "sha512-+rKdLoe9mKmiXz4JuigIYSHhKKJFIiFkr6CVV0ilS7UqJjk5KHCSycb88cyGQIA0/p5UpgzVdzxbwESh93/Xrg==",
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@dozerg/condition/-/condition-1.0.10.tgz",
"integrity": "sha512-TenWvtppkVU/MnGEG/Z8O62ZmAjvR45vNXdtV8YkJb6d+RCrqqmo4vgcrWjre6WO4u+wciTOtFa6Xvafp/LicA==",
"license": "MIT"
},
"node_modules/@dozerg/end-of-line": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/@dozerg/end-of-line/-/end-of-line-1.0.20.tgz",
"integrity": "sha512-DuKvHM/02lJldJDAExcvtsJEp7wteGyE9KbnGWX2Y/lrsAMca0PZY7kDs3xunGHh+rlWogMwf1jbR6rW4Qh5/w==",
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/@dozerg/end-of-line/-/end-of-line-1.0.19.tgz",
"integrity": "sha512-l8lOHu9O8tv/HxKknlRLVZ563mTSRuMfGGD1rVBUPCphzCV8J9z4epu10AWCye61m9nMllm0EvcgpoUZ7GKqkg==",
"license": "MIT"
},
"node_modules/@dozerg/find-up": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@dozerg/find-up/-/find-up-1.0.9.tgz",
"integrity": "sha512-lWaeo2wMcTR5y40TIKao4d2eTKfUEaWDTXFRNigzyd4V/3o0HrvlpnP9Twoz+8ErLgW5qg9FpIR0ocNOYbPqIg==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@dozerg/find-up/-/find-up-1.0.8.tgz",
"integrity": "sha512-mIOqSViLKe+Mpz7hcCHPwuuNnmfwmQ1DGUWTJowO3ilHZYC8ET9bxIkKygEn6qavmXxcLIgeqcnOec4yHtakrg==",
"license": "MIT",
"dependencies": {
"@dozerg/condition": "^1.0.11"
"@dozerg/condition": "^1.0.10"
}
},
"node_modules/@dozerg/merge-options": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@dozerg/merge-options/-/merge-options-1.0.12.tgz",
"integrity": "sha512-RK50IaS0R+CzIzNicZqxmnIjz8TyWoN8y/RfeOanTBeAR4DnRtGV/1Ee3Mbcodrvta1eBcSQvgEbPcKI9u40PQ==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@dozerg/merge-options/-/merge-options-1.0.11.tgz",
"integrity": "sha512-4JTleitLzGXhLaZ39fxM5pLdQNHu7FX/udJx0p/vsbHUgthw3gM+eQtEOUO4J+IC6HRGpQ7m0qKEc4P276HWNQ==",
"license": "MIT",
"dependencies": {
"@dozerg/condition": "^1.0.11"
"@dozerg/condition": "^1.0.10"
}
},
"node_modules/@dozerg/no-new": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dozerg/no-new/-/no-new-0.0.9.tgz",
"integrity": "sha512-0hTRFDQOSDq4obHYSS/6M2N1YgXbxr2RF2L1ONVMLEtfLsJP7UPJ7CHUkTV3RCv1sNgmPCJEfIgCm79PrXGcmg==",
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@dozerg/no-new/-/no-new-0.0.7.tgz",
"integrity": "sha512-ElmuHoVmQrTA0feh1lQRHWVNeh2g6lYJXV+R2ciKmNQCdQwADgsoGAQ8BCp7PTZck4Q/3SZuy5j41Br41E6UGA==",
"license": "MIT"
},
"node_modules/@dozerg/require-module": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/@dozerg/require-module/-/require-module-0.0.10.tgz",
"integrity": "sha512-3CE3tPYeHxoDVctWE5xxjYSgGOB+NWThRnXqX3lMU76Rn2Njk7VCiPu9UVTlTD2SN9NiFmTLmxYZGFOf88noNw==",
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@dozerg/require-module/-/require-module-0.0.8.tgz",
"integrity": "sha512-tnW9TQzCSbZ2UNDeLjlsilihS6BdtNY1TIfiCT6r6qQ4h+OHEKaNSFG/k0FJIZDPFE/fmEVlZErHdkPK1j5VYQ==",
"license": "MIT",
"dependencies": {
"@dozerg/find-up": "^1.0.9",
"@dozerg/find-up": "^1.0.7",
"log4js": "^6.9.1"
}
},
@@ -385,13 +385,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
"undici-types": "~7.8.0"
}
},
"node_modules/@vue/compiler-core": {
@@ -958,32 +958,32 @@
"license": "ISC"
},
"node_modules/format-imports": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/format-imports/-/format-imports-4.0.8.tgz",
"integrity": "sha512-lHZlTxZlfqqiCzlLUe3Lx1d7vzqHHM5nW4xsLnZzc7gcrhqA6lCJSuUnjKGaYVpkTFgp4435RyamEXSU0YxvZg==",
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/format-imports/-/format-imports-4.0.7.tgz",
"integrity": "sha512-0zT3DL9wtFjqR0kKAI2rBLBZlrDxPoihbxrLicC3wqtjEGRL8ojjpSzPBnKqgJkzYR2TDY8DIuaYfcGqWHhePA==",
"license": "MIT",
"dependencies": {
"@dozerg/condition": "^1.0.11",
"@dozerg/end-of-line": "^1.0.20",
"@dozerg/find-up": "^1.0.9",
"@dozerg/merge-options": "^1.0.12",
"@dozerg/no-new": "^0.0.9",
"@dozerg/require-module": "^0.0.10",
"@dozerg/condition": "^1.0.9",
"@dozerg/end-of-line": "^1.0.18",
"@dozerg/find-up": "^1.0.7",
"@dozerg/merge-options": "^1.0.10",
"@dozerg/no-new": "^0.0.7",
"@dozerg/require-module": "^0.0.8",
"@vue/compiler-sfc": "3.3.11",
"eslint": "^8.57.1",
"fs-extra": "^11.3.0",
"immutable": "^5.1.3",
"fs-extra": "^11.2.0",
"immutable": "^5.0.3",
"is-builtin-module": "^3.2.1",
"log4js": "^6.9.1",
"minimatch": "^10.0.3",
"minimatch": "^10.0.1",
"node-cache": "^5.1.2",
"optionator": "^0.9.4",
"prettier": "^3.6.2",
"segment-sort": "^1.0.9",
"prettier": "^3.4.2",
"segment-sort": "^1.0.7",
"tmp": "^0.2.3",
"typescript": "^5.7.2",
"utility-types": "^3.11.0",
"validator": "^13.15.15"
"validator": "^13.12.0"
},
"bin": {
"format-imports": "dist/bin/main.js"
@@ -1509,9 +1509,9 @@
"license": "MIT"
},
"node_modules/segment-sort": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/segment-sort/-/segment-sort-1.0.9.tgz",
"integrity": "sha512-MnxEGYVhbmQ66R1WlCH+NVJGAwsJorio6pNO8IRpUWZiv1lF601Mi6s1P0qr6q+PHnjc6IKO2XEnXB6c+6eUrQ==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/segment-sort/-/segment-sort-1.0.8.tgz",
"integrity": "sha512-CUNjTlN5/Q7kmIGiyK6wFcfHgnQOIb88qRgCMJ61U3KCgLbPmRdkft8QW7b+Aoh4T1iEII54/e5cf3uSQwoGvg==",
"license": "MIT"
},
"node_modules/semver": {
@@ -1724,9 +1724,9 @@
}
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true,
"license": "MIT"
},

View File

@@ -44845,15 +44845,6 @@ components:
activate_user_on_success:
type: boolean
description: Activate users upon completion of stage.
recovery_max_attempts:
type: integer
maximum: 2147483647
minimum: 0
recovery_cache_timeout:
type: string
description: 'The time window used to count recent account recovery attempts.
If the number of attempts exceed recovery_max_attempts within this period,
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
required:
- component
- meta_model_name
@@ -44914,16 +44905,6 @@ components:
activate_user_on_success:
type: boolean
description: Activate users upon completion of stage.
recovery_max_attempts:
type: integer
maximum: 2147483647
minimum: 0
recovery_cache_timeout:
type: string
minLength: 1
description: 'The time window used to count recent account recovery attempts.
If the number of attempts exceed recovery_max_attempts within this period,
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
required:
- name
Endpoint:
@@ -49522,10 +49503,6 @@ components:
type: array
items:
$ref: '#/components/schemas/RedirectURI'
backchannel_logout_uri:
type: string
title: Back-Channel Logout URI
format: uri
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -49633,10 +49610,6 @@ components:
type: array
items:
$ref: '#/components/schemas/RedirectURIRequest'
backchannel_logout_uri:
type: string
title: Back-Channel Logout URI
format: uri
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'
@@ -53413,16 +53386,6 @@ components:
activate_user_on_success:
type: boolean
description: Activate users upon completion of stage.
recovery_max_attempts:
type: integer
maximum: 2147483647
minimum: 0
recovery_cache_timeout:
type: string
minLength: 1
description: 'The time window used to count recent account recovery attempts.
If the number of attempts exceed recovery_max_attempts within this period,
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
PatchedEndpointDeviceRequest:
type: object
description: Serializer for Endpoint authenticator devices
@@ -54556,10 +54519,6 @@ components:
type: array
items:
$ref: '#/components/schemas/RedirectURIRequest'
backchannel_logout_uri:
type: string
title: Back-Channel Logout URI
format: uri
sub_mode:
allOf:
- $ref: '#/components/schemas/SubModeEnum'

31
uv.lock generated
View File

@@ -86,15 +86,15 @@ wheels = [
[[package]]
name = "anyio"
version = "4.10.0"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
[[package]]
@@ -557,30 +557,30 @@ wheels = [
[[package]]
name = "boto3"
version = "1.40.2"
version = "1.40.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/c0/9ceff05d2243f169765ae9db08fa6f085d026af71a778cd083dc972f0f2b/boto3-1.40.2.tar.gz", hash = "sha256:2dfbc214fdbf94abfd61eec687ea39089d05af43bb00be792c76f3a6c1393f7b", size = 111826, upload-time = "2025-08-04T19:31:51.959Z" }
sdist = { url = "https://files.pythonhosted.org/packages/48/4d/70d209fdebf0377db233f80dfdf26ca2bc25d2b2e89d4882e0edccd2227f/boto3-1.40.1.tar.gz", hash = "sha256:985ed4bf64729807f870eadbc46ad98baf93096917f7194ec39d743ff75b3f1d", size = 111817, upload-time = "2025-08-01T19:24:18.017Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/66/01bccaaebcd1365ce1334be042765e49ccf23787887afb8e43c6d4bc2f6e/boto3-1.40.2-py3-none-any.whl", hash = "sha256:3d99325ee874190e8f3bfd38823987327c826cdfbab943420851bdb7684d727c", size = 139882, upload-time = "2025-08-04T19:31:50.493Z" },
{ url = "https://files.pythonhosted.org/packages/97/0e/f0cb4f71c40ba07e6ed5b47699a737a080d3c4f4b7b26657d5671de48621/boto3-1.40.1-py3-none-any.whl", hash = "sha256:7c007d5c8ee549e9fcad0927536502da199b27891006ef515330f429aca9671f", size = 139880, upload-time = "2025-08-01T19:24:16.581Z" },
]
[[package]]
name = "botocore"
version = "1.40.2"
version = "1.40.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/e7d68381042a6d50510c8d4629f39922ce27ff32f45baf852ba6534342c5/botocore-1.40.2.tar.gz", hash = "sha256:77c4710bf37b28e897833b5b1f47d6a83e45a29985cd01a560dfdb8b6ad524e5", size = 14284599, upload-time = "2025-08-04T19:31:42.064Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c6/d2/d914999f4a128f0f840f2a9cc8327cd98aa661d6b33b331a81a8111ab970/botocore-1.40.1.tar.gz", hash = "sha256:bdf30e2c0e8cdb939d81fc243182a6d1dd39c416694b406c5f2ea079b1c2f3f5", size = 14280398, upload-time = "2025-08-01T19:24:08.599Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/56/dd25fb9e47060e8f7e353208678fefb65d1b06704ea30983cad8bdd81370/botocore-1.40.2-py3-none-any.whl", hash = "sha256:a31e6269af05498f8dc1c7f2b3f34448a0f16c79a8601c0389ecddab51b2c2ab", size = 13944886, upload-time = "2025-08-04T19:31:37.027Z" },
{ url = "https://files.pythonhosted.org/packages/d4/c1/aa7922c9bf74b6d6594d2430af6f854d234faff23187e269aaba89c326c8/botocore-1.40.1-py3-none-any.whl", hash = "sha256:e039774b55fbd6fe59f0f4fea51d156a2433bd4d8faa64fc1b87aee9a03f415d", size = 13940950, upload-time = "2025-08-01T19:24:03.889Z" },
]
[[package]]
@@ -603,15 +603,14 @@ wheels = [
[[package]]
name = "cattrs"
version = "25.1.1"
version = "24.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/2b/561d78f488dcc303da4639e02021311728fb7fda8006dd2835550cddd9ed/cattrs-25.1.1.tar.gz", hash = "sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c", size = 435016, upload-time = "2025-06-04T20:27:15.44Z" }
sdist = { url = "https://files.pythonhosted.org/packages/29/7b/da4aa2f95afb2f28010453d03d6eedf018f9e085bd001f039e15731aba89/cattrs-24.1.3.tar.gz", hash = "sha256:981a6ef05875b5bb0c7fb68885546186d306f10f0f6718fe9b96c226e68821ff", size = 426684, upload-time = "2025-03-25T15:01:00.325Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/b0/215274ef0d835bbc1056392a367646648b6084e39d489099959aefcca2af/cattrs-25.1.1-py3-none-any.whl", hash = "sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064", size = 69386, upload-time = "2025-06-04T20:27:13.969Z" },
{ url = "https://files.pythonhosted.org/packages/3c/ee/d68a3de23867a9156bab7e0a22fb9a0305067ee639032a22982cf7f725e7/cattrs-24.1.3-py3-none-any.whl", hash = "sha256:adf957dddd26840f27ffbd060a6c4dd3b2192c5b7c2c0525ef1bd8131d8a83f5", size = 66462, upload-time = "2025-03-25T15:00:58.663Z" },
]
[[package]]
@@ -632,11 +631,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2025.8.3"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
]
[[package]]

1948
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,6 @@
}
},
"dependencies": {
"@codecov/bundle-analyzer": "^1.9.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
@@ -95,7 +94,7 @@
"@floating-ui/dom": "^1.7.3",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^7.0.0",
"@goauthentik/api": "^2025.6.4-1754491498",
"@goauthentik/api": "^2025.6.4-1754241870",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
@@ -114,19 +113,19 @@
"@openlayers-elements/maps": "^0.4.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^10.2.0",
"@spotlightjs/spotlight": "^3.0.2",
"@storybook/addon-docs": "^9.1.1",
"@storybook/addon-links": "^9.1.1",
"@storybook/web-components": "^9.1.1",
"@storybook/web-components-vite": "^9.1.1",
"@sentry/browser": "^10.0.0",
"@spotlightjs/spotlight": "^3.0.1",
"@storybook/addon-docs": "^9.1.0",
"@storybook/addon-links": "^9.1.0",
"@storybook/web-components": "^9.1.0",
"@storybook/web-components-vite": "^9.1.0",
"@types/codemirror": "^5.60.16",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.3",
"@types/mocha": "^10.0.10",
"@types/node": "^24.2.0",
"@types/node": "^24.1.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.7",
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
@@ -163,7 +162,7 @@
"pseudolocale": "^2.1.0",
"rapidoc": "^9.3.8",
"react": "^19.1.0",
"react-dom": "^19.1.1",
"react-dom": "^19.1.0",
"rehype-highlight": "^7.0.2",
"rehype-mermaid": "^3.0.0",
"rehype-parse": "^9.0.1",
@@ -178,7 +177,7 @@
"ts-pattern": "^5.8.0",
"turnstile-types": "^1.2.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.39.0",
"typescript-eslint": "^8.38.0",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",

View File

@@ -47,7 +47,7 @@
"dependencies": {
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@types/node": "^24.2.0",
"@types/node": "^24.1.0",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
},

View File

@@ -14,7 +14,6 @@ import { NodeEnvironment } from "@goauthentik/core/environment/node";
import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node";
import { readBuildIdentifier } from "@goauthentik/core/version/node";
import { createAndUploadReport } from "@codecov/bundle-analyzer";
import { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild";
import { copy } from "esbuild-plugin-copy";
@@ -206,54 +205,6 @@ async function doBuild() {
await esbuild.build(buildOptions);
console.log("Build complete");
if (process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
await doBundleSizeReport(buildOptions);
}
}
/**
* @param {esbuild.BuildOptions} buildOpts
*/
async function doBundleSizeReport(buildOpts) {
/**
* @type {import("@codecov/bundle-analyzer").BundleAnalyzerOptions}
*/
const bundleAnalyzerOpts = {
ignorePatterns: ["*.map"],
};
/**
* @type {import("@codecov/bundle-analyzer").Options}
*/
const coreOpts = {
enableBundleAnalysis: true,
gitService: "github",
oidc: {
useGitHubOIDC: true,
},
};
console.group(`${logPrefix} 🚀 Uploading bundle report:`);
await Promise.all(
Object.entries(EntryPoint)
.filter(([id]) => id !== "Polyfill")
.map(([entrypointID, target]) => {
return createAndUploadReport(
[path.dirname(target.out)],
deepmerge(coreOpts, {
bundleName: `@goauthentik/authentik-web-${entrypointID.toLowerCase()}`,
}),
bundleAnalyzerOpts,
)
.then((reportAsJson) =>
console.log(`Report successfully generated and uploaded: ${reportAsJson}`),
)
.catch((error) => console.error("Failed to generate or upload report:", error));
}),
);
console.groupEnd();
}
async function doProxy() {

View File

@@ -20,7 +20,6 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { ModelForm } from "#elements/forms/ModelForm";
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { navigate } from "#elements/router/RouterOutlet";
import { iconHelperText } from "#admin/helperText";
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
@@ -34,90 +33,83 @@ import { ifDefined } from "lit/directives/if-defined.js";
@customElement("ak-application-form")
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
#api = new CoreApi(DEFAULT_CONFIG);
constructor() {
super();
this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this);
this.makeRemoveBackchannelProviderHandler =
this.makeRemoveBackchannelProviderHandler.bind(this);
}
protected override async loadInstance(pk: string): Promise<Application> {
const app = await this.#api.coreApplicationsRetrieve({
async loadInstance(pk: string): Promise<Application> {
const app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({
slug: pk,
});
this.clearIcon = false;
this.backchannelProviders = app.backchannelProvidersObj || [];
return app;
}
@property({ attribute: false })
public provider?: number;
provider?: number;
@state()
protected backchannelProviders: Provider[] = [];
backchannelProviders: Provider[] = [];
@property({ type: Boolean })
public clearIcon = false;
clearIcon = false;
protected override getSuccessMessage(): string {
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated application.")
: msg("Successfully created application.");
}
public override async send(applicationRequest: Application): Promise<Application | void> {
applicationRequest.backchannelProviders = this.backchannelProviders.map((p) => p.pk);
const currentSlug = this.instance?.slug;
const app = await (currentSlug
? this.#api.coreApplicationsUpdate({
applicationRequest,
slug: currentSlug,
})
: this.#api.coreApplicationsCreate({ applicationRequest }));
const nextSlug = app.slug;
async send(data: Application): Promise<Application | void> {
let app: Application;
data.backchannelProviders = this.backchannelProviders.map((p) => p.pk);
if (this.instance) {
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsUpdate({
slug: this.instance.slug,
applicationRequest: data,
});
} else {
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({
applicationRequest: data,
});
}
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.files().get("metaIcon");
if (icon || this.clearIcon) {
await this.#api.coreApplicationsSetIconCreate({
slug: nextSlug,
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
slug: app.slug,
file: icon,
clear: this.clearIcon,
});
}
} else {
await this.#api.coreApplicationsSetIconUrlCreate({
slug: nextSlug,
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconUrlCreate({
slug: app.slug,
filePathRequest: {
url: applicationRequest.metaIcon || "",
url: data.metaIcon || "",
},
});
}
if (currentSlug && currentSlug !== nextSlug) {
// TODO: This needs refining.
this.instancePk = nextSlug;
navigate(`/core/applications/${nextSlug}`);
}
return app;
}
#handleConfirmBackchannelProviders = (items: Provider[]) => {
handleConfirmBackchannelProviders(items: Provider[]) {
this.backchannelProviders = items;
this.requestUpdate();
return Promise.resolve();
};
}
#makeRemoveBackchannelProviderHandler = (provider: Provider) => {
makeRemoveBackchannelProviderHandler(provider: Provider) {
return () => {
const idx = this.backchannelProviders.indexOf(provider);
this.backchannelProviders.splice(idx, 1);
this.requestUpdate();
};
};
}
handleClearIcon(ev: Event) {
ev.stopPropagation();
@@ -127,25 +119,22 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
}
public override renderForm(): TemplateResult {
renderForm(): TemplateResult {
const alertMsg = msg(
"Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.",
);
return html`
return html`<form class="pf-c-form pf-m-horizontal">
${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
<ak-text-input
name="name"
autocomplete="off"
placeholder=${msg("Application name")}
value=${ifDefined(this.instance?.name)}
label=${msg("Name")}
required
help=${msg("The name displayed in the application library.")}
help=${msg("Application's display Name.")}
></ak-text-input>
<ak-slug-input
name="slug"
autocomplete="off"
value=${ifDefined(this.instance?.slug)}
label=${msg("Slug")}
required
@@ -156,7 +145,6 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
name="group"
value=${ifDefined(this.instance?.group)}
label=${msg("Group")}
placeholder=${msg("e.g. Collaboration, Communication, Internal, etc.")}
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
@@ -176,8 +164,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
"Select backchannel providers which augment the functionality of the main provider.",
)}
.providers=${this.backchannelProviders}
.confirm=${this.#handleConfirmBackchannelProviders}
.remover=${this.#makeRemoveBackchannelProviderHandler}
.confirm=${this.handleConfirmBackchannelProviders}
.remover=${this.makeRemoveBackchannelProviderHandler}
.tooltip=${html`<pf-tooltip
position="top"
content=${msg("Add provider")}
@@ -196,7 +184,6 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}
placeholder="https://..."
value=${ifDefined(this.instance?.metaLaunchUrl)}
help=${msg(
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
@@ -248,7 +235,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
></ak-textarea-input>
</div>
</ak-form-group>
`;
</form>`;
}
}

View File

@@ -13,7 +13,6 @@ import "#elements/buttons/SpinnerButton/ak-spinner-button";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { AKElement } from "#elements/Base";
@@ -24,7 +23,7 @@ import {
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { CSSResult, html, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -41,6 +40,15 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-application-view")
export class ApplicationViewPage extends AKElement {
@property({ type: String })
applicationSlug?: string;
@state()
application?: Application;
@state()
missingOutpost = false;
static styles: CSSResult[] = [
PFBase,
PFList,
@@ -53,28 +61,7 @@ export class ApplicationViewPage extends AKElement {
PFCard,
];
//#region Properties
@property({ type: String })
public applicationSlug?: string;
//#endregion
//#region State
@state()
protected application?: Application;
@state()
protected error?: APIError;
@state()
protected missingOutpost = false;
//#endregion
//#region Lifecycle
protected fetchIsMissingOutpost(providersByPk: Array<number>) {
fetchIsMissingOutpost(providersByPk: Array<number>) {
new OutpostsApi(DEFAULT_CONFIG)
.outpostsInstancesList({
providersByPk,
@@ -87,34 +74,27 @@ export class ApplicationViewPage extends AKElement {
});
}
protected fetchApplication(slug: string) {
new CoreApi(DEFAULT_CONFIG)
.coreApplicationsRetrieve({ slug })
.then((app) => {
this.application = app;
if (
app.providerObj &&
[
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersProxyProxyprovider.toString(),
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersLdapLdapprovider.toString(),
].includes(app.providerObj.metaModelName)
) {
this.fetchIsMissingOutpost([app.provider || 0]);
}
})
.catch(async (error) => {
this.error = await parseAPIResponseError(error);
});
fetchApplication(slug: string) {
new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({ slug }).then((app) => {
this.application = app;
if (
app.providerObj &&
[
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersProxyProxyprovider.toString(),
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersLdapLdapprovider.toString(),
].includes(app.providerObj.metaModelName)
) {
this.fetchIsMissingOutpost([app.provider || 0]);
}
});
}
public override willUpdate(changedProperties: PropertyValues<this>) {
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("applicationSlug") && this.applicationSlug) {
this.fetchApplication(this.applicationSlug);
}
}
//#region Render
render(): TemplateResult {
return html`<ak-page-header
header=${this.application?.name || msg("Loading")}
@@ -131,17 +111,9 @@ export class ApplicationViewPage extends AKElement {
}
renderApp(): TemplateResult {
if (this.error) {
return html`<ak-empty-state icon="fa-ban"
><span>${msg(str`Failed to fetch application "${this.applicationSlug}".`)}</span>
<div slot="body">${pluckErrorDetail(this.error)}</div>
</ak-empty-state>`;
}
if (!this.application) {
return html`<ak-empty-state default-label></ak-empty-state>`;
}
return html`<ak-tabs>
${this.missingOutpost
? html`<div slot="header" class="pf-c-banner pf-m-warning">
@@ -216,7 +188,7 @@ export class ApplicationViewPage extends AKElement {
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text pf-m-monospace">
<div class="pf-c-description-list__text">
${this.application.policyEngineMode?.toUpperCase()}
</div>
</dd>

View File

@@ -15,12 +15,12 @@ import { WizardStep } from "#components/ak-wizard/WizardStep";
import { styles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles";
import { ApplicationRequest, ValidationError } from "@goauthentik/api";
import { ValidationError } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { property, query } from "lit/decorators.js";
export class ApplicationWizardStep<T = Partial<ApplicationRequest>> extends WizardStep {
export class ApplicationWizardStep<T = Record<string, unknown>> extends WizardStep {
static styles = [...WizardStep.styles, ...styles];
@property({ type: Object, attribute: false })
@@ -28,15 +28,15 @@ export class ApplicationWizardStep<T = Partial<ApplicationRequest>> extends Wiza
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes.
protected wizardTitle = msg("New application");
protected wizardDescription = msg("Create a new application and configure a provider for it.");
public canCancel = true;
wizardTitle = msg("New application");
wizardDescription = msg("Create a new application and configure a provider for it.");
canCancel = true;
// This should be overridden in the children for more precise targeting.
@query("form")
protected form!: HTMLFormElement;
form!: HTMLFormElement;
protected get formValues(): T {
get formValues(): T {
return serializeForm<T>([
...this.form.querySelectorAll("ak-form-element-horizontal"),
...this.form.querySelectorAll("[data-ak-control]"),

View File

@@ -50,16 +50,7 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
}
function renderRACOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as RACProvider;
return renderSummary("RAC", provider.name, [
[msg("Connection expiry"), provider.connectionExpiry ?? "-"],
[
msg("Property mappings"),
Array.isArray(provider.propertyMappings) && provider.propertyMappings.length
? provider.propertyMappings.join(", ")
: msg("None"),
],
]);
const _provider = rawProvider as RACProvider;
}
function formatRedirectUris(uris: RedirectURI[] = []) {

View File

@@ -8,6 +8,8 @@ import "#elements/forms/HorizontalFormElement";
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types.js";
import { camelToSnake } from "#common/utils";
import { isSlug } from "#elements/router/utils";
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types";
@@ -17,31 +19,25 @@ import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
import { type ApplicationRequest } from "@goauthentik/api";
import { snakeCase } from "change-case";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
function trimMany<T extends object, K extends keyof T>(target: T, keys: K[]): Pick<T, K> {
const output = {} as Record<K, unknown>;
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
for (const key of keys) {
const value = target[key];
const trimMany = (o: Record<string, unknown>, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
output[key] = typeof value === "string" ? value.trim() : value;
}
return output as Pick<T, K>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isStr = (v: any): v is string => typeof v === "string";
@customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
label = msg("Application");
@state()
errors = new Map<keyof ApplicationRequest, string>();
errors = new Map<string, string>();
@query("form#applicationform")
form!: HTMLFormElement;
@@ -52,10 +48,12 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
this.enabled = true;
}
protected errorMessages(name: keyof ApplicationRequest) {
errorMessages(name: string) {
return this.errors.has(name)
? [this.errors.get(name)]
: (this.wizard.errors?.app?.[name] ?? this.wizard.errors?.app?.[snakeCase(name)] ?? []);
: (this.wizard.errors?.app?.[name] ??
this.wizard.errors?.app?.[camelToSnake(name)] ??
[]);
}
get buttons(): WizardButton[] {
@@ -64,53 +62,51 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
get valid() {
this.errors = new Map();
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
const values = trimMany(this.formValues, ["metaLaunchUrl", "name", "slug"]);
if (!values.name) {
if (values.name === "") {
this.errors.set("name", msg("An application name is required"));
}
if (!values.metaLaunchUrl || !URL.canParse(values.metaLaunchUrl)) {
if (
!(
isStr(values.metaLaunchUrl) &&
(values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl))
)
) {
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
}
if (!values.slug || !isSlug(values.slug)) {
if (!(isStr(values.slug) && values.slug !== "" && isSlug(values.slug))) {
this.errors.set("slug", msg("Not a valid slug"));
}
return this.errors.size === 0;
}
override handleButton(button: NavigableButton) {
if (button.kind !== "next") {
return super.handleButton(button);
}
if (button.kind === "next") {
if (!this.valid) {
this.handleEnabling({
disabled: ["provider-choice", "provider", "bindings", "submit"],
});
return;
}
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
if (!this.valid) {
this.handleEnabling({
disabled: ["provider-choice", "provider", "bindings", "submit"],
let payload: ApplicationWizardStateUpdate = {
app: this.formValues,
errors: this.removeErrors("app"),
};
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
payload = {
...payload,
provider: { name: `Provider for ${app.name}` },
};
}
this.handleUpdate(payload, button.destination, {
enable: "provider-choice",
});
return;
}
const app = { ...this.formValues };
const payload: ApplicationWizardStateUpdate = {
app,
errors: this.removeErrors("app"),
};
if (!this.wizard.provider?.name?.trim() && app.name) {
payload.provider = {
name: `Provider for ${app.name}`,
};
}
this.handleUpdate(payload, button.destination, {
enable: "provider-choice",
});
super.handleButton(button);
}
renderForm(app: Partial<ApplicationRequest>, errors: ValidationRecord) {
@@ -118,14 +114,12 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
<form id="applicationform" class="pf-c-form pf-m-horizontal" slot="form">
<ak-text-input
name="name"
autocomplete="off"
placeholder=${msg("Application name")}
value=${ifDefined(app.name)}
label=${msg("Name")}
required
?invalid=${this.errors.has("name")}
.errorMessages=${errors.name ?? this.errorMessages("name")}
help=${msg("The name displayed in the application library.")}
help=${msg("Application's display Name.")}
></ak-text-input>
<ak-slug-input
name="slug"
@@ -141,8 +135,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
name="group"
value=${ifDefined(app.group)}
label=${msg("Group")}
placeholder=${msg("e.g. Collaboration, Communication, Internal, etc.")}
.errorMessages=${errors.group}
.errorMessages=${errors.group ?? []}
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
@@ -154,14 +147,13 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
name="policyEngineMode"
.options=${policyEngineModes}
.value=${app.policyEngineMode}
.errorMessages=${errors.policyEngineMode}
.errorMessages=${errors.policyEngineMode ?? []}
></ak-radio-input>
<ak-form-group label=${msg("UI Settings")}>
<div class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}
placeholder="https://..."
value=${ifDefined(app.metaLaunchUrl)}
?invalid=${this.errors.has("metaLaunchUrl")}
.errorMessages=${errors.metaLaunchUrl ??

View File

@@ -297,15 +297,11 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
renderReview(app: Partial<ApplicationRequest>, provider: OneOfProvider) {
const renderer = providerRenderers.get(this.wizard.providerModel);
if (!renderer) {
throw new Error(
`Provider ${this.wizard.providerModel ?? "-- undefined --"} has no summary renderer.`,
);
}
const metaLaunchUrl = app.metaLaunchUrl?.trim();
return html`
<div class="ak-wizard-main-content">
<ak-wizard-title>${msg("Review the Application and Provider")}</ak-wizard-title>
@@ -325,10 +321,12 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
${app.policyEngineMode?.toUpperCase()}
</dt>
</div>
${metaLaunchUrl
${(app.metaLaunchUrl ?? "").trim() !== ""
? html` <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Launch URL")}</dt>
<dt class="pf-c-description-list__description">${metaLaunchUrl}</dt>
<dt class="pf-c-description-list__description">
${app.metaLaunchUrl}
</dt>
</div>`
: nothing}
</dl>

View File

@@ -8,11 +8,11 @@ import "#elements/forms/HorizontalFormElement";
import { styles as AwadStyles } from "../../ApplicationWizardFormStepStyles.styles.js";
import { type ApplicationWizardState, type OneOfProvider } from "../../types.js";
import { camelToSnake } from "#common/utils";
import { AKElement } from "#elements/Base";
import { serializeForm } from "#elements/forms/Form";
import { snakeCase } from "change-case";
import { CSSResult } from "lit";
import { property, query } from "lit/decorators.js";
@@ -46,7 +46,7 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
return name in this.errors
? [this.errors[name]]
: (this.wizard.errors?.provider?.[name] ??
this.wizard.errors?.provider?.[snakeCase(name)] ??
this.wizard.errors?.provider?.[camelToSnake(name)] ??
[]);
}

View File

@@ -7,13 +7,12 @@ import { groupBy } from "#common/utils";
import { ModelForm } from "#elements/forms/ModelForm";
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
import {
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowStageBinding,
InvalidResponseActionEnum,
PolicyEngineMode,
Stage,
StagesAllListRequest,
StagesApi,
@@ -203,7 +202,22 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
required
name="policyEngineMode"
>
<ak-radio .options=${policyEngineModes} .value=${this.instance?.policyEngineMode}>
<ak-radio
.options=${[
{
label: "any",
value: PolicyEngineMode.Any,
default: true,
description: html`${msg("Any policy must match to grant access")}`,
},
{
label: "all",
value: PolicyEngineMode.All,
description: html`${msg("All policies must match to grant access")}`,
},
]}
.value=${this.instance?.policyEngineMode}
>
</ak-radio>
</ak-form-element-horizontal>`;
}

View File

@@ -1,21 +1,17 @@
import type { RadioOption } from "#elements/forms/Radio";
import { PolicyEngineMode } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
export const policyEngineModes: RadioOption<PolicyEngineMode>[] = [
export const policyEngineModes = [
{
label: "ANY",
className: "pf-m-monospace",
label: "any",
value: PolicyEngineMode.Any,
default: true,
description: html`${msg("Any policy must match to grant access")}`,
},
{
label: "ALL",
className: "pf-m-monospace",
label: "all",
value: PolicyEngineMode.All,
description: html`${msg("All policies must match to grant access")}`,
},

View File

@@ -51,7 +51,7 @@ export function renderForm(
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
@@ -87,7 +87,7 @@ export function renderForm(
label=${msg("Bind flow")}
required
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow}
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
label=${msg("Bind flow")}
@@ -111,7 +111,7 @@ export function renderForm(
.currentFlow=${provider?.invalidationFlow}
.brandFlow=${brand?.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
.errorMessages=${errors?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
@@ -127,7 +127,7 @@ export function renderForm(
required
value="${provider?.baseDn ?? "DC=ldap,DC=goauthentik,DC=io"}"
input-hint="code"
.errorMessages=${errors?.baseDn}
.errorMessages=${errors?.baseDn ?? []}
help=${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
@@ -137,7 +137,7 @@ export function renderForm(
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate}
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.certificate ?? nothing)}
@@ -151,7 +151,7 @@ export function renderForm(
label=${msg("TLS Server name")}
name="tlsServerName"
value="${provider?.tlsServerName ?? ""}"
.errorMessages=${errors?.tlsServerName}
.errorMessages=${errors?.tlsServerName ?? []}
help=${tlsServerNameHelp}
input-hint="code"
></ak-text-input>
@@ -161,7 +161,7 @@ export function renderForm(
required
name="uidStartNumber"
value="${provider?.uidStartNumber ?? 2000}"
.errorMessages=${errors?.uidStartNumber}
.errorMessages=${errors?.uidStartNumber ?? []}
help=${uidStartNumberHelp}
></ak-number-input>
@@ -170,7 +170,7 @@ export function renderForm(
required
name="gidStartNumber"
value="${provider?.gidStartNumber ?? 4000}"
.errorMessages=${errors?.gidStartNumber}
.errorMessages=${errors?.gidStartNumber ?? []}
help=${gidStartNumberHelp}
></ak-number-input>
</div>

View File

@@ -20,8 +20,6 @@ import { oauth2SourcesProvider, oauth2SourcesSelector } from "./OAuth2Sources.js
import { ascii_letters, digits, randomString } from "#common/utils";
import { RadioOption } from "#elements/forms/Radio";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
@@ -37,7 +35,7 @@ import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
export const clientTypeOptions: RadioOption<ClientTypeEnum>[] = [
export const clientTypeOptions = [
{
label: msg("Confidential"),
value: ClientTypeEnum.Confidential,
@@ -55,7 +53,7 @@ export const clientTypeOptions: RadioOption<ClientTypeEnum>[] = [
},
];
export const subjectModeOptions: RadioOption<SubModeEnum>[] = [
export const subjectModeOptions = [
{
label: msg("Based on the User's hashed ID"),
value: SubModeEnum.HashedUserId,
@@ -87,7 +85,7 @@ export const subjectModeOptions: RadioOption<SubModeEnum>[] = [
},
];
export const issuerModeOptions: RadioOption<IssuerModeEnum>[] = [
export const issuerModeOptions = [
{
label: msg("Each provider has a different issuer, based on the application slug"),
value: IssuerModeEnum.PerProvider,
@@ -99,7 +97,7 @@ export const issuerModeOptions: RadioOption<IssuerModeEnum>[] = [
},
];
const redirectUriHelpMessages: string[] = [
const redirectUriHelpMessages = [
msg(
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
),
@@ -111,14 +109,9 @@ const redirectUriHelpMessages: string[] = [
),
];
const backchannelLogoutUriHelpMessages: string[] = [
msg(
"URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.",
),
msg(
"These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.",
),
];
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
type ShowClientSecret = (show: boolean) => void;
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
@@ -133,7 +126,6 @@ export function renderForm(
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name}
required
></ak-text-input>
@@ -145,7 +137,6 @@ export function renderForm(
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
.errorMessages=${errors?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
@@ -171,7 +162,6 @@ export function renderForm(
value="${provider?.clientId ?? randomString(40, ascii_letters + digits)}"
required
input-hint="code"
.errorMessages=${errors?.clientId}
>
</ak-text-input>
<ak-hidden-text-input
@@ -184,6 +174,7 @@ export function renderForm(
>
</ak-hidden-text-input>
<ak-form-element-horizontal
flow-direction="row"
label=${msg("Redirect URIs/Origins (RegEx)")}
name="redirectUris"
>
@@ -200,22 +191,9 @@ export function renderForm(
}}
>
</ak-array-input>
${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-text-input
label=${msg("Back-Channel Logout URI")}
name="backchannelLogoutUri"
value="${provider?.backchannelLogoutUri ?? ""}"
input-hint="code"
placeholder="https://..."
.help=${backchannelLogoutUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}
></ak-text-input>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search

View File

@@ -4,7 +4,6 @@ import "#components/events/ObjectChangelog";
import "#elements/CodeMirror";
import "#elements/EmptyState";
import "#elements/Tabs";
import "#elements/tasks/TaskList";
import "#elements/ak-mdx/index";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/index";
@@ -20,7 +19,6 @@ import {
ClientTypeEnum,
CoreApi,
CoreUsersListRequest,
ModelEnum,
OAuth2Provider,
OAuth2ProviderSetupURLs,
PropertyMappingPreview,
@@ -172,7 +170,6 @@ export class OAuth2ProviderViewPage extends AKElement {
if (!this.provider) {
return html``;
}
const [appLabel, modelName] = ModelEnum.AuthentikProvidersOauth2Oauth2provider.split(".");
return html` ${this.provider?.assignedApplicationName
? html``
: html`<div slot="header" class="pf-c-banner pf-m-warning">
@@ -249,18 +246,6 @@ export class OAuth2ProviderViewPage extends AKElement {
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Back-Channel Logout URI")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text pf-m-monospace">
${this.provider.backchannelLogoutUri}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
@@ -370,18 +355,6 @@ export class OAuth2ProviderViewPage extends AKElement {
</form>
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
>
<div class="pf-c-card pf-l-grid__item pf-m-12-col-on-2xl">
<div class="pf-c-card__title">${msg("Tasks")}</div>
<ak-task-list
.relObjAppLabel=${appLabel}
.relObjModel=${modelName}
.relObjId="${this.provider.pk}"
></ak-task-list>
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
>

View File

@@ -88,7 +88,7 @@ function renderProxySettings(provider: Partial<ProxyProvider>, errors?: Validati
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
@@ -99,7 +99,7 @@ function renderProxySettings(provider: Partial<ProxyProvider>, errors?: Validati
label=${msg("Internal host")}
value="${ifDefined(provider?.internalHost)}"
required
.errorMessages=${errors?.internalHost}
.errorMessages=${errors?.internalHost ?? []}
help=${msg("Upstream host that the requests are forwarded to.")}
input-hint="code"
></ak-text-input>
@@ -124,7 +124,7 @@ function renderForwardSingleSettings(provider: Partial<ProxyProvider>, errors?:
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
@@ -154,7 +154,7 @@ function renderForwardDomainSettings(provider: Partial<ProxyProvider>, errors?:
label=${msg("Authentication URL")}
value="${provider?.externalHost ?? window.location.origin}"
required
.errorMessages=${errors?.externalHost}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
@@ -165,7 +165,7 @@ function renderForwardDomainSettings(provider: Partial<ProxyProvider>, errors?:
name="cookieDomain"
value="${ifDefined(provider?.cookieDomain)}"
required
.errorMessages=${errors?.cookieDomain}
.errorMessages=${errors?.cookieDomain ?? []}
help=${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
@@ -196,7 +196,7 @@ export function renderForm(
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
@@ -224,7 +224,7 @@ export function renderForm(
label=${msg("Token validity")}
name="accessTokenValidity"
value="${provider?.accessTokenValidity ?? "hours=24"}"
.errorMessages=${errors?.accessTokenValidity}
.errorMessages=${errors?.accessTokenValidity ?? []}
required
.help=${msg("Configure how long tokens are valid for.")}
input-hint="code"

View File

@@ -46,7 +46,7 @@ export function renderForm(
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name}
.errorMessages=${errors?.name ?? []}
required
>
</ak-text-input>
@@ -55,7 +55,7 @@ export function renderForm(
label=${msg("Authentication flow")}
required
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow}
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
@@ -79,7 +79,7 @@ export function renderForm(
<ak-hidden-text-input
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret}
.errorMessages=${errors?.sharedSecret ?? []}
value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)}
required
input-hint="code"
@@ -88,7 +88,7 @@ export function renderForm(
name="clientNetworks"
label=${msg("Client Networks")}
value=${provider?.clientNetworks ?? "0.0.0.0/0, ::/0"}
.errorMessages=${errors?.clientNetworks}
.errorMessages=${errors?.clientNetworks ?? []}
required
help=${clientNetworksHelp}
input-hint="code"
@@ -118,7 +118,7 @@ export function renderForm(
placeholder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []}
defaultFlowSlug="default-invalidation-flow"
required
></ak-flow-search>

View File

@@ -12,8 +12,6 @@ import { digestAlgorithmOptions, signatureAlgorithmOptions } from "./SAMLProvide
import { DEFAULT_CONFIG } from "#common/api/config";
import { RadioOption } from "#elements/forms/Radio";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
@@ -29,7 +27,7 @@ import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
const serviceProviderBindingOptions: RadioOption<SpBindingEnum>[] = [
const serviceProviderBindingOptions = [
{
label: msg("Redirect"),
value: SpBindingEnum.Redirect,
@@ -41,11 +39,11 @@ const serviceProviderBindingOptions: RadioOption<SpBindingEnum>[] = [
},
];
function renderHasSigningKp(provider: Partial<SAMLProvider>) {
function renderHasSigningKp(provider?: Partial<SAMLProvider>) {
return html` <ak-switch-input
name="signAssertion"
label=${msg("Sign assertions")}
?checked=${provider.signAssertion ?? true}
?checked=${provider?.signAssertion ?? true}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>
@@ -53,7 +51,7 @@ function renderHasSigningKp(provider: Partial<SAMLProvider>) {
<ak-switch-input
name="signResponse"
label=${msg("Sign responses")}
?checked=${provider.signResponse ?? false}
?checked=${provider?.signResponse ?? false}
help=${msg("When enabled, the SAML response will be signed.")}
>
</ak-switch-input>`;
@@ -67,10 +65,10 @@ export function renderForm(
) {
return html` <ak-text-input
name="name"
value=${ifDefined(provider.name)}
value=${ifDefined(provider?.name)}
label=${msg("Name")}
required
.errorMessages=${errors?.name}
.errorMessages=${errors?.name ?? []}
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
@@ -79,9 +77,9 @@ export function renderForm(
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider.authorizationFlow}
.errorMessages=${errors?.authorizationFlow}
.currentFlow=${provider?.authorizationFlow}
required
.errorMessages=${errors?.authorizationFlow ?? []}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
@@ -93,16 +91,16 @@ export function renderForm(
<ak-text-input
name="acsUrl"
label=${msg("ACS URL")}
value="${ifDefined(provider.acsUrl)}"
value="${ifDefined(provider?.acsUrl)}"
required
.errorMessages=${errors?.acsUrl}
.errorMessages=${errors?.acsUrl ?? []}
></ak-text-input>
<ak-text-input
label=${msg("Issuer")}
name="issuer"
value="${provider.issuer || "authentik"}"
value="${provider?.issuer || "authentik"}"
required
.errorMessages=${errors?.issuer}
.errorMessages=${errors?.issuer ?? []}
help=${msg("Also known as EntityID.")}
></ak-text-input>
<ak-radio-input
@@ -110,7 +108,7 @@ export function renderForm(
name="spBinding"
required
.options=${serviceProviderBindingOptions}
.value=${provider.spBinding}
.value=${provider?.spBinding}
help=${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
@@ -119,8 +117,8 @@ export function renderForm(
<ak-text-input
name="audience"
label=${msg("Audience")}
value="${ifDefined(provider.audience)}"
.errorMessages=${errors?.audience}
value="${ifDefined(provider?.audience)}"
.errorMessages=${errors?.audience ?? []}
></ak-text-input>
</div>
</ak-form-group>
@@ -133,7 +131,7 @@ export function renderForm(
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider.authenticationFlow}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
@@ -148,7 +146,7 @@ export function renderForm(
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider.invalidationFlow}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
@@ -163,7 +161,7 @@ export function renderForm(
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
<ak-crypto-certificate-search
.certificate=${provider.signingKp}
.certificate=${provider?.signingKp}
@input=${setHasSigningKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
@@ -179,7 +177,7 @@ export function renderForm(
name="verificationKp"
>
<ak-crypto-certificate-search
.certificate=${provider.verificationKp}
.certificate=${provider?.verificationKp}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
@@ -193,7 +191,7 @@ export function renderForm(
name="encryptionKp"
>
<ak-crypto-certificate-search
.certificate=${provider.encryptionKp}
.certificate=${provider?.encryptionKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("When selected, assertions will be encrypted using this keypair.")}
@@ -205,7 +203,7 @@ export function renderForm(
>
<ak-dual-select-dynamic-selected
.provider=${propertyMappingsProvider}
.selector=${propertyMappingsSelector(provider.propertyMappings)}
.selector=${propertyMappingsSelector(provider?.propertyMappings)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
@@ -215,7 +213,6 @@ export function renderForm(
name="nameIdMapping"
>
<ak-search-select
required
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
@@ -235,7 +232,7 @@ export function renderForm(
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return provider.nameIdMapping === item.pk;
return provider?.nameIdMapping === item.pk;
}}
blankable
>
@@ -251,7 +248,6 @@ export function renderForm(
name="authnContextClassRefMapping"
>
<ak-search-select
required
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
@@ -271,7 +267,7 @@ export function renderForm(
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return provider.authnContextClassRefMapping === item.pk;
return provider?.authnContextClassRefMapping === item.pk;
}}
blankable
>
@@ -286,35 +282,35 @@ export function renderForm(
<ak-text-input
name="assertionValidNotBefore"
label=${msg("Assertion valid not before")}
value="${provider.assertionValidNotBefore || "minutes=-5"}"
value="${provider?.assertionValidNotBefore || "minutes=-5"}"
required
.errorMessages=${errors?.assertionValidNotBefore}
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Configure the maximum allowed time drift for an assertion.")}
></ak-text-input>
<ak-text-input
name="assertionValidNotOnOrAfter"
label=${msg("Assertion valid not on or after")}
value="${provider.assertionValidNotOnOrAfter || "minutes=5"}"
value="${provider?.assertionValidNotOnOrAfter || "minutes=5"}"
required
.errorMessages=${errors?.assertionValidNotBefore}
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Assertion not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="sessionValidNotOnOrAfter"
label=${msg("Session valid not on or after")}
value="${provider.sessionValidNotOnOrAfter || "minutes=86400"}"
value="${provider?.sessionValidNotOnOrAfter || "minutes=86400"}"
required
.errorMessages=${errors?.sessionValidNotOnOrAfter}
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg("Session not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="defaultRelayState"
label=${msg("Default relay state")}
value="${provider.defaultRelayState || ""}"
.errorMessages=${errors?.sessionValidNotOnOrAfter}
value="${provider?.defaultRelayState || ""}"
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
@@ -372,7 +368,7 @@ export function renderForm(
name="digestAlgorithm"
label=${msg("Digest algorithm")}
.options=${digestAlgorithmOptions}
.value=${provider.digestAlgorithm}
.value=${provider?.digestAlgorithm}
required
>
</ak-radio-input>
@@ -381,7 +377,7 @@ export function renderForm(
name="signatureAlgorithm"
label=${msg("Signature algorithm")}
.options=${signatureAlgorithmOptions}
.value=${provider.signatureAlgorithm}
.value=${provider?.signatureAlgorithm}
required
>
</ak-radio-input>

View File

@@ -28,7 +28,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
@@ -38,7 +38,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
name="url"
label=${msg("URL")}
value="${provider?.url ?? ""}"
.errorMessages=${errors?.url}
.errorMessages=${errors?.url ?? []}
required
help=${msg("SCIM base url, usually ends in /v2.")}
input-hint="code"
@@ -55,7 +55,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
name="token"
label=${msg("Token")}
value="${provider?.token ?? ""}"
.errorMessages=${errors?.token}
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",

View File

@@ -232,36 +232,6 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
})}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Account Recovery Max Attempts")}
required
name="recoveryMaxAttempts"
>
<input
type="number"
value="${this.instance?.recoveryMaxAttempts ?? 5}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Account Recovery Cache Timeout")}
required
name="recoveryCacheTimeout"
>
<input
type="text"
value="${ifDefined(this.instance?.recoveryCacheTimeout || "minutes=5")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The time window used to count recent account recovery attempts.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
</div>
</ak-form-group>
${this.renderConnectionSettings()}`;

View File

@@ -144,14 +144,7 @@ export function composeResponseErrorDescriptor(descriptor: ResponseErrorDescript
return `${descriptor.headline}: ${descriptor.reason}`;
}
export const ErrorFieldFallbackKeys = [
// ---
"detail", // OpenAPI
"non_field_errors", // ValidationError.non_field_errors
"message", // Error.prototype.message
"string", // OpenAPI
] as const;
export const ErrorFieldFallbackKeys = ["detail", "message", "non_field_errors"] as const;
export type FallbackError = Record<(typeof ErrorFieldFallbackKeys)[number], string | undefined>;
/**

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