Compare commits
21 Commits
web/cleanu
...
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