Merge branch 'main' into dev

* main:
  web: clean up some repetitive types (#9241)
  core: fix logic for token expiration (#9426)
  ci: fix ci pipeline (#9427)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9424)
  web: Add resolved and integrity fields back to package-lock.json (#9419)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9407)
  stages/identification: don't check source component (#9410)
  core: bump selenium from 4.19.0 to 4.20.0 (#9411)
  core: bump black from 24.4.0 to 24.4.1 (#9412)
  ci: bump golangci/golangci-lint-action from 4 to 5 (#9413)
  core: bump goauthentik.io/api/v3 from 3.2024023.2 to 3.2024040.1 (#9414)
  web: bump @sentry/browser from 7.112.1 to 7.112.2 in /web in the sentry group (#9416)
  sources/oauth: ensure all UI sources return a valid source (#9401)
  web: markdown: display markdown even when frontmatter is missing (#9404)
This commit is contained in:
Ken Sternberg
2024-04-25 08:38:08 -07:00
29 changed files with 5354 additions and 166 deletions

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API - name: Generate API
run: make gen-client-go run: make gen-client-go
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v4 uses: golangci/golangci-lint-action@v5
with: with:
version: v1.54.2 version: v1.54.2
args: --timeout 5000s --verbose args: --timeout 5000s --verbose

View File

@ -34,6 +34,13 @@ jobs:
- name: Eslint - name: Eslint
working-directory: ${{ matrix.project }}/ working-directory: ${{ matrix.project }}/
run: npm run lint run: npm run lint
lint-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: web/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-build: lint-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -95,6 +102,7 @@ jobs:
run: npm run lit-analyse run: npm run lit-analyse
ci-web-mark: ci-web-mark:
needs: needs:
- lint-lockfile
- lint-eslint - lint-eslint
- lint-prettier - lint-prettier
- lint-lit-analyse - lint-lit-analyse

View File

@ -12,6 +12,13 @@ on:
- version-* - version-*
jobs: jobs:
lint-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: website/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-prettier: lint-prettier:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -62,6 +69,7 @@ jobs:
run: npm run ${{ matrix.job }} run: npm run ${{ matrix.job }}
ci-website-mark: ci-website-mark:
needs: needs:
- lint-lockfile
- lint-prettier - lint-prettier
- test - test
- build - build

View File

@ -2,6 +2,7 @@
from typing import Any from typing import Any
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user from guardian.shortcuts import assign_perm, get_anonymous_user
@ -27,7 +28,6 @@ from authentik.core.models import (
TokenIntents, TokenIntents,
User, User,
default_token_duration, default_token_duration,
token_expires_from_timedelta,
) )
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict from authentik.events.utils import model_to_dict
@ -68,15 +68,17 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
max_token_lifetime_dt = default_token_duration() max_token_lifetime_dt = default_token_duration()
if max_token_lifetime is not None: if max_token_lifetime is not None:
try: try:
max_token_lifetime_dt = timedelta_from_string(max_token_lifetime) max_token_lifetime_dt = now() + timedelta_from_string(max_token_lifetime)
except ValueError: except ValueError:
max_token_lifetime_dt = default_token_duration() pass
if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta( if "expires" in attrs and attrs.get("expires") > max_token_lifetime_dt:
max_token_lifetime_dt
):
raise ValidationError( raise ValidationError(
{"expires": f"Token expires exceeds maximum lifetime ({max_token_lifetime})."} {
"expires": (
f"Token expires exceeds maximum lifetime ({max_token_lifetime_dt} UTC)."
)
}
) )
elif attrs.get("intent") == TokenIntents.INTENT_API: elif attrs.get("intent") == TokenIntents.INTENT_API:
# For API tokens, expires cannot be overridden # For API tokens, expires cannot be overridden

View File

@ -1,6 +1,6 @@
"""authentik core models""" """authentik core models"""
from datetime import datetime, timedelta from datetime import datetime
from hashlib import sha256 from hashlib import sha256
from typing import Any, Optional, Self from typing import Any, Optional, Self
from uuid import uuid4 from uuid import uuid4
@ -68,11 +68,6 @@ def default_token_duration() -> datetime:
return now() + timedelta_from_string(token_duration) return now() + timedelta_from_string(token_duration)
def token_expires_from_timedelta(dt: timedelta) -> datetime:
"""Return a `datetime.datetime` object with the duration of the Token"""
return now() + dt
def default_token_key() -> str: def default_token_key() -> str:
"""Default token key""" """Default token key"""
current_tenant = get_current_tenant() current_tenant = get_current_tenant()

View File

@ -0,0 +1,37 @@
"""Apple Type tests"""
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.root.middleware import SessionMiddleware
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.registry import registry
class TestTypeApple(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="apple",
authorization_url="",
profile_url="",
consumer_key=generate_id(),
)
self.factory = RequestFactory()
def test_login_challenge(self):
"""Test login_challenge"""
request = self.factory.get("/")
request.user = get_anonymous_user()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
oauth_type = registry.find_type("apple")
challenge = oauth_type().login_challenge(self.source, request)
self.assertTrue(challenge.is_valid(raise_exception=True))

View File

@ -125,7 +125,7 @@ class AppleType(SourceType):
) )
args = apple_client.get_redirect_args() args = apple_client.get_redirect_args()
return AppleLoginChallenge( return AppleLoginChallenge(
instance={ data={
"client_id": apple_client.get_client_id(), "client_id": apple_client.get_client_id(),
"scope": "name email", "scope": "name email",
"redirect_uri": args["redirect_uri"], "redirect_uri": args["redirect_uri"],

View File

@ -66,7 +66,7 @@ class PlexSource(Source):
icon = static("authentik/sources/plex.svg") icon = static("authentik/sources/plex.svg")
return UILoginButton( return UILoginButton(
challenge=PlexAuthenticationChallenge( challenge=PlexAuthenticationChallenge(
{ data={
"type": ChallengeTypes.NATIVE.value, "type": ChallengeTypes.NATIVE.value,
"component": "ak-source-plex", "component": "ak-source-plex",
"client_id": self.client_id, "client_id": self.client_id,

View File

@ -40,6 +40,11 @@ class TestPlexSource(TestCase):
slug="test", slug="test",
) )
def test_login_challenge(self):
"""Test login_challenge"""
ui_login_button = self.source.ui_login_button(None)
self.assertTrue(ui_login_button.challenge.is_valid(raise_exception=True))
def test_get_user_info(self): def test_get_user_info(self):
"""Test get_user_info""" """Test get_user_info"""
token = generate_key() token = generate_key()

View File

@ -23,7 +23,7 @@ LOGGER = get_logger()
VALID_SCHEMA_NAME = re.compile(r"^t_[a-z0-9]{1,61}$") VALID_SCHEMA_NAME = re.compile(r"^t_[a-z0-9]{1,61}$")
DEFAULT_TOKEN_DURATION = "minutes=30" # nosec DEFAULT_TOKEN_DURATION = "days=1" # nosec
DEFAULT_TOKEN_LENGTH = 60 DEFAULT_TOKEN_LENGTH = 60

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024023.2 goauthentik.io/api/v3 v3.2024040.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.19.0 golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0 golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024023.2 h1:lSVaZAKTpsDhtw11wnkGjPalkDzv9H2VKEJllBi2aXs= goauthentik.io/api/v3 v3.2024040.1 h1:0Mp8XLYuscQEWVTR2lNk74WLKDpOVHX0mlbvbvcC6fw=
goauthentik.io/api/v3 v3.2024023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2024040.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

File diff suppressed because it is too large Load Diff

52
poetry.lock generated
View File

@ -392,33 +392,33 @@ files = [
[[package]] [[package]]
name = "black" name = "black"
version = "24.4.0" version = "24.4.1"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, {file = "black-24.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f7749fd0d97ff9415975a1432fac7df89bf13c3833cea079e55fa004d5f28c0"},
{file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, {file = "black-24.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859f3cc5d2051adadf8fd504a01e02b0fd866d7549fff54bc9202d524d2e8bd7"},
{file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, {file = "black-24.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59271c9c29dfa97f7fda51f56c7809b3f78e72fd8d2205189bbd23022a0618b6"},
{file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, {file = "black-24.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:5ed9c34cba223149b5a0144951a0f33d65507cf82c5449cb3c35fe4b515fea9a"},
{file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, {file = "black-24.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dae3ae59d6f2dc93700fd5034a3115434686e66fd6e63d4dcaa48d19880f2b0"},
{file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, {file = "black-24.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5f8698974a81af83283eb47644f2711b5261138d6d9180c863fce673cbe04b13"},
{file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, {file = "black-24.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f404b6e77043b23d0321fb7772522b876b6de737ad3cb97d6b156638d68ce81"},
{file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, {file = "black-24.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:c94e52b766477bdcd010b872ba0714d5458536dc9d0734eff6583ba7266ffd89"},
{file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, {file = "black-24.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:962d9e953872cdb83b97bb737ad47244ce2938054dc946685a4cad98520dab38"},
{file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, {file = "black-24.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d8e3b2486b7dd522b1ab2ba1ec4907f0aa8f5e10a33c4271fb331d1d10b70c"},
{file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, {file = "black-24.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed77e214b785148f57e43ca425b6e0850165144aa727d66ac604e56a70bb7825"},
{file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, {file = "black-24.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4ef4764437d7eba8386689cd06e1fb5341ee0ae2e9e22582b21178782de7ed94"},
{file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"}, {file = "black-24.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:92b183f8eef5baf7b20a513abcf982ad616f544f593f6688bb2850d2982911f1"},
{file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"}, {file = "black-24.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:945abd7b3572add997757c94295bb3e73c6ffaf3366b1f26cb2356a4bffd1dc3"},
{file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"}, {file = "black-24.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db5154b9e5b478031371d8bc41ff37b33855fa223a6cfba456c9b73fb96f77d4"},
{file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"}, {file = "black-24.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:afc84c33c1a9aaf3d73140cee776b4ddf73ff429ffe6b7c56dc1c9c10725856d"},
{file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, {file = "black-24.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0889f4eb8b3bdf8b189e41a71cf0dbb8141a98346cd1a2695dea5995d416e940"},
{file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, {file = "black-24.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5bb0143f175db45a55227eefd63e90849d96c266330ba31719e9667d0d5ec3b9"},
{file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, {file = "black-24.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713a04a78e78f28ef7e8df7a16fe075670ea164860fcef3885e4f3dffc0184b3"},
{file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, {file = "black-24.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:171959bc879637a8cdbc53dc3fddae2a83e151937a28cf605fd175ce61e0e94a"},
{file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, {file = "black-24.4.1-py3-none-any.whl", hash = "sha256:ecbab810604fe02c70b3a08afd39beb599f7cc9afd13e81f5336014133b4fe35"},
{file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, {file = "black-24.4.1.tar.gz", hash = "sha256:5241612dc8cad5b6fd47432b8bd04db80e07cfbc53bb69e9ae18985063bcb8dd"},
] ]
[package.dependencies] [package.dependencies]
@ -3587,13 +3587,13 @@ django-query = ["django (>=3.2)"]
[[package]] [[package]]
name = "selenium" name = "selenium"
version = "4.19.0" version = "4.20.0"
description = "" description = ""
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "selenium-4.19.0-py3-none-any.whl", hash = "sha256:5b4f49240d61e687a73f7968ae2517d403882aae3550eae2a229c745e619f1d9"}, {file = "selenium-4.20.0-py3-none-any.whl", hash = "sha256:b1d0c33b38ca27d0499183e48e1dd09ff26973481f5d3ef2983073813ae6588d"},
{file = "selenium-4.19.0.tar.gz", hash = "sha256:d9dfd6d0b021d71d0a48b865fe7746490ba82b81e9c87b212360006629eb1853"}, {file = "selenium-4.20.0.tar.gz", hash = "sha256:0bd564ee166980d419a8aaf4ac00289bc152afcf2eadca5efe8c8e36711853fd"},
] ]
[package.dependencies] [package.dependencies]

2034
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@
"@open-wc/lit-helpers": "^0.7.0", "@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^3.0.1", "@patternfly/elements": "^3.0.1",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.112.1", "@sentry/browser": "^7.112.2",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"chart.js": "^4.4.2", "chart.js": "^4.4.2",

View File

@ -214,28 +214,23 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
name="sources" name="sources"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${this.sources?.results.map((source) => { ${this.sources?.results
let selected = Array.from(this.instance?.sources || []).some( .filter((source) => {
(su) => { return source.component !== "";
return su == source.pk; })
}, .map((source) => {
); const selected = Array.from(this.instance?.sources || []).some(
// Creating a new instance, auto-select built-in source (su) => {
// Only when no other sources exist return su == source.pk;
if ( },
!this.instance && );
source.component === "" && return html`<option
(this.sources?.results || []).length < 2 value=${ifDefined(source.pk)}
) { ?selected=${selected}
selected = true; >
} ${source.name}
return html`<option </option>`;
value=${ifDefined(source.pk)} })}
?selected=${selected}
>
${source.name}
</option>`;
})}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(

View File

@ -111,6 +111,21 @@ export function dateTimeLocal(date: Date): string {
return `${parts[0]}:${parts[1]}`; return `${parts[0]}:${parts[1]}`;
} }
export function dateToUTC(date: Date): Date {
// Sigh...so our API is UTC/can take TZ info in the ISO format as it should.
// datetime-local fields (which is almost the only date-time input we use)
// can return its value as a UTC timestamp...however the generated API client
// _requires_ a Date object, only to then convert it to an ISO string anyways
// JS Dates don't include timezone info in the ISO string, so that just sends
// the local time as UTC...which is wrong
// Instead we have to do this, convert the given date to a UTC timestamp,
// then subtract the timezone offset to create an "invalid" date (correct time&date)
// but it still "thinks" it's in local TZ
const timestamp = date.getTime();
const offset = -1 * (new Date().getTimezoneOffset() * 60000);
return new Date(timestamp - offset);
}
// Lit is extremely well-typed with regard to CSS, and Storybook's `build` does not currently have a // 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 // 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 // in `dev,` but in `build` it fails. Storied components will have to map their textual CSS imports

View File

@ -1,23 +1,22 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController } from "lit";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api"; import { CoreApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface"; import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class BrandContextController implements ReactiveController { export class BrandContextController implements ReactiveController {
host!: ReactiveElementHost; host!: ReactiveElementHost<AkInterface>;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>; context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
constructor(host: ReactiveElementHost) { constructor(host: ReactiveElementHost<AkInterface>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikBrandContext, context: authentikBrandContext,

View File

@ -2,23 +2,22 @@ import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController } from "lit";
import type { Config } from "@goauthentik/api"; import type { Config } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api"; import { RootApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface"; import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class ConfigContextController implements ReactiveController { export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost; host!: ReactiveElementHost<AkInterface>;
context!: ContextProvider<{ __context__: Config | undefined }>; context!: ContextProvider<{ __context__: Config | undefined }>;
constructor(host: ReactiveElementHost) { constructor(host: ReactiveElementHost<AkInterface>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikConfigContext, context: authentikConfigContext,

View File

@ -1,23 +1,22 @@
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants"; import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController } from "lit";
import type { LicenseSummary } from "@goauthentik/api"; import type { LicenseSummary } from "@goauthentik/api";
import { EnterpriseApi } from "@goauthentik/api"; import { EnterpriseApi } from "@goauthentik/api";
import type { AkEnterpriseInterface } from "./Interface"; import type { AkEnterpriseInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkEnterpriseInterface;
export class EnterpriseContextController implements ReactiveController { export class EnterpriseContextController implements ReactiveController {
host!: ReactiveElementHost; host!: ReactiveElementHost<AkEnterpriseInterface>;
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>; context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
constructor(host: ReactiveElementHost) { constructor(host: ReactiveElementHost<AkEnterpriseInterface>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikEnterpriseContext, context: authentikEnterpriseContext,

View File

@ -1,13 +1,11 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { Constructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { Config } from "@goauthentik/api"; import type { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T;
export function WithAuthentikConfig<T extends Constructor<LitElement>>( export function WithAuthentikConfig<T extends Constructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,

View File

@ -1,14 +1,12 @@
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithBrandConfig<T extends Constructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,
) { ) {

View File

@ -1,4 +1,5 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
@ -6,9 +7,6 @@ import type { LitElement } from "lit";
import { CapabilitiesEnum } from "@goauthentik/api"; import { CapabilitiesEnum } from "@goauthentik/api";
import { Config } from "@goauthentik/api"; import { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
// Using a unique, lexically scoped, and locally static symbol as the field name for the context // Using a unique, lexically scoped, and locally static symbol as the field name for the context
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy // means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
// guarantees in JavaScript. // guarantees in JavaScript.
@ -45,7 +43,7 @@ class WCC {
* *
*/ */
export function WithCapabilitiesConfig<T extends Constructor<LitElement>>( export function WithCapabilitiesConfig<T extends AbstractConstructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,
) { ) {

View File

@ -1,13 +1,11 @@
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import { Constructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { LicenseSummary } from "@goauthentik/api"; import type { LicenseSummary } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithLicenseSummary<T extends Constructor<LitElement>>( export function WithLicenseSummary<T extends Constructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,

View File

@ -87,7 +87,7 @@ export class Markdown extends AKElement {
const parsedContent = matter(this.md); const parsedContent = matter(this.md);
const parsedHTML = this.converter.makeHtml(parsedContent.content); const parsedHTML = this.converter.makeHtml(parsedContent.content);
const replacers = [...this.defaultReplacers, ...this.replacers]; const replacers = [...this.defaultReplacers, ...this.replacers];
this.docTitle = parsedContent.data["title"] ?? ""; this.docTitle = parsedContent?.data?.title ?? "";
this.docHtml = replacers.reduce( this.docHtml = replacers.reduce(
(html, replacer) => replacer(html, { path: this.meta }), (html, replacer) => replacer(html, { path: this.meta }),
parsedHTML, parsedHTML,

View File

@ -1,6 +1,6 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { camelToSnake, convertToSlug } from "@goauthentik/common/utils"; import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
@ -104,7 +104,7 @@ export function serializeForm<T extends KeyUnknown>(
inputElement.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "datetime-local" inputElement.type === "datetime-local"
) { ) {
assignValue(inputElement, new Date(inputElement.valueAsNumber), json); assignValue(inputElement, dateToUTC(new Date(inputElement.valueAsNumber)), json);
} else if ( } else if (
inputElement.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
"type" in inputElement.dataset && "type" in inputElement.dataset &&
@ -112,7 +112,7 @@ export function serializeForm<T extends KeyUnknown>(
) { ) {
// Workaround for Firefox <93, since 92 and older don't support // Workaround for Firefox <93, since 92 and older don't support
// datetime-local fields // datetime-local fields
assignValue(inputElement, new Date(inputElement.value), json); assignValue(inputElement, dateToUTC(new Date(inputElement.value)), json);
} else if ( } else if (
inputElement.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "checkbox" inputElement.type === "checkbox"

11
web/src/elements/types.ts Normal file
View File

@ -0,0 +1,11 @@
import { AKElement } from "@goauthentik/elements/Base";
import { ReactiveControllerHost } from "lit";
export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T = object> = new (...args: any[]) => T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;

View File

@ -1,3 +1,4 @@
import { dateTimeLocal } from "@goauthentik/authentik/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -44,11 +45,8 @@ export class UserTokenForm extends ModelForm<Token, string> {
renderForm(): TemplateResult { renderForm(): TemplateResult {
const now = new Date(); const now = new Date();
const expiringDate = this.instance?.expires const expiringDate = this.instance?.expires
? new Date( ? new Date(this.instance.expires.getTime())
this.instance.expires.getTime() - : new Date(now.getTime() + 30 * 60000);
this.instance.expires.getTimezoneOffset() * 60000,
)
: new Date(now.getTime() + 30 * 60000 - now.getTimezoneOffset() * 60000);
return html` <ak-form-element-horizontal return html` <ak-form-element-horizontal
label=${msg("Identifier")} label=${msg("Identifier")}
@ -73,8 +71,8 @@ export class UserTokenForm extends ModelForm<Token, string> {
? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires"> ? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires">
<input <input
type="datetime-local" type="datetime-local"
value="${expiringDate.toISOString().slice(0, -8)}" value="${dateTimeLocal(expiringDate)}"
min="${now.toISOString().slice(0, -8)}" min="${dateTimeLocal(now)}"
class="pf-c-form-control" class="pf-c-form-control"
/> />
</ak-form-element-horizontal>` </ak-form-element-horizontal>`