Compare commits
	
		
			21 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ca70c963e5 | |||
| 4c89d4a4a4 | |||
| 8a47acac3a | |||
| 4a3b22491c | |||
| f991d656c7 | |||
| e86aa11131 | |||
| 03725ae086 | |||
| f2a37e8c7c | |||
| e935690b1b | |||
| 02709e4ede | |||
| f78adab9d1 | |||
| 61f3a72fd9 | |||
| 541becfe30 | |||
| 11ff7955f7 | |||
| afa4234036 | |||
| ca22a4deaf | |||
| 7b7a3d34ec | |||
| b1ca579397 | |||
| c8072579c8 | |||
| 378a701fb9 | |||
| bba793d94c | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2024.2.3
 | 
					current_version = 2024.4.1
 | 
				
			||||||
tag = True
 | 
					tag = True
 | 
				
			||||||
commit = True
 | 
					commit = True
 | 
				
			||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
 | 
					parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
 | 
				
			||||||
@ -21,6 +21,8 @@ optional_value = final
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:schema.yml]
 | 
					[bumpversion:file:schema.yml]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bumpversion:file:blueprints/schema.json]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:authentik/__init__.py]
 | 
					[bumpversion:file:authentik/__init__.py]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[bumpversion:file:internal/constants/constants.go]
 | 
					[bumpversion:file:internal/constants/constants.go]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							@ -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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from os import environ
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = "2024.2.3"
 | 
					__version__ = "2024.4.1"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										37
									
								
								authentik/sources/oauth/tests/test_type_apple.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								authentik/sources/oauth/tests/test_type_apple.py
									
									
									
									
									
										Normal 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))
 | 
				
			||||||
@ -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"],
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
@ -2,9 +2,11 @@ from django.db.models import Model
 | 
				
			|||||||
from django.db.models.signals import pre_delete, pre_save
 | 
					from django.db.models.signals import pre_delete, pre_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import Token, TokenIntents, User, UserTypes
 | 
					from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
 | 
				
			||||||
from authentik.sources.scim.models import SCIMSource
 | 
					from authentik.sources.scim.models import SCIMSource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(pre_save, sender=SCIMSource)
 | 
					@receiver(pre_save, sender=SCIMSource)
 | 
				
			||||||
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
 | 
					def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
 | 
				
			||||||
@ -16,6 +18,7 @@ def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
 | 
				
			|||||||
        username=identifier,
 | 
					        username=identifier,
 | 
				
			||||||
        name=f"SCIM Source {instance.name} Service-Account",
 | 
					        name=f"SCIM Source {instance.name} Service-Account",
 | 
				
			||||||
        type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
 | 
					        type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
 | 
				
			||||||
 | 
					        path=USER_PATH_SOURCE_SCIM,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    token = Token.objects.create(
 | 
					    token = Token.objects.create(
 | 
				
			||||||
        user=user,
 | 
					        user=user,
 | 
				
			||||||
 | 
				
			|||||||
@ -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,7 +2,7 @@
 | 
				
			|||||||
    "$schema": "http://json-schema.org/draft-07/schema",
 | 
					    "$schema": "http://json-schema.org/draft-07/schema",
 | 
				
			||||||
    "$id": "https://goauthentik.io/blueprints/schema.json",
 | 
					    "$id": "https://goauthentik.io/blueprints/schema.json",
 | 
				
			||||||
    "type": "object",
 | 
					    "type": "object",
 | 
				
			||||||
    "title": "authentik 2024.2.3 Blueprint schema",
 | 
					    "title": "authentik 2024.4.1 Blueprint schema",
 | 
				
			||||||
    "required": [
 | 
					    "required": [
 | 
				
			||||||
        "version",
 | 
					        "version",
 | 
				
			||||||
        "entries"
 | 
					        "entries"
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ services:
 | 
				
			|||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - redis:/data
 | 
					      - redis:/data
 | 
				
			||||||
  server:
 | 
					  server:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: server
 | 
					    command: server
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
@ -53,7 +53,7 @@ services:
 | 
				
			|||||||
      - postgresql
 | 
					      - postgresql
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
  worker:
 | 
					  worker:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: worker
 | 
					    command: worker
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
 | 
				
			|||||||
@ -29,4 +29,4 @@ func UserAgent() string {
 | 
				
			|||||||
	return fmt.Sprintf("authentik@%s", FullVersion())
 | 
						return fmt.Sprintf("authentik@%s", FullVersion())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VERSION = "2024.2.3"
 | 
					const VERSION = "2024.4.1"
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,7 @@ function cleanup {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function prepare_debug {
 | 
					function prepare_debug {
 | 
				
			||||||
    poetry install --no-ansi --no-interaction
 | 
					    VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
 | 
				
			||||||
    touch /unittest.xml
 | 
					    touch /unittest.xml
 | 
				
			||||||
    chown authentik:authentik /unittest.xml
 | 
					    chown authentik:authentik /unittest.xml
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@
 | 
				
			|||||||
import authentik. This is done by the dockerfile."""
 | 
					import authentik. This is done by the dockerfile."""
 | 
				
			||||||
from sys import exit as sysexit
 | 
					from sys import exit as sysexit
 | 
				
			||||||
from time import sleep
 | 
					from time import sleep
 | 
				
			||||||
from urllib.parse import quote_plus
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from psycopg import OperationalError, connect
 | 
					from psycopg import OperationalError, connect
 | 
				
			||||||
from redis import Redis
 | 
					from redis import Redis
 | 
				
			||||||
@ -35,7 +34,7 @@ def check_postgres():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def check_redis():
 | 
					def check_redis():
 | 
				
			||||||
    url = redis_url(CONFIG.get("redis.db"))
 | 
					    url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db"))
 | 
				
			||||||
    while True:
 | 
					    while True:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            redis = Redis.from_url(url)
 | 
					            redis = Redis.from_url(url)
 | 
				
			||||||
@ -43,10 +42,7 @@ def check_redis():
 | 
				
			|||||||
            break
 | 
					            break
 | 
				
			||||||
        except RedisError as exc:
 | 
					        except RedisError as exc:
 | 
				
			||||||
            sleep(1)
 | 
					            sleep(1)
 | 
				
			||||||
            sanitized_url = url.replace(quote_plus(CONFIG.get("redis.password")), "******")
 | 
					            CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})")
 | 
				
			||||||
            CONFIG.log(
 | 
					 | 
				
			||||||
                "info", f"Redis Connection failed, retrying... ({exc})", redis_url=sanitized_url
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
    CONFIG.log("info", "Redis Connection successful")
 | 
					    CONFIG.log("info", "Redis Connection successful")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
[tool.poetry]
 | 
					[tool.poetry]
 | 
				
			||||||
name = "authentik"
 | 
					name = "authentik"
 | 
				
			||||||
version = "2024.2.3"
 | 
					version = "2024.4.1"
 | 
				
			||||||
description = ""
 | 
					description = ""
 | 
				
			||||||
authors = ["authentik Team <hello@goauthentik.io>"]
 | 
					authors = ["authentik Team <hello@goauthentik.io>"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
openapi: 3.0.3
 | 
					openapi: 3.0.3
 | 
				
			||||||
info:
 | 
					info:
 | 
				
			||||||
  title: authentik
 | 
					  title: authentik
 | 
				
			||||||
  version: 2024.2.3
 | 
					  version: 2024.4.1
 | 
				
			||||||
  description: Making authentication simple.
 | 
					  description: Making authentication simple.
 | 
				
			||||||
  contact:
 | 
					  contact:
 | 
				
			||||||
    email: hello@goauthentik.io
 | 
					    email: hello@goauthentik.io
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1922
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1922
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -128,6 +128,14 @@ export class UserForm extends ModelForm<User, number> {
 | 
				
			|||||||
                                "Service accounts should be used for machine-to-machine authentication or other automations.",
 | 
					                                "Service accounts should be used for machine-to-machine authentication or other automations.",
 | 
				
			||||||
                            )}`,
 | 
					                            )}`,
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Internal Service account",
 | 
				
			||||||
 | 
					                            value: UserTypeEnum.InternalServiceAccount,
 | 
				
			||||||
 | 
					                            disabled: true,
 | 
				
			||||||
 | 
					                            description: html`${msg(
 | 
				
			||||||
 | 
					                                "Internal Service accounts are created and managed by authentik and cannot be created manually.",
 | 
				
			||||||
 | 
					                            )}`,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
                    ]}
 | 
					                    ]}
 | 
				
			||||||
                    .value=${this.instance?.type}
 | 
					                    .value=${this.instance?.type}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
 | 
				
			|||||||
export const ERROR_CLASS = "pf-m-danger";
 | 
					export const ERROR_CLASS = "pf-m-danger";
 | 
				
			||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
 | 
					export const PROGRESS_CLASS = "pf-m-in-progress";
 | 
				
			||||||
export const CURRENT_CLASS = "pf-m-current";
 | 
					export const CURRENT_CLASS = "pf-m-current";
 | 
				
			||||||
export const VERSION = "2024.2.3";
 | 
					export const VERSION = "2024.4.1";
 | 
				
			||||||
export const TITLE_DEFAULT = "authentik";
 | 
					export const TITLE_DEFAULT = "authentik";
 | 
				
			||||||
export const ROUTE_SEPARATOR = ";";
 | 
					export const ROUTE_SEPARATOR = ";";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -187,6 +187,9 @@ input[type="date"]::-webkit-calendar-picker-indicator {
 | 
				
			|||||||
.pf-c-select__menu-item.pf-m-focus {
 | 
					.pf-c-select__menu-item.pf-m-focus {
 | 
				
			||||||
    --pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
 | 
					    --pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					.pf-c-button:disabled {
 | 
				
			||||||
 | 
					    color: var(--ak-dark-background-lighter);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
.pf-c-button.pf-m-plain:hover {
 | 
					.pf-c-button.pf-m-plain:hover {
 | 
				
			||||||
    color: var(--ak-dark-foreground);
 | 
					    color: var(--ak-dark-foreground);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ export function me(): Promise<SessionUser> {
 | 
				
			|||||||
                if (!user.user.settings || !("locale" in user.user.settings)) {
 | 
					                if (!user.user.settings || !("locale" in user.user.settings)) {
 | 
				
			||||||
                    return user;
 | 
					                    return user;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                const locale = user.user.settings.locale;
 | 
					                const locale: string | undefined = user.user.settings.locale;
 | 
				
			||||||
                if (locale && locale !== "") {
 | 
					                if (locale && locale !== "") {
 | 
				
			||||||
                    console.debug(
 | 
					                    console.debug(
 | 
				
			||||||
                        `authentik/locale: Activating user's configured locale '${locale}'`,
 | 
					                        `authentik/locale: Activating user's configured locale '${locale}'`,
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
 | 
				
			|||||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
 | 
					import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg } from "@lit/localize";
 | 
					import { msg } from "@lit/localize";
 | 
				
			||||||
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
 | 
					import { CSSResult, TemplateResult, css, html } from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators.js";
 | 
					import { customElement, property } from "lit/decorators.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
					import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
				
			||||||
@ -107,21 +107,23 @@ export class PageHeader extends WithBrandConfig(AKElement) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setTitle(value: string) {
 | 
					    setTitle(header?: string) {
 | 
				
			||||||
        const currentIf = currentInterface();
 | 
					        const currentIf = currentInterface();
 | 
				
			||||||
        const title = this.brand?.brandingTitle || TITLE_DEFAULT;
 | 
					        let title = this.brand?.brandingTitle || TITLE_DEFAULT;
 | 
				
			||||||
        document.title =
 | 
					        if (currentIf === "admin") {
 | 
				
			||||||
            currentIf === "admin"
 | 
					            title = `${msg("Admin")} - ${title}`;
 | 
				
			||||||
                ? `${msg("Admin")} - ${title}`
 | 
					        }
 | 
				
			||||||
                : value !== ""
 | 
					        // Prepend the header to the title
 | 
				
			||||||
                  ? `${value} - ${title}`
 | 
					        if (header !== undefined && header !== "") {
 | 
				
			||||||
                  : title;
 | 
					            title = `${header} - ${title}`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        document.title = title;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    willUpdate(changedProperties: PropertyValues<this>) {
 | 
					    willUpdate() {
 | 
				
			||||||
        if (changedProperties.has("header") && this.header) {
 | 
					        // Always update title, even if there's no header value set,
 | 
				
			||||||
            this.setTitle(this.header);
 | 
					        // as in that case we still need to return to the generic title
 | 
				
			||||||
        }
 | 
					        this.setTitle(this.header);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderIcon(): TemplateResult {
 | 
					    renderIcon(): TemplateResult {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/c
 | 
				
			|||||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
 | 
					import { customEvent } from "@goauthentik/elements/utils/customEvents";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { LitElement, html } from "lit";
 | 
					import { LitElement, html } from "lit";
 | 
				
			||||||
import { customElement, property, state } from "lit/decorators.js";
 | 
					import { customElement, property } from "lit/decorators.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { WithBrandConfig } from "../Interface/brandProvider";
 | 
					import { WithBrandConfig } from "../Interface/brandProvider";
 | 
				
			||||||
import { initializeLocalization } from "./configureLocale";
 | 
					import { initializeLocalization } from "./configureLocale";
 | 
				
			||||||
@ -38,9 +38,6 @@ export class LocaleContext extends LocaleContextBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    setLocale: LocaleSetter;
 | 
					    setLocale: LocaleSetter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @state()
 | 
					 | 
				
			||||||
    userLocale = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(code = DEFAULT_LOCALE) {
 | 
					    constructor(code = DEFAULT_LOCALE) {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
        this.notifyApplication = this.notifyApplication.bind(this);
 | 
					        this.notifyApplication = this.notifyApplication.bind(this);
 | 
				
			||||||
@ -59,30 +56,22 @@ export class LocaleContext extends LocaleContextBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    connectedCallback() {
 | 
					    connectedCallback() {
 | 
				
			||||||
        super.connectedCallback();
 | 
					        super.connectedCallback();
 | 
				
			||||||
        // Commenting out until we can come up with a better way of separating the
 | 
					 | 
				
			||||||
        // "request user identity" with the session expiration heartbeat.
 | 
					 | 
				
			||||||
        /*
 | 
					 | 
				
			||||||
            new CoreApi(DEFAULT_CONFIG)
 | 
					 | 
				
			||||||
                .coreUsersMeRetrieve()
 | 
					 | 
				
			||||||
                .then((user) => (this.userLocale = user?.user?.settings?.locale ?? ""))
 | 
					 | 
				
			||||||
                .catch(() => {});
 | 
					 | 
				
			||||||
        */
 | 
					 | 
				
			||||||
        this.updateLocale();
 | 
					        this.updateLocale();
 | 
				
			||||||
        window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
 | 
					        window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    disconnectedCallback() {
 | 
					    disconnectedCallback() {
 | 
				
			||||||
        window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
 | 
					        window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener);
 | 
				
			||||||
        super.disconnectedCallback();
 | 
					        super.disconnectedCallback();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    updateLocaleHandler(_ev: Event) {
 | 
					    updateLocaleHandler(ev: CustomEvent<{ locale: string }>) {
 | 
				
			||||||
        console.debug("authentik/locale: Locale update request received.");
 | 
					        console.debug("authentik/locale: Locale update request received.");
 | 
				
			||||||
        this.updateLocale();
 | 
					        this.updateLocale(ev.detail.locale);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    updateLocale() {
 | 
					    updateLocale(requestedLocale: string | undefined = undefined) {
 | 
				
			||||||
        const localeRequest = autoDetectLanguage(this.userLocale, this.brand?.defaultLocale);
 | 
					        const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale);
 | 
				
			||||||
        const locale = getBestMatchLocale(localeRequest);
 | 
					        const locale = getBestMatchLocale(localeRequest);
 | 
				
			||||||
        if (!locale) {
 | 
					        if (!locale) {
 | 
				
			||||||
            console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
 | 
					            console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
 | 
				
			||||||
 | 
				
			|||||||
@ -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"
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,7 @@ export interface RadioOption<T> {
 | 
				
			|||||||
    description?: TemplateResult;
 | 
					    description?: TemplateResult;
 | 
				
			||||||
    default?: boolean;
 | 
					    default?: boolean;
 | 
				
			||||||
    value: T;
 | 
					    value: T;
 | 
				
			||||||
 | 
					    disabled?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ak-radio")
 | 
					@customElement("ak-radio")
 | 
				
			||||||
@ -77,6 +78,9 @@ export class Radio<T> extends CustomEmitterElement(AKElement) {
 | 
				
			|||||||
            // This is a controlled input. Stop the native event from escaping or affecting the
 | 
					            // This is a controlled input. Stop the native event from escaping or affecting the
 | 
				
			||||||
            // value.  We'll do that ourselves.
 | 
					            // value.  We'll do that ourselves.
 | 
				
			||||||
            ev.stopPropagation();
 | 
					            ev.stopPropagation();
 | 
				
			||||||
 | 
					            if (option.disabled) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            this.value = option.value;
 | 
					            this.value = option.value;
 | 
				
			||||||
            this.dispatchCustomEvent("change", { value: option.value });
 | 
					            this.dispatchCustomEvent("change", { value: option.value });
 | 
				
			||||||
            this.dispatchCustomEvent("input", { value: option.value });
 | 
					            this.dispatchCustomEvent("input", { value: option.value });
 | 
				
			||||||
@ -93,6 +97,7 @@ export class Radio<T> extends CustomEmitterElement(AKElement) {
 | 
				
			|||||||
                name="${this.name}"
 | 
					                name="${this.name}"
 | 
				
			||||||
                id=${elId}
 | 
					                id=${elId}
 | 
				
			||||||
                .checked=${option.value === this.value}
 | 
					                .checked=${option.value === this.value}
 | 
				
			||||||
 | 
					                .disabled=${option.disabled}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <label class="pf-c-radio__label" for=${elId}>${option.label}</label>
 | 
					            <label class="pf-c-radio__label" for=${elId}>${option.label}</label>
 | 
				
			||||||
            ${option.description
 | 
					            ${option.description
 | 
				
			||||||
 | 
				
			|||||||
@ -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>`
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user