Compare commits
	
		
			21 Commits
		
	
	
		
			providers/
			...
			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] | ||||
| current_version = 2024.2.3 | ||||
| current_version = 2024.4.1 | ||||
| tag = 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*))? | ||||
| @ -21,6 +21,8 @@ optional_value = final | ||||
|  | ||||
| [bumpversion:file:schema.yml] | ||||
|  | ||||
| [bumpversion:file:blueprints/schema.json] | ||||
|  | ||||
| [bumpversion:file:authentik/__init__.py] | ||||
|  | ||||
| [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 | ||||
|         working-directory: ${{ matrix.project }}/ | ||||
|         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: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
| @ -95,6 +102,7 @@ jobs: | ||||
|         run: npm run lit-analyse | ||||
|   ci-web-mark: | ||||
|     needs: | ||||
|       - lint-lockfile | ||||
|       - lint-eslint | ||||
|       - lint-prettier | ||||
|       - lint-lit-analyse | ||||
|  | ||||
							
								
								
									
										8
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-website.yml
									
									
									
									
										vendored
									
									
								
							| @ -12,6 +12,13 @@ on: | ||||
|       - version-* | ||||
|  | ||||
| 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: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
| @ -62,6 +69,7 @@ jobs: | ||||
|         run: npm run ${{ matrix.job }} | ||||
|   ci-website-mark: | ||||
|     needs: | ||||
|       - lint-lockfile | ||||
|       - lint-prettier | ||||
|       - test | ||||
|       - build | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from os import environ | ||||
|  | ||||
| __version__ = "2024.2.3" | ||||
| __version__ = "2024.4.1" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| from django.utils.timezone import now | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer | ||||
| from guardian.shortcuts import assign_perm, get_anonymous_user | ||||
| @ -27,7 +28,6 @@ from authentik.core.models import ( | ||||
|     TokenIntents, | ||||
|     User, | ||||
|     default_token_duration, | ||||
|     token_expires_from_timedelta, | ||||
| ) | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.utils import model_to_dict | ||||
| @ -68,15 +68,17 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): | ||||
|             max_token_lifetime_dt = default_token_duration() | ||||
|             if max_token_lifetime is not None: | ||||
|                 try: | ||||
|                     max_token_lifetime_dt = timedelta_from_string(max_token_lifetime) | ||||
|                     max_token_lifetime_dt = now() + timedelta_from_string(max_token_lifetime) | ||||
|                 except ValueError: | ||||
|                     max_token_lifetime_dt = default_token_duration() | ||||
|                     pass | ||||
|  | ||||
|             if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta( | ||||
|                 max_token_lifetime_dt | ||||
|             ): | ||||
|             if "expires" in attrs and attrs.get("expires") > max_token_lifetime_dt: | ||||
|                 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: | ||||
|             # For API tokens, expires cannot be overridden | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| """authentik core models""" | ||||
|  | ||||
| from datetime import datetime, timedelta | ||||
| from datetime import datetime | ||||
| from hashlib import sha256 | ||||
| from typing import Any, Optional, Self | ||||
| from uuid import uuid4 | ||||
| @ -68,11 +68,6 @@ def default_token_duration() -> datetime: | ||||
|     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: | ||||
|     """Default token key""" | ||||
|     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() | ||||
|         return AppleLoginChallenge( | ||||
|             instance={ | ||||
|             data={ | ||||
|                 "client_id": apple_client.get_client_id(), | ||||
|                 "scope": "name email", | ||||
|                 "redirect_uri": args["redirect_uri"], | ||||
|  | ||||
| @ -66,7 +66,7 @@ class PlexSource(Source): | ||||
|             icon = static("authentik/sources/plex.svg") | ||||
|         return UILoginButton( | ||||
|             challenge=PlexAuthenticationChallenge( | ||||
|                 { | ||||
|                 data={ | ||||
|                     "type": ChallengeTypes.NATIVE.value, | ||||
|                     "component": "ak-source-plex", | ||||
|                     "client_id": self.client_id, | ||||
|  | ||||
| @ -40,6 +40,11 @@ class TestPlexSource(TestCase): | ||||
|             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): | ||||
|         """Test get_user_info""" | ||||
|         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.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 | ||||
|  | ||||
| USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim" | ||||
|  | ||||
|  | ||||
| @receiver(pre_save, sender=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, | ||||
|         name=f"SCIM Source {instance.name} Service-Account", | ||||
|         type=UserTypes.INTERNAL_SERVICE_ACCOUNT, | ||||
|         path=USER_PATH_SOURCE_SCIM, | ||||
|     ) | ||||
|     token = Token.objects.create( | ||||
|         user=user, | ||||
|  | ||||
| @ -23,7 +23,7 @@ LOGGER = get_logger() | ||||
|  | ||||
| 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 | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|     "$schema": "http://json-schema.org/draft-07/schema", | ||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||
|     "type": "object", | ||||
|     "title": "authentik 2024.2.3 Blueprint schema", | ||||
|     "title": "authentik 2024.4.1 Blueprint schema", | ||||
|     "required": [ | ||||
|         "version", | ||||
|         "entries" | ||||
|  | ||||
| @ -32,7 +32,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   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 | ||||
|     command: server | ||||
|     environment: | ||||
| @ -53,7 +53,7 @@ services: | ||||
|       - postgresql | ||||
|       - redis | ||||
|   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 | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
| @ -29,4 +29,4 @@ func UserAgent() string { | ||||
| 	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 { | ||||
|     poetry install --no-ansi --no-interaction | ||||
|     VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction | ||||
|     touch /unittest.xml | ||||
|     chown authentik:authentik /unittest.xml | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,6 @@ | ||||
| import authentik. This is done by the dockerfile.""" | ||||
| from sys import exit as sysexit | ||||
| from time import sleep | ||||
| from urllib.parse import quote_plus | ||||
|  | ||||
| from psycopg import OperationalError, connect | ||||
| from redis import Redis | ||||
| @ -35,7 +34,7 @@ def check_postgres(): | ||||
|  | ||||
|  | ||||
| def check_redis(): | ||||
|     url = redis_url(CONFIG.get("redis.db")) | ||||
|     url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")) | ||||
|     while True: | ||||
|         try: | ||||
|             redis = Redis.from_url(url) | ||||
| @ -43,10 +42,7 @@ def check_redis(): | ||||
|             break | ||||
|         except RedisError as exc: | ||||
|             sleep(1) | ||||
|             sanitized_url = url.replace(quote_plus(CONFIG.get("redis.password")), "******") | ||||
|             CONFIG.log( | ||||
|                 "info", f"Redis Connection failed, retrying... ({exc})", redis_url=sanitized_url | ||||
|             ) | ||||
|             CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})") | ||||
|     CONFIG.log("info", "Redis Connection successful") | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [tool.poetry] | ||||
| name = "authentik" | ||||
| version = "2024.2.3" | ||||
| version = "2024.4.1" | ||||
| description = "" | ||||
| authors = ["authentik Team <hello@goauthentik.io>"] | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2024.2.3 | ||||
|   version: 2024.4.1 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     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" | ||||
|                     > | ||||
|                         <select class="pf-c-form-control" multiple> | ||||
|                             ${this.sources?.results.map((source) => { | ||||
|                                 let selected = Array.from(this.instance?.sources || []).some( | ||||
|                             ${this.sources?.results | ||||
|                                 .filter((source) => { | ||||
|                                     return source.component !== ""; | ||||
|                                 }) | ||||
|                                 .map((source) => { | ||||
|                                     const selected = Array.from(this.instance?.sources || []).some( | ||||
|                                         (su) => { | ||||
|                                             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 | ||||
|                                         value=${ifDefined(source.pk)} | ||||
|                                         ?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.", | ||||
|                             )}`, | ||||
|                         }, | ||||
|                         { | ||||
|                             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} | ||||
|                 > | ||||
|  | ||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | ||||
| export const ERROR_CLASS = "pf-m-danger"; | ||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||
| 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 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--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 { | ||||
|     color: var(--ak-dark-foreground); | ||||
| } | ||||
|  | ||||
| @ -18,7 +18,7 @@ export function me(): Promise<SessionUser> { | ||||
|                 if (!user.user.settings || !("locale" in user.user.settings)) { | ||||
|                     return user; | ||||
|                 } | ||||
|                 const locale = user.user.settings.locale; | ||||
|                 const locale: string | undefined = user.user.settings.locale; | ||||
|                 if (locale && locale !== "") { | ||||
|                     console.debug( | ||||
|                         `authentik/locale: Activating user's configured locale '${locale}'`, | ||||
|  | ||||
| @ -111,6 +111,21 @@ export function dateTimeLocal(date: Date): string { | ||||
|     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 | ||||
| // 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 | ||||
|  | ||||
| @ -87,7 +87,7 @@ export class Markdown extends AKElement { | ||||
|             const parsedContent = matter(this.md); | ||||
|             const parsedHTML = this.converter.makeHtml(parsedContent.content); | ||||
|             const replacers = [...this.defaultReplacers, ...this.replacers]; | ||||
|             this.docTitle = parsedContent.data["title"] ?? ""; | ||||
|             this.docTitle = parsedContent?.data?.title ?? ""; | ||||
|             this.docHtml = replacers.reduce( | ||||
|                 (html, replacer) => replacer(html, { path: this.meta }), | ||||
|                 parsedHTML, | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
|  | ||||
| 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 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 title = this.brand?.brandingTitle || TITLE_DEFAULT; | ||||
|         document.title = | ||||
|             currentIf === "admin" | ||||
|                 ? `${msg("Admin")} - ${title}` | ||||
|                 : value !== "" | ||||
|                   ? `${value} - ${title}` | ||||
|                   : title; | ||||
|         let title = this.brand?.brandingTitle || TITLE_DEFAULT; | ||||
|         if (currentIf === "admin") { | ||||
|             title = `${msg("Admin")} - ${title}`; | ||||
|         } | ||||
|         // Prepend the header to the title | ||||
|         if (header !== undefined && header !== "") { | ||||
|             title = `${header} - ${title}`; | ||||
|         } | ||||
|         document.title = title; | ||||
|     } | ||||
|  | ||||
|     willUpdate(changedProperties: PropertyValues<this>) { | ||||
|         if (changedProperties.has("header") && this.header) { | ||||
|     willUpdate() { | ||||
|         // 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); | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     renderIcon(): TemplateResult { | ||||
|         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 { 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 { initializeLocalization } from "./configureLocale"; | ||||
| @ -38,9 +38,6 @@ export class LocaleContext extends LocaleContextBase { | ||||
|  | ||||
|     setLocale: LocaleSetter; | ||||
|  | ||||
|     @state() | ||||
|     userLocale = ""; | ||||
|  | ||||
|     constructor(code = DEFAULT_LOCALE) { | ||||
|         super(); | ||||
|         this.notifyApplication = this.notifyApplication.bind(this); | ||||
| @ -59,30 +56,22 @@ export class LocaleContext extends LocaleContextBase { | ||||
|  | ||||
|     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(); | ||||
|         window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler); | ||||
|         window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); | ||||
|     } | ||||
|  | ||||
|     disconnectedCallback() { | ||||
|         window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler); | ||||
|         window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); | ||||
|         super.disconnectedCallback(); | ||||
|     } | ||||
|  | ||||
|     updateLocaleHandler(_ev: Event) { | ||||
|     updateLocaleHandler(ev: CustomEvent<{ locale: string }>) { | ||||
|         console.debug("authentik/locale: Locale update request received."); | ||||
|         this.updateLocale(); | ||||
|         this.updateLocale(ev.detail.locale); | ||||
|     } | ||||
|  | ||||
|     updateLocale() { | ||||
|         const localeRequest = autoDetectLanguage(this.userLocale, this.brand?.defaultLocale); | ||||
|     updateLocale(requestedLocale: string | undefined = undefined) { | ||||
|         const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale); | ||||
|         const locale = getBestMatchLocale(localeRequest); | ||||
|         if (!locale) { | ||||
|             console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| 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 { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; | ||||
| @ -104,7 +104,7 @@ export function serializeForm<T extends KeyUnknown>( | ||||
|             inputElement.tagName.toLowerCase() === "input" && | ||||
|             inputElement.type === "datetime-local" | ||||
|         ) { | ||||
|             assignValue(inputElement, new Date(inputElement.valueAsNumber), json); | ||||
|             assignValue(inputElement, dateToUTC(new Date(inputElement.valueAsNumber)), json); | ||||
|         } else if ( | ||||
|             inputElement.tagName.toLowerCase() === "input" && | ||||
|             "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 | ||||
|             // datetime-local fields | ||||
|             assignValue(inputElement, new Date(inputElement.value), json); | ||||
|             assignValue(inputElement, dateToUTC(new Date(inputElement.value)), json); | ||||
|         } else if ( | ||||
|             inputElement.tagName.toLowerCase() === "input" && | ||||
|             inputElement.type === "checkbox" | ||||
|  | ||||
| @ -16,6 +16,7 @@ export interface RadioOption<T> { | ||||
|     description?: TemplateResult; | ||||
|     default?: boolean; | ||||
|     value: T; | ||||
|     disabled?: boolean; | ||||
| } | ||||
|  | ||||
| @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 | ||||
|             // value.  We'll do that ourselves. | ||||
|             ev.stopPropagation(); | ||||
|             if (option.disabled) { | ||||
|                 return; | ||||
|             } | ||||
|             this.value = option.value; | ||||
|             this.dispatchCustomEvent("change", { value: option.value }); | ||||
|             this.dispatchCustomEvent("input", { value: option.value }); | ||||
| @ -93,6 +97,7 @@ export class Radio<T> extends CustomEmitterElement(AKElement) { | ||||
|                 name="${this.name}" | ||||
|                 id=${elId} | ||||
|                 .checked=${option.value === this.value} | ||||
|                 .disabled=${option.disabled} | ||||
|             /> | ||||
|             <label class="pf-c-radio__label" for=${elId}>${option.label}</label> | ||||
|             ${option.description | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { dateTimeLocal } from "@goauthentik/authentik/common/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; | ||||
| @ -44,11 +45,8 @@ export class UserTokenForm extends ModelForm<Token, string> { | ||||
|     renderForm(): TemplateResult { | ||||
|         const now = new Date(); | ||||
|         const expiringDate = this.instance?.expires | ||||
|             ? new Date( | ||||
|                   this.instance.expires.getTime() - | ||||
|                       this.instance.expires.getTimezoneOffset() * 60000, | ||||
|               ) | ||||
|             : new Date(now.getTime() + 30 * 60000 - now.getTimezoneOffset() * 60000); | ||||
|             ? new Date(this.instance.expires.getTime()) | ||||
|             : new Date(now.getTime() + 30 * 60000); | ||||
|  | ||||
|         return html` <ak-form-element-horizontal | ||||
|                 label=${msg("Identifier")} | ||||
| @ -73,8 +71,8 @@ export class UserTokenForm extends ModelForm<Token, string> { | ||||
|                 ? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires"> | ||||
|                       <input | ||||
|                           type="datetime-local" | ||||
|                           value="${expiringDate.toISOString().slice(0, -8)}" | ||||
|                           min="${now.toISOString().slice(0, -8)}" | ||||
|                           value="${dateTimeLocal(expiringDate)}" | ||||
|                           min="${dateTimeLocal(now)}" | ||||
|                           class="pf-c-form-control" | ||||
|                       /> | ||||
|                   </ak-form-element-horizontal>` | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	