mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 23:52:38 +02:00
Compare commits
3 Commits
web/bundle
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7614b8c608 | ||
|
|
99fa5ec7cc | ||
|
|
2800211dd0 |
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
5
.github/workflows/api-py-publish.yml
vendored
5
.github/workflows/api-py-publish.yml
vendored
@@ -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' }}
|
||||
|
||||
5
.github/workflows/api-ts-publish.yml
vendored
5
.github/workflows/api-ts-publish.yml
vendored
@@ -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' }}
|
||||
|
||||
3
.github/workflows/ci-api-docs.yml
vendored
3
.github/workflows/ci-api-docs.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - API Docs
|
||||
name: authentik-ci-api-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
3
.github/workflows/ci-aws-cfn.yml
vendored
3
.github/workflows/ci-aws-cfn.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - AWS cfn
|
||||
name: authentik-ci-aws-cfn
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
3
.github/workflows/ci-docs.yml
vendored
3
.github/workflows/ci-docs.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - Docs
|
||||
name: authentik-ci-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.github/workflows/ci-main-daily.yml
vendored
2
.github/workflows/ci-main-daily.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: CI - Main daily
|
||||
name: authentik-ci-main-daily
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
20
.github/workflows/ci-main.yml
vendored
20
.github/workflows/ci-main.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: CI - Outpost
|
||||
name: authentik-ci-outpost
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
3
.github/workflows/ci-web.yml
vendored
3
.github/workflows/ci-web.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - Web
|
||||
name: authentik-ci-web
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: QA - CodeQL
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
name: Gen - Webauthn MDS
|
||||
|
||||
name: authentik-gen-update-webauthn-mds
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
|
||||
@@ -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:
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: GH - GHCR retention
|
||||
name: ghcr-retention
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Gen - Compress images
|
||||
name: authentik-compress-images
|
||||
|
||||
on:
|
||||
push:
|
||||
5
.github/workflows/packages-npm-publish.yml
vendored
5
.github/workflows/packages-npm-publish.yml
vendored
@@ -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' }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: CI - Source code docs
|
||||
name: authentik-publish-source-docs
|
||||
|
||||
on:
|
||||
push:
|
||||
3
.github/workflows/release-next-branch.yml
vendored
3
.github/workflows/release-next-branch.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Release - Update next branch
|
||||
name: authentik-on-release-next-branch
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
||||
2
.github/workflows/release-publish.yml
vendored
2
.github/workflows/release-publish.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Release - On publish
|
||||
name: authentik-on-release
|
||||
|
||||
on:
|
||||
release:
|
||||
|
||||
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Release - On tag
|
||||
name: authentik-on-tag
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
3
.github/workflows/repo-mirror-cleanup.yml
vendored
3
.github/workflows/repo-mirror-cleanup.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Repo - Cleanup internal mirror
|
||||
name: "authentik-repo-mirror-cleanup"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
3
.github/workflows/repo-mirror.yml
vendored
3
.github/workflows/repo-mirror.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Repo - Mirror to internal
|
||||
name: "authentik-repo-mirror"
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
|
||||
3
.github/workflows/repo-stale.yml
vendored
3
.github/workflows/repo-stale.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Repo - Mark and close stale issues
|
||||
name: "authentik-repo-stale"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
||||
@@ -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
|
||||
3
.github/workflows/translation-advice.yml
vendored
3
.github/workflows/translation-advice.yml
vendored
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: Translation - Post advice
|
||||
name: authentik-translation-advice
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
name: Translation - Extract and compile
|
||||
|
||||
name: authentik-translate-extract-compile
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # every day at midnight
|
||||
|
||||
3
.github/workflows/translation-rename.yml
vendored
3
.github/workflows/translation-rename.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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.5 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -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.6
|
||||
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
4
go.sum
@@ -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.6 h1:s9DaQ8x93T9IjDBqSX69VTuB5kBH3nHyI3/2Mlhlf08=
|
||||
goauthentik.io/api/v3 v3.2025064.6/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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1024.0",
|
||||
"aws-cdk": "^2.1023.0",
|
||||
"cross-env": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-08-06 00:11+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"
|
||||
@@ -3250,13 +3250,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 +3274,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 ""
|
||||
|
||||
@@ -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-07-28 16:09+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"
|
||||
@@ -1664,29 +1664,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 +1809,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 +1937,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
|
||||
@@ -3611,17 +3598,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,15 +3622,6 @@ 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."
|
||||
|
||||
29
packages/docusaurus-config/package-lock.json
generated
29
packages/docusaurus-config/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
482
packages/eslint-config/package-lock.json
generated
482
packages/eslint-config/package-lock.json
generated
@@ -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": {
|
||||
|
||||
92
packages/prettier-config/package-lock.json
generated
92
packages/prettier-config/package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@@ -25,6 +25,8 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
175
web/e2e/fixtures/FormFixture.ts
Normal file
175
web/e2e/fixtures/FormFixture.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
import type { LocatorContext } from "#e2e/selectors/types";
|
||||
|
||||
import { expect, Page } from "@playwright/test";
|
||||
|
||||
export class FormFixture extends PageFixture {
|
||||
static fixtureName = "Form";
|
||||
|
||||
//#region Selector Methods
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Field Methods
|
||||
|
||||
/**
|
||||
* Set the value of a text input.
|
||||
*
|
||||
* @param fieldName The name of the form element.
|
||||
* @param value the value to set.
|
||||
*/
|
||||
public fill = async (
|
||||
fieldName: string,
|
||||
value: string,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const control = parent
|
||||
.getByRole("textbox", {
|
||||
name: fieldName,
|
||||
})
|
||||
.or(
|
||||
parent.getByRole("spinbutton", {
|
||||
name: fieldName,
|
||||
}),
|
||||
)
|
||||
.first();
|
||||
|
||||
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
|
||||
|
||||
await control.fill(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a radio or checkbox input.
|
||||
*
|
||||
* @param fieldName The name of the form element.
|
||||
* @param value the value to set.
|
||||
*/
|
||||
public setInputCheck = async (
|
||||
fieldName: string,
|
||||
value: boolean = true,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const control = parent.locator("ak-switch-input", {
|
||||
hasText: fieldName,
|
||||
});
|
||||
|
||||
await control.scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
|
||||
|
||||
const currentChecked = await control
|
||||
.getAttribute("checked")
|
||||
.then((value) => value !== null);
|
||||
|
||||
if (currentChecked === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await control.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a radio or checkbox input.
|
||||
*
|
||||
* @param fieldName The name of the form element.
|
||||
* @param pattern the value to set.
|
||||
*/
|
||||
public setRadio = async (
|
||||
groupName: string,
|
||||
fieldName: string,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const group = parent.getByRole("group", { name: groupName });
|
||||
|
||||
await expect(group, `Field "${groupName}" should be visible`).toBeVisible();
|
||||
const control = parent.getByRole("radio", { name: fieldName });
|
||||
|
||||
await control.setChecked(true, {
|
||||
force: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a search select input.
|
||||
*
|
||||
* @param fieldLabel The name of the search select element.
|
||||
* @param pattern The text to match against the search select entry.
|
||||
*/
|
||||
public selectSearchValue = async (
|
||||
fieldLabel: string,
|
||||
pattern: string | RegExp,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const control = parent.getByRole("textbox", { name: fieldLabel });
|
||||
|
||||
await expect(
|
||||
control,
|
||||
`Search select control (${fieldLabel}) should be visible`,
|
||||
).toBeVisible();
|
||||
|
||||
const fieldName = await control.getAttribute("name");
|
||||
|
||||
if (!fieldName) {
|
||||
throw new Error(`Unable to find name attribute on search select (${fieldLabel})`);
|
||||
}
|
||||
|
||||
// Find the search select input control and activate it.
|
||||
await control.click();
|
||||
|
||||
const button = this.page
|
||||
// ---
|
||||
.locator(`div[data-managed-for*="${fieldName}"] button`, {
|
||||
hasText: pattern,
|
||||
});
|
||||
|
||||
if (!button) {
|
||||
throw new Error(
|
||||
`Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
await button.click();
|
||||
await this.page.keyboard.press("Tab");
|
||||
await control.blur();
|
||||
};
|
||||
|
||||
public setFormGroup = async (
|
||||
pattern: string | RegExp,
|
||||
value: boolean = true,
|
||||
parent: LocatorContext = this.page,
|
||||
) => {
|
||||
const control = parent
|
||||
.locator("ak-form-group", {
|
||||
hasText: pattern,
|
||||
})
|
||||
.first();
|
||||
|
||||
const currentOpen = await control.getAttribute("open").then((value) => value !== null);
|
||||
|
||||
if (currentOpen === value) {
|
||||
this.logger.debug(`Form group ${pattern} is already ${value ? "open" : "closed"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`Toggling form group ${pattern} to ${value ? "open" : "closed"}`);
|
||||
|
||||
await control.click();
|
||||
|
||||
if (value) {
|
||||
await expect(control).toHaveAttribute("open");
|
||||
} else {
|
||||
await expect(control).not.toHaveAttribute("open");
|
||||
}
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
constructor(page: Page, testName: string) {
|
||||
super({ page, testName });
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
30
web/e2e/fixtures/PageFixture.ts
Normal file
30
web/e2e/fixtures/PageFixture.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ConsoleLogger, FixtureLogger } from "#logger/node";
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export interface PageFixtureOptions {
|
||||
page: Page;
|
||||
testName: string;
|
||||
}
|
||||
|
||||
export abstract class PageFixture {
|
||||
/**
|
||||
* The name of the fixture.
|
||||
*
|
||||
* Used for logging.
|
||||
*/
|
||||
static fixtureName: string;
|
||||
|
||||
protected readonly logger: FixtureLogger;
|
||||
protected readonly page: Page;
|
||||
protected readonly testName: string;
|
||||
|
||||
constructor({ page, testName }: PageFixtureOptions) {
|
||||
this.page = page;
|
||||
this.testName = testName;
|
||||
|
||||
const Constructor = this.constructor as typeof PageFixture;
|
||||
|
||||
this.logger = ConsoleLogger.fixture(Constructor.fixtureName, this.testName);
|
||||
}
|
||||
}
|
||||
42
web/e2e/fixtures/PointerFixture.ts
Normal file
42
web/e2e/fixtures/PointerFixture.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
import type { LocatorContext } from "#e2e/selectors/types";
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export type GetByRoleParameters = Parameters<Page["getByRole"]>;
|
||||
export type ARIARole = GetByRoleParameters[0];
|
||||
export type ARIAOptions = GetByRoleParameters[1];
|
||||
|
||||
export type ClickByName = (name: string) => Promise<void>;
|
||||
export type ClickByRole = (
|
||||
role: ARIARole,
|
||||
options?: ARIAOptions,
|
||||
context?: LocatorContext,
|
||||
) => Promise<void>;
|
||||
|
||||
export class PointerFixture extends PageFixture {
|
||||
public static fixtureName = "Pointer";
|
||||
|
||||
public click = (
|
||||
name: string,
|
||||
optionsOrRole?: ARIAOptions | ARIARole,
|
||||
context: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
if (typeof optionsOrRole === "string") {
|
||||
return context.getByRole(optionsOrRole, { name }).click();
|
||||
}
|
||||
|
||||
const options = {
|
||||
...optionsOrRole,
|
||||
name,
|
||||
};
|
||||
|
||||
return (
|
||||
context
|
||||
// ---
|
||||
.getByRole("button", options)
|
||||
.or(context.getByRole("link", options))
|
||||
.click()
|
||||
);
|
||||
};
|
||||
}
|
||||
102
web/e2e/fixtures/SessionFixture.ts
Normal file
102
web/e2e/fixtures/SessionFixture.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export const GOOD_USERNAME = "test-admin@goauthentik.io";
|
||||
export const GOOD_PASSWORD = "test-runner";
|
||||
|
||||
export const BAD_USERNAME = "bad-username@bad-login.io";
|
||||
export const BAD_PASSWORD = "-this-is-a-bad-password-";
|
||||
|
||||
export interface LoginInit {
|
||||
username?: string;
|
||||
password?: string;
|
||||
to?: URL | string;
|
||||
}
|
||||
|
||||
export class SessionFixture extends PageFixture {
|
||||
static fixtureName = "Session";
|
||||
|
||||
public static readonly pathname = "/if/flow/default-authentication-flow/";
|
||||
|
||||
//#region Selectors
|
||||
|
||||
public $identificationStage = this.page.locator("ak-stage-identification");
|
||||
|
||||
/**
|
||||
* The username field on the login page.
|
||||
*/
|
||||
public $usernameField = this.page.getByLabel("Username");
|
||||
|
||||
public $passwordStage = this.page.locator("ak-stage-password");
|
||||
public $passwordField = this.page.getByLabel("Password");
|
||||
|
||||
/**
|
||||
* The button to submit the the login flow,
|
||||
* typically redirecting to the authenticated interface.
|
||||
*/
|
||||
public $submitButton = this.page.locator('button[type="submit"]');
|
||||
|
||||
/**
|
||||
* A possible authentication failure message.
|
||||
*/
|
||||
public $authFailureMessage = this.page.locator(".pf-m-error");
|
||||
|
||||
//#endregion
|
||||
|
||||
constructor(page: Page, testName: string) {
|
||||
super({ page, testName });
|
||||
}
|
||||
|
||||
//#region Specific interactions
|
||||
|
||||
public checkAuthenticated = async (): Promise<boolean> => {
|
||||
// TODO: Check if the user is authenticated via API
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Log into the application.
|
||||
*/
|
||||
public async login({
|
||||
username = GOOD_USERNAME,
|
||||
password = GOOD_PASSWORD,
|
||||
to = SessionFixture.pathname,
|
||||
}: LoginInit = {}) {
|
||||
this.logger.info("Logging in...");
|
||||
|
||||
const initialURL = new URL(this.page.url());
|
||||
|
||||
if (initialURL.pathname === SessionFixture.pathname) {
|
||||
this.logger.info("Skipping navigation because we're already in a authentication flow");
|
||||
} else {
|
||||
await this.page.goto(to.toString());
|
||||
}
|
||||
|
||||
await this.$usernameField.fill(username);
|
||||
|
||||
const passwordFieldVisible = await this.$passwordField.isVisible();
|
||||
|
||||
if (!passwordFieldVisible) {
|
||||
await this.$submitButton.click();
|
||||
|
||||
await this.$passwordField.waitFor({ state: "visible" });
|
||||
}
|
||||
|
||||
await this.$passwordField.fill(password);
|
||||
|
||||
await this.$submitButton.click();
|
||||
|
||||
const expectedPathname = typeof to === "string" ? to : to.pathname;
|
||||
|
||||
await this.page.waitForURL(`**${expectedPathname}`);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Navigation
|
||||
|
||||
public async toLoginPage() {
|
||||
await this.page.goto(SessionFixture.pathname);
|
||||
}
|
||||
}
|
||||
37
web/e2e/index.ts
Normal file
37
web/e2e/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @file Playwright e2e test helpers.
|
||||
*/
|
||||
|
||||
import { FormFixture } from "#e2e/fixtures/FormFixture";
|
||||
import { PointerFixture } from "#e2e/fixtures/PointerFixture";
|
||||
import { SessionFixture } from "#e2e/fixtures/SessionFixture";
|
||||
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
|
||||
interface E2EFixturesTestScope {
|
||||
session: SessionFixture;
|
||||
pointer: PointerFixture;
|
||||
form: FormFixture;
|
||||
}
|
||||
|
||||
interface E2EWorkerScope {
|
||||
selectorRegistration: void;
|
||||
}
|
||||
|
||||
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
|
||||
session: async ({ page }, use, { title }) => {
|
||||
await use(new SessionFixture(page, title));
|
||||
},
|
||||
|
||||
form: async ({ page }, use, { title }) => {
|
||||
await use(new FormFixture(page, title));
|
||||
},
|
||||
|
||||
pointer: async ({ page }, use, { title }) => {
|
||||
await use(new PointerFixture({ page, testName: title }));
|
||||
},
|
||||
});
|
||||
13
web/e2e/selectors/types.ts
Normal file
13
web/e2e/selectors/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
|
||||
export type LocatorContext = Pick<
|
||||
Locator,
|
||||
| "locator"
|
||||
| "getByRole"
|
||||
| "getByTestId"
|
||||
| "getByText"
|
||||
| "getByLabel"
|
||||
| "getByAltText"
|
||||
| "getByTitle"
|
||||
| "getByPlaceholder"
|
||||
>;
|
||||
60
web/e2e/utils/generators.ts
Normal file
60
web/e2e/utils/generators.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
|
||||
import {
|
||||
adjectives,
|
||||
colors,
|
||||
Config as NameConfig,
|
||||
uniqueNamesGenerator,
|
||||
} from "unique-names-generator";
|
||||
|
||||
/**
|
||||
* Given a dictionary of words, slice the dictionary to only include words that start with the given letter.
|
||||
*/
|
||||
export function alliterate(dictionary: string[], letter: string): string[] {
|
||||
let firstIndex = 0;
|
||||
|
||||
for (let i = 0; i < dictionary.length; i++) {
|
||||
if (dictionary[i][0] === letter) {
|
||||
firstIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let lastIndex = firstIndex;
|
||||
|
||||
for (let i = firstIndex; i < dictionary.length; i++) {
|
||||
if (dictionary[i][0] !== letter) {
|
||||
lastIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dictionary.slice(firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
export function createRandomName({
|
||||
seed = IDGenerator.randomID(),
|
||||
...config
|
||||
}: Partial<NameConfig> = {}) {
|
||||
const randomLetterIndex =
|
||||
typeof seed === "number"
|
||||
? seed
|
||||
: Array.from(seed).reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
|
||||
const letter = adjectives[randomLetterIndex % adjectives.length][0];
|
||||
|
||||
const availableAdjectives = alliterate(adjectives, letter);
|
||||
|
||||
const availableColors = alliterate(colors, letter);
|
||||
|
||||
const name = uniqueNamesGenerator({
|
||||
dictionaries: [availableAdjectives, availableAdjectives, availableColors],
|
||||
style: "capital",
|
||||
separator: " ",
|
||||
length: 3,
|
||||
seed,
|
||||
...config,
|
||||
});
|
||||
|
||||
return name;
|
||||
}
|
||||
102
web/logger/node.js
Normal file
102
web/logger/node.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Application logger.
|
||||
*
|
||||
* @import { LoggerOptions, Logger, Level, ChildLoggerOptions } from "pino"
|
||||
* @import { PrettyOptions } from "pino-pretty"
|
||||
*/
|
||||
|
||||
import { pino } from "pino";
|
||||
|
||||
//#region Constants
|
||||
|
||||
/**
|
||||
* Default options for creating a Pino logger.
|
||||
*
|
||||
* @category Logger
|
||||
* @satisfies {LoggerOptions<never, false>}
|
||||
*/
|
||||
export const DEFAULT_PINO_LOGGER_OPTIONS = {
|
||||
enabled: true,
|
||||
level: "info",
|
||||
transport: {
|
||||
target: "./transport.js",
|
||||
options: /** @satisfies {PrettyOptions} */ ({
|
||||
colorize: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Functions
|
||||
|
||||
/**
|
||||
* Read the log level from the environment.
|
||||
* @return {Level}
|
||||
*/
|
||||
export function readLogLevel() {
|
||||
return process.env.AK_LOG_LEVEL || DEFAULT_PINO_LOGGER_OPTIONS.level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Logger} FixtureLogger
|
||||
*/
|
||||
|
||||
/**
|
||||
* @this {Logger}
|
||||
* @param {string} fixtureName
|
||||
* @param {string} [testName]
|
||||
* @param {ChildLoggerOptions} [options]
|
||||
* @returns {FixtureLogger}
|
||||
*/
|
||||
function createFixtureLogger(fixtureName, testName, options) {
|
||||
return this.child(
|
||||
{ name: fixtureName },
|
||||
{
|
||||
msgPrefix: `[${testName}] `,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} CustomLoggerMethods
|
||||
* @property {typeof createFixtureLogger} fixture
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Logger & CustomLoggerMethods} ConsoleLogger
|
||||
*/
|
||||
|
||||
/**
|
||||
* A singleton logger instance for Node.js.
|
||||
*
|
||||
* ```js
|
||||
* import { ConsoleLogger } from "#logger/node";
|
||||
*
|
||||
* ConsoleLogger.info("Hello, world!");
|
||||
* ```
|
||||
*
|
||||
* @runtime node
|
||||
* @type {ConsoleLogger}
|
||||
*/
|
||||
export const ConsoleLogger = Object.assign(
|
||||
pino({
|
||||
...DEFAULT_PINO_LOGGER_OPTIONS,
|
||||
level: readLogLevel(),
|
||||
}),
|
||||
{ fixture: createFixtureLogger },
|
||||
);
|
||||
|
||||
/**
|
||||
* @typedef {ReturnType<ConsoleLogger['child']>} ChildConsoleLogger
|
||||
*/
|
||||
|
||||
//#region Aliases
|
||||
|
||||
export const info = ConsoleLogger.info.bind(ConsoleLogger);
|
||||
export const debug = ConsoleLogger.debug.bind(ConsoleLogger);
|
||||
export const warn = ConsoleLogger.warn.bind(ConsoleLogger);
|
||||
export const error = ConsoleLogger.error.bind(ConsoleLogger);
|
||||
|
||||
//#endregion
|
||||
22
web/logger/transport.js
Normal file
22
web/logger/transport.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @file Pretty transport for Pino
|
||||
*
|
||||
* @import { PrettyOptions } from "pino-pretty"
|
||||
*/
|
||||
|
||||
import PinoPretty from "pino-pretty";
|
||||
|
||||
/**
|
||||
* @param {PrettyOptions} options
|
||||
*/
|
||||
function prettyTransporter(options) {
|
||||
const pretty = PinoPretty({
|
||||
...options,
|
||||
ignore: "pid,hostname",
|
||||
translateTime: "SYS:HH:MM:ss",
|
||||
});
|
||||
|
||||
return pretty;
|
||||
}
|
||||
|
||||
export default prettyTransporter;
|
||||
3651
web/package-lock.json
generated
3651
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,8 @@
|
||||
"pseudolocalize": "node ./scripts/pseudolocalize.mjs",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "wireit",
|
||||
"test": "wireit",
|
||||
"test:e2e": "wireit",
|
||||
"test": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:watch": "wireit",
|
||||
"test:watch": "wireit",
|
||||
"tsc": "wireit",
|
||||
@@ -69,6 +69,9 @@
|
||||
"#flow/*": "./src/flow/*.js",
|
||||
"#locales/*": "./src/locales/*.js",
|
||||
"#stories/*": "./src/stories/*.js",
|
||||
"#tests/*": "./tests/*.js",
|
||||
"#e2e": "./e2e/index.ts",
|
||||
"#e2e/*": "./e2e/*.ts",
|
||||
"#*/browser": {
|
||||
"types": "./out/*/browser.d.ts",
|
||||
"import": "./*/browser.js"
|
||||
@@ -83,7 +86,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",
|
||||
@@ -114,21 +116,23 @@
|
||||
"@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",
|
||||
"@playwright/test": "^1.54.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",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
@@ -159,11 +163,14 @@
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"playwright": "^1.54.1",
|
||||
"prettier": "^3.6.2",
|
||||
"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,8 +185,11 @@
|
||||
"ts-pattern": "^5.8.0",
|
||||
"turnstile-types": "^1.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.39.0",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^7.0.6",
|
||||
"vitest": "^3.2.4",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
"yaml": "^2.8.0"
|
||||
@@ -267,7 +277,7 @@
|
||||
"command": "lit-analyzer src"
|
||||
},
|
||||
"lint:types:tests": {
|
||||
"command": "tsc --noEmit -p ./tests"
|
||||
"command": "tsc --noEmit -p tsconfig.test.json"
|
||||
},
|
||||
"lint:types": {
|
||||
"command": "tsc -p .",
|
||||
@@ -316,7 +326,7 @@
|
||||
],
|
||||
"env": {
|
||||
"CI": "true",
|
||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:e2e:watch": {
|
||||
@@ -325,7 +335,7 @@
|
||||
"build"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:watch": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
94
web/playwright.config.js
Normal file
94
web/playwright.config.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file Playwright configuration.
|
||||
*
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*
|
||||
* @import { LogFn, Logger } from "pino"
|
||||
*/
|
||||
|
||||
import { ConsoleLogger } from "#logger/node";
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const CI = !!process.env.CI;
|
||||
|
||||
/**
|
||||
* @type {Map<string, Logger>}
|
||||
*/
|
||||
const LoggerCache = new Map();
|
||||
|
||||
const baseURL = process.env.AK_TEST_RUNNER_PAGE_URL ?? "http://localhost:9000";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./test/browser",
|
||||
fullyParallel: true,
|
||||
forbidOnly: CI,
|
||||
retries: CI ? 2 : 0,
|
||||
workers: CI ? 1 : undefined,
|
||||
reporter: CI
|
||||
? "github"
|
||||
: [
|
||||
// ---
|
||||
["list", { printSteps: true }],
|
||||
["html", { open: "never" }],
|
||||
],
|
||||
use: {
|
||||
testIdAttribute: "data-test-id",
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
launchOptions: {
|
||||
logger: {
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
log: (name, severity, message, args) => {
|
||||
let logger = LoggerCache.get(name);
|
||||
|
||||
if (!logger) {
|
||||
logger = ConsoleLogger.child({
|
||||
name: `Playwright ${name.toUpperCase()}`,
|
||||
});
|
||||
LoggerCache.set(name, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {LogFn}
|
||||
*/
|
||||
let log;
|
||||
|
||||
switch (severity) {
|
||||
case "verbose":
|
||||
log = logger.debug;
|
||||
break;
|
||||
case "warning":
|
||||
log = logger.warn;
|
||||
break;
|
||||
case "error":
|
||||
log = logger.error;
|
||||
break;
|
||||
default:
|
||||
log = logger.info;
|
||||
break;
|
||||
}
|
||||
|
||||
if (name === "api") {
|
||||
log = logger.debug;
|
||||
}
|
||||
|
||||
log.call(logger, message.toString(), args);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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() {
|
||||
|
||||
@@ -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]"),
|
||||
|
||||
@@ -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[] = []) {
|
||||
|
||||
@@ -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) {
|
||||
@@ -141,8 +137,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,7 +149,7 @@ 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)] ??
|
||||
[]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,7 +109,11 @@ const redirectUriHelpMessages: string[] = [
|
||||
),
|
||||
];
|
||||
|
||||
const backchannelLogoutUriHelpMessages: string[] = [
|
||||
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
|
||||
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||
)}`;
|
||||
|
||||
const backchannelLogoutUriHelpMessages = [
|
||||
msg(
|
||||
"URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.",
|
||||
),
|
||||
@@ -120,6 +122,10 @@ const backchannelLogoutUriHelpMessages: string[] = [
|
||||
),
|
||||
];
|
||||
|
||||
export const backchannelLogoutUriHelp = html`${backchannelLogoutUriHelpMessages.map(
|
||||
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||
)}`;
|
||||
|
||||
type ShowClientSecret = (show: boolean) => void;
|
||||
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
|
||||
|
||||
@@ -133,7 +139,6 @@ export function renderForm(
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name}
|
||||
required
|
||||
></ak-text-input>
|
||||
|
||||
@@ -145,7 +150,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 +175,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 +187,7 @@ export function renderForm(
|
||||
>
|
||||
</ak-hidden-text-input>
|
||||
<ak-form-element-horizontal
|
||||
flow-direction="row"
|
||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||
name="redirectUris"
|
||||
>
|
||||
@@ -200,21 +204,19 @@ 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
|
||||
<ak-form-element-horizontal
|
||||
flow-direction="row"
|
||||
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-text-input
|
||||
name="backchannelLogoutUri"
|
||||
value="${provider?.backchannelLogoutUri ?? ""}"
|
||||
placeholder=${msg("URL")}
|
||||
></ak-text-input>
|
||||
${backchannelLogoutUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -292,7 +292,7 @@ export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "au
|
||||
* @todo Can this be handled with a Lit Mixin?
|
||||
*/
|
||||
export function rootInterface<T extends HTMLElement = HTMLElement>(): T {
|
||||
const element = document.body.querySelector<T>("[data-ak-interface-root]");
|
||||
const element = document.body.querySelector<T>("[data-test-id=interface-root]");
|
||||
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { css, CSSResult } from "lit";
|
||||
|
||||
export function getCookie(name: string): string {
|
||||
let cookieValue = "";
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
@@ -32,6 +34,18 @@ export function truncate(string: string, length = 10): string {
|
||||
return string.length > length ? `${string.substring(0, length)}...` : string;
|
||||
}
|
||||
|
||||
export function camelToSnake(key: string): string {
|
||||
const result = key.replace(/([A-Z])/g, " $1");
|
||||
return result.split(" ").join("_").toLowerCase();
|
||||
}
|
||||
|
||||
const capitalize = (key: string) => (key.length === 0 ? "" : key[0].toUpperCase() + key.slice(1));
|
||||
|
||||
export function snakeToCamel(key: string) {
|
||||
const [start, ...rest] = key.split("_");
|
||||
return [start, ...rest.map(capitalize)].join("");
|
||||
}
|
||||
|
||||
export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[string, T[]]> {
|
||||
const m = new Map<string, T[]>();
|
||||
objects.forEach((obj) => {
|
||||
@@ -63,6 +77,30 @@ export function randomString(len: number, charset: string): string {
|
||||
for (let index = 0; index < len; index++) {
|
||||
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]);
|
||||
}
|
||||
|
||||
return chars.join("");
|
||||
}
|
||||
|
||||
// Lit is extremely well-typed with regard to CSS, and Storybook's `build` does not currently have a
|
||||
// coherent way of importing CSS-as-text into CSSStyleSheet. It works well when Storybook is running
|
||||
// in `dev,` but in `build` it fails. Storied components will have to map their textual CSS imports
|
||||
// using the function below.
|
||||
type AdaptableStylesheet = Readonly<string | CSSResult | CSSStyleSheet>;
|
||||
type AdaptedStylesheets = CSSStyleSheet | CSSStyleSheet[];
|
||||
|
||||
const isCSSResult = (v: unknown): v is CSSResult =>
|
||||
v instanceof CSSResult && v.styleSheet !== undefined;
|
||||
|
||||
// prettier-ignore
|
||||
export const _adaptCSS = (sheet: AdaptableStylesheet): CSSStyleSheet =>
|
||||
(typeof sheet === "string" ? css([sheet] as unknown as TemplateStringsArray, []).styleSheet
|
||||
: isCSSResult(sheet) ? sheet.styleSheet
|
||||
: sheet) as CSSStyleSheet;
|
||||
|
||||
// Overloaded function definitions inform consumers that if you pass it an array, expect an array in
|
||||
// return; if you pass it a scaler, expect a scalar in return.
|
||||
|
||||
export function adaptCSS(sheet: AdaptableStylesheet): CSSStyleSheet;
|
||||
export function adaptCSS(sheet: AdaptableStylesheet[]): CSSStyleSheet[];
|
||||
export function adaptCSS(sheet: AdaptableStylesheet | AdaptableStylesheet[]): AdaptedStylesheets {
|
||||
return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { SlottedTemplateResult } from "../elements/types";
|
||||
|
||||
import { AKElement, type AKElementProps } from "#elements/Base";
|
||||
|
||||
import { ErrorProp } from "#components/ak-field-errors";
|
||||
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
|
||||
import { html, nothing, TemplateResult } from "lit";
|
||||
@@ -20,7 +18,7 @@ export interface HorizontalLightComponentProps<T> extends AKElementProps {
|
||||
bighelp?: SlottedTemplateResult | SlottedTemplateResult[];
|
||||
hidden?: boolean;
|
||||
invalid?: boolean;
|
||||
errorMessages?: ErrorProp[];
|
||||
errorMessages?: string[];
|
||||
value?: T;
|
||||
inputHint?: string;
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { pluckErrorDetail } from "#common/errors/network";
|
||||
|
||||
import { LitFC } from "#elements/types";
|
||||
|
||||
import { ErrorDetail, ValidationError } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
/**
|
||||
* An error originating from a form field.
|
||||
*/
|
||||
export type FieldErrorTuple = [fieldName: string, detail: string];
|
||||
|
||||
export type ErrorProp = string | Error | ErrorDetail | ValidationError | FieldErrorTuple;
|
||||
|
||||
export interface AKFormErrorsProps {
|
||||
errors?: ErrorProp[];
|
||||
}
|
||||
|
||||
function renderError(detail: string) {
|
||||
if (!detail) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||
<span class="pf-c-form__helper-text-icon">
|
||||
<i class="fas fa-exclamation-circle" aria-hidden="true"></i> </span
|
||||
>${detail}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
export const AKFormErrors: LitFC<AKFormErrorsProps> = ({ errors } = {}) => {
|
||||
if (!errors?.length) return nothing;
|
||||
|
||||
return errors.flatMap((error) => {
|
||||
if (Array.isArray(error) && error.length === 2) {
|
||||
const [fieldName, detail] = error;
|
||||
|
||||
return renderError(msg(str`${fieldName}: ${detail}`));
|
||||
}
|
||||
|
||||
return renderError(pluckErrorDetail(error));
|
||||
});
|
||||
};
|
||||
@@ -96,17 +96,17 @@ export class WizardStep extends AKElement {
|
||||
* steps. Recommendation: Set this, the description, and `canCancel` in a subclass, and stop
|
||||
* worrying about them.
|
||||
*/
|
||||
protected wizardTitle = "--unset--";
|
||||
wizardTitle = "--unset--";
|
||||
|
||||
/**
|
||||
* The text for a descriptive subtitle for the wizard
|
||||
*/
|
||||
protected wizardDescription?: string;
|
||||
wizardDescription?: string;
|
||||
|
||||
/**
|
||||
* Show the [Cancel] icon and offer the [Cancel] button
|
||||
*/
|
||||
public canCancel = false;
|
||||
canCancel = false;
|
||||
|
||||
/**
|
||||
* The ID of the current step.
|
||||
|
||||
@@ -28,6 +28,6 @@ export abstract class Interface extends AKElement {
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
|
||||
this.dataset.testId = "interface-root";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ type ContentValue = SlottedTemplateResult | undefined;
|
||||
*/
|
||||
export function akLoadingOverlay(
|
||||
properties: ILoadingOverlay = {},
|
||||
content: ILoadingOverlayContent = {},
|
||||
content: string | ILoadingOverlayContent = {},
|
||||
) {
|
||||
// `heading` here is an Object.key of ILoadingOverlayContent, not the obsolete
|
||||
// slot-name.
|
||||
|
||||
@@ -57,17 +57,21 @@ export type Replacer = (input: string) => string;
|
||||
|
||||
@customElement("ak-mdx")
|
||||
export class AKMDX extends AKElement {
|
||||
@property({ type: String, reflect: true })
|
||||
public url?: string;
|
||||
@property({
|
||||
reflect: true,
|
||||
})
|
||||
url: string = "";
|
||||
|
||||
@property()
|
||||
public content?: string;
|
||||
content: string = "";
|
||||
|
||||
@property({ attribute: false })
|
||||
public replacers: Replacer[] = [];
|
||||
replacers: Replacer[] = [];
|
||||
|
||||
#reactRoot: Root | null = null;
|
||||
|
||||
resolvedHTML = "";
|
||||
|
||||
static styles = [
|
||||
PFBase,
|
||||
PFList,
|
||||
@@ -177,7 +181,7 @@ export class AKMDX extends AKElement {
|
||||
nextMDXModule = await fetchMDXModule(pathname);
|
||||
} else {
|
||||
nextMDXModule = {
|
||||
content: this.content || "",
|
||||
content: this.content,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,6 @@ export const MODAL_BUTTON_STYLES = css`
|
||||
:host {
|
||||
text-align: left;
|
||||
font-size: var(--pf-global--FontSize--md);
|
||||
|
||||
/* Fixes issue where browser inherits cursor from parent, typically a button. */
|
||||
cursor: initial;
|
||||
}
|
||||
.pf-c-modal-box > .pf-c-button + * {
|
||||
margin-right: 0;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EVENT_REFRESH } from "#common/constants";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
import { dateToUTC } from "#common/temporal";
|
||||
import { camelToSnake } from "#common/utils";
|
||||
|
||||
import { isControlElement } from "#elements/AkControlElement";
|
||||
import { AKElement } from "#elements/Base";
|
||||
@@ -13,8 +14,6 @@ import { createFileMap, isNamedElement, NamedElement } from "#elements/utils/inp
|
||||
|
||||
import { instanceOfValidationError } from "@goauthentik/api";
|
||||
|
||||
import { snakeCase } from "change-case";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
@@ -307,14 +306,13 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
|
||||
for (const element of elements) {
|
||||
elements.forEach((element) => {
|
||||
element.requestUpdate();
|
||||
|
||||
const elementName = element.name;
|
||||
if (!elementName) return;
|
||||
|
||||
if (!elementName) continue;
|
||||
|
||||
const snakeProperty = snakeCase(elementName);
|
||||
const snakeProperty = camelToSnake(elementName);
|
||||
|
||||
if (snakeProperty in parsedError) {
|
||||
element.errorMessages = parsedError[snakeProperty];
|
||||
@@ -323,7 +321,7 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
element.errorMessages = [];
|
||||
element.invalid = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (parsedError.nonFieldErrors) {
|
||||
this.nonFieldErrors = parsedError.nonFieldErrors;
|
||||
|
||||
@@ -27,9 +27,9 @@ export class AKFormGroup extends AKElement {
|
||||
PFFormControl,
|
||||
|
||||
css`
|
||||
:host([theme="dark"]) {
|
||||
:host {
|
||||
--marker-color: var(--pf-global--Color--200);
|
||||
--marker-color-hover: var(--ak-dark-foreground-darker);
|
||||
--marker-color-hover: var(--pf-global--Color--100);
|
||||
}
|
||||
|
||||
.pf-c-form__field-group-header-description {
|
||||
@@ -53,7 +53,7 @@ export class AKFormGroup extends AKElement {
|
||||
user-select: none;
|
||||
|
||||
&::marker {
|
||||
color: var(--marker-color, var(--pf-global--Color--200));
|
||||
color: var(--marker-color);
|
||||
transition: var(--pf-c-form__field-group-toggle-icon--Transition);
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
@@ -61,7 +61,7 @@ export class AKFormGroup extends AKElement {
|
||||
|
||||
&:hover::marker {
|
||||
outline: 1px dashed red;
|
||||
color: var(--marker-color-hover, var(--pf-global--Color--100));
|
||||
color: var(--marker-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +125,9 @@ export class AKFormGroup extends AKElement {
|
||||
<details
|
||||
${ref(this.#detailsRef)}
|
||||
?open=${this.open}
|
||||
aria-expanded=${this.open ? "true" : "false"}
|
||||
?aria-expanded="${this.open}"
|
||||
role="group"
|
||||
aria-owns="form-group-expandable-content"
|
||||
aria-labelledby="form-group-header-title"
|
||||
aria-describedby="form-group-expandable-content-description"
|
||||
>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { AKFormGroup } from "#elements/forms/FormGroup";
|
||||
|
||||
import { AKFormErrors, ErrorProp } from "#components/ak-field-errors";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -60,6 +59,18 @@ export class HorizontalFormElement extends AKElement {
|
||||
var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth)
|
||||
var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
|
||||
}
|
||||
|
||||
.pf-c-form__group-label {
|
||||
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
|
||||
}
|
||||
|
||||
.pf-c-form__label[aria-required] .pf-c-form__label-text::after {
|
||||
content: "*";
|
||||
user-select: none;
|
||||
margin-left: var(--pf-c-form__label-required--MarginLeft);
|
||||
font-size: var(--pf-c-form__label-required--FontSize);
|
||||
color: var(--pf-c-form__label-required--Color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -73,7 +84,7 @@ export class HorizontalFormElement extends AKElement {
|
||||
public required = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public errorMessages?: ErrorProp[];
|
||||
public errorMessages: string[] | string[][] = [];
|
||||
|
||||
_invalid = false;
|
||||
|
||||
@@ -97,9 +108,11 @@ export class HorizontalFormElement extends AKElement {
|
||||
@property({ type: String })
|
||||
public name = "";
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
@property({
|
||||
type: String,
|
||||
attribute: "flow-direction",
|
||||
})
|
||||
public flowDirection: "row" | "column" = "column";
|
||||
|
||||
firstUpdated(): void {
|
||||
this.updated();
|
||||
@@ -125,7 +138,12 @@ export class HorizontalFormElement extends AKElement {
|
||||
|
||||
render(): TemplateResult {
|
||||
this.updated();
|
||||
return html`<div class="pf-c-form__group" role="group" aria-label="${this.label}">
|
||||
return html`<div
|
||||
class="pf-c-form__group"
|
||||
role="group"
|
||||
aria-label="${this.label}"
|
||||
data-flow-direction="${this.flowDirection}"
|
||||
>
|
||||
<div class="pf-c-form__group-label">
|
||||
<label
|
||||
id="group-label"
|
||||
@@ -139,7 +157,21 @@ export class HorizontalFormElement extends AKElement {
|
||||
<div class="pf-c-form__group-control">
|
||||
<slot class="pf-c-form__horizontal-group"></slot>
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
${AKFormErrors({ errors: this.errorMessages })}
|
||||
${this.errorMessages.map((message) => {
|
||||
if (message instanceof Object) {
|
||||
return html`${Object.entries(message).map(([field, errMsg]) => {
|
||||
return html`<p
|
||||
class="pf-c-form__helper-text pf-m-error"
|
||||
aria-live="polite"
|
||||
>
|
||||
${msg(str`${field}: ${errMsg}`)}
|
||||
</p>`;
|
||||
})}`;
|
||||
}
|
||||
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||
${message}
|
||||
</p>`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -54,19 +54,6 @@ export type LitPropertyRecord<T extends object> = {
|
||||
*/
|
||||
export type LitPropertyKey<K> = K extends string ? `.${K}` | `?${K}` | K : K;
|
||||
|
||||
/**
|
||||
* A React-like functional component. Used to render a component in a template.
|
||||
*
|
||||
* @template P The type of the props object.
|
||||
* @param props The props object.
|
||||
* @param children The children to render.
|
||||
* @returns The rendered template.
|
||||
*/
|
||||
export type LitFC<P> = (
|
||||
props: P,
|
||||
children?: SlottedTemplateResult,
|
||||
) => SlottedTemplateResult | SlottedTemplateResult[];
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Host/Controller
|
||||
|
||||
@@ -27,16 +27,17 @@ export class UserSettingsPromptStage extends PromptStage {
|
||||
}
|
||||
|
||||
renderField(prompt: StagePrompt): TemplateResult {
|
||||
const errors = this.challenge?.responseErrors?.[prompt.fieldKey];
|
||||
|
||||
const errors = (this.challenge?.responseErrors || {})[prompt.fieldKey];
|
||||
if (this.shouldRenderInWrapper(prompt)) {
|
||||
return html`
|
||||
<ak-form-element-horizontal
|
||||
label=${msg(str`${prompt.label}`)}
|
||||
?required=${prompt.required}
|
||||
name=${prompt.fieldKey}
|
||||
?invalid=${!!errors}
|
||||
.errorMessages=${errors}
|
||||
?invalid=${errors !== undefined}
|
||||
.errorMessages=${(errors || []).map((error) => {
|
||||
return error.string;
|
||||
})}
|
||||
>
|
||||
${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
217
web/test/browser/providers.test.ts
Normal file
217
web/test/browser/providers.test.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { expect, test } from "#e2e";
|
||||
import { createRandomName } from "#e2e/utils/generators";
|
||||
import { ConsoleLogger } from "#logger/node";
|
||||
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
import { series } from "@goauthentik/core/promises";
|
||||
|
||||
test.describe("Provider Wizard", () => {
|
||||
const providerNames = new Map<string, string>();
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
test.beforeEach("Configure Providers", async ({ page, session }, { testId }) => {
|
||||
const seed = IDGenerator.randomID(6);
|
||||
const providerName = `${createRandomName({ seed })} (${seed})`;
|
||||
|
||||
providerNames.set(testId, providerName);
|
||||
|
||||
const wizard = page.getByRole("dialog", { name: "New provider" });
|
||||
|
||||
await test.step("Authenticate", async () => {
|
||||
await session.login({
|
||||
to: "/if/admin/#/core/providers",
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Navigate to provider wizard", async () => {
|
||||
await expect(wizard, "Wizard is initially closed").toBeHidden();
|
||||
|
||||
await page.getByRole("button", { name: "New Provider" }).click();
|
||||
|
||||
await expect(wizard, "Wizard opens after clicking on New Provider").toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole("listbox", { name: "Select a provider type" }),
|
||||
"Wizard opens with a list of provider types",
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
wizard.getByRole("navigation").getByRole("button", {
|
||||
name: /next|finish/i,
|
||||
}),
|
||||
"Wizard can't be navigated to next step",
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach("Verification", async ({ page }, { testId }) => {
|
||||
//#region Confirm provider
|
||||
|
||||
const providerName = providerNames.get(testId)!;
|
||||
|
||||
const $provider = await test.step("Find provider via search", async () => {
|
||||
const searchInput = page.getByRole("search").getByPlaceholder("Search for providers");
|
||||
|
||||
await searchInput.fill(providerName);
|
||||
|
||||
// We have to wait for the provider to appear in the table,
|
||||
// but several UI elements will be rendered asynchronously.
|
||||
// We attempt several times to find the provider to avoid flakiness.
|
||||
|
||||
const tries = 10;
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < tries; i++) {
|
||||
await searchInput.press("Enter");
|
||||
await searchInput.blur();
|
||||
|
||||
const $rowEntry = page.getByRole("row", {
|
||||
name: providerName,
|
||||
});
|
||||
|
||||
ConsoleLogger.info(
|
||||
`${i + 1}/${tries} Waiting for provider ${providerName} to appear in the table`,
|
||||
);
|
||||
|
||||
found = await $rowEntry
|
||||
.waitFor({
|
||||
timeout: 1500,
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (found) {
|
||||
ConsoleLogger.info(`Provider ${providerName} found in the table`);
|
||||
return $rowEntry;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Provider ${providerName} not found in the table`);
|
||||
});
|
||||
|
||||
await expect($provider, "Provider is visible").toBeVisible();
|
||||
|
||||
//#endregion
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region OAuth2
|
||||
|
||||
test("Simple OAuth2 Provider", async ({ form, pointer }, testInfo) => {
|
||||
const providerName = providerNames.get(testInfo.testId)!;
|
||||
const { fill, selectSearchValue } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
await series(
|
||||
[click, "OAuth2/OpenID", "option"],
|
||||
[click, "Next"],
|
||||
[fill, "Provider name", providerName],
|
||||
[
|
||||
selectSearchValue,
|
||||
"Authorization flow",
|
||||
/default-provider-authorization-explicit-consent/,
|
||||
],
|
||||
[click, "Finish"],
|
||||
);
|
||||
});
|
||||
|
||||
test("Complete OAuth2 Provider", async ({ page, form, pointer }, testInfo) => {
|
||||
const providerName = providerNames.get(testInfo.testId)!;
|
||||
|
||||
const { fill, selectSearchValue, setFormGroup, setRadio, setInputCheck } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
const $clientSecretInput = page.getByRole("textbox", { name: "Client Secret" });
|
||||
|
||||
await series(
|
||||
[click, "OAuth2/OpenID", "option"],
|
||||
[click, "Next"],
|
||||
[fill, "Provider name", providerName],
|
||||
[
|
||||
selectSearchValue,
|
||||
"Authorization flow",
|
||||
/default-provider-authorization-explicit-consent/,
|
||||
],
|
||||
[setFormGroup, "Protocol settings", true],
|
||||
[setRadio, "Client Type", "Public"],
|
||||
[
|
||||
expect(
|
||||
$clientSecretInput,
|
||||
"Client Secret should be hidden when Client Type is Public",
|
||||
).toBeHidden,
|
||||
],
|
||||
[setRadio, "Client Type", "Confidential"],
|
||||
[
|
||||
expect(
|
||||
$clientSecretInput,
|
||||
"Client Secret should be visible when Client Type is Confidential",
|
||||
).toBeVisible,
|
||||
],
|
||||
[selectSearchValue, "Signing Key", /authentik Self-signed Certificate/],
|
||||
[selectSearchValue, "Encryption Key", /authentik Self-signed Certificate/],
|
||||
[setFormGroup, "Advanced flow settings", true],
|
||||
[selectSearchValue, "Authentication flow", /default-source-authentication/],
|
||||
[selectSearchValue, "Invalidation flow", /default-invalidation-flow/],
|
||||
[setFormGroup, "Advanced protocol settings", true],
|
||||
[fill, "Access code validity", "minutes=2"],
|
||||
[fill, "Access token validity", "minutes=10"],
|
||||
[fill, "Refresh token validity", "days=40"],
|
||||
[setInputCheck, "Include claims in id_token", false],
|
||||
[setRadio, "Subject mode", "Based on the User's username"],
|
||||
[setRadio, "Issuer mode", "Same identifier is used for all providers"],
|
||||
[setFormGroup, "Machine-to-Machine authentication settings", true],
|
||||
[click, "Finish", "button", page.getByRole("dialog", { name: "New Provider" })],
|
||||
);
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region LDAP
|
||||
|
||||
test("Complete LDAP Provider", async ({ page, pointer, form }, testInfo) => {
|
||||
const providerName = providerNames.get(testInfo.testId)!;
|
||||
const { fill, setFormGroup, selectSearchValue, setInputCheck, setRadio } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
await series(
|
||||
[click, "LDAP", "option"],
|
||||
[click, "Next"],
|
||||
|
||||
[fill, "Provider name", providerName],
|
||||
[setFormGroup, "Flow settings", true],
|
||||
[setFormGroup, "Protocol settings", true],
|
||||
[selectSearchValue, "Bind flow", /default-authentication-flow/],
|
||||
[fill, "Base DN", "DC=ldap-2,DC=goauthentik,DC=io"],
|
||||
[selectSearchValue, "Certificate", /authentik Self-signed Certificate/],
|
||||
[fill, "TLS Server name", "goauthentik.io"],
|
||||
[fill, "UID start number", "2001"],
|
||||
[fill, "GID start number", "4001"],
|
||||
[setRadio, "Search mode", "Direct querying"],
|
||||
[setRadio, "Bind mode", "Direct binding"],
|
||||
[setInputCheck, "MFA Support", false],
|
||||
[click, "Finish", "button", page.getByRole("dialog", { name: "New Provider" })],
|
||||
);
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region RADIUS
|
||||
|
||||
test("Complete RADIUS Provider", async ({ page, pointer, form }, testInfo) => {
|
||||
const providerName = providerNames.get(testInfo.testId)!;
|
||||
const { fill, selectSearchValue } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
await series(
|
||||
[click, "RADIUS", "option"],
|
||||
[click, "Next"],
|
||||
[fill, "Provider name", providerName],
|
||||
[selectSearchValue, "Authentication flow", /default-authentication-flow/],
|
||||
[click, "Finish", "button", page.getByRole("dialog", { name: "New Provider" })],
|
||||
);
|
||||
});
|
||||
|
||||
//#endregion
|
||||
});
|
||||
37
web/test/browser/session.test.ts
Normal file
37
web/test/browser/session.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { expect, test } from "#e2e";
|
||||
import {
|
||||
BAD_PASSWORD,
|
||||
BAD_USERNAME,
|
||||
GOOD_PASSWORD,
|
||||
GOOD_USERNAME,
|
||||
} from "#e2e/fixtures/SessionFixture";
|
||||
|
||||
test.beforeEach(async ({ session }) => {
|
||||
await session.toLoginPage();
|
||||
});
|
||||
|
||||
test.describe("Session management", () => {
|
||||
test("Login with valid credentials", async ({ session, page }) => {
|
||||
await session.login({ username: GOOD_USERNAME, password: GOOD_PASSWORD });
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
level: 1,
|
||||
}),
|
||||
).toHaveText("My applications");
|
||||
});
|
||||
|
||||
test("Reject bad username", async ({ session }) => {
|
||||
await session.login({ username: BAD_USERNAME, password: GOOD_PASSWORD });
|
||||
|
||||
await expect(session.$authFailureMessage).toBeVisible();
|
||||
await expect(session.$authFailureMessage).toHaveText("Invalid password");
|
||||
});
|
||||
|
||||
test("Reject bad password", async ({ session }) => {
|
||||
await session.login({ username: GOOD_USERNAME, password: BAD_PASSWORD });
|
||||
|
||||
await expect(session.$authFailureMessage).toBeVisible();
|
||||
await expect(session.$authFailureMessage).toHaveText("Invalid password");
|
||||
});
|
||||
});
|
||||
87
web/test/lit/rendering.js
Normal file
87
web/test/lit/rendering.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file Vitest browser utilities for Lit.
|
||||
*
|
||||
* @import { LocatorSelectors } from '@vitest/browser/context'
|
||||
* @import { PrettyDOMOptions } from '@vitest/browser/utils'
|
||||
* @import { RenderOptions as LitRenderOptions } from 'lit'
|
||||
*/
|
||||
|
||||
import { debug, getElementLocatorSelectors } from "@vitest/browser/utils";
|
||||
|
||||
import { render as renderLit } from "lit";
|
||||
|
||||
/**
|
||||
* @implements {Disposable}
|
||||
*/
|
||||
export class LitViteContext {
|
||||
/**
|
||||
* @type {Set<Disposable>}
|
||||
*/
|
||||
static #resources = new Set();
|
||||
|
||||
/**
|
||||
* @param {unknown} template
|
||||
* @param {HTMLElement} [container]
|
||||
* @param {LitRenderOptions} [options]
|
||||
*
|
||||
* @returns {LitViteContext}
|
||||
*/
|
||||
static render = (template, container = document.createElement("div"), options) => {
|
||||
const context = new LitViteContext(container);
|
||||
context.render(template, options);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
static [Symbol.dispose] = () => {
|
||||
this.#resources.forEach((resource) => resource[Symbol.dispose]());
|
||||
this.#resources.clear();
|
||||
};
|
||||
|
||||
static cleanup = () => {
|
||||
return this[Symbol.dispose]();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {unknown} template
|
||||
* @param {LitRenderOptions} [options]
|
||||
*/
|
||||
render(template, options) {
|
||||
return renderLit(template, this.container, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {HTMLElement} container
|
||||
*/
|
||||
container;
|
||||
|
||||
/**
|
||||
* @type {LocatorSelectors}
|
||||
*/
|
||||
$;
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
*/
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.$ = getElementLocatorSelectors(container);
|
||||
}
|
||||
|
||||
toFragment() {
|
||||
return document.createRange().createContextualFragment(this.container.innerHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [maxLength]
|
||||
* @param {PrettyDOMOptions} [options]
|
||||
*/
|
||||
debug(maxLength, options) {
|
||||
return debug(this.container, maxLength, options);
|
||||
}
|
||||
|
||||
[Symbol.dispose] = () => {
|
||||
this.container.remove();
|
||||
LitViteContext.#resources.delete(this);
|
||||
};
|
||||
}
|
||||
12
web/test/lit/setup.js
Normal file
12
web/test/lit/setup.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { LitViteContext } from "./rendering.js";
|
||||
|
||||
import { page } from "@vitest/browser/context";
|
||||
import { beforeEach } from "vitest";
|
||||
|
||||
page.extend({
|
||||
// @ts-ignore
|
||||
renderLit: LitViteContext.render,
|
||||
[Symbol.for("vitest:component-cleanup")]: LitViteContext.cleanup,
|
||||
});
|
||||
|
||||
beforeEach(() => LitViteContext.cleanup());
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
|
||||
@@ -2,5 +2,11 @@
|
||||
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["src/**/*.test.ts", "./tests"]
|
||||
"exclude": [
|
||||
// ---
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.comp.ts",
|
||||
"./**/*.stories.ts",
|
||||
"./tests"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
// @file TSConfig used during tests.
|
||||
// @file TSConfig used by the web package during build.
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"types": ["node", "webdriverio/async", "@wdio/cucumber-framework", "expect-webdriverio"],
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"ES5",
|
||||
"ES2015",
|
||||
"ES2016",
|
||||
"ES2017",
|
||||
"ES2018",
|
||||
"ES2019",
|
||||
"ES2020",
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"WebWorker"
|
||||
]
|
||||
}
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
|
||||
23
web/types/node.d.ts
vendored
23
web/types/node.d.ts
vendored
@@ -14,12 +14,13 @@ declare module "module" {
|
||||
* const relativeDirname = dirname(fileURLToPath(import.meta.url));
|
||||
* ```
|
||||
*/
|
||||
|
||||
var __dirname: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "process" {
|
||||
import { Level } from "pino";
|
||||
|
||||
global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
@@ -30,6 +31,26 @@ declare module "process" {
|
||||
* @see {@link https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production | The difference between development and production}
|
||||
*/
|
||||
readonly NODE_ENV?: "development" | "production";
|
||||
|
||||
/**
|
||||
* Whether or not we are running on a CI server.
|
||||
*/
|
||||
readonly CI?: string;
|
||||
|
||||
/**
|
||||
* The application log level.
|
||||
*/
|
||||
readonly AK_LOG_LEVEL?: Level;
|
||||
|
||||
/**
|
||||
* The base URL of web server to run the tests against.
|
||||
*
|
||||
* Typically this is `http://localhost:9000`.
|
||||
*
|
||||
* @format url
|
||||
*/
|
||||
readonly AK_TEST_RUNNER_PAGE_URL?: string;
|
||||
|
||||
/**
|
||||
* @todo Determine where this is used and if it is needed,
|
||||
* give it a better name.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference types="vitest/config" />
|
||||
|
||||
import { createBundleDefinitions } from "#bundler/utils/node";
|
||||
import { inlineCSSPlugin } from "#bundler/vite-plugin-lit-css/node";
|
||||
|
||||
@@ -9,4 +11,41 @@ export default defineConfig({
|
||||
// ---
|
||||
inlineCSSPlugin(),
|
||||
],
|
||||
test: {
|
||||
dir: "./test",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/.{idea,git,cache,output,temp}/**",
|
||||
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
include: ["./unit/**/*.{test,spec}.ts", "**/*.unit.{test,spec}.ts"],
|
||||
name: "unit",
|
||||
environment: "node",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
setupFiles: ["./test/lit/setup.js"],
|
||||
|
||||
include: ["./browser/**/*.{test,spec}.ts", "**/*.browser.{test,spec}.ts"],
|
||||
name: "browser",
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: "playwright",
|
||||
|
||||
instances: [
|
||||
{
|
||||
browser: "chromium",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user