Compare commits
	
		
			21 Commits
		
	
	
		
			root/move-
			...
			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,21 +214,16 @@ 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) => { | ||||||
|  |                                     return source.component !== ""; | ||||||
|  |                                 }) | ||||||
|  |                                 .map((source) => { | ||||||
|  |                                     const selected = Array.from(this.instance?.sources || []).some( | ||||||
|                                         (su) => { |                                         (su) => { | ||||||
|                                             return su == source.pk; |                                             return su == source.pk; | ||||||
|                                         }, |                                         }, | ||||||
|                                     ); |                                     ); | ||||||
|                                 // Creating a new instance, auto-select built-in source |  | ||||||
|                                 // Only when no other sources exist |  | ||||||
|                                 if ( |  | ||||||
|                                     !this.instance && |  | ||||||
|                                     source.component === "" && |  | ||||||
|                                     (this.sources?.results || []).length < 2 |  | ||||||
|                                 ) { |  | ||||||
|                                     selected = true; |  | ||||||
|                                 } |  | ||||||
|                                     return html`<option |                                     return html`<option | ||||||
|                                         value=${ifDefined(source.pk)} |                                         value=${ifDefined(source.pk)} | ||||||
|                                         ?selected=${selected} |                                         ?selected=${selected} | ||||||
|  | |||||||
| @ -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,22 +107,24 @@ 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, | ||||||
|  |         // as in that case we still need to return to the generic title | ||||||
|         this.setTitle(this.header); |         this.setTitle(this.header); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderIcon(): TemplateResult { |     renderIcon(): TemplateResult { | ||||||
|         if (this.icon) { |         if (this.icon) { | ||||||
|  | |||||||
| @ -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
	