mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 07:32:23 +02:00
Compare commits
3 Commits
developer-
...
root/move-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3b6d562a0 | ||
|
|
42e4011c1a | ||
|
|
e791742587 |
@@ -33,12 +33,17 @@ packages/prettier-config @goauthentik/frontend
|
||||
packages/tsconfig @goauthentik/frontend
|
||||
# Web
|
||||
web/ @goauthentik/frontend
|
||||
tests/wdio/ @goauthentik/frontend
|
||||
# Locale
|
||||
locale/ @goauthentik/backend @goauthentik/frontend
|
||||
web/xliff/ @goauthentik/backend @goauthentik/frontend
|
||||
# Docs
|
||||
# Docs & Website
|
||||
docs/ @goauthentik/docs
|
||||
# TODO Remove after moving website to docs
|
||||
website/ @goauthentik/docs
|
||||
CODE_OF_CONDUCT.md @goauthentik/docs
|
||||
# Security
|
||||
SECURITY.md @goauthentik/security @goauthentik/docs
|
||||
# TODO Remove after moving website to docs
|
||||
website/security/ @goauthentik/security @goauthentik/docs
|
||||
docs/security/ @goauthentik/security @goauthentik/docs
|
||||
|
||||
38
Dockerfile
38
Dockerfile
@@ -76,12 +76,12 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.13 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.11 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.7-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
PATH="/ak-root/lifecycle:/ak-root/venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
@@ -145,8 +145,6 @@ LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
@@ -157,28 +155,26 @@ RUN apt-get update && \
|
||||
pip3 install --no-cache-dir --upgrade pip && \
|
||||
apt-get clean && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /ak-root authentik && \
|
||||
mkdir -p /certs /media /blueprints && \
|
||||
mkdir -p /authentik/.ssh && \
|
||||
mkdir -p /ak-root && \
|
||||
chown authentik:authentik /certs /media /authentik/.ssh /ak-root
|
||||
mkdir -p /ak-root/authentik/.ssh && \
|
||||
chown authentik:authentik /certs /media /ak-root/authentik/.ssh /ak-root
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./uv.lock /
|
||||
COPY ./schemas /schemas
|
||||
COPY ./locale /locale
|
||||
COPY ./tests /tests
|
||||
COPY ./manage.py /
|
||||
COPY ./authentik/ /ak-root/authentik
|
||||
COPY ./pyproject.toml /ak-root/
|
||||
COPY ./uv.lock /ak-root/
|
||||
COPY ./schemas /ak-root/schemas
|
||||
COPY ./locale /ak-root/locale
|
||||
COPY ./tests /ak-root/tests
|
||||
COPY ./manage.py /ak-root/
|
||||
COPY ./blueprints /blueprints
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY ./lifecycle/ /ak-root/lifecycle
|
||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||
COPY --from=go-builder /go/authentik /bin/authentik
|
||||
COPY ./packages/ /ak-root/packages
|
||||
RUN ln -s /ak-root/packages /packages
|
||||
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
||||
COPY --from=node-builder /work/web/dist/ /web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /web/authentik/
|
||||
COPY --from=node-builder /work/web/dist/ /ak-root/web/dist/
|
||||
COPY --from=node-builder /work/web/authentik/ /ak-root/web/authentik/
|
||||
COPY --from=geoip /usr/share/GeoIP /geoip
|
||||
|
||||
USER 1000
|
||||
@@ -190,4 +186,6 @@ ENV TMPDIR=/dev/shm/ \
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||
|
||||
WORKDIR /ak-root
|
||||
|
||||
ENTRYPOINT [ "dumb-init", "--", "ak" ]
|
||||
|
||||
25
SECURITY.md
25
SECURITY.md
@@ -20,33 +20,12 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2025.4.x | ✅ |
|
||||
| 2025.6.x | ✅ |
|
||||
| 2025.8.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a potential vulnerability, please report it responsibly through one of the following channels:
|
||||
|
||||
- **Email**: [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
- **GitHub**: Submit a private security advisory via our [repository’s advisory portal](https://github.com/goauthentik/authentik/security/advisories/new)
|
||||
|
||||
When submitting a report, please include as much detail as possible, such as:
|
||||
|
||||
- **Affected version(s)**: The version of authentik where the issue was identified.
|
||||
- **Steps to reproduce**: A clear description or proof of concept to help us verify the issue.
|
||||
- **Impact assessment**: How the vulnerability could be exploited and its potential effect.
|
||||
- **Additional information**: Logs, configuration details (if relevant), or any suggested mitigations.
|
||||
|
||||
We kindly ask that you do not disclose the vulnerability publicly until we have confirmed and addressed the issue.
|
||||
|
||||
Our team will:
|
||||
|
||||
- Acknowledge receipt of your report as quickly as possible.
|
||||
- Keep you updated on the investigation and resolution progress.
|
||||
|
||||
## Researcher Recognition
|
||||
|
||||
We value contributions from the security community. For each valid report, we will publish a dedicated entry on our Security Advisory page that optionally includes the reporter’s name (or preferred alias). Please note that while we do not currently offer monetary bounties, we are committed to giving researchers appropriate credit for their efforts in keeping authentik secure.
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the issue.
|
||||
|
||||
## Severity levels
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ worker:
|
||||
consumer_listen_timeout: "seconds=30"
|
||||
task_max_retries: 20
|
||||
task_default_time_limit: "minutes=10"
|
||||
lock_purge_interval: "minutes=1"
|
||||
task_purge_interval: "days=1"
|
||||
task_expiration: "days=30"
|
||||
scheduler_interval: "seconds=60"
|
||||
|
||||
@@ -76,7 +76,6 @@ class OutpostConfig:
|
||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
|
||||
kubernetes_ingress_class_name: str | None = field(default=None)
|
||||
kubernetes_ingress_path_type: str | None = field(default=None)
|
||||
kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list)
|
||||
kubernetes_service_type: str = field(default="ClusterIP")
|
||||
@@ -152,7 +151,7 @@ class OutpostServiceConnection(ScheduledModel, models.Model):
|
||||
|
||||
state = cache.get(self.state_key, None)
|
||||
if not state:
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self)
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk), rel_obj=self)
|
||||
return OutpostServiceConnectionState("", False)
|
||||
return state
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ def migrate_sessions(apps, schema_editor, model):
|
||||
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
objs = list(Model.objects.using(db_alias).select_related("old_session").all())
|
||||
for obj in objs:
|
||||
for obj in Model.objects.using(db_alias).all():
|
||||
if not obj.old_session:
|
||||
continue
|
||||
obj.session = (
|
||||
|
||||
@@ -23,12 +23,7 @@ def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
|
||||
|
||||
backchannel_logout_notification_dispatch.send(
|
||||
revocations=[
|
||||
(
|
||||
token.provider_id,
|
||||
token.id_token.iss,
|
||||
token.id_token.sub,
|
||||
instance.session.session_key,
|
||||
)
|
||||
(token.provider_id, token.id_token.iss, token.session.user.uid)
|
||||
for token in access_tokens
|
||||
],
|
||||
)
|
||||
|
||||
@@ -14,19 +14,13 @@ LOGGER = get_logger()
|
||||
|
||||
|
||||
@actor(description=_("Send a back-channel logout request to the registered client"))
|
||||
def send_backchannel_logout_request(
|
||||
provider_pk: int,
|
||||
iss: str,
|
||||
sub: str | None = None,
|
||||
session_key: str | None = None,
|
||||
) -> bool:
|
||||
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
|
||||
"""Send a back-channel logout request to the registered client
|
||||
|
||||
Args:
|
||||
provider_pk: The OAuth2 provider's primary key
|
||||
iss: The issuer URL for the logout token
|
||||
sub: The subject identifier to include in the logout token
|
||||
session_key: The authentik session key to hash and include in the logout token
|
||||
|
||||
Returns:
|
||||
bool: True if the request was sent successfully, False otherwise
|
||||
@@ -39,10 +33,11 @@ def send_backchannel_logout_request(
|
||||
return
|
||||
|
||||
# Generate the logout token
|
||||
logout_token = create_logout_token(provider, iss, sub, session_key)
|
||||
logout_token = create_logout_token(iss, provider, None, sub)
|
||||
|
||||
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
|
||||
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
|
||||
|
||||
backchannel_logout_uri = provider.backchannel_logout_uri
|
||||
if not backchannel_logout_uri:
|
||||
self.info("No back-channel logout URI found for provider")
|
||||
@@ -65,9 +60,9 @@ def send_backchannel_logout_request(
|
||||
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
|
||||
"""Handle backchannel logout notifications dispatched via signal"""
|
||||
for revocation in revocations:
|
||||
provider_pk, iss, sub, session_key = revocation
|
||||
provider_pk, iss, sub = revocation
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
send_backchannel_logout_request.send_with_options(
|
||||
args=(provider_pk, iss, sub, session_key),
|
||||
args=(provider_pk, iss, sub),
|
||||
rel_obj=provider,
|
||||
)
|
||||
|
||||
@@ -217,17 +217,17 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
|
||||
|
||||
|
||||
def create_logout_token(
|
||||
provider: OAuth2Provider,
|
||||
iss: str,
|
||||
sub: str | None = None,
|
||||
provider: OAuth2Provider,
|
||||
session_key: str | None = None,
|
||||
sub: str | None = None,
|
||||
) -> str:
|
||||
"""Create a logout token for Back-Channel Logout
|
||||
|
||||
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
|
||||
"""
|
||||
|
||||
LOGGER.debug("Creating logout token", provider=provider, sub=sub)
|
||||
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
|
||||
|
||||
# Create the logout token payload
|
||||
payload = {
|
||||
|
||||
@@ -127,9 +127,6 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
and self.controller.outpost.config.kubernetes_ingress_secret_name
|
||||
):
|
||||
tls_hosts.append(external_host_name.hostname)
|
||||
path_type = "Prefix"
|
||||
if self.controller.outpost.config.kubernetes_ingress_path_type:
|
||||
path_type = self.controller.outpost.config.kubernetes_ingress_path_type
|
||||
if proxy_provider.mode in [
|
||||
ProxyMode.FORWARD_SINGLE,
|
||||
ProxyMode.FORWARD_DOMAIN,
|
||||
@@ -146,7 +143,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
),
|
||||
),
|
||||
path="/outpost.goauthentik.io",
|
||||
path_type=path_type,
|
||||
path_type="Prefix",
|
||||
)
|
||||
]
|
||||
),
|
||||
@@ -164,7 +161,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
),
|
||||
),
|
||||
path="/",
|
||||
path_type=path_type,
|
||||
path_type="Prefix",
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import importlib
|
||||
from collections import OrderedDict
|
||||
from hashlib import sha512
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
import orjson
|
||||
from sentry_sdk import set_tag
|
||||
@@ -368,9 +369,6 @@ DRAMATIQ = {
|
||||
"broker_class": "authentik.tasks.broker.Broker",
|
||||
"channel_prefix": "authentik",
|
||||
"task_model": "authentik.tasks.models.Task",
|
||||
"lock_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.lock_purge_interval")
|
||||
).total_seconds(),
|
||||
"task_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.task_purge_interval")
|
||||
).total_seconds(),
|
||||
@@ -427,6 +425,7 @@ DRAMATIQ = {
|
||||
(
|
||||
"authentik.tasks.middleware.MetricsMiddleware",
|
||||
{
|
||||
"multiproc_dir": str(Path(gettempdir()) / "authentik_prometheus_tmp"),
|
||||
"prefix": "authentik",
|
||||
},
|
||||
),
|
||||
|
||||
@@ -198,10 +198,7 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
return {"error": "", "count": created}
|
||||
except RuntimeError as exc:
|
||||
LOGGER.warning("failed to get users from duo", exc=exc)
|
||||
return {
|
||||
"error": "An internal error occurred while importing devices.",
|
||||
"count": created,
|
||||
}
|
||||
return {"error": str(exc), "count": created}
|
||||
|
||||
|
||||
class DuoDeviceSerializer(ModelSerializer):
|
||||
|
||||
@@ -168,8 +168,6 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
client_secret=generate_id(),
|
||||
api_hostname=generate_id(),
|
||||
)
|
||||
|
||||
# Test missing admin credentials
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-import-devices-automatic",
|
||||
@@ -180,31 +178,6 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Test internal error handling
|
||||
stage.admin_integration_key = generate_id()
|
||||
stage.admin_secret_key = generate_id()
|
||||
stage.save()
|
||||
with patch(
|
||||
"duo_client.admin.Admin.get_users_iterator",
|
||||
MagicMock(side_effect=RuntimeError("Duo API error")),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-import-devices-automatic",
|
||||
kwargs={
|
||||
"pk": str(stage.pk),
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{
|
||||
"error": "An internal error occurred while importing devices.",
|
||||
"count": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def test_api_import_automatic(self):
|
||||
"""test `import_devices_automatic`"""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@@ -35,12 +35,7 @@ class Command(TenantCommand):
|
||||
template_context={},
|
||||
)
|
||||
try:
|
||||
if not stage.use_global_settings:
|
||||
message.from_email = stage.from_address
|
||||
|
||||
send_mail.send(message.__dict__, stage.pk).get_result(block=True)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Test email sent to {options['to']}"))
|
||||
send_mail(message.__dict__, stage.pk)
|
||||
finally:
|
||||
if delete_stage:
|
||||
stage.delete()
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
"""Test email management commands"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
|
||||
class TestEmailManagementCommands(TestCase):
|
||||
"""Test email management commands"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
def test_test_email_command_with_stage(self):
|
||||
"""Test test_email command with specified stage"""
|
||||
EmailStage.objects.create(
|
||||
name="test-stage",
|
||||
from_address="test@authentik.local",
|
||||
host="localhost",
|
||||
port=25,
|
||||
)
|
||||
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com", stage="test-stage")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
|
||||
def test_test_email_command_with_global_settings(self):
|
||||
"""Test test_email command with global settings"""
|
||||
# Mock the backend to use Django's locmem backend
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
|
||||
def test_test_email_command_invalid_stage(self):
|
||||
"""Test test_email command with invalid stage"""
|
||||
call_command("test_email", "test@example.com", stage="nonexistent")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_test_email_command_with_custom_from(self):
|
||||
"""Test test_email command respects custom from address"""
|
||||
EmailStage.objects.create(
|
||||
name="test-stage",
|
||||
from_address="custom@authentik.local",
|
||||
host="localhost",
|
||||
port=25,
|
||||
)
|
||||
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com", stage="test-stage")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].from_email, "custom@authentik.local")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
@@ -100,15 +100,10 @@ class MessagesMiddleware(Middleware):
|
||||
TaskStatus.ERROR,
|
||||
exception,
|
||||
)
|
||||
event_kwargs = {
|
||||
"actor": task.actor_name,
|
||||
}
|
||||
if task.rel_obj:
|
||||
event_kwargs["rel_obj"] = task.rel_obj
|
||||
Event.new(
|
||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||
message=f"Task {task.actor_name} encountered an error",
|
||||
**event_kwargs,
|
||||
actor=task.actor_name,
|
||||
).with_exception(exception).save()
|
||||
|
||||
def after_skip_message(self, broker: Broker, message: Message):
|
||||
@@ -156,6 +151,7 @@ class DescriptionMiddleware(Middleware):
|
||||
|
||||
|
||||
class _healthcheck_handler(BaseHTTPRequestHandler):
|
||||
|
||||
def log_request(self, code="-", size="-"):
|
||||
HEALTHCHECK_LOGGER.info(
|
||||
self.path,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "ak createsuperuser should not be used. Instead, use ak create_admin_group"
|
||||
|
||||
def handle(self, *args, **options): # noqa: ANN001, D401
|
||||
raise RuntimeError(
|
||||
"ak createsuperuser should not be used. Instead, use ak create_admin_group"
|
||||
)
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
blueprints.goauthentik.io/system-bootstrap: "true"
|
||||
blueprints.goauthentik.io/system: "true"
|
||||
blueprints.goauthentik.io/description: |
|
||||
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://docs.goauthentik.io/docs/install-config/automated-install?utm_source=bootstrap_blueprint).
|
||||
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://goauthentik.io/docs/installation/automated-install).
|
||||
context:
|
||||
username: akadmin
|
||||
group_name: authentik Admins
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,7 +27,7 @@ require (
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025100.2
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
|
||||
4
go.sum
4
go.sum
@@ -169,8 +169,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4=
|
||||
github.com/wwt/guac v1.3.2/go.mod h1:eKm+NrnK7A88l4UBEcYNpZQGMpZRryYKoz4D/0/n1C0=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
// https://github.com/gorilla/handlers/issues/259#issuecomment-2671695039
|
||||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// compressHandler is an HTTP handler that adds the Content-Encoding header
|
||||
// back to responses when removed by the http.FileServer.
|
||||
//
|
||||
// handlers.CompressHandler(newCompressHandler(http.FileServer(...)))
|
||||
type compressHandler struct {
|
||||
// handler is an HTTP handler, usually an http.FileServer.
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
var _ http.Handler = &compressHandler{}
|
||||
|
||||
func NewCompressHandler(handler http.Handler) http.Handler {
|
||||
h := &compressHandler{
|
||||
handler: handler,
|
||||
}
|
||||
return handlers.CompressHandler(h)
|
||||
}
|
||||
|
||||
func (h *compressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// The wrapped response writer saves the incoming content encoding so
|
||||
// it can be restored when writing the response headers.
|
||||
cw := &compressedResponseWriter{
|
||||
encoding: w.Header().Get("Content-Encoding"),
|
||||
fixed: false,
|
||||
responseWriter: w,
|
||||
}
|
||||
h.handler.ServeHTTP(cw, r)
|
||||
}
|
||||
|
||||
// compressedResponseWriter is an http.ResponseWriter that ensures that a
|
||||
// previously-set Content-Encoding header is in place before writing the
|
||||
// response.
|
||||
type compressedResponseWriter struct {
|
||||
encoding string
|
||||
fixed bool
|
||||
responseWriter http.ResponseWriter
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = &compressedResponseWriter{}
|
||||
|
||||
func (w *compressedResponseWriter) Header() http.Header {
|
||||
return w.responseWriter.Header()
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) fixContentEncoding() {
|
||||
if w.fixed {
|
||||
return
|
||||
}
|
||||
w.fixed = true
|
||||
// The Go 1.23 http.FileServer() removes headers like Content-Encoding
|
||||
// from error responses. This breaks gzip and deflate encoding.
|
||||
// https://github.com/gorilla/handlers/issues/259
|
||||
// https://github.com/golang/go/issues/66343
|
||||
if w.encoding == "gzip" || w.encoding == "deflate" {
|
||||
if w.Header().Get("Content-Encoding") == "" {
|
||||
w.Header().Set("Content-Encoding", w.encoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) Write(data []byte) (int, error) {
|
||||
w.fixContentEncoding()
|
||||
return w.responseWriter.Write(data)
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) WriteHeader(statusCode int) {
|
||||
w.fixContentEncoding()
|
||||
w.responseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *compressedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := w.responseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, http.ErrNotSupported
|
||||
}
|
||||
|
||||
// Ensure our compressedResponseWriter implements the necessary interfaces.
|
||||
var _ http.ResponseWriter = &compressedResponseWriter{}
|
||||
var _ http.Hijacker = &compressedResponseWriter{}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/utils/sentry"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
staticWeb "goauthentik.io/web"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -90,81 +88,19 @@ func (ws *WebServer) configureProxy() {
|
||||
}
|
||||
|
||||
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
|
||||
accept := req.Header.Get("Accept")
|
||||
|
||||
header := rw.Header()
|
||||
|
||||
if errors.Is(err, ErrAuthentikStarting) {
|
||||
header.Set("Retry-After", "5")
|
||||
|
||||
if strings.Contains(accept, "application/json") {
|
||||
header.Set("Content-Type", "application/json")
|
||||
|
||||
err = json.NewEncoder(rw).Encode(map[string]string{
|
||||
"error": "authentik starting",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write error message")
|
||||
return
|
||||
}
|
||||
} else if strings.Contains(accept, "text/html") {
|
||||
header.Set("Content-Type", "text/html")
|
||||
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
loadingSplashFile, err := staticWeb.StaticDir.Open("standalone/loading/startup.html")
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to open startup splash screen")
|
||||
return
|
||||
}
|
||||
|
||||
loadingSplashHTML, err := io.ReadAll(loadingSplashFile)
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to read startup splash screen")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = rw.Write(loadingSplashHTML)
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write startup splash screen")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
header.Set("Content-Type", "text/plain")
|
||||
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
// Fallback to just a status message
|
||||
_, err = rw.Write([]byte("authentik starting"))
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write initializing HTML")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
if !errors.Is(err, ErrAuthentikStarting) {
|
||||
ws.log.WithError(err).Warning("failed to proxy to backend")
|
||||
}
|
||||
|
||||
ws.log.WithError(err).Warning("failed to proxy to backend")
|
||||
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
|
||||
|
||||
if strings.Contains(accept, "application/json") {
|
||||
header.Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
|
||||
// return json if the client asks for json
|
||||
if req.Header.Get("Accept") == "application/json" {
|
||||
err = json.NewEncoder(rw).Encode(map[string]string{
|
||||
"error": em,
|
||||
})
|
||||
} else {
|
||||
header.Set("Content-Type", "text/plain")
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
|
||||
_, err = rw.Write([]byte(em))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ws.log.WithError(err).Warning("failed to write error message")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ func (ws *WebServer) configureStatic() {
|
||||
// Setup routers
|
||||
staticRouter := ws.loggingRouter.NewRoute().Subrouter()
|
||||
staticRouter.Use(ws.staticHeaderMiddleware)
|
||||
staticRouter.Use(web.DisableIndex)
|
||||
indexLessRouter := staticRouter.NewRoute().Subrouter()
|
||||
// Specifically disable index
|
||||
indexLessRouter.Use(web.DisableIndex)
|
||||
|
||||
distFs := http.FileServer(http.Dir("./web/dist"))
|
||||
|
||||
@@ -29,18 +31,18 @@ func (ws *WebServer) configureStatic() {
|
||||
return h
|
||||
}
|
||||
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/dist/").Handler(pathStripper(
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/dist/").Handler(pathStripper(
|
||||
distFs,
|
||||
"static/dist/",
|
||||
config.Get().Web.Path,
|
||||
))
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/authentik/").Handler(pathStripper(
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/static/authentik/").Handler(pathStripper(
|
||||
http.FileServer(http.Dir("./web/authentik")),
|
||||
"static/authentik/",
|
||||
config.Get().Web.Path,
|
||||
))
|
||||
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
pathStripper(
|
||||
@@ -49,9 +51,9 @@ func (ws *WebServer) configureStatic() {
|
||||
config.Get().Web.Path,
|
||||
).ServeHTTP(rw, r)
|
||||
})
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/admin/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/admin", config.Get().Web.Path), distFs))
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/user/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/user", config.Get().Web.Path), distFs))
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/admin/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/admin", config.Get().Web.Path), distFs))
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/user/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/user", config.Get().Web.Path), distFs))
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
pathStripper(
|
||||
@@ -64,7 +66,7 @@ func (ws *WebServer) configureStatic() {
|
||||
// Media files, if backend is file
|
||||
if config.Get().Storage.Media.Backend == "file" {
|
||||
fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
|
||||
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
|
||||
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
fsMedia.ServeHTTP(w, r)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -59,7 +60,7 @@ func NewWebServer() *WebServer {
|
||||
l := log.WithField("logger", "authentik.router")
|
||||
mainHandler := mux.NewRouter()
|
||||
mainHandler.Use(web.ProxyHeaders())
|
||||
mainHandler.Use(web.NewCompressHandler)
|
||||
mainHandler.Use(handlers.CompressHandler)
|
||||
loggingHandler := mainHandler.NewRoute().Subrouter()
|
||||
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
||||
|
||||
|
||||
26
lifecycle/ak
26
lifecycle/ak
@@ -68,15 +68,33 @@ function prepare_debug {
|
||||
chown authentik:authentik /unittest.xml
|
||||
}
|
||||
|
||||
if [[ -z "${PROMETHEUS_MULTIPROC_DIR}" ]]; then
|
||||
export PROMETHEUS_MULTIPROC_DIR="${TMPDIR:-/tmp}"
|
||||
fi
|
||||
mkdir -p "${PROMETHEUS_MULTIPROC_DIR}"
|
||||
function migrate_container_change_root_dir {
|
||||
# With authentik 2025.10 we're moving the root directory of the authentik app
|
||||
# into /ak-root, mainly to not clutter the root filesystem of the container
|
||||
# and to make it possible to use devcontainers in the future.
|
||||
# In most installs this migration isn't required as no files are mounted into
|
||||
# these directories, however it is used if scripts are overwritten from the outside
|
||||
# or more commonly the flow background image is overwritten in `/web`
|
||||
# Check if we're in a container
|
||||
if [ ! -d /ak-root ]; then
|
||||
return
|
||||
fi
|
||||
if [ -d /authentik ]; then
|
||||
log "Legacy /authentik folder exist, migrating files"
|
||||
cp -rp /authentik/* /ak-root/authentik
|
||||
fi
|
||||
if [ ! -d /web ]; then
|
||||
log "Legacy /web folder exist, migrating files"
|
||||
cp -rp /web/* /ak-root/web
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$(python -m authentik.lib.config debugger 2>/dev/null)" == "True" ]]; then
|
||||
prepare_debug
|
||||
fi
|
||||
|
||||
migrate_container_change_root_dir
|
||||
|
||||
if [[ "$1" == "server" ]]; then
|
||||
set_mode "server"
|
||||
run_authentik
|
||||
|
||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1026.0",
|
||||
"aws-cdk": "^2.1025.0",
|
||||
"cross-env": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -24,9 +24,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1026.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1026.0.tgz",
|
||||
"integrity": "sha512-JdXR20s9gMHY3niweK5/D9tILLG8u2FOyJjWgSaNZGJ+pq9u0sBFxufXPO4VxJzDitGFOIW5VvQThXP+Y2VrVA==",
|
||||
"version": "2.1025.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1025.0.tgz",
|
||||
"integrity": "sha512-qKYM+RG5+U/UbGpjTt8ZaxBEfKJMPdOmtPtFNidsIGlrdIWSIFdNcFYi13zo33FkMk6ZFA6yBnjfDry3fNR+hQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1026.0",
|
||||
"aws-cdk": "^2.1025.0",
|
||||
"cross-env": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,15 @@ wait_for_db()
|
||||
_tmp = Path(gettempdir())
|
||||
worker_class = "lifecycle.worker.DjangoUvicornWorker"
|
||||
worker_tmp_dir = str(_tmp.joinpath("authentik_gunicorn_tmp"))
|
||||
prometheus_tmp_dir = str(_tmp.joinpath("authentik_prometheus_tmp"))
|
||||
|
||||
os.makedirs(worker_tmp_dir, exist_ok=True)
|
||||
os.makedirs(prometheus_tmp_dir, exist_ok=True)
|
||||
|
||||
bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
|
||||
|
||||
preload_app = True
|
||||
|
||||
|
||||
@@ -237,9 +237,6 @@ class _PostgresConsumer(Consumer):
|
||||
# Override because dramatiq doesn't allow us setting this manually
|
||||
self.timeout = Conf().worker["consumer_listen_timeout"]
|
||||
|
||||
self.lock_purge_interval = timezone.timedelta(seconds=Conf().lock_purge_interval)
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
self.task_purge_interval = timezone.timedelta(seconds=Conf().task_purge_interval)
|
||||
self.task_purge_last_run = timezone.now() - self.task_purge_interval
|
||||
|
||||
@@ -381,8 +378,6 @@ class _PostgresConsumer(Consumer):
|
||||
# Force creation of listen connection
|
||||
_ = self.listen_connection
|
||||
|
||||
self._purge_locks()
|
||||
|
||||
processing = len(self.in_processing)
|
||||
if processing >= self.prefetch:
|
||||
# Wait and don't consume the message, other worker will be faster
|
||||
@@ -420,26 +415,24 @@ class _PostgresConsumer(Consumer):
|
||||
)
|
||||
|
||||
# No message to process
|
||||
self._purge_locks()
|
||||
self._auto_purge()
|
||||
self._scheduler()
|
||||
|
||||
return None
|
||||
|
||||
def _purge_locks(self):
|
||||
if timezone.now() - self.lock_purge_last_run < self.lock_purge_interval:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
message_id = self.unlock_queue.get(block=False)
|
||||
except Empty:
|
||||
break
|
||||
return
|
||||
self.logger.debug("Unlocking message", message_id=message_id)
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT pg_advisory_unlock(%s)", (self._get_message_lock_id(message_id),)
|
||||
)
|
||||
self.unlock_queue.task_done()
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
def _auto_purge(self):
|
||||
if timezone.now() - self.task_purge_last_run < self.task_purge_interval:
|
||||
@@ -451,7 +444,6 @@ class _PostgresConsumer(Consumer):
|
||||
result_expiry__lte=timezone.now(),
|
||||
).delete()
|
||||
self.logger.info("Purged messages in all queues", count=count)
|
||||
self.task_purge_last_run = timezone.now()
|
||||
|
||||
def _scheduler(self):
|
||||
if not self.scheduler:
|
||||
@@ -459,7 +451,6 @@ class _PostgresConsumer(Consumer):
|
||||
if timezone.now() - self.scheduler_last_run < self.scheduler_interval:
|
||||
return
|
||||
self.scheduler.run()
|
||||
self.schedule_last_run = timezone.now()
|
||||
|
||||
@raise_connection_error
|
||||
def close(self):
|
||||
@@ -474,7 +465,4 @@ class _PostgresConsumer(Consumer):
|
||||
if self._listen_connection is not None:
|
||||
conn = self._listen_connection
|
||||
self._listen_connection = None
|
||||
try:
|
||||
conn.close()
|
||||
except DatabaseError:
|
||||
pass
|
||||
conn.close()
|
||||
|
||||
@@ -56,10 +56,6 @@ class Conf:
|
||||
def task_model(self) -> str:
|
||||
return self.conf["task_model"]
|
||||
|
||||
@property
|
||||
def lock_purge_interval(self) -> int:
|
||||
return self.conf.get("lock_purge_interval", 60)
|
||||
|
||||
@property
|
||||
def task_purge_interval(self) -> int:
|
||||
# 24 hours
|
||||
|
||||
@@ -26,7 +26,7 @@ class HTTPServer(BaseHTTPServer):
|
||||
self.socket.close()
|
||||
|
||||
host, port = self.server_address[:2]
|
||||
if host == "0.0.0.0" and socket.has_dualstack_ipv6(): # nosec
|
||||
if host == "0.0.0.0": # nosec
|
||||
host = "::" # nosec
|
||||
|
||||
# Strip IPv6 brackets
|
||||
@@ -36,9 +36,7 @@ class HTTPServer(BaseHTTPServer):
|
||||
self.server_address = (host, port)
|
||||
|
||||
self.address_family = (
|
||||
socket.AF_INET6
|
||||
if socket.has_dualstack_ipv6() and isinstance(ip_address(host), IPv6Address)
|
||||
else socket.AF_INET
|
||||
socket.AF_INET6 if isinstance(ip_address(host), IPv6Address) else socket.AF_INET
|
||||
)
|
||||
|
||||
self.socket = socket.create_server(
|
||||
@@ -143,6 +141,7 @@ class MetricsMiddleware(Middleware):
|
||||
def __init__(
|
||||
self,
|
||||
prefix: str,
|
||||
multiproc_dir: str,
|
||||
labels: list[str] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
@@ -152,6 +151,9 @@ class MetricsMiddleware(Middleware):
|
||||
self.delayed_messages = set()
|
||||
self.message_start_times = {}
|
||||
|
||||
os.makedirs(multiproc_dir, exist_ok=True)
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", multiproc_dir)
|
||||
|
||||
@property
|
||||
def forks(self):
|
||||
from django_dramatiq_postgres.forks import worker_metrics
|
||||
|
||||
15
packages/docusaurus-config/package-lock.json
generated
15
packages/docusaurus-config/package-lock.json
generated
@@ -10,7 +10,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"prism-react-renderer": "^2.4.1"
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/theme-common": "^3.8.1",
|
||||
@@ -34,8 +35,7 @@
|
||||
"@docusaurus/theme-common": "^3.8.1",
|
||||
"@docusaurus/theme-search-algolia": "^3.8.1",
|
||||
"@docusaurus/types": "^3.8.0",
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
"react": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@docusaurus/theme-search-algolia": {
|
||||
@@ -43,9 +43,6 @@
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4646,9 +4643,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
122
packages/eslint-config/package-lock.json
generated
122
packages/eslint-config/package-lock.json
generated
@@ -502,17 +502,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
|
||||
"integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/type-utils": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/type-utils": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -526,7 +526,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -542,16 +542,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz",
|
||||
"integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -567,14 +567,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
|
||||
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
|
||||
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.40.0",
|
||||
"@typescript-eslint/types": "^8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.39.1",
|
||||
"@typescript-eslint/types": "^8.39.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -589,14 +589,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
|
||||
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
|
||||
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0"
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -607,9 +607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -624,15 +624,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -649,9 +649,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
|
||||
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -663,16 +663,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
|
||||
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
|
||||
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"@typescript-eslint/project-service": "8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -731,16 +731,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
|
||||
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
|
||||
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0"
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -755,13 +755,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
|
||||
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
|
||||
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4709,16 +4709,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz",
|
||||
"integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz",
|
||||
"integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.39.1",
|
||||
"@typescript-eslint/parser": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.13.*"
|
||||
dependencies = [
|
||||
"argon2-cffi==25.1.0",
|
||||
"channels==4.3.1",
|
||||
"channels==4.3.0",
|
||||
"channels-redis==4.3.0",
|
||||
"cryptography==45.0.5",
|
||||
"dacite==1.9.2",
|
||||
@@ -79,7 +79,7 @@ dev = [
|
||||
"aws-cdk-lib==2.188.0",
|
||||
"bandit==1.8.3",
|
||||
"black==25.1.0",
|
||||
"channels[daphne]==4.3.1",
|
||||
"channels[daphne]==4.3.0",
|
||||
"codespell==2.4.1",
|
||||
"colorama==0.4.6",
|
||||
"constructs==10.4.2",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
services:
|
||||
chrome:
|
||||
platform: linux/x86_64
|
||||
image: docker.io/selenium/standalone-chrome:139.0
|
||||
image: docker.io/selenium/standalone-chrome:138.0
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
restart: always
|
||||
mailpit:
|
||||
image: docker.io/axllent/mailpit:v1.27.6
|
||||
image: docker.io/axllent/mailpit:v1.27.4
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
|
||||
12
uv.lock
generated
12
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = "==3.13.*"
|
||||
|
||||
[manifest]
|
||||
@@ -260,7 +260,7 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "argon2-cffi", specifier = "==25.1.0" },
|
||||
{ name = "channels", specifier = "==4.3.1" },
|
||||
{ name = "channels", specifier = "==4.3.0" },
|
||||
{ name = "channels-redis", specifier = "==4.3.0" },
|
||||
{ name = "cryptography", specifier = "==45.0.5" },
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
@@ -333,7 +333,7 @@ dev = [
|
||||
{ name = "aws-cdk-lib", specifier = "==2.188.0" },
|
||||
{ name = "bandit", specifier = "==1.8.3" },
|
||||
{ name = "black", specifier = "==25.1.0" },
|
||||
{ name = "channels", extras = ["daphne"], specifier = "==4.3.1" },
|
||||
{ name = "channels", extras = ["daphne"], specifier = "==4.3.0" },
|
||||
{ name = "codespell", specifier = "==2.4.1" },
|
||||
{ name = "colorama", specifier = "==0.4.6" },
|
||||
{ name = "constructs", specifier = "==10.4.2" },
|
||||
@@ -652,15 +652,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "channels"
|
||||
version = "4.3.1"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896, upload-time = "2025-08-01T13:25:19.952Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/04/6768c7a887f9c593c4d49f99130c8aec4ea06e750bc17c306b689f6caf3b/channels-4.3.0.tar.gz", hash = "sha256:7db32c61dcd88eada1647e6c6f6ad2eb724b75d4852eeff26ad1c51ccd1a37f7", size = 26816, upload-time = "2025-07-28T13:52:50.334Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286, upload-time = "2025-08-01T13:25:18.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/59/0866202ee593e1b0dab0b472ebb8169e1b2b7886ad3008d193da2bbe10cb/channels-4.3.0-py3-none-any.whl", hash = "sha256:0497f3affb95e621b37d6bae1b6a5d9e8e1e1221007a2566f280091cf30ffcce", size = 31238, upload-time = "2025-07-28T13:52:49.117Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
348
web/package-lock.json
generated
348
web/package-lock.json
generated
@@ -40,7 +40,7 @@
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.2.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@spotlightjs/spotlight": "^3.0.2",
|
||||
@@ -50,7 +50,7 @@
|
||||
"@storybook/web-components-vite": "^9.1.2",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.4",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
@@ -64,7 +64,7 @@
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.45.1",
|
||||
"core-js": "^3.45.0",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
@@ -85,7 +85,7 @@
|
||||
"lit": "^3.3.1",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.10.0",
|
||||
"mermaid": "^11.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"ts-pattern": "^5.8.0",
|
||||
"turnstile-types": "^1.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
@@ -119,9 +119,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/linux-arm64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.3",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.2",
|
||||
"@wdio/browser-runner": "^9.19.1",
|
||||
"@wdio/cli": "^9.19.1",
|
||||
"@wdio/spec-reporter": "^9.19.1",
|
||||
@@ -4042,16 +4042,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/elements": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/elements/-/elements-4.2.0.tgz",
|
||||
"integrity": "sha512-GXVpEfiQzfZwEprUJ9QhSdRyIXDJRm1LT0r88+zlXCGGFDLzMdOlI3+krxQJlv1b+v3VPph4HY/VaGZT8Uxo+w==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/elements/-/elements-4.1.0.tgz",
|
||||
"integrity": "sha512-5gUcB0Kwe7hvraCXayyj9Ut3+F0d9baLkZ74IEGWSyEvAynd7kXn68kmP8URzURL1YcES1g4gKPRs9rhVdkQiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lit/context": "^1.1.5",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@patternfly/icons": "^1.0.3",
|
||||
"@patternfly/pfe-core": "^5.0.2",
|
||||
"lit": "^3.3.0",
|
||||
"tslib": "^2.8.1"
|
||||
"@patternfly/pfe-core": "^5.0.0",
|
||||
"lit": "^3.2.1",
|
||||
"tslib": "^2.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/icons": {
|
||||
@@ -4065,14 +4065,14 @@
|
||||
"integrity": "sha512-io0huj+LCP5FgDZJDaLv1snxktTYs8iCFz/W1VDRneYoebNHLmGfQdF7Yn8bS6PF7qmN6oJKEBlq3AjmmE8vdA=="
|
||||
},
|
||||
"node_modules/@patternfly/pfe-core": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/pfe-core/-/pfe-core-5.0.3.tgz",
|
||||
"integrity": "sha512-tKc9YfbXD5kzUr6ssa5gKicKgWf+CEax3auvv0yv5jJ42RFMhDRVO0YWalPi5iBl70XHNentpMmdHioC00TJAw==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/pfe-core/-/pfe-core-5.0.1.tgz",
|
||||
"integrity": "sha512-oNctdm9XUEOrwTIMobcPUmYPt9toShCewPOcyLcdGraseuy518ZWkiHpFSEAsmuf7OjhWRD2gDWVaV/3TflK4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.5",
|
||||
"lit": "^3.3.0"
|
||||
"@lit/context": "^1.1.3",
|
||||
"lit": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@petamoriken/float16": {
|
||||
@@ -4309,9 +4309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.3.tgz",
|
||||
"integrity": "sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
|
||||
"integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4322,9 +4322,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.3.tgz",
|
||||
"integrity": "sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
|
||||
"integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4335,9 +4335,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.3.tgz",
|
||||
"integrity": "sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
|
||||
"integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4348,9 +4348,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.3.tgz",
|
||||
"integrity": "sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
|
||||
"integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4361,9 +4361,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.3.tgz",
|
||||
"integrity": "sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
|
||||
"integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4374,9 +4374,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.3.tgz",
|
||||
"integrity": "sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
|
||||
"integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4387,9 +4387,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.3.tgz",
|
||||
"integrity": "sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
|
||||
"integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4400,9 +4400,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.3.tgz",
|
||||
"integrity": "sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
|
||||
"integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4413,9 +4413,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4426,9 +4426,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.3.tgz",
|
||||
"integrity": "sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
|
||||
"integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4439,9 +4439,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -4452,9 +4452,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -4465,9 +4465,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4478,9 +4478,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.3.tgz",
|
||||
"integrity": "sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
|
||||
"integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4491,9 +4491,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -4504,9 +4504,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.3.tgz",
|
||||
"integrity": "sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
|
||||
"integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4517,9 +4517,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.3.tgz",
|
||||
"integrity": "sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
|
||||
"integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4530,9 +4530,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.3.tgz",
|
||||
"integrity": "sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
|
||||
"integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4543,9 +4543,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.3.tgz",
|
||||
"integrity": "sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
|
||||
"integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -4556,9 +4556,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.3.tgz",
|
||||
"integrity": "sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
|
||||
"integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -6482,9 +6482,9 @@
|
||||
"integrity": "sha512-fFxMtjAvXXMYTzDFK5NpcVB7WHnrHVLl00QzEGpuFxSAC789io6M+vjcn+g5FTEamIJtJr/IHkCDsqvJxeWDyw=="
|
||||
},
|
||||
"node_modules/@types/guacamole-common-js": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.4.tgz",
|
||||
"integrity": "sha512-RswPKwgqSgbbfvL8fHD6J2nmXhpQuejiuPrzfUkejBMUFRW5hYUEMaK4ySyDTM/Q4hWUe3F7KLxBvaI7LtqYXQ==",
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.3.tgz",
|
||||
"integrity": "sha512-PDW2kRwwIgzw0ys82X65g13+OHRPW4Ek/919vIoacWGEUU8jGGfULmH+6TuufLDGMO0cqXR03nxer8ceRDmy3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
@@ -6838,16 +6838,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
|
||||
"integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/type-utils": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/type-utils": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -6861,7 +6861,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -6876,15 +6876,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz",
|
||||
"integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6900,13 +6900,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
|
||||
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
|
||||
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.40.0",
|
||||
"@typescript-eslint/types": "^8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.39.1",
|
||||
"@typescript-eslint/types": "^8.39.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6921,13 +6921,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
|
||||
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
|
||||
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0"
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6938,9 +6938,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6954,14 +6954,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -6978,9 +6978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
|
||||
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -6991,15 +6991,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
|
||||
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
|
||||
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"@typescript-eslint/project-service": "8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -7019,15 +7019,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
|
||||
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
|
||||
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0"
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -7042,12 +7042,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
|
||||
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
|
||||
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -11147,9 +11147,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.45.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
|
||||
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
|
||||
"version": "3.45.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
|
||||
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -19109,9 +19109,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.10.0.tgz",
|
||||
"integrity": "sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==",
|
||||
"version": "11.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz",
|
||||
"integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
@@ -23326,9 +23326,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.46.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.3.tgz",
|
||||
"integrity": "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==",
|
||||
"version": "4.46.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
|
||||
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -23341,26 +23341,26 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.46.3",
|
||||
"@rollup/rollup-android-arm64": "4.46.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.46.3",
|
||||
"@rollup/rollup-darwin-x64": "4.46.3",
|
||||
"@rollup/rollup-freebsd-arm64": "4.46.3",
|
||||
"@rollup/rollup-freebsd-x64": "4.46.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.46.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.46.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.46.3",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.46.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.46.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.46.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.46.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.46.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.46.3",
|
||||
"@rollup/rollup-android-arm-eabi": "4.46.2",
|
||||
"@rollup/rollup-android-arm64": "4.46.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.46.2",
|
||||
"@rollup/rollup-darwin-x64": "4.46.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.46.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.46.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.46.2",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.46.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.46.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.46.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.46.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.46.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.46.2",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -25708,15 +25708,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz",
|
||||
"integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz",
|
||||
"integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0",
|
||||
"@typescript-eslint/utils": "8.40.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.39.1",
|
||||
"@typescript-eslint/parser": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -27928,7 +27928,7 @@
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.46.3",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.2.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@spotlightjs/spotlight": "^3.0.2",
|
||||
@@ -121,7 +121,7 @@
|
||||
"@storybook/web-components-vite": "^9.1.2",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.4",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
@@ -135,7 +135,7 @@
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.45.1",
|
||||
"core-js": "^3.45.0",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
@@ -156,7 +156,7 @@
|
||||
"lit": "^3.3.1",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.10.0",
|
||||
"mermaid": "^11.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
@@ -177,7 +177,7 @@
|
||||
"ts-pattern": "^5.8.0",
|
||||
"turnstile-types": "^1.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
@@ -187,9 +187,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/linux-arm64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.3",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.2",
|
||||
"@wdio/browser-runner": "^9.19.1",
|
||||
"@wdio/cli": "^9.19.1",
|
||||
"@wdio/spec-reporter": "^9.19.1",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.46.3",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
|
||||
@@ -48,11 +48,6 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
plugins: [
|
||||
copy({
|
||||
assets: [
|
||||
{
|
||||
from: path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup", "**"),
|
||||
to: path.dirname(EntryPoint.StandaloneLoading.out),
|
||||
},
|
||||
|
||||
{
|
||||
from: path.join(patternflyPath, "patternfly.min.css"),
|
||||
to: ".",
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
quickActions: QuickAction[] = [
|
||||
[msg("Create a new application"), paramURL("/core/applications", { createWizard: true })],
|
||||
[msg("Check the logs"), paramURL("/events/log")],
|
||||
[msg("Explore integrations"), "https://integrations.goauthentik.io/", true],
|
||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
||||
[msg("Manage users"), paramURL("/identity/users")],
|
||||
[
|
||||
msg("Check the release notes"),
|
||||
|
||||
@@ -5,10 +5,6 @@ import { groupBy } from "#common/utils";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { IDGenerator } from "#packages/core/id";
|
||||
|
||||
import { Provider, ProvidersAllListRequest, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
@@ -42,13 +38,11 @@ export class AkProviderInput extends AKElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label?: string;
|
||||
label = "";
|
||||
|
||||
@property({ type: Number })
|
||||
value?: number;
|
||||
@@ -66,26 +60,14 @@ export class AkProviderInput extends AKElement {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
}
|
||||
/**
|
||||
* A unique ID to associate with the input and label.
|
||||
* @property
|
||||
*/
|
||||
@property({ type: String, reflect: false })
|
||||
public fieldID?: string = IDGenerator.elementID().toString();
|
||||
|
||||
selected(item: Provider) {
|
||||
return this.value !== undefined && this.value === item.pk;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
render() {
|
||||
return html` <ak-form-element-horizontal name=${this.name}>
|
||||
<div slot="label" class="pf-c-form__group-label">
|
||||
${AKLabel({ htmlFor: this.fieldID, required: this.required }, this.label)}
|
||||
</div>
|
||||
|
||||
return html` <ak-form-element-horizontal label=${this.label} name=${this.name}>
|
||||
<ak-search-select
|
||||
.fieldID=${this.fieldID}
|
||||
.selected=${this.selected}
|
||||
.fetchObjects=${fetch}
|
||||
.renderElement=${renderElement}
|
||||
|
||||
@@ -44,7 +44,6 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
}
|
||||
|
||||
async send(data: Brand): Promise<Brand> {
|
||||
data.attributes ??= {};
|
||||
if (this.instance?.brandUuid) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreBrandsUpdate({
|
||||
brandUuid: this.instance.brandUuid,
|
||||
|
||||
@@ -53,7 +53,6 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
}
|
||||
|
||||
async send(data: Group): Promise<Group> {
|
||||
data.attributes ??= {};
|
||||
if (this.instance?.pk) {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
|
||||
groupUuid: this.instance.pk,
|
||||
@@ -146,7 +145,7 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} required name="attributes">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(this.instance?.attributes ?? {})}"
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { APIError } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
import { APIMessage } from "#elements/messages/Message";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
@@ -12,14 +8,4 @@ export abstract class BaseProviderForm<T> extends ModelForm<T, number> {
|
||||
? msg("Successfully updated provider.")
|
||||
: msg("Successfully created provider.");
|
||||
}
|
||||
|
||||
protected override formatAPIErrorMessage(error: APIError): APIMessage {
|
||||
return {
|
||||
level: MessageLevel.error,
|
||||
...super.formatAPIErrorMessage(error),
|
||||
message: this.instance
|
||||
? msg("An error occurred while updating the provider.")
|
||||
: msg("An error occurred while creating the provider."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export function renderForm(
|
||||
.redirectURI=${redirectURI}
|
||||
name="oauth2-redirect-uri"
|
||||
style="width: 100%"
|
||||
input-id="redirect-uri-${idx}"
|
||||
inputID="redirect-uri-${idx}"
|
||||
></ak-provider-oauth2-redirect-uri>`;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -71,10 +71,10 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
metadata?: SAMLMetadata;
|
||||
|
||||
@state()
|
||||
signer: CertificateKeyPair | null = null;
|
||||
signer?: CertificateKeyPair;
|
||||
|
||||
@state()
|
||||
verifier: CertificateKeyPair | null = null;
|
||||
verifier?: CertificateKeyPair;
|
||||
|
||||
@state()
|
||||
previewUser?: User;
|
||||
@@ -97,7 +97,7 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
if (!this.provider?.pk) return;
|
||||
this.fetchProvider(this.provider.pk);
|
||||
this.providerID = this.provider?.pk;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,32 +117,20 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
}
|
||||
|
||||
fetchSigningCertificate(kpUuid: string) {
|
||||
this.fetchCertificate(kpUuid).then((kp) => {
|
||||
this.signer = kp;
|
||||
this.requestUpdate("signer");
|
||||
});
|
||||
this.fetchCertificate(kpUuid).then((kp) => (this.signer = kp));
|
||||
}
|
||||
|
||||
fetchVerificationCertificate(kpUuid: string) {
|
||||
this.fetchCertificate(kpUuid).then((kp) => {
|
||||
this.verifier = kp;
|
||||
this.requestUpdate("verifier");
|
||||
});
|
||||
this.fetchCertificate(kpUuid).then((kp) => (this.verifier = kp));
|
||||
}
|
||||
|
||||
fetchProvider(id: number) {
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersSamlRetrieve({ id }).then((prov) => {
|
||||
this.provider = prov;
|
||||
// Clear existing signing certificate if the provider has none
|
||||
if (!this.provider.signingKp) {
|
||||
this.signer = null;
|
||||
} else {
|
||||
if (this.provider.signingKp) {
|
||||
this.fetchSigningCertificate(this.provider.signingKp);
|
||||
}
|
||||
// Clear existing verification certificate if the provider has none
|
||||
if (!this.provider.verificationKp) {
|
||||
this.verifier = null;
|
||||
} else {
|
||||
if (this.provider.verificationKp) {
|
||||
this.fetchVerificationCertificate(this.provider.verificationKp);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -530,8 +530,9 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -414,8 +414,9 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -574,8 +574,9 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -180,7 +180,11 @@ export class UserForm extends ModelForm<User, number> {
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Attributes")}
|
||||
?required=${false}
|
||||
name="attributes"
|
||||
>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { APIMessage } from "#elements/messages/Message";
|
||||
|
||||
import { CoreApi, ImpersonationRequest } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-impersonate-form")
|
||||
export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||
@property({ type: Number })
|
||||
public instancePk?: number;
|
||||
|
||||
protected override formatAPISuccessMessage(): APIMessage | null {
|
||||
return {
|
||||
level: MessageLevel.success,
|
||||
message: msg(str`Impersonating user...`),
|
||||
description: msg("This may take a few seconds."),
|
||||
};
|
||||
}
|
||||
instancePk?: number;
|
||||
|
||||
async send(data: ImpersonationRequest): Promise<void> {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
@@ -32,7 +23,7 @@ export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||
impersonationRequest: data,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
window.location.href = globalAK().api.base;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,12 +31,7 @@ export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||
return html`<ak-text-input
|
||||
name="reason"
|
||||
label=${msg("Reason")}
|
||||
autocomplete="off"
|
||||
placeholder=${msg("Reason for impersonating the user")}
|
||||
help=${msg(
|
||||
"A brief explanation of why you are impersonating the user. This will be included in audit logs.",
|
||||
)}
|
||||
required
|
||||
help=${msg("Reason for impersonating the user")}
|
||||
></ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
ValidationErrorFromJSON,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { sentenceCase } from "change-case";
|
||||
|
||||
//#region HTTP
|
||||
|
||||
/**
|
||||
@@ -235,25 +233,3 @@ export async function parseAPIResponseError<T extends APIError = APIError>(
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Validation errors
|
||||
|
||||
/**
|
||||
* Pluck a field error from a validation error.
|
||||
*
|
||||
* This is used to create a fallback error message when the API returns
|
||||
* a validation error that isn't associated with field within the form.
|
||||
*
|
||||
* We can still show the error message, to at least give the user some feedback.
|
||||
*/
|
||||
export function pluckFallbackFieldErrors(parsedError: APIError): string[] {
|
||||
for (const [fieldName, fieldErrors] of Object.entries(parsedError)) {
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
return [`${sentenceCase(fieldName)}: ${fieldErrors.join(", ")}`];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -5,12 +5,12 @@ import { SlottedTemplateResult } from "../elements/types";
|
||||
import { AKElement, type AKElementProps } from "#elements/Base";
|
||||
|
||||
import { ErrorProp } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
|
||||
import { html, nothing, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
export interface HorizontalLightComponentProps<T> extends AKElementProps {
|
||||
name: string;
|
||||
@@ -40,8 +40,6 @@ export abstract class HorizontalLightComponent<T>
|
||||
return this;
|
||||
}
|
||||
|
||||
//#region Properties
|
||||
|
||||
/**
|
||||
* The name attribute for the form element
|
||||
* @property
|
||||
@@ -63,7 +61,7 @@ export abstract class HorizontalLightComponent<T>
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public required?: boolean;
|
||||
required = false;
|
||||
|
||||
/**
|
||||
* Help text to display below the form element. Optional
|
||||
@@ -98,9 +96,10 @@ export abstract class HorizontalLightComponent<T>
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public errorMessages?: ErrorProp[];
|
||||
errorMessages: string[] = [];
|
||||
|
||||
/**
|
||||
* @attribute
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
@@ -115,21 +114,11 @@ export abstract class HorizontalLightComponent<T>
|
||||
@property({ type: String, attribute: "input-hint" })
|
||||
inputHint?: string;
|
||||
|
||||
/**
|
||||
* A unique ID to associate with the input and label.
|
||||
* @property
|
||||
*/
|
||||
@property({ type: String, reflect: false })
|
||||
public fieldID?: string = IDGenerator.elementID().toString();
|
||||
protected renderControl() {
|
||||
throw new Error("Must be implemented in a subclass");
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
/**
|
||||
* Render the control element, e.g. an input, textarea, select, etc.
|
||||
*/
|
||||
protected abstract renderControl(): SlottedTemplateResult;
|
||||
protected fieldID = IDGenerator.elementID().toString();
|
||||
|
||||
protected renderHelp(): SlottedTemplateResult | SlottedTemplateResult[] {
|
||||
const bigHelp: SlottedTemplateResult[] = Array.isArray(this.bighelp)
|
||||
@@ -144,19 +133,14 @@ export abstract class HorizontalLightComponent<T>
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
.fieldID=${this.fieldID}
|
||||
fieldID=${this.fieldID}
|
||||
label=${ifDefined(this.label)}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
.errorMessages=${this.errorMessages}
|
||||
>
|
||||
<div slot="label" class="pf-c-form__group-label">
|
||||
${AKLabel({ htmlFor: this.fieldID, required: this.required }, this.label)}
|
||||
</div>
|
||||
|
||||
${this.renderControl()} ${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
|
||||
|
||||
public override renderControl() {
|
||||
return html`<input
|
||||
id=${ifDefined(this.fieldID)}
|
||||
@input=${(ev: Event) => this.handleTouch(ev)}
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
|
||||
@@ -17,7 +17,6 @@ export class AkTextareaInput extends HorizontalLightComponent<string> {
|
||||
// Prevent the leading spaces added by Prettier's whitespace algo
|
||||
// prettier-ignore
|
||||
return html`<textarea
|
||||
id=${ifDefined(this.fieldID)}
|
||||
@input=${setValue}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { html } from "lit";
|
||||
const ACTIONS: QuickAction[] = [
|
||||
["Create a new application", "/core/applications"],
|
||||
["Check the logs", "/events/log"],
|
||||
["Explore integrations", "https://integrations.goauthentik.io/", true],
|
||||
["Explore integrations", "https://goauthentik.io/integrations/", true],
|
||||
["Manage users", "/identity/users"],
|
||||
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { html } from "lit";
|
||||
const ACTIONS: QuickAction[] = [
|
||||
["Create a new application", "/core/applications"],
|
||||
["Check the logs", "/events/log"],
|
||||
["Explore integrations", "https://integrations.goauthentik.io/", true],
|
||||
["Explore integrations", "https://goauthentik.io/integrations/", true],
|
||||
["Manage users", "/identity/users"],
|
||||
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
|
||||
];
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import {
|
||||
APIError,
|
||||
parseAPIResponseError,
|
||||
pluckErrorDetail,
|
||||
pluckFallbackFieldErrors,
|
||||
} from "#common/errors/network";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
import { dateToUTC } from "#common/temporal";
|
||||
|
||||
@@ -13,18 +8,17 @@ import { AKElement } from "#elements/Base";
|
||||
import { reportValidityDeep } from "#elements/forms/FormGroup";
|
||||
import { PreventFormSubmit } from "#elements/forms/helpers";
|
||||
import { HorizontalFormElement } from "#elements/forms/HorizontalFormElement";
|
||||
import { APIMessage } from "#elements/messages/Message";
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { createFileMap, isNamedElement, NamedElement } from "#elements/utils/inputs";
|
||||
|
||||
import { ErrorProp } from "#components/ak-field-errors";
|
||||
|
||||
import { instanceOfValidationError, ValidationError } from "@goauthentik/api";
|
||||
import { instanceOfValidationError } from "@goauthentik/api";
|
||||
|
||||
import { snakeCase } from "change-case";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -149,41 +143,6 @@ export function serializeForm<T = Record<string, unknown>>(elements: Iterable<AK
|
||||
return json as unknown as T;
|
||||
}
|
||||
|
||||
//#region Validation Reporting
|
||||
|
||||
/**
|
||||
* Assign all input-related errors to their respective elements.
|
||||
*/
|
||||
function reportInvalidFields(
|
||||
parsedError: ValidationError,
|
||||
elements: Iterable<HorizontalFormElement>,
|
||||
): HorizontalFormElement[] {
|
||||
const invalidFields: HorizontalFormElement[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
element.requestUpdate();
|
||||
|
||||
const elementName = element.name;
|
||||
|
||||
if (!elementName) continue;
|
||||
|
||||
const snakeProperty = snakeCase(elementName);
|
||||
const errorMessages: ErrorProp[] = parsedError[snakeProperty] ?? [];
|
||||
|
||||
element.errorMessages = errorMessages;
|
||||
|
||||
if (Array.isArray(errorMessages) && errorMessages.length) {
|
||||
invalidFields.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return invalidFields;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Form
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
@@ -221,8 +180,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: String })
|
||||
public successMessage?: string;
|
||||
@property()
|
||||
public successMessage = "";
|
||||
|
||||
@property({ type: String })
|
||||
public autocomplete?: AutoFill;
|
||||
@@ -267,38 +226,11 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
/**
|
||||
* An overridable method for returning a success message after a successful submission.
|
||||
*
|
||||
* @deprecated Use `formatAPISuccessMessage` instead.
|
||||
*/
|
||||
protected getSuccessMessage(): string | undefined {
|
||||
protected getSuccessMessage(): string {
|
||||
return this.successMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for returning a formatted message after a successful submission.
|
||||
*/
|
||||
protected formatAPISuccessMessage(response: unknown): APIMessage | null {
|
||||
const message = this.getSuccessMessage();
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
level: MessageLevel.success,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for returning a formatted error message after a failed submission.
|
||||
*/
|
||||
protected formatAPIErrorMessage(error: APIError): APIMessage | null {
|
||||
return {
|
||||
message: msg("There was an error submitting the form."),
|
||||
description: pluckErrorDetail(error, pluckFallbackFieldErrors(error)[0]),
|
||||
level: MessageLevel.error,
|
||||
};
|
||||
}
|
||||
|
||||
//#region Public methods
|
||||
|
||||
public reset(): void {
|
||||
@@ -362,7 +294,10 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
return this.send(data)
|
||||
.then((response) => {
|
||||
showMessage(this.formatAPISuccessMessage(response));
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: this.getSuccessMessage(),
|
||||
});
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
@@ -379,32 +314,81 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
}
|
||||
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
let errorMessage = pluckErrorDetail(error);
|
||||
let focused = false;
|
||||
|
||||
//#region Validation errors
|
||||
|
||||
if (instanceOfValidationError(parsedError)) {
|
||||
const invalidFields = reportInvalidFields(
|
||||
parsedError,
|
||||
this.renderRoot.querySelectorAll("ak-form-element-horizontal"),
|
||||
);
|
||||
// assign all input-related errors to their elements
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
|
||||
const focusTarget = Iterator.from(invalidFields)
|
||||
.map(({ focusTarget }) => focusTarget)
|
||||
.find(Boolean);
|
||||
for (const element of elements) {
|
||||
element.requestUpdate();
|
||||
|
||||
if (focusTarget) {
|
||||
requestAnimationFrame(() => focusTarget.focus());
|
||||
} else if (Array.isArray(parsedError.nonFieldErrors)) {
|
||||
const elementName = element.name;
|
||||
|
||||
if (!elementName) continue;
|
||||
|
||||
const snakeProperty = snakeCase(elementName);
|
||||
const errorMessages: ErrorProp[] = parsedError[snakeProperty] ?? [];
|
||||
|
||||
element.errorMessages = errorMessages;
|
||||
const { controlledElement } = element;
|
||||
|
||||
if (!focused && Array.isArray(errorMessages) && errorMessages.length) {
|
||||
if (
|
||||
controlledElement?.checkVisibility() &&
|
||||
controlledElement instanceof HTMLElement
|
||||
) {
|
||||
focused = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
return controlledElement.focus?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedError.nonFieldErrors) {
|
||||
this.nonFieldErrors = parsedError.nonFieldErrors;
|
||||
} else {
|
||||
this.nonFieldErrors = pluckFallbackFieldErrors(parsedError);
|
||||
} else if (!focused) {
|
||||
// It's possible that the API has returned a field error that we're
|
||||
// not aware of. We can still show the error message, to at least
|
||||
// give the user some feedback.
|
||||
for (const [fieldName, fieldErrors] of Object.entries(parsedError)) {
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
this.nonFieldErrors = [
|
||||
msg(str`${fieldName}: ${fieldErrors.join(", ")}`),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
"authentik/forms: API rejected the form submission due to an invalid field that doesn't appear to be in the form. This is likely a bug in authentik.",
|
||||
parsedError,
|
||||
);
|
||||
}
|
||||
|
||||
errorMessage = msg("Invalid update request.");
|
||||
|
||||
// Only change the message when we have `detail`.
|
||||
// Everything else is handled in the form.
|
||||
if ("detail" in parsedError) {
|
||||
errorMessage = parsedError.detail;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(this.formatAPIErrorMessage(parsedError), true);
|
||||
//#endregion
|
||||
|
||||
showMessage({
|
||||
message: errorMessage,
|
||||
level: MessageLevel.error,
|
||||
});
|
||||
|
||||
// Rethrow the error so the form doesn't close.
|
||||
throw error;
|
||||
|
||||
70
web/src/elements/forms/FormElement.ts
Normal file
70
web/src/elements/forms/FormElement.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { ErrorDetail } from "@goauthentik/api";
|
||||
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
/**
|
||||
* This is used in two places outside of Flow, and in both cases is used primarily to
|
||||
* display content, not take input. It displays the TOTP QR code, and the static
|
||||
* recovery tokens. But it's used a lot in Flow.
|
||||
*/
|
||||
|
||||
@customElement("ak-form-element")
|
||||
export class FormElement extends AKElement {
|
||||
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
|
||||
|
||||
@property()
|
||||
label?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
set errors(value: ErrorDetail[] | undefined) {
|
||||
this._errors = value;
|
||||
const hasError = (value || []).length > 0;
|
||||
this.querySelectorAll("input").forEach((input) => {
|
||||
input.setAttribute("aria-invalid", hasError.toString());
|
||||
});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_errors?: ErrorDetail[];
|
||||
|
||||
updated(): void {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text">${this.label}</span>
|
||||
${this.required
|
||||
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
|
||||
: html``}
|
||||
</label>
|
||||
<slot></slot>
|
||||
${(this._errors || []).map((error) => {
|
||||
return html`<p class="pf-c-form__helper-text pf-m-error">
|
||||
<span class="pf-c-form__helper-text-icon">
|
||||
<i class="fas fa-exclamation-circle" aria-hidden="true"></i> </span
|
||||
>${error.string}
|
||||
</p>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-form-element": FormElement;
|
||||
}
|
||||
}
|
||||
@@ -70,20 +70,7 @@ export class HorizontalFormElement extends AKElement {
|
||||
|
||||
//#endregion
|
||||
|
||||
#controlledElement: AkControlElement | NamedElement | null = null;
|
||||
|
||||
/**
|
||||
* The element that should be focused when the form is submitted.
|
||||
*/
|
||||
public get focusTarget(): AkControlElement | NamedElement<HTMLElement> | null {
|
||||
if (!(this.#controlledElement instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.#controlledElement.checkVisibility()) return null;
|
||||
|
||||
return this.#controlledElement;
|
||||
}
|
||||
public controlledElement: NamedElement | AkControlElement | null = null;
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
@@ -92,8 +79,8 @@ export class HorizontalFormElement extends AKElement {
|
||||
}
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("errorMessages") && this.#controlledElement) {
|
||||
this.#controlledElement.setAttribute(
|
||||
if (changedProperties.has("errorMessages") && this.controlledElement) {
|
||||
this.controlledElement.setAttribute(
|
||||
"aria-invalid",
|
||||
this.errorMessages?.length ? "true" : "false",
|
||||
);
|
||||
@@ -112,13 +99,12 @@ export class HorizontalFormElement extends AKElement {
|
||||
for (const element of this.querySelectorAll("*")) {
|
||||
// Is this element capable of being named?
|
||||
if (!isControlElement(element) && !isNameableElement(element)) continue;
|
||||
// And does the element already match the name?
|
||||
if (element.getAttribute("name") === this.name) continue;
|
||||
|
||||
this.#controlledElement = element;
|
||||
|
||||
if (element.getAttribute("name") !== this.name) {
|
||||
element.setAttribute("name", this.name);
|
||||
}
|
||||
element.setAttribute("name", this.name);
|
||||
|
||||
this.controlledElement = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,18 +82,7 @@ export class SearchSelectBase<T> extends AkControlElement<string> implements ISe
|
||||
|
||||
// Used to inform the form of the name of the object
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
/**
|
||||
* A unique ID to associate with the input and label.
|
||||
* @property
|
||||
*/
|
||||
@property({ type: String, reflect: false })
|
||||
public fieldID?: string;
|
||||
|
||||
// Used to inform the form of the input label.
|
||||
@property()
|
||||
public label?: string;
|
||||
name?: string;
|
||||
|
||||
// The textual placeholder for the search's <input> object, if currently empty. Used as the
|
||||
// native <input> object's `placeholder` field.
|
||||
@@ -266,7 +255,6 @@ export class SearchSelectBase<T> extends AkControlElement<string> implements ISe
|
||||
|
||||
return html`<ak-search-select-view
|
||||
managed
|
||||
.fieldID=${this.fieldID}
|
||||
.options=${options}
|
||||
value=${ifDefined(value)}
|
||||
?blankable=${this.blankable}
|
||||
|
||||
@@ -27,11 +27,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
*
|
||||
* @todo Consider making this a static method on singleton {@linkcode MessageContainer}
|
||||
*/
|
||||
export function showMessage(message: APIMessage | null, unique = false): void {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
export function showMessage(message: APIMessage, unique = false): void {
|
||||
const container = document.querySelector<MessageContainer>("ak-message-container");
|
||||
|
||||
if (!container) {
|
||||
@@ -39,10 +35,7 @@ export function showMessage(message: APIMessage | null, unique = false): void {
|
||||
}
|
||||
|
||||
if (!message.message.trim()) {
|
||||
console.warn("authentik/messages: `showMessage` received an empty message", message);
|
||||
|
||||
message.message = msg("An unknown error occurred");
|
||||
message.description ??= msg("Please check the browser console for more details.");
|
||||
message.message = msg("Error");
|
||||
}
|
||||
|
||||
container.addMessage(message, unique);
|
||||
|
||||
@@ -45,7 +45,7 @@ export function isNameableElement(element: Element): element is NamedElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
return NameableElements.has(element.tagName) || element.getAttribute("name") !== null;
|
||||
return NameableElements.has(element.tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AKElement } from "#elements/Base";
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
|
||||
@@ -17,44 +18,24 @@ export class FormStatic extends AKElement {
|
||||
static styles: CSSResult[] = [
|
||||
PFAvatar,
|
||||
css`
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-block-start: var(--pf-global--spacer--sm);
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
|
||||
.pf-c-avatar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.primary-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
max-width: 20rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
display: box;
|
||||
display: -webkit-box;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -63,22 +44,17 @@ export class FormStatic extends AKElement {
|
||||
if (!this.user) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="form-control-static">
|
||||
<div class="primary-content">
|
||||
${this.userAvatar
|
||||
? html`<img
|
||||
class="pf-c-avatar"
|
||||
src=${this.userAvatar}
|
||||
alt=${msg("User's avatar")}
|
||||
/>`
|
||||
: nothing}
|
||||
<div class="username" aria-label=${msg("Username")}>${this.user}</div>
|
||||
</div>
|
||||
<div class="links">
|
||||
<slot name="link"></slot>
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="pf-c-avatar"
|
||||
src="${ifDefined(this.userAvatar)}"
|
||||
alt="${msg("User's avatar")}"
|
||||
/>
|
||||
${this.user}
|
||||
</div>
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,13 @@ export class FlowCard extends AKElement {
|
||||
PFLogin,
|
||||
PFTitle,
|
||||
css`
|
||||
.pf-c-login__main-footer {
|
||||
display: block;
|
||||
slot[name="footer"],
|
||||
slot[name="footer-band"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
slot[name="footer-band"] {
|
||||
text-align: center;
|
||||
background-color: var(--pf-c-login__main-footer-band--BackgroundColor);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "#elements/forms/FormElement";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
import { isActiveElement } from "#elements/utils/focus";
|
||||
|
||||
import { AKFormErrors, ErrorProp } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
@@ -12,7 +11,6 @@ import { classMap } from "lit/directives/class-map.js";
|
||||
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -41,7 +39,7 @@ const Visibility = {
|
||||
|
||||
@customElement("ak-flow-input-password")
|
||||
export class InputPassword extends AKElement {
|
||||
static styles = [PFBase, PFForm, PFInputGroup, PFFormControl, PFButton];
|
||||
static styles = [PFBase, PFInputGroup, PFFormControl, PFButton];
|
||||
|
||||
//#region Properties
|
||||
|
||||
@@ -51,7 +49,7 @@ export class InputPassword extends AKElement {
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, attribute: "input-id" })
|
||||
public inputID = "ak-stage-password-input";
|
||||
inputId = "ak-stage-password-input";
|
||||
|
||||
/**
|
||||
* The name of the input field.
|
||||
@@ -88,8 +86,8 @@ export class InputPassword extends AKElement {
|
||||
/**
|
||||
* The errors for the input field.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public errors?: ErrorProp[];
|
||||
@property({ type: Object })
|
||||
errors: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Whether to allow the user to toggle the visibility of the password.
|
||||
@@ -308,32 +306,37 @@ export class InputPassword extends AKElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${AKLabel({ required: true, htmlFor: this.inputID }, this.label)}
|
||||
<div class="pf-c-form__group">
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-input-group">
|
||||
<input
|
||||
type=${this.passwordVisible ? "text" : "password"}
|
||||
id=${this.inputID}
|
||||
name=${this.name}
|
||||
placeholder=${this.placeholder}
|
||||
autocomplete="current-password"
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-icon": true,
|
||||
"pf-m-caps-lock": this.capsLock,
|
||||
})}"
|
||||
required
|
||||
aria-invalid=${this.errors?.length ? "true" : "false"}
|
||||
value=${this.initialValue}
|
||||
${ref(this.inputRef)}
|
||||
/>
|
||||
return html` <ak-form-element
|
||||
label="${this.label}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${this.errors}
|
||||
>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-input-group">
|
||||
<input
|
||||
type=${this.passwordVisible ? "text" : "password"}
|
||||
id=${this.inputId}
|
||||
name=${this.name}
|
||||
placeholder=${this.placeholder}
|
||||
autocomplete="current-password"
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-icon": true,
|
||||
"pf-m-caps-lock": this.capsLock,
|
||||
})}"
|
||||
required
|
||||
aria-invalid=${this.errors?.length ? "true" : "false"}
|
||||
value=${this.initialValue}
|
||||
${ref(this.inputRef)}
|
||||
/>
|
||||
|
||||
${this.renderVisibilityToggle()}
|
||||
</div>
|
||||
${AKFormErrors({ errors: this.errors })} ${this.renderHelperText()}
|
||||
${this.renderVisibilityToggle()}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
${this.renderHelperText()}
|
||||
</div>
|
||||
</ak-form-element>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -18,7 +16,6 @@ import { customElement } from "lit/decorators.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -28,24 +25,15 @@ export class OAuth2DeviceCode extends BaseStage<
|
||||
OAuthDeviceCodeChallenge,
|
||||
OAuthDeviceCodeChallengeResponseRequest
|
||||
> {
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
PFInputGroup,
|
||||
];
|
||||
static styles: CSSResult[] = [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "device-code-input" }, msg("Device Code"))}
|
||||
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${this.submitForm}
|
||||
>
|
||||
<input
|
||||
id="device-code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
@@ -57,8 +45,7 @@ export class OAuth2DeviceCode extends BaseStage<
|
||||
value=""
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -20,7 +18,6 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -36,7 +33,6 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
];
|
||||
@@ -55,13 +51,13 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{ required: true, htmlFor: "email-input" },
|
||||
msg("Configure your email"),
|
||||
)}
|
||||
<ak-form-element
|
||||
label="${msg("Configure your email")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).email}
|
||||
>
|
||||
<input
|
||||
id="email-input"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="${msg("Please enter your email address.")}"
|
||||
@@ -70,8 +66,7 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.email })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
@@ -98,10 +93,13 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
A verification token has been sent to your configured email address
|
||||
${ifDefined(this.challenge.email)}
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "code-input" }, msg("Code"))}
|
||||
<ak-form-element
|
||||
label="${msg("Code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).code}
|
||||
>
|
||||
<input
|
||||
id="code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
@@ -112,8 +110,7 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -20,7 +18,6 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -36,18 +33,15 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
];
|
||||
|
||||
renderPhoneNumber(): TemplateResult {
|
||||
return html`<ak-flow-card .challenge=${this.challenge}>
|
||||
<form class="pf-c-form" @submit=${this.submitForm}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar=${this.challenge.pendingUserAvatar}
|
||||
user=${this.challenge.pendingUser}
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${this.submitForm}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
@@ -55,12 +49,12 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{ required: true, htmlFor: "phone-number-input" },
|
||||
msg("Phone number"),
|
||||
)}
|
||||
|
||||
<ak-form-element
|
||||
label="${msg("Phone number")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).phone_number}
|
||||
>
|
||||
<input
|
||||
type="tel"
|
||||
name="phoneNumber"
|
||||
@@ -70,8 +64,7 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.phone_number })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
@@ -96,10 +89,13 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "sms-code-input" }, msg("Code"))}
|
||||
<ak-form-element
|
||||
label="${msg("Code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).code}
|
||||
>
|
||||
<input
|
||||
id="sms-code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
@@ -110,8 +106,7 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
${this.renderNonFieldErrors()}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
@@ -16,7 +17,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -31,7 +31,6 @@ export class AuthenticatorStaticStage extends BaseStage<
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
css`
|
||||
@@ -65,13 +64,13 @@ export class AuthenticatorStaticStage extends BaseStage<
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<div class="pf-c-form__group">
|
||||
<ak-form-element label="" class="pf-c-form__group">
|
||||
<ul>
|
||||
${this.challenge.codes.map((token) => {
|
||||
return html`<li class="pf-m-monospace">${token}</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</ak-form-element>
|
||||
<p>${msg("Make sure to keep these tokens in a safe place.")}</p>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
import "webcomponent-qr-code";
|
||||
@@ -6,9 +7,6 @@ import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -24,7 +22,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -39,7 +36,6 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
css`
|
||||
@@ -66,8 +62,7 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
<ak-form-element>
|
||||
<div class="qr-container">
|
||||
<qr-code data="${this.challenge.configUrl}"></qr-code>
|
||||
<button
|
||||
@@ -97,16 +92,20 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
${msg("Copy")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ak-form-element>
|
||||
<p>
|
||||
${msg(
|
||||
"Please scan the QR code above using the Microsoft Authenticator, Google Authenticator, or other authenticator apps on your device, and enter the code the device displays below to finish setting up the MFA device.",
|
||||
)}
|
||||
</p>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: "totp-code-input" }, msg("Code"))}
|
||||
<ak-form-element
|
||||
label="${msg("Code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).code}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input
|
||||
id="totp-code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
@@ -118,8 +117,7 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
spellcheck="false"
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseDeviceStage } from "#flow/stages/authenticator_validate/base";
|
||||
import { PasswordManagerPrefill } from "#flow/stages/identification/IdentificationStage";
|
||||
|
||||
@@ -26,7 +24,6 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
css`
|
||||
.icon-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-description i {
|
||||
font-size: 2em;
|
||||
@@ -77,15 +74,16 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
|
||||
<p>${this.deviceMessage()}</p>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{ required: true, htmlFor: "validation-code-input" },
|
||||
this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
? msg("Static token")
|
||||
: msg("Authentication code"),
|
||||
)}
|
||||
<ak-form-element
|
||||
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
? msg("Static token")
|
||||
: msg("Authentication code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {}).code}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input
|
||||
id="validation-code-input"
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
@@ -101,8 +99,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
value="${PasswordManagerPrefill.totp || ""}"
|
||||
required
|
||||
/>
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/forms/FormElement";
|
||||
|
||||
import { BaseDeviceStage } from "#flow/stages/authenticator_validate/base";
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { property } from "lit/decorators.js";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -30,13 +29,12 @@ export class BaseDeviceStage<
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
css`
|
||||
.pf-c-form__group.pf-m-action {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
gap: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import "#elements/Divider";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/components/ak-flow-card";
|
||||
import "#flow/components/ak-flow-password-input";
|
||||
import "#flow/stages/captcha/CaptchaStage";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { renderSourceIcon } from "#admin/sources/utils";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
@@ -22,7 +20,7 @@ import {
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
@@ -60,12 +58,6 @@ export class IdentificationStage extends BaseStage<
|
||||
PFButton,
|
||||
...AkRememberMeController.styles,
|
||||
css`
|
||||
.pf-c-form__group.pf-m-action {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* login page's icons */
|
||||
.pf-c-login__main-footer-links-item button {
|
||||
background-color: transparent;
|
||||
@@ -95,14 +87,6 @@ export class IdentificationStage extends BaseStage<
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* The ID of the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, attribute: "input-id" })
|
||||
public inputID = "ak-identifier-input";
|
||||
|
||||
#form?: HTMLFormElement;
|
||||
|
||||
#rememberMe = new AkRememberMeController(this);
|
||||
@@ -317,7 +301,6 @@ export class IdentificationStage extends BaseStage<
|
||||
[UserFieldsEnum.Upn]: msg("UPN"),
|
||||
};
|
||||
const label = OR_LIST_FORMATTERS.format(fields.map((f) => uiFields[f]));
|
||||
|
||||
return html`${this.challenge.flowDesignation === FlowDesignationEnum.Recovery
|
||||
? html`
|
||||
<p>
|
||||
@@ -327,10 +310,13 @@ export class IdentificationStage extends BaseStage<
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<div class="pf-c-form__group">
|
||||
${AKLabel({ required: true, htmlFor: this.inputID }, label)}
|
||||
<ak-form-element
|
||||
label=${label}
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge.responseErrors || {}).uid_field}
|
||||
>
|
||||
<input
|
||||
id=${this.inputID}
|
||||
type=${type}
|
||||
name="uidField"
|
||||
placeholder=${label}
|
||||
@@ -342,16 +328,15 @@ export class IdentificationStage extends BaseStage<
|
||||
required
|
||||
/>
|
||||
${this.#rememberMe.render()}
|
||||
${AKFormErrors({ errors: this.challenge.responseErrors?.uid_field })}
|
||||
</div>
|
||||
</ak-form-element>
|
||||
${this.challenge.passwordFields
|
||||
? html`
|
||||
<ak-flow-input-password
|
||||
label=${msg("Password")}
|
||||
input-id="ak-stage-identification-password"
|
||||
inputId="ak-stage-identification-password"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${this.challenge?.responseErrors?.password}
|
||||
.errors=${(this.challenge?.responseErrors || {}).password}
|
||||
?allow-show-password=${this.challenge.allowShowPassword}
|
||||
prefill=${PasswordManagerPrefill.password ?? ""}
|
||||
></ak-flow-input-password>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
import "#flow/components/ak-flow-password-input";
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import "#elements/Divider";
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import { LOCALES } from "#elements/ak-locale-context/definitions";
|
||||
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
|
||||
import { AKFormErrors } from "#components/ak-field-errors";
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import {
|
||||
@@ -26,7 +24,6 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -41,7 +38,6 @@ export class PromptStage extends WithCapabilitiesConfig(
|
||||
PFAlert,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFInputGroup,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
PFCheck,
|
||||
@@ -55,13 +51,10 @@ export class PromptStage extends WithCapabilitiesConfig(
|
||||
];
|
||||
|
||||
renderPromptInner(prompt: StagePrompt): TemplateResult {
|
||||
const fieldId = `field-${prompt.fieldKey}`;
|
||||
|
||||
switch (prompt.type) {
|
||||
case PromptTypeEnum.Text:
|
||||
return html`<input
|
||||
type="text"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
autocomplete="off"
|
||||
@@ -71,7 +64,6 @@ export class PromptStage extends WithCapabilitiesConfig(
|
||||
/>`;
|
||||
case PromptTypeEnum.TextArea:
|
||||
return html`<textarea
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
autocomplete="off"
|
||||
@@ -83,7 +75,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.TextReadOnly:
|
||||
return html`<input
|
||||
type="text"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -92,7 +83,6 @@ ${prompt.initialValue}</textarea
|
||||
/>`;
|
||||
case PromptTypeEnum.TextAreaReadOnly:
|
||||
return html`<textarea
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -103,7 +93,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.Username:
|
||||
return html`<input
|
||||
type="text"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
autocomplete="username"
|
||||
@@ -115,7 +104,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.Email:
|
||||
return html`<input
|
||||
type="email"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -125,7 +113,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.Password:
|
||||
return html`<input
|
||||
type="password"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
autocomplete="new-password"
|
||||
@@ -135,7 +122,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.Number:
|
||||
return html`<input
|
||||
type="number"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -145,7 +131,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.Date:
|
||||
return html`<input
|
||||
type="date"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -155,7 +140,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.DateTime:
|
||||
return html`<input
|
||||
type="datetime"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -165,7 +149,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.File:
|
||||
return html`<input
|
||||
type="file"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
placeholder="${prompt.placeholder}"
|
||||
class="pf-c-form-control"
|
||||
@@ -177,7 +160,6 @@ ${prompt.initialValue}</textarea
|
||||
case PromptTypeEnum.Hidden:
|
||||
return html`<input
|
||||
type="hidden"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
value="${prompt.initialValue}"
|
||||
class="pf-c-form-control"
|
||||
@@ -226,11 +208,7 @@ ${prompt.initialValue}</textarea
|
||||
</option> `,
|
||||
);
|
||||
|
||||
return html`<select
|
||||
class="pf-c-form-control"
|
||||
id=${fieldId}
|
||||
name="${prompt.fieldKey}"
|
||||
>
|
||||
return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
|
||||
<option value="" ?selected=${prompt.initialValue === ""}>
|
||||
${msg("Auto-detect (based on your browser)")}
|
||||
</option>
|
||||
@@ -278,19 +256,14 @@ ${prompt.initialValue}</textarea
|
||||
</div>`;
|
||||
}
|
||||
if (this.shouldRenderInWrapper(prompt)) {
|
||||
const errors = this.challenge?.responseErrors?.[prompt.fieldKey];
|
||||
|
||||
return html`<div class="pf-c-form__group">
|
||||
${AKLabel(
|
||||
{
|
||||
required: prompt.required,
|
||||
htmlFor: `field-${prompt.fieldKey}`,
|
||||
},
|
||||
prompt.label,
|
||||
)}
|
||||
return html`<ak-form-element
|
||||
label="${prompt.label}"
|
||||
?required="${prompt.required}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
||||
>
|
||||
${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
|
||||
${AKFormErrors({ errors })}
|
||||
</div>`;
|
||||
</ak-form-element>`;
|
||||
}
|
||||
return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#elements/forms/FormElement";
|
||||
import "#flow/FormStatic";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
|
||||
@@ -1,802 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<title>authentik</title>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: clamp(16px, 3dvw, 20px);
|
||||
background-color: #000;
|
||||
background-color: Canvas;
|
||||
color: #fff;
|
||||
color: CanvasText;
|
||||
font-family: system-ui, ui-sans-serif, sans-serif;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
min-height: 100dvh;
|
||||
margin: 0;
|
||||
padding-block: 0 6em;
|
||||
padding-inline: 0.5em;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 3.5em;
|
||||
max-width: 90dvw;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container canvas {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg
|
||||
class="logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 144.29"
|
||||
aria-label="authentik"
|
||||
aria-role="heading"
|
||||
aria-level="1"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M106 41.08h25.39v101.2H106v-10.7a50 50 0 0 1-14.92 10.19 41.84 41.84 0 0 1-16.21 3.11q-19.61 0-33.91-15.21T26.64 91.86q0-23.43 13.85-38.41t33.63-15a42.78 42.78 0 0 1 17.09 3.44A46.82 46.82 0 0 1 106 52.24ZM79.29 61.91a25.65 25.65 0 0 0-19.56 8.33q-7.78 8.33-7.79 21.34t7.93 21.58a25.66 25.66 0 0 0 19.51 8.47 26.15 26.15 0 0 0 19.84-8.33q7.88-8.33 7.88-21.81 0-13.2-7.88-21.39t-19.93-8.19ZM168.39 41.08h25.67v48.74q0 14.22 2 19.76a17.24 17.24 0 0 0 6.29 8.61 18.06 18.06 0 0 0 10.65 3.07 18.6 18.6 0 0 0 10.77-3 17.7 17.7 0 0 0 6.57-8.88q1.59-4.36 1.59-18.7v-49.6h25.39V84q0 26.51-4.18 36.27a39.6 39.6 0 0 1-15.07 18.28q-10 6.38-25.3 6.37-16.65 0-26.93-7.44t-14.48-20.78q-3-9.21-3-33.49ZM297.3 3.78h25.39v37.3h15.07v21.85h-15.07v79.35H297.3V62.93h-13V41.08h13ZM362.86 2h25.21v49.3a57.74 57.74 0 0 1 15-9.63 38.56 38.56 0 0 1 15.25-3.21 34.36 34.36 0 0 1 25.39 10.42q8.83 9 8.84 26.51v66.88h-25V97.91q0-17.58-1.68-23.81t-5.71-9.3a16.07 16.07 0 0 0-10-3.07 18.85 18.85 0 0 0-13.26 5.11q-5.53 5.11-7.67 14-1.12 4.56-1.12 20.84v40.65h-25.25ZM589.91 99h-81.58q1.77 10.78 9.44 17.16t19.58 6.37a33.86 33.86 0 0 0 24.46-10l21.4 10a50.54 50.54 0 0 1-19.16 16.79q-11.16 5.44-26.51 5.44-23.82 0-38.79-15t-15-37.63q0-23.16 14.93-38.46t37.44-15.3q23.91 0 38.88 15.3t15 40.42Zm-25.4-20a25.48 25.48 0 0 0-9.92-13.77A28.81 28.81 0 0 0 537.4 60a30.42 30.42 0 0 0-18.64 5.95q-5 3.72-9.31 13.12ZM621.89 41.08h25.39v10.37q8.64-7.29 15.65-10.13a37.82 37.82 0 0 1 14.35-2.85A34.77 34.77 0 0 1 702.83 49q8.82 8.94 8.82 26.42v66.88h-25.11V98q0-18.12-1.63-24.06a16.44 16.44 0 0 0-5.66-9.06 15.8 15.8 0 0 0-10-3.11 18.73 18.73 0 0 0-13.23 5.15q-5.49 5.08-7.62 14.22-1.12 4.74-1.12 20.54v40.6h-25.39ZM750.71 3.78h25.39v37.3h15.07v21.85H776.1v79.35h-25.39V62.93h-13V41.08h13ZM826.09-.6a15.55 15.55 0 0 1 11.45 4.84A16.08 16.08 0 0 1 842.31 16a15.87 15.87 0 0 1-4.72 11.58 15.34 15.34 0 0 1-11.32 4.79 15.6 15.6 0 0 1-11.55-4.88 16.35 16.35 0 0 1-4.72-11.9 15.57 15.57 0 0 1 4.73-11.44A15.53 15.53 0 0 1 826.09-.6ZM813.39 41.08h25.39v101.2h-25.39zM873.47 2h25.39v80.8l37.39-41.72h31.89l-43.59 48.5 48.81 52.7h-31.53l-43-46.64v46.64h-25.36Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p>Server is starting up. Refreshing in a few seconds...</p>
|
||||
|
||||
<div class="container" aria-hidden="true">
|
||||
<canvas id="waves-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
let timeoutID = -1;
|
||||
|
||||
function checkStatus() {
|
||||
console.log("Checking server status...");
|
||||
|
||||
return fetch(window.location.href, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
console.log("Server is ready! Reloading...");
|
||||
clearTimeout(timeoutID);
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 503) {
|
||||
throw new Error("Server is still starting up...");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Checking server status failed: ${error}`);
|
||||
timeoutID = setTimeout(checkStatus, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// if (window.location.origin.startsWith("http") && !queryParams.has("debug")) {
|
||||
// console.log("Checking server status...");
|
||||
// checkStatus();
|
||||
// }
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
/**
|
||||
* @file Wave animation module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data offsets (11 properties per particle)
|
||||
*/
|
||||
const ParticleOffsets = {
|
||||
BASE_X: 0,
|
||||
BASE_Z: 1,
|
||||
BASE_Y: 2,
|
||||
R: 3,
|
||||
G: 4,
|
||||
B: 5,
|
||||
A: 6,
|
||||
SIZE: 7,
|
||||
PERSPECTIVE_SIZE: 8,
|
||||
HALF_SIZE: 9,
|
||||
PERSPECTIVE_DEPTH_ALPHA: 10,
|
||||
PARTICLE_SIZE: 11,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} Particle
|
||||
* @property {number} baseX
|
||||
* @property {number} baseZ
|
||||
* @property {number} baseY
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} r
|
||||
* @property {number} g
|
||||
* @property {number} b
|
||||
* @property {number} a
|
||||
* @property {number} size
|
||||
* @property {number} perspectiveSize
|
||||
* @property {number} halfSize
|
||||
* @property {number} perspectiveDepthAlpha
|
||||
*/
|
||||
class WavesCanvas {
|
||||
//#region Properties
|
||||
|
||||
width = 0;
|
||||
height = 0;
|
||||
dpi = Math.max(1, devicePixelRatio);
|
||||
|
||||
// Wave parameters
|
||||
turbulence = Math.PI * 6.0;
|
||||
pointSize = 1 * this.dpi;
|
||||
pointSizeCutoff = 5;
|
||||
speed = 10;
|
||||
waveHeight = 5;
|
||||
distance = 3;
|
||||
fov = (60 * Math.PI) / 180;
|
||||
|
||||
aspectRatio = 1;
|
||||
cameraZ = 95;
|
||||
|
||||
lodThreshold = this.cameraZ * 0.9;
|
||||
|
||||
/**
|
||||
* Tangent of the field of view, i.e the angle between the horizon and the camera
|
||||
*/
|
||||
f = Math.tan(Math.PI * 0.5 - 0.5 * this.fov);
|
||||
|
||||
/**
|
||||
* Flat array storing particle data: [baseX, baseZ, baseY, r, g, b, a, size, perspectiveSize, halfSize, perspectiveDepthAlpha]
|
||||
* @type {Float32Array}
|
||||
*/
|
||||
#particleData = new Float32Array(0);
|
||||
#particleOffsetCount = this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Performance optimizations
|
||||
/**
|
||||
* @type {ImageData}
|
||||
*/
|
||||
// @ts-expect-error - Assigned in resize listener
|
||||
#buffer;
|
||||
|
||||
#gridWidth = 0;
|
||||
#gridDepth = 0;
|
||||
#fieldWidth = 0;
|
||||
#fieldHeight = 0;
|
||||
#fieldDepth = 0;
|
||||
|
||||
#relativeWidth = 0;
|
||||
#relativeHeight = 0;
|
||||
|
||||
// Frustum culling bounds
|
||||
#frustumLeft = 0;
|
||||
#frustumRight = 0;
|
||||
#frustumTop = 0;
|
||||
#frustumBottom = 0;
|
||||
#frustumNear = 0;
|
||||
#frustumFar = 0;
|
||||
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
#canvas;
|
||||
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
#ctx;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* @param {string | HTMLCanvasElement} target
|
||||
*/
|
||||
constructor(target) {
|
||||
const element =
|
||||
typeof target === "string" ? document.querySelector(target) : target;
|
||||
|
||||
if (!(element instanceof HTMLCanvasElement)) {
|
||||
throw new TypeError("Invalid canvas element");
|
||||
}
|
||||
|
||||
this.#canvas = element;
|
||||
|
||||
const ctx = this.#canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
desynchronized: true,
|
||||
});
|
||||
|
||||
if (!ctx) {
|
||||
throw new TypeError("Failed to get 2D context");
|
||||
}
|
||||
|
||||
this.#ctx = ctx;
|
||||
|
||||
// We apply a background color to the canvas element itself for a slight performance boost.
|
||||
this.#canvas.style.background = getComputedStyle(document.body).backgroundColor;
|
||||
// All containment rules are applied to the element to further improve performance.
|
||||
this.#canvas.style.contain = "strict";
|
||||
// Applying a null transform on the canvas forces the browser to use the GPU for rendering.
|
||||
this.#canvas.style.willChange = "transform";
|
||||
this.#canvas.style.transform = "translate3d(0, 0, 0)";
|
||||
this.#canvas.style.pointerEvents = "none";
|
||||
|
||||
window.addEventListener("resize", this.refresh);
|
||||
const colorSchemeChangeListener = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
);
|
||||
|
||||
colorSchemeChangeListener.addEventListener("change", this.refresh);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const container = this.#canvas.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
this.width = container.offsetWidth;
|
||||
this.height = container.offsetHeight;
|
||||
this.aspectRatio = this.width / this.height;
|
||||
|
||||
this.#relativeWidth = this.width * this.dpi;
|
||||
this.#relativeHeight = this.height * this.dpi;
|
||||
|
||||
this.#canvas.width = this.#relativeWidth;
|
||||
this.#canvas.height = this.#relativeHeight;
|
||||
|
||||
this.#canvas.style.width = this.width + "px";
|
||||
this.#canvas.style.height = this.height + "px";
|
||||
|
||||
this.#ctx.scale(this.dpi, this.dpi);
|
||||
|
||||
this.#ctx.fillStyle = getComputedStyle(document.body).backgroundColor;
|
||||
|
||||
this.#buffer = this.#ctx.createImageData(
|
||||
this.width * this.dpi,
|
||||
this.height * this.dpi,
|
||||
);
|
||||
|
||||
this.#gridWidth = 300 * this.aspectRatio;
|
||||
this.#gridDepth = 300;
|
||||
|
||||
this.#fieldWidth = this.#gridWidth;
|
||||
this.#fieldHeight = this.waveHeight * this.aspectRatio;
|
||||
this.#fieldDepth = this.#gridDepth;
|
||||
|
||||
this.#calculateFrustumBounds();
|
||||
this.#generateParticles();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Frustum Culling
|
||||
|
||||
#calculateFrustumBounds() {
|
||||
const halfFov = this.fov / 2;
|
||||
const tanHalfFov = Math.tan(halfFov);
|
||||
|
||||
// Calculate frustum bounds at different depths
|
||||
this.#frustumNear = 1;
|
||||
this.#frustumFar = this.cameraZ + this.#gridDepth;
|
||||
|
||||
// At near plane
|
||||
const nearHeight = 2 * this.#frustumNear * tanHalfFov;
|
||||
const nearWidth = nearHeight * this.aspectRatio;
|
||||
|
||||
// Store half-widths and half-heights for faster testing
|
||||
this.#frustumLeft = -nearWidth / 2;
|
||||
this.#frustumRight = nearWidth / 2;
|
||||
this.#frustumTop = nearHeight / 2;
|
||||
this.#frustumBottom = -nearHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a 3D point is within the view frustum
|
||||
* @param {number} x - World X coordinate
|
||||
* @param {number} y - World Y coordinate
|
||||
* @param {number} z - World Z coordinate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isInFrustum(x, y, z) {
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
// Behind camera or too far
|
||||
if (projectedZ <= this.#frustumNear || projectedZ > this.#frustumFar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate frustum bounds at this depth
|
||||
const scale = projectedZ / this.#frustumNear;
|
||||
const left = this.#frustumLeft * scale;
|
||||
const right = this.#frustumRight * scale;
|
||||
const top = this.#frustumTop * scale;
|
||||
const bottom = this.#frustumBottom * scale;
|
||||
|
||||
// Test if point is within frustum bounds
|
||||
return x >= left && x <= right && y >= bottom && y <= top;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Particle Generation
|
||||
|
||||
#generateParticles() {
|
||||
const particleCount =
|
||||
Math.floor(this.#gridWidth / this.distance) *
|
||||
Math.floor(this.#gridDepth / this.distance);
|
||||
|
||||
this.#particleData = new Float32Array(
|
||||
particleCount * ParticleOffsets.PARTICLE_SIZE,
|
||||
);
|
||||
this.#particleOffsetCount =
|
||||
this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
let particleIndex = 0;
|
||||
|
||||
for (let x = 0; x < this.#gridWidth; x += this.distance) {
|
||||
const baseX = Math.round(-this.#gridWidth / 2 + x);
|
||||
|
||||
for (let z = 0; z < this.#gridDepth; z += this.distance) {
|
||||
const baseZ = -this.#gridDepth / 2 + z;
|
||||
const depthFactor = (z / this.#gridDepth) * 0.9;
|
||||
const horizonFactor = (x / this.#gridWidth) * 1;
|
||||
|
||||
// Pre-calculate perspective size based on fixed depth
|
||||
const projectedZ = baseZ + this.cameraZ;
|
||||
const baseSize = (this.height / 250) * this.pointSize * this.dpi;
|
||||
|
||||
// More exaggerated scaling with logarithmic falloff for horizon effect
|
||||
const normalizedDepth = Math.max(0, (projectedZ - 50) / 200);
|
||||
// Log scale for dramatic falloff
|
||||
const logScale = Math.log(1 + normalizedDepth * 4) / Math.log(6);
|
||||
// Invert so closer = larger
|
||||
const distanceFactor = Math.max(0.05, 1 - logScale);
|
||||
const perspectiveSize = baseSize * distanceFactor * 3;
|
||||
|
||||
const alpha = Math.max(Math.floor(z / this.#gridDepth), 0.85);
|
||||
const depthAlpha = Math.max(0.6, Math.pow(distanceFactor, 2));
|
||||
|
||||
const offset = particleIndex * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Store particle data in flat array
|
||||
this.#particleData[offset + ParticleOffsets.BASE_X] = baseX;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Z] = baseZ;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Y] =
|
||||
this.cameraZ * -0.4;
|
||||
this.#particleData[offset + ParticleOffsets.R] = Math.min(
|
||||
Math.floor(253 / (depthFactor * horizonFactor)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.G] = Math.min(
|
||||
Math.floor(75 / horizonFactor),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.B] = Math.min(
|
||||
Math.floor(45 / (depthFactor * horizonFactor * 2)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.A] = alpha;
|
||||
this.#particleData[offset + ParticleOffsets.SIZE] = baseSize;
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE] =
|
||||
perspectiveSize;
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE] = Math.max(
|
||||
1,
|
||||
Math.round(perspectiveSize / 1.5),
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA] =
|
||||
depthAlpha ** 2;
|
||||
|
||||
particleIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Projection
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
* @returns {{x: number, y: number, z: number} | null}
|
||||
*/
|
||||
project3DTo2D(x, y, z) {
|
||||
const projectedX = (x * this.f) / this.aspectRatio;
|
||||
const projectedY = y * this.f;
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
if (projectedZ <= 0) return null;
|
||||
|
||||
// Convert to screen coordinates
|
||||
// top of canvas is horizon (y=0), bottom is near
|
||||
|
||||
const screenX = ((projectedX / projectedZ) * this.width) / 2 + this.width / 2;
|
||||
const screenY =
|
||||
this.height / 2 -
|
||||
((projectedY / projectedZ) * this.height) / 2 -
|
||||
this.cameraZ * 2;
|
||||
|
||||
return { x: screenX, y: screenY, z: projectedZ };
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} z
|
||||
* @param {number} time
|
||||
* @returns {number}
|
||||
*/
|
||||
calculateWaveY(x, z, time) {
|
||||
return (
|
||||
(Math.cos((x / this.#fieldWidth) * this.turbulence + time * this.speed) +
|
||||
Math.sin(
|
||||
(z / this.#fieldDepth) * this.turbulence + time * this.speed,
|
||||
)) *
|
||||
this.#fieldHeight
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a simple point particle (for distant objects)
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawPointParticle(x, y, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
if (centerX >= 0 && centerX < width && centerY >= 0 && centerY < height) {
|
||||
const index = (centerY * width + centerX) * 4;
|
||||
const alpha = Math.round(a * 255);
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
data[index] = Math.min(255, data[index] + (r * blendFactor) / 255);
|
||||
data[index + 1] = Math.min(255, data[index + 1] + (g * blendFactor) / 255);
|
||||
data[index + 2] = Math.min(255, data[index + 2] + (b * blendFactor) / 255);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a hollow circle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawCircleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
const radius = halfSize;
|
||||
|
||||
// Draw hollow circle - just the outline
|
||||
for (let py = -radius; py <= radius; py++) {
|
||||
for (let px = -radius; px <= radius; px++) {
|
||||
const distance = Math.sqrt(px * px + py * py);
|
||||
|
||||
// Only draw pixels that are on the circle edge (within 1 pixel of radius)
|
||||
if (distance >= radius - 1 && distance <= radius) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a filled circle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawFilledCircleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
const radius = halfSize;
|
||||
|
||||
// Draw solid circle
|
||||
for (let py = -radius; py <= radius; py++) {
|
||||
for (let px = -radius; px <= radius; px++) {
|
||||
const distance = Math.sqrt(px * px + py * py);
|
||||
|
||||
// Draw all pixels within the radius
|
||||
if (distance <= radius) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an isosceles triangle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawTriangleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
|
||||
// Draw hollow isosceles triangle pointing up - just the outline
|
||||
for (let py = -halfSize; py <= halfSize; py++) {
|
||||
// Calculate max width at this height - steeper taper for more triangular shape
|
||||
// 0 at top, 1 at bottom
|
||||
const normalizedHeight = (py + halfSize) / (2 * halfSize);
|
||||
const triangleWidth = Math.round(halfSize * normalizedHeight);
|
||||
|
||||
for (let px = -triangleWidth; px <= triangleWidth; px++) {
|
||||
// Only draw if on the edge (left edge, right edge, or bottom edge)
|
||||
const isLeftEdge = px === -triangleWidth;
|
||||
const isRightEdge = px === triangleWidth;
|
||||
const isBottomEdge = py === halfSize;
|
||||
|
||||
if (isLeftEdge || isRightEdge || isBottomEdge) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#render = (time = performance.now()) => {
|
||||
const timestamp = time / 6000;
|
||||
this.#buffer.data.fill(0);
|
||||
|
||||
for (let i = 0; i < this.#particleOffsetCount; i++) {
|
||||
const offset = i * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
const baseX = this.#particleData[offset + ParticleOffsets.BASE_X];
|
||||
const baseZ = this.#particleData[offset + ParticleOffsets.BASE_Z];
|
||||
const baseY = this.#particleData[offset + ParticleOffsets.BASE_Y];
|
||||
|
||||
const waveY = this.calculateWaveY(baseX, baseZ, timestamp);
|
||||
const worldY = baseY + waveY;
|
||||
|
||||
if (!this.#isInFrustum(baseX, worldY, baseZ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const projected = this.project3DTo2D(baseX, worldY, baseZ);
|
||||
|
||||
if (
|
||||
!projected ||
|
||||
projected.x < -50 ||
|
||||
projected.x > this.width + 50 ||
|
||||
projected.y < 0 ||
|
||||
projected.y > this.height + 50
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = Math.abs(projected.z - this.cameraZ);
|
||||
const perspectiveSize =
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE];
|
||||
|
||||
if (
|
||||
distance > this.lodThreshold ||
|
||||
perspectiveSize < this.pointSizeCutoff
|
||||
) {
|
||||
this.#drawPointParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[
|
||||
offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// Close enough for detailed triangle
|
||||
this.#drawFilledCircleParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE],
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[
|
||||
offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#ctx.putImageData(this.#buffer, 0, 0);
|
||||
this.#ctx.globalCompositeOperation = "soft-light";
|
||||
this.#ctx.fillRect(0, 0, this.#relativeWidth, this.#relativeHeight);
|
||||
this.#ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
this.#renderFrameID = requestAnimationFrame(this.#render);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public Methods
|
||||
|
||||
#renderFrameID = -1;
|
||||
|
||||
play = () => {
|
||||
this.refresh();
|
||||
|
||||
this.#render();
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
cancelAnimationFrame(this.#renderFrameID);
|
||||
};
|
||||
}
|
||||
|
||||
function drawAnimation() {
|
||||
const canvas = document.getElementById("waves-canvas");
|
||||
const waves = new WavesCanvas(canvas);
|
||||
waves.play();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", drawAnimation);
|
||||
} else {
|
||||
drawAnimation();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,802 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<title>authentik</title>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: clamp(16px, 3dvw, 20px);
|
||||
background-color: #000;
|
||||
background-color: Canvas;
|
||||
color: #fff;
|
||||
color: CanvasText;
|
||||
font-family: system-ui, ui-sans-serif, sans-serif;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
min-height: 100dvh;
|
||||
margin: 0;
|
||||
padding-block: 0 6em;
|
||||
padding-inline: 0.5em;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 3.5em;
|
||||
max-width: 90dvw;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container canvas {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg
|
||||
class="logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 144.29"
|
||||
aria-label="authentik"
|
||||
aria-role="heading"
|
||||
aria-level="1"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M106 41.08h25.39v101.2H106v-10.7a50 50 0 0 1-14.92 10.19 41.84 41.84 0 0 1-16.21 3.11q-19.61 0-33.91-15.21T26.64 91.86q0-23.43 13.85-38.41t33.63-15a42.78 42.78 0 0 1 17.09 3.44A46.82 46.82 0 0 1 106 52.24ZM79.29 61.91a25.65 25.65 0 0 0-19.56 8.33q-7.78 8.33-7.79 21.34t7.93 21.58a25.66 25.66 0 0 0 19.51 8.47 26.15 26.15 0 0 0 19.84-8.33q7.88-8.33 7.88-21.81 0-13.2-7.88-21.39t-19.93-8.19ZM168.39 41.08h25.67v48.74q0 14.22 2 19.76a17.24 17.24 0 0 0 6.29 8.61 18.06 18.06 0 0 0 10.65 3.07 18.6 18.6 0 0 0 10.77-3 17.7 17.7 0 0 0 6.57-8.88q1.59-4.36 1.59-18.7v-49.6h25.39V84q0 26.51-4.18 36.27a39.6 39.6 0 0 1-15.07 18.28q-10 6.38-25.3 6.37-16.65 0-26.93-7.44t-14.48-20.78q-3-9.21-3-33.49ZM297.3 3.78h25.39v37.3h15.07v21.85h-15.07v79.35H297.3V62.93h-13V41.08h13ZM362.86 2h25.21v49.3a57.74 57.74 0 0 1 15-9.63 38.56 38.56 0 0 1 15.25-3.21 34.36 34.36 0 0 1 25.39 10.42q8.83 9 8.84 26.51v66.88h-25V97.91q0-17.58-1.68-23.81t-5.71-9.3a16.07 16.07 0 0 0-10-3.07 18.85 18.85 0 0 0-13.26 5.11q-5.53 5.11-7.67 14-1.12 4.56-1.12 20.84v40.65h-25.25ZM589.91 99h-81.58q1.77 10.78 9.44 17.16t19.58 6.37a33.86 33.86 0 0 0 24.46-10l21.4 10a50.54 50.54 0 0 1-19.16 16.79q-11.16 5.44-26.51 5.44-23.82 0-38.79-15t-15-37.63q0-23.16 14.93-38.46t37.44-15.3q23.91 0 38.88 15.3t15 40.42Zm-25.4-20a25.48 25.48 0 0 0-9.92-13.77A28.81 28.81 0 0 0 537.4 60a30.42 30.42 0 0 0-18.64 5.95q-5 3.72-9.31 13.12ZM621.89 41.08h25.39v10.37q8.64-7.29 15.65-10.13a37.82 37.82 0 0 1 14.35-2.85A34.77 34.77 0 0 1 702.83 49q8.82 8.94 8.82 26.42v66.88h-25.11V98q0-18.12-1.63-24.06a16.44 16.44 0 0 0-5.66-9.06 15.8 15.8 0 0 0-10-3.11 18.73 18.73 0 0 0-13.23 5.15q-5.49 5.08-7.62 14.22-1.12 4.74-1.12 20.54v40.6h-25.39ZM750.71 3.78h25.39v37.3h15.07v21.85H776.1v79.35h-25.39V62.93h-13V41.08h13ZM826.09-.6a15.55 15.55 0 0 1 11.45 4.84A16.08 16.08 0 0 1 842.31 16a15.87 15.87 0 0 1-4.72 11.58 15.34 15.34 0 0 1-11.32 4.79 15.6 15.6 0 0 1-11.55-4.88 16.35 16.35 0 0 1-4.72-11.9 15.57 15.57 0 0 1 4.73-11.44A15.53 15.53 0 0 1 826.09-.6ZM813.39 41.08h25.39v101.2h-25.39zM873.47 2h25.39v80.8l37.39-41.72h31.89l-43.59 48.5 48.81 52.7h-31.53l-43-46.64v46.64h-25.36Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p>Server is starting up. Refreshing in a few seconds...</p>
|
||||
|
||||
<div class="container" aria-hidden="true">
|
||||
<canvas id="waves-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
let timeoutID = -1;
|
||||
|
||||
function checkStatus() {
|
||||
console.log("Checking server status...");
|
||||
|
||||
return fetch(window.location.href, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
console.log("Server is ready! Reloading...");
|
||||
clearTimeout(timeoutID);
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 503) {
|
||||
throw new Error("Server is still starting up...");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Checking server status failed: ${error}`);
|
||||
timeoutID = setTimeout(checkStatus, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// if (window.location.origin.startsWith("http") && !queryParams.has("debug")) {
|
||||
// console.log("Checking server status...");
|
||||
// checkStatus();
|
||||
// }
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
/**
|
||||
* @file Wave animation module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data offsets (11 properties per particle)
|
||||
*/
|
||||
const ParticleOffsets = {
|
||||
BASE_X: 0,
|
||||
BASE_Z: 1,
|
||||
BASE_Y: 2,
|
||||
R: 3,
|
||||
G: 4,
|
||||
B: 5,
|
||||
A: 6,
|
||||
SIZE: 7,
|
||||
PERSPECTIVE_SIZE: 8,
|
||||
HALF_SIZE: 9,
|
||||
PERSPECTIVE_DEPTH_ALPHA: 10,
|
||||
PARTICLE_SIZE: 11,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} Particle
|
||||
* @property {number} baseX
|
||||
* @property {number} baseZ
|
||||
* @property {number} baseY
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} r
|
||||
* @property {number} g
|
||||
* @property {number} b
|
||||
* @property {number} a
|
||||
* @property {number} size
|
||||
* @property {number} perspectiveSize
|
||||
* @property {number} halfSize
|
||||
* @property {number} perspectiveDepthAlpha
|
||||
*/
|
||||
class WavesCanvas {
|
||||
//#region Properties
|
||||
|
||||
width = 0;
|
||||
height = 0;
|
||||
dpi = Math.max(1, devicePixelRatio);
|
||||
|
||||
// Wave parameters
|
||||
turbulence = Math.PI * 6.0;
|
||||
pointSize = 1 * this.dpi;
|
||||
pointSizeCutoff = 5;
|
||||
speed = 10;
|
||||
waveHeight = 5;
|
||||
distance = 3;
|
||||
fov = (60 * Math.PI) / 180;
|
||||
|
||||
aspectRatio = 1;
|
||||
cameraZ = 95;
|
||||
|
||||
lodThreshold = this.cameraZ * 0.9;
|
||||
|
||||
/**
|
||||
* Tangent of the field of view, i.e the angle between the horizon and the camera
|
||||
*/
|
||||
f = Math.tan(Math.PI * 0.5 - 0.5 * this.fov);
|
||||
|
||||
/**
|
||||
* Flat array storing particle data: [baseX, baseZ, baseY, r, g, b, a, size, perspectiveSize, halfSize, perspectiveDepthAlpha]
|
||||
* @type {Float32Array}
|
||||
*/
|
||||
#particleData = new Float32Array(0);
|
||||
#particleOffsetCount = this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Performance optimizations
|
||||
/**
|
||||
* @type {ImageData}
|
||||
*/
|
||||
// @ts-expect-error - Assigned in resize listener
|
||||
#buffer;
|
||||
|
||||
#gridWidth = 0;
|
||||
#gridDepth = 0;
|
||||
#fieldWidth = 0;
|
||||
#fieldHeight = 0;
|
||||
#fieldDepth = 0;
|
||||
|
||||
#relativeWidth = 0;
|
||||
#relativeHeight = 0;
|
||||
|
||||
// Frustum culling bounds
|
||||
#frustumLeft = 0;
|
||||
#frustumRight = 0;
|
||||
#frustumTop = 0;
|
||||
#frustumBottom = 0;
|
||||
#frustumNear = 0;
|
||||
#frustumFar = 0;
|
||||
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
#canvas;
|
||||
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
#ctx;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* @param {string | HTMLCanvasElement} target
|
||||
*/
|
||||
constructor(target) {
|
||||
const element =
|
||||
typeof target === "string" ? document.querySelector(target) : target;
|
||||
|
||||
if (!(element instanceof HTMLCanvasElement)) {
|
||||
throw new TypeError("Invalid canvas element");
|
||||
}
|
||||
|
||||
this.#canvas = element;
|
||||
|
||||
const ctx = this.#canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
desynchronized: true,
|
||||
});
|
||||
|
||||
if (!ctx) {
|
||||
throw new TypeError("Failed to get 2D context");
|
||||
}
|
||||
|
||||
this.#ctx = ctx;
|
||||
|
||||
// We apply a background color to the canvas element itself for a slight performance boost.
|
||||
this.#canvas.style.background = getComputedStyle(document.body).backgroundColor;
|
||||
// All containment rules are applied to the element to further improve performance.
|
||||
this.#canvas.style.contain = "strict";
|
||||
// Applying a null transform on the canvas forces the browser to use the GPU for rendering.
|
||||
this.#canvas.style.willChange = "transform";
|
||||
this.#canvas.style.transform = "translate3d(0, 0, 0)";
|
||||
this.#canvas.style.pointerEvents = "none";
|
||||
|
||||
window.addEventListener("resize", this.refresh);
|
||||
const colorSchemeChangeListener = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
);
|
||||
|
||||
colorSchemeChangeListener.addEventListener("change", this.refresh);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const container = this.#canvas.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
this.width = container.offsetWidth;
|
||||
this.height = container.offsetHeight;
|
||||
this.aspectRatio = this.width / this.height;
|
||||
|
||||
this.#relativeWidth = this.width * this.dpi;
|
||||
this.#relativeHeight = this.height * this.dpi;
|
||||
|
||||
this.#canvas.width = this.#relativeWidth;
|
||||
this.#canvas.height = this.#relativeHeight;
|
||||
|
||||
this.#canvas.style.width = this.width + "px";
|
||||
this.#canvas.style.height = this.height + "px";
|
||||
|
||||
this.#ctx.scale(this.dpi, this.dpi);
|
||||
|
||||
this.#ctx.fillStyle = getComputedStyle(document.body).backgroundColor;
|
||||
|
||||
this.#buffer = this.#ctx.createImageData(
|
||||
this.width * this.dpi,
|
||||
this.height * this.dpi,
|
||||
);
|
||||
|
||||
this.#gridWidth = 300 * this.aspectRatio;
|
||||
this.#gridDepth = 300;
|
||||
|
||||
this.#fieldWidth = this.#gridWidth;
|
||||
this.#fieldHeight = this.waveHeight * this.aspectRatio;
|
||||
this.#fieldDepth = this.#gridDepth;
|
||||
|
||||
this.#calculateFrustumBounds();
|
||||
this.#generateParticles();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Frustum Culling
|
||||
|
||||
#calculateFrustumBounds() {
|
||||
const halfFov = this.fov / 2;
|
||||
const tanHalfFov = Math.tan(halfFov);
|
||||
|
||||
// Calculate frustum bounds at different depths
|
||||
this.#frustumNear = 1;
|
||||
this.#frustumFar = this.cameraZ + this.#gridDepth;
|
||||
|
||||
// At near plane
|
||||
const nearHeight = 2 * this.#frustumNear * tanHalfFov;
|
||||
const nearWidth = nearHeight * this.aspectRatio;
|
||||
|
||||
// Store half-widths and half-heights for faster testing
|
||||
this.#frustumLeft = -nearWidth / 2;
|
||||
this.#frustumRight = nearWidth / 2;
|
||||
this.#frustumTop = nearHeight / 2;
|
||||
this.#frustumBottom = -nearHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a 3D point is within the view frustum
|
||||
* @param {number} x - World X coordinate
|
||||
* @param {number} y - World Y coordinate
|
||||
* @param {number} z - World Z coordinate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isInFrustum(x, y, z) {
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
// Behind camera or too far
|
||||
if (projectedZ <= this.#frustumNear || projectedZ > this.#frustumFar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate frustum bounds at this depth
|
||||
const scale = projectedZ / this.#frustumNear;
|
||||
const left = this.#frustumLeft * scale;
|
||||
const right = this.#frustumRight * scale;
|
||||
const top = this.#frustumTop * scale;
|
||||
const bottom = this.#frustumBottom * scale;
|
||||
|
||||
// Test if point is within frustum bounds
|
||||
return x >= left && x <= right && y >= bottom && y <= top;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Particle Generation
|
||||
|
||||
#generateParticles() {
|
||||
const particleCount =
|
||||
Math.floor(this.#gridWidth / this.distance) *
|
||||
Math.floor(this.#gridDepth / this.distance);
|
||||
|
||||
this.#particleData = new Float32Array(
|
||||
particleCount * ParticleOffsets.PARTICLE_SIZE,
|
||||
);
|
||||
this.#particleOffsetCount =
|
||||
this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
let particleIndex = 0;
|
||||
|
||||
for (let x = 0; x < this.#gridWidth; x += this.distance) {
|
||||
const baseX = Math.round(-this.#gridWidth / 2 + x);
|
||||
|
||||
for (let z = 0; z < this.#gridDepth; z += this.distance) {
|
||||
const baseZ = -this.#gridDepth / 2 + z;
|
||||
const depthFactor = (z / this.#gridDepth) * 0.9;
|
||||
const horizonFactor = (x / this.#gridWidth) * 1;
|
||||
|
||||
// Pre-calculate perspective size based on fixed depth
|
||||
const projectedZ = baseZ + this.cameraZ;
|
||||
const baseSize = (this.height / 250) * this.pointSize * this.dpi;
|
||||
|
||||
// More exaggerated scaling with logarithmic falloff for horizon effect
|
||||
const normalizedDepth = Math.max(0, (projectedZ - 50) / 200);
|
||||
// Log scale for dramatic falloff
|
||||
const logScale = Math.log(1 + normalizedDepth * 4) / Math.log(6);
|
||||
// Invert so closer = larger
|
||||
const distanceFactor = Math.max(0.05, 1 - logScale);
|
||||
const perspectiveSize = baseSize * distanceFactor * 3;
|
||||
|
||||
const alpha = Math.max(Math.floor(z / this.#gridDepth), 0.85);
|
||||
const depthAlpha = Math.max(0.6, Math.pow(distanceFactor, 2));
|
||||
|
||||
const offset = particleIndex * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Store particle data in flat array
|
||||
this.#particleData[offset + ParticleOffsets.BASE_X] = baseX;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Z] = baseZ;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Y] =
|
||||
this.cameraZ * -0.4;
|
||||
this.#particleData[offset + ParticleOffsets.R] = Math.min(
|
||||
Math.floor(253 / (depthFactor * horizonFactor)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.G] = Math.min(
|
||||
Math.floor(75 / horizonFactor),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.B] = Math.min(
|
||||
Math.floor(45 / (depthFactor * horizonFactor * 2)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.A] = alpha;
|
||||
this.#particleData[offset + ParticleOffsets.SIZE] = baseSize;
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE] =
|
||||
perspectiveSize;
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE] = Math.max(
|
||||
1,
|
||||
Math.round(perspectiveSize / 1.5),
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA] =
|
||||
depthAlpha ** 2;
|
||||
|
||||
particleIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Projection
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
* @returns {{x: number, y: number, z: number} | null}
|
||||
*/
|
||||
project3DTo2D(x, y, z) {
|
||||
const projectedX = (x * this.f) / this.aspectRatio;
|
||||
const projectedY = y * this.f;
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
if (projectedZ <= 0) return null;
|
||||
|
||||
// Convert to screen coordinates
|
||||
// top of canvas is horizon (y=0), bottom is near
|
||||
|
||||
const screenX = ((projectedX / projectedZ) * this.width) / 2 + this.width / 2;
|
||||
const screenY =
|
||||
this.height / 2 -
|
||||
((projectedY / projectedZ) * this.height) / 2 -
|
||||
this.cameraZ * 2;
|
||||
|
||||
return { x: screenX, y: screenY, z: projectedZ };
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} z
|
||||
* @param {number} time
|
||||
* @returns {number}
|
||||
*/
|
||||
calculateWaveY(x, z, time) {
|
||||
return (
|
||||
(Math.cos((x / this.#fieldWidth) * this.turbulence + time * this.speed) +
|
||||
Math.sin(
|
||||
(z / this.#fieldDepth) * this.turbulence + time * this.speed,
|
||||
)) *
|
||||
this.#fieldHeight
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a simple point particle (for distant objects)
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawPointParticle(x, y, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
if (centerX >= 0 && centerX < width && centerY >= 0 && centerY < height) {
|
||||
const index = (centerY * width + centerX) * 4;
|
||||
const alpha = Math.round(a * 255);
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
data[index] = Math.min(255, data[index] + (r * blendFactor) / 255);
|
||||
data[index + 1] = Math.min(255, data[index + 1] + (g * blendFactor) / 255);
|
||||
data[index + 2] = Math.min(255, data[index + 2] + (b * blendFactor) / 255);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a hollow circle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawCircleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
const radius = halfSize;
|
||||
|
||||
// Draw hollow circle - just the outline
|
||||
for (let py = -radius; py <= radius; py++) {
|
||||
for (let px = -radius; px <= radius; px++) {
|
||||
const distance = Math.sqrt(px * px + py * py);
|
||||
|
||||
// Only draw pixels that are on the circle edge (within 1 pixel of radius)
|
||||
if (distance >= radius - 1 && distance <= radius) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a filled circle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawFilledCircleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
const radius = halfSize;
|
||||
|
||||
// Draw solid circle
|
||||
for (let py = -radius; py <= radius; py++) {
|
||||
for (let px = -radius; px <= radius; px++) {
|
||||
const distance = Math.sqrt(px * px + py * py);
|
||||
|
||||
// Draw all pixels within the radius
|
||||
if (distance <= radius) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an isosceles triangle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawTriangleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
|
||||
// Draw hollow isosceles triangle pointing up - just the outline
|
||||
for (let py = -halfSize; py <= halfSize; py++) {
|
||||
// Calculate max width at this height - steeper taper for more triangular shape
|
||||
// 0 at top, 1 at bottom
|
||||
const normalizedHeight = (py + halfSize) / (2 * halfSize);
|
||||
const triangleWidth = Math.round(halfSize * normalizedHeight);
|
||||
|
||||
for (let px = -triangleWidth; px <= triangleWidth; px++) {
|
||||
// Only draw if on the edge (left edge, right edge, or bottom edge)
|
||||
const isLeftEdge = px === -triangleWidth;
|
||||
const isRightEdge = px === triangleWidth;
|
||||
const isBottomEdge = py === halfSize;
|
||||
|
||||
if (isLeftEdge || isRightEdge || isBottomEdge) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#render = (time = performance.now()) => {
|
||||
const timestamp = time / 6000;
|
||||
this.#buffer.data.fill(0);
|
||||
|
||||
for (let i = 0; i < this.#particleOffsetCount; i++) {
|
||||
const offset = i * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
const baseX = this.#particleData[offset + ParticleOffsets.BASE_X];
|
||||
const baseZ = this.#particleData[offset + ParticleOffsets.BASE_Z];
|
||||
const baseY = this.#particleData[offset + ParticleOffsets.BASE_Y];
|
||||
|
||||
const waveY = this.calculateWaveY(baseX, baseZ, timestamp);
|
||||
const worldY = baseY + waveY;
|
||||
|
||||
if (!this.#isInFrustum(baseX, worldY, baseZ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const projected = this.project3DTo2D(baseX, worldY, baseZ);
|
||||
|
||||
if (
|
||||
!projected ||
|
||||
projected.x < -50 ||
|
||||
projected.x > this.width + 50 ||
|
||||
projected.y < 0 ||
|
||||
projected.y > this.height + 50
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = Math.abs(projected.z - this.cameraZ);
|
||||
const perspectiveSize =
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE];
|
||||
|
||||
if (
|
||||
distance > this.lodThreshold ||
|
||||
perspectiveSize < this.pointSizeCutoff
|
||||
) {
|
||||
this.#drawPointParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[
|
||||
offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// Close enough for detailed triangle
|
||||
this.#drawCircleParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE],
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[
|
||||
offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#ctx.putImageData(this.#buffer, 0, 0);
|
||||
this.#ctx.globalCompositeOperation = "soft-light";
|
||||
this.#ctx.fillRect(0, 0, this.#relativeWidth, this.#relativeHeight);
|
||||
this.#ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
this.#renderFrameID = requestAnimationFrame(this.#render);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public Methods
|
||||
|
||||
#renderFrameID = -1;
|
||||
|
||||
play = () => {
|
||||
this.refresh();
|
||||
|
||||
this.#render();
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
cancelAnimationFrame(this.#renderFrameID);
|
||||
};
|
||||
}
|
||||
|
||||
function drawAnimation() {
|
||||
const canvas = document.getElementById("waves-canvas");
|
||||
const waves = new WavesCanvas(canvas);
|
||||
waves.play();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", drawAnimation);
|
||||
} else {
|
||||
drawAnimation();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,750 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<title>authentik</title>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: clamp(16px, 3dvw, 20px);
|
||||
background-color: #000;
|
||||
background-color: Canvas;
|
||||
color: #fff;
|
||||
color: CanvasText;
|
||||
font-family: system-ui, ui-sans-serif, sans-serif;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
min-height: 100dvh;
|
||||
margin: 0;
|
||||
padding-block: 0 6em;
|
||||
padding-inline: 0.5em;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 3.5em;
|
||||
max-width: 90dvw;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container canvas {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg
|
||||
class="logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 144.29"
|
||||
aria-label="authentik"
|
||||
aria-role="heading"
|
||||
aria-level="1"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M106 41.08h25.39v101.2H106v-10.7a50 50 0 0 1-14.92 10.19 41.84 41.84 0 0 1-16.21 3.11q-19.61 0-33.91-15.21T26.64 91.86q0-23.43 13.85-38.41t33.63-15a42.78 42.78 0 0 1 17.09 3.44A46.82 46.82 0 0 1 106 52.24ZM79.29 61.91a25.65 25.65 0 0 0-19.56 8.33q-7.78 8.33-7.79 21.34t7.93 21.58a25.66 25.66 0 0 0 19.51 8.47 26.15 26.15 0 0 0 19.84-8.33q7.88-8.33 7.88-21.81 0-13.2-7.88-21.39t-19.93-8.19ZM168.39 41.08h25.67v48.74q0 14.22 2 19.76a17.24 17.24 0 0 0 6.29 8.61 18.06 18.06 0 0 0 10.65 3.07 18.6 18.6 0 0 0 10.77-3 17.7 17.7 0 0 0 6.57-8.88q1.59-4.36 1.59-18.7v-49.6h25.39V84q0 26.51-4.18 36.27a39.6 39.6 0 0 1-15.07 18.28q-10 6.38-25.3 6.37-16.65 0-26.93-7.44t-14.48-20.78q-3-9.21-3-33.49ZM297.3 3.78h25.39v37.3h15.07v21.85h-15.07v79.35H297.3V62.93h-13V41.08h13ZM362.86 2h25.21v49.3a57.74 57.74 0 0 1 15-9.63 38.56 38.56 0 0 1 15.25-3.21 34.36 34.36 0 0 1 25.39 10.42q8.83 9 8.84 26.51v66.88h-25V97.91q0-17.58-1.68-23.81t-5.71-9.3a16.07 16.07 0 0 0-10-3.07 18.85 18.85 0 0 0-13.26 5.11q-5.53 5.11-7.67 14-1.12 4.56-1.12 20.84v40.65h-25.25ZM589.91 99h-81.58q1.77 10.78 9.44 17.16t19.58 6.37a33.86 33.86 0 0 0 24.46-10l21.4 10a50.54 50.54 0 0 1-19.16 16.79q-11.16 5.44-26.51 5.44-23.82 0-38.79-15t-15-37.63q0-23.16 14.93-38.46t37.44-15.3q23.91 0 38.88 15.3t15 40.42Zm-25.4-20a25.48 25.48 0 0 0-9.92-13.77A28.81 28.81 0 0 0 537.4 60a30.42 30.42 0 0 0-18.64 5.95q-5 3.72-9.31 13.12ZM621.89 41.08h25.39v10.37q8.64-7.29 15.65-10.13a37.82 37.82 0 0 1 14.35-2.85A34.77 34.77 0 0 1 702.83 49q8.82 8.94 8.82 26.42v66.88h-25.11V98q0-18.12-1.63-24.06a16.44 16.44 0 0 0-5.66-9.06 15.8 15.8 0 0 0-10-3.11 18.73 18.73 0 0 0-13.23 5.15q-5.49 5.08-7.62 14.22-1.12 4.74-1.12 20.54v40.6h-25.39ZM750.71 3.78h25.39v37.3h15.07v21.85H776.1v79.35h-25.39V62.93h-13V41.08h13ZM826.09-.6a15.55 15.55 0 0 1 11.45 4.84A16.08 16.08 0 0 1 842.31 16a15.87 15.87 0 0 1-4.72 11.58 15.34 15.34 0 0 1-11.32 4.79 15.6 15.6 0 0 1-11.55-4.88 16.35 16.35 0 0 1-4.72-11.9 15.57 15.57 0 0 1 4.73-11.44A15.53 15.53 0 0 1 826.09-.6ZM813.39 41.08h25.39v101.2h-25.39zM873.47 2h25.39v80.8l37.39-41.72h31.89l-43.59 48.5 48.81 52.7h-31.53l-43-46.64v46.64h-25.36Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p>Server is starting up. Refreshing in a few seconds...</p>
|
||||
|
||||
<div class="container" aria-hidden="true">
|
||||
<canvas id="waves-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
let timeoutID = -1;
|
||||
|
||||
function checkStatus() {
|
||||
console.log("Checking server status...");
|
||||
|
||||
return fetch(window.location.href, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
console.log("Server is ready! Reloading...");
|
||||
clearTimeout(timeoutID);
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 503) {
|
||||
throw new Error("Server is still starting up...");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Checking server status failed: ${error}`);
|
||||
timeoutID = setTimeout(checkStatus, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// if (window.location.origin.startsWith("http") && !queryParams.has("debug")) {
|
||||
// console.log("Checking server status...");
|
||||
// checkStatus();
|
||||
// }
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
/**
|
||||
* @file Wave animation module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data offsets (11 properties per particle)
|
||||
*/
|
||||
const ParticleOffsets = {
|
||||
BASE_X: 0,
|
||||
BASE_Z: 1,
|
||||
BASE_Y: 2,
|
||||
R: 3,
|
||||
G: 4,
|
||||
B: 5,
|
||||
A: 6,
|
||||
SIZE: 7,
|
||||
PERSPECTIVE_SIZE: 8,
|
||||
HALF_SIZE: 9,
|
||||
PERSPECTIVE_DEPTH_ALPHA: 10,
|
||||
PARTICLE_SIZE: 11,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} Particle
|
||||
* @property {number} baseX
|
||||
* @property {number} baseZ
|
||||
* @property {number} baseY
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} r
|
||||
* @property {number} g
|
||||
* @property {number} b
|
||||
* @property {number} a
|
||||
* @property {number} size
|
||||
* @property {number} perspectiveSize
|
||||
* @property {number} halfSize
|
||||
* @property {number} perspectiveDepthAlpha
|
||||
*/
|
||||
class WavesCanvas {
|
||||
//#region Properties
|
||||
|
||||
width = 0;
|
||||
height = 0;
|
||||
dpi = Math.max(1, devicePixelRatio);
|
||||
|
||||
// Wave parameters
|
||||
turbulence = Math.PI * 6.0;
|
||||
pointSize = 1 * this.dpi;
|
||||
pointSizeCutoff = 5;
|
||||
speed = 10;
|
||||
waveHeight = 5;
|
||||
distance = 3;
|
||||
|
||||
fov = (60 * Math.PI) / 180;
|
||||
aspectRatio = 1;
|
||||
cameraZ = 95;
|
||||
|
||||
lodThreshold = this.cameraZ * 0.9;
|
||||
|
||||
/**
|
||||
* Tangent of the field of view, i.e the angle between the horizon and the camera
|
||||
*/
|
||||
f = Math.tan(Math.PI * 0.5 - 0.5 * this.fov);
|
||||
|
||||
/**
|
||||
* Flat array storing particle data:
|
||||
* [baseX, baseZ, baseY, r, g, b, a, size, perspectiveSize, halfSize, perspectiveDepthAlpha]
|
||||
* @type {Float32Array}
|
||||
*/
|
||||
#particleData = new Float32Array(0);
|
||||
#particleOffsetCount = this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Performance optimizations
|
||||
/**
|
||||
* @type {ImageData}
|
||||
*/
|
||||
// @ts-expect-error - Assigned in resize listener
|
||||
#buffer;
|
||||
|
||||
#gridWidth = 0;
|
||||
#gridDepth = 0;
|
||||
#fieldWidth = 0;
|
||||
#fieldHeight = 0;
|
||||
#fieldDepth = 0;
|
||||
|
||||
#relativeWidth = 0;
|
||||
#relativeHeight = 0;
|
||||
|
||||
// Frustum culling bounds
|
||||
#frustumLeft = 0;
|
||||
#frustumRight = 0;
|
||||
#frustumTop = 0;
|
||||
#frustumBottom = 0;
|
||||
#frustumNear = 0;
|
||||
#frustumFar = 0;
|
||||
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
#canvas;
|
||||
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
#ctx;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* @param {string | HTMLCanvasElement} target
|
||||
* @param {"circle" | "triangle"} shape
|
||||
*/
|
||||
constructor(target, shape = "circle") {
|
||||
const element =
|
||||
typeof target === "string" ? document.querySelector(target) : target;
|
||||
|
||||
if (!(element instanceof HTMLCanvasElement)) {
|
||||
throw new TypeError("Invalid canvas element");
|
||||
}
|
||||
|
||||
this.#canvas = element;
|
||||
|
||||
const ctx = this.#canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
desynchronized: true,
|
||||
});
|
||||
|
||||
if (!ctx) {
|
||||
throw new TypeError("Failed to get 2D context");
|
||||
}
|
||||
|
||||
this.#ctx = ctx;
|
||||
|
||||
// We apply a background color to the canvas element itself for a slight performance boost.
|
||||
this.#canvas.style.background = getComputedStyle(document.body).backgroundColor;
|
||||
// All containment rules are applied to the element to further improve performance.
|
||||
this.#canvas.style.contain = "strict";
|
||||
// Applying a null transform on the canvas forces the browser to use the GPU for rendering.
|
||||
this.#canvas.style.willChange = "transform";
|
||||
this.#canvas.style.transform = "translate3d(0, 0, 0)";
|
||||
this.#canvas.style.pointerEvents = "none";
|
||||
|
||||
this.drawShape =
|
||||
shape === "circle" ? this.#drawCircleParticle : this.#drawTriangleParticle;
|
||||
|
||||
window.addEventListener("resize", this.refresh);
|
||||
const colorSchemeChangeListener = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
);
|
||||
|
||||
colorSchemeChangeListener.addEventListener("change", this.refresh);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const container = this.#canvas.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
this.width = container.offsetWidth;
|
||||
this.height = container.offsetHeight;
|
||||
this.aspectRatio = this.width / this.height;
|
||||
|
||||
this.#relativeWidth = this.width * this.dpi;
|
||||
this.#relativeHeight = this.height * this.dpi;
|
||||
|
||||
this.#canvas.width = this.#relativeWidth;
|
||||
this.#canvas.height = this.#relativeHeight;
|
||||
|
||||
this.#canvas.style.width = this.width + "px";
|
||||
this.#canvas.style.height = this.height + "px";
|
||||
|
||||
this.#ctx.scale(this.dpi, this.dpi);
|
||||
|
||||
this.#ctx.fillStyle = getComputedStyle(document.body).backgroundColor;
|
||||
|
||||
this.#buffer = this.#ctx.createImageData(
|
||||
this.width * this.dpi,
|
||||
this.height * this.dpi,
|
||||
);
|
||||
|
||||
this.#gridWidth = 300 * this.aspectRatio;
|
||||
this.#gridDepth = 300;
|
||||
|
||||
this.#fieldWidth = this.#gridWidth;
|
||||
this.#fieldHeight = this.waveHeight * this.aspectRatio;
|
||||
this.#fieldDepth = this.#gridDepth;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
);
|
||||
|
||||
if (prefersReducedMotion.matches) {
|
||||
this.speed = 5;
|
||||
}
|
||||
|
||||
this.#calculateFrustumBounds();
|
||||
this.#generateParticles();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Frustum Culling
|
||||
|
||||
#calculateFrustumBounds() {
|
||||
const halfFov = this.fov / 2;
|
||||
const tanHalfFov = Math.tan(halfFov);
|
||||
|
||||
this.#frustumNear = 1;
|
||||
this.#frustumFar = this.cameraZ + this.#gridDepth;
|
||||
|
||||
const nearHeight = 2 * this.#frustumNear * tanHalfFov;
|
||||
const nearWidth = nearHeight * this.aspectRatio;
|
||||
|
||||
this.#frustumLeft = -nearWidth / 2;
|
||||
this.#frustumRight = nearWidth / 2;
|
||||
this.#frustumTop = -1 * (this.aspectRatio / this.height);
|
||||
this.#frustumBottom = -1.25 * nearHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a 3D point is within the view frustum
|
||||
* @param {number} x - World X coordinate
|
||||
* @param {number} y - World Y coordinate
|
||||
* @param {number} z - World Z coordinate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isInFrustum(x, y, z) {
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
// Behind camera or too far
|
||||
if (projectedZ <= this.#frustumNear || projectedZ > this.#frustumFar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate frustum bounds at this depth
|
||||
const scale = projectedZ / this.#frustumNear;
|
||||
const left = this.#frustumLeft * scale;
|
||||
const right = this.#frustumRight * scale;
|
||||
const top = this.#frustumTop * scale;
|
||||
const bottom = this.#frustumBottom * scale;
|
||||
|
||||
// Test if point is within frustum bounds
|
||||
return x >= left && x <= right && y >= bottom && y <= top;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Particle Generation
|
||||
|
||||
#generateParticles() {
|
||||
const particleCount =
|
||||
Math.floor(this.#gridWidth / this.distance) *
|
||||
Math.floor(this.#gridDepth / this.distance);
|
||||
|
||||
this.#particleData = new Float32Array(
|
||||
particleCount * ParticleOffsets.PARTICLE_SIZE,
|
||||
);
|
||||
this.#particleOffsetCount =
|
||||
this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
let particleIndex = 0;
|
||||
|
||||
for (let x = 0; x < this.#gridWidth; x += this.distance) {
|
||||
const baseX = Math.round(-this.#gridWidth / 2 + x);
|
||||
|
||||
for (let z = 0; z < this.#gridDepth; z += this.distance) {
|
||||
const baseZ = -this.#gridDepth / 2 + z;
|
||||
const depthFactor = (z / this.#gridDepth) * 0.9;
|
||||
const horizonFactor = (x / this.#gridWidth) * 1;
|
||||
|
||||
// Pre-calculate perspective size based on fixed depth
|
||||
const projectedZ = baseZ + this.cameraZ;
|
||||
const baseSize = (this.height / 250) * this.pointSize * this.dpi;
|
||||
|
||||
// More exaggerated scaling with logarithmic falloff for horizon effect
|
||||
const normalizedDepth = Math.max(0, (projectedZ - 50) / 200);
|
||||
// Log scale for dramatic falloff
|
||||
const logScale = Math.log(1 + normalizedDepth * 4) / Math.log(6);
|
||||
// Invert so closer = larger
|
||||
const distanceFactor = Math.max(0.05, 1 - logScale);
|
||||
const perspectiveSize = baseSize * distanceFactor * 3;
|
||||
|
||||
const alpha = Math.max(Math.floor(z / this.#gridDepth), 0.85);
|
||||
const depthAlpha = Math.max(0.6, Math.pow(distanceFactor, 2));
|
||||
|
||||
const offset = particleIndex * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Store particle data in flat array
|
||||
this.#particleData[offset + ParticleOffsets.BASE_X] = baseX;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Z] = baseZ;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Y] =
|
||||
this.cameraZ * -0.4;
|
||||
this.#particleData[offset + ParticleOffsets.R] = Math.min(
|
||||
Math.floor(253 / (depthFactor * horizonFactor)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.G] = Math.min(
|
||||
Math.floor(75 / horizonFactor),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.B] = Math.min(
|
||||
Math.floor(45 / (depthFactor * horizonFactor * 2)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.A] = alpha;
|
||||
this.#particleData[offset + ParticleOffsets.SIZE] = baseSize;
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE] =
|
||||
perspectiveSize;
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE] = Math.max(
|
||||
1,
|
||||
Math.round(perspectiveSize / 1.5),
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA] =
|
||||
depthAlpha ** 2;
|
||||
|
||||
particleIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Projection
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
* @returns {{x: number, y: number, z: number} | null}
|
||||
*/
|
||||
project3DTo2D(x, y, z) {
|
||||
const projectedX = (x * this.f) / this.aspectRatio;
|
||||
const projectedY = y * this.f;
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
if (projectedZ <= 0) return null;
|
||||
|
||||
// Convert to screen coordinates
|
||||
// top of canvas is horizon (y=0), bottom is near
|
||||
|
||||
const screenX = ((projectedX / projectedZ) * this.width) / 2 + this.width / 2;
|
||||
const screenY =
|
||||
this.height / 2 -
|
||||
((projectedY / projectedZ) * this.height) / 2 -
|
||||
this.cameraZ * 2;
|
||||
|
||||
return { x: screenX, y: screenY, z: projectedZ };
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} z
|
||||
* @param {number} time
|
||||
* @returns {number}
|
||||
*/
|
||||
calculateWaveY(x, z, time) {
|
||||
return (
|
||||
(Math.cos((x / this.#fieldWidth) * this.turbulence + time * this.speed) +
|
||||
Math.sin(
|
||||
(z / this.#fieldDepth) * this.turbulence + time * this.speed,
|
||||
)) *
|
||||
this.#fieldHeight
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a simple point particle (for distant objects)
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawPointParticle(x, y, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
if (centerX >= 0 && centerX < width && centerY >= 0 && centerY < height) {
|
||||
const index = (centerY * width + centerX) * 4;
|
||||
const alpha = Math.round(a * 255);
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
data[index] = Math.min(255, data[index] + (r * blendFactor) / 255);
|
||||
data[index + 1] = Math.min(255, data[index + 1] + (g * blendFactor) / 255);
|
||||
data[index + 2] = Math.min(255, data[index + 2] + (b * blendFactor) / 255);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a hollow circle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawCircleParticle = (x, y, halfSize, r, g, b, a) => {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
const radius = halfSize;
|
||||
|
||||
// Draw hollow circle - just the outline
|
||||
for (let py = -radius; py <= radius; py++) {
|
||||
for (let px = -radius; px <= radius; px++) {
|
||||
const distance = Math.sqrt(px * px + py * py);
|
||||
|
||||
// Only draw pixels that are on the circle edge (within 1 pixel of radius)
|
||||
if (distance >= radius - 1 && distance <= radius) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw an isosceles triangle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawTriangleParticle = (x, y, halfSize, r, g, b, a) => {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
|
||||
// Draw hollow isosceles triangle pointing up - just the outline
|
||||
for (let py = -halfSize; py <= halfSize; py++) {
|
||||
// Calculate max width at this height - steeper taper for more triangular shape
|
||||
// 0 at top, 1 at bottom
|
||||
const normalizedHeight = (py + halfSize) / (2 * halfSize);
|
||||
const triangleWidth = Math.round(halfSize * normalizedHeight);
|
||||
|
||||
for (let px = -triangleWidth; px <= triangleWidth; px++) {
|
||||
// Only draw if on the edge (left edge, right edge, or bottom edge)
|
||||
const isLeftEdge = px === -triangleWidth;
|
||||
const isRightEdge = px === triangleWidth;
|
||||
const isBottomEdge = py === halfSize;
|
||||
|
||||
if (isLeftEdge || isRightEdge || isBottomEdge) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (
|
||||
xPixel >= 0 &&
|
||||
xPixel < width &&
|
||||
yPixel >= 0 &&
|
||||
yPixel < height
|
||||
) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(
|
||||
255,
|
||||
data[index] + (r * blendFactor) / 255,
|
||||
);
|
||||
data[index + 1] = Math.min(
|
||||
255,
|
||||
data[index + 1] + (g * blendFactor) / 255,
|
||||
);
|
||||
data[index + 2] = Math.min(
|
||||
255,
|
||||
data[index + 2] + (b * blendFactor) / 255,
|
||||
);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#render = (time = performance.now()) => {
|
||||
const timestamp = time / 6000;
|
||||
this.#buffer.data.fill(0);
|
||||
|
||||
for (let i = 0; i < this.#particleOffsetCount; i++) {
|
||||
const offset = i * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
const baseX = this.#particleData[offset + ParticleOffsets.BASE_X];
|
||||
const baseZ = this.#particleData[offset + ParticleOffsets.BASE_Z];
|
||||
const baseY = this.#particleData[offset + ParticleOffsets.BASE_Y];
|
||||
|
||||
const waveY = this.calculateWaveY(baseX, baseZ, timestamp);
|
||||
const worldY = baseY + waveY;
|
||||
|
||||
if (!this.#isInFrustum(baseX, worldY, baseZ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const projected = this.project3DTo2D(baseX, worldY, baseZ);
|
||||
|
||||
if (
|
||||
!projected ||
|
||||
projected.x < -50 ||
|
||||
projected.x > this.width + 50 ||
|
||||
projected.y < 0 ||
|
||||
projected.y > this.height + 50
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = Math.abs(projected.z - this.cameraZ);
|
||||
const perspectiveSize =
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE];
|
||||
|
||||
if (
|
||||
distance > this.lodThreshold ||
|
||||
perspectiveSize < this.pointSizeCutoff
|
||||
) {
|
||||
this.#drawPointParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[
|
||||
offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA
|
||||
],
|
||||
);
|
||||
} else {
|
||||
this.#drawCircleParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE],
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[
|
||||
offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#ctx.putImageData(this.#buffer, 0, 0);
|
||||
this.#ctx.globalCompositeOperation = "soft-light";
|
||||
this.#ctx.fillRect(0, 0, this.#relativeWidth, this.#relativeHeight);
|
||||
this.#ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
this.#renderFrameID = requestAnimationFrame(this.#render);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public Methods
|
||||
|
||||
#renderFrameID = -1;
|
||||
|
||||
play = () => {
|
||||
this.refresh();
|
||||
|
||||
this.#render();
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
cancelAnimationFrame(this.#renderFrameID);
|
||||
};
|
||||
}
|
||||
|
||||
function drawAnimation() {
|
||||
const canvas = document.getElementById("waves-canvas");
|
||||
const waves = new WavesCanvas(canvas);
|
||||
waves.play();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", drawAnimation);
|
||||
} else {
|
||||
drawAnimation();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,508 +0,0 @@
|
||||
/**
|
||||
* @file Wave animation module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data offsets (11 properties per particle)
|
||||
*/
|
||||
const ParticleOffsets = {
|
||||
BASE_X: 0,
|
||||
BASE_Z: 1,
|
||||
BASE_Y: 2,
|
||||
R: 3,
|
||||
G: 4,
|
||||
B: 5,
|
||||
A: 6,
|
||||
SIZE: 7,
|
||||
PERSPECTIVE_SIZE: 8,
|
||||
HALF_SIZE: 9,
|
||||
PERSPECTIVE_DEPTH_ALPHA: 10,
|
||||
PARTICLE_SIZE: 11,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} Particle
|
||||
* @property {number} baseX
|
||||
* @property {number} baseZ
|
||||
* @property {number} baseY
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} r
|
||||
* @property {number} g
|
||||
* @property {number} b
|
||||
* @property {number} a
|
||||
* @property {number} size
|
||||
* @property {number} perspectiveSize
|
||||
* @property {number} halfSize
|
||||
* @property {number} perspectiveDepthAlpha
|
||||
*/
|
||||
export class WavesCanvas {
|
||||
//#region Properties
|
||||
|
||||
width = 0;
|
||||
height = 0;
|
||||
dpi = Math.max(1, devicePixelRatio);
|
||||
|
||||
// Wave parameters
|
||||
turbulence = Math.PI * 6.0;
|
||||
pointSize = 1.5 * this.dpi;
|
||||
pointSizeCutoff = 5;
|
||||
speed = 10;
|
||||
waveHeight = 5;
|
||||
distance = 2;
|
||||
fov = (60 * Math.PI) / 180;
|
||||
|
||||
aspectRatio = 1;
|
||||
cameraZ = 95;
|
||||
|
||||
lodThreshold = this.cameraZ * 0.9;
|
||||
|
||||
/**
|
||||
* Tangent of the field of view, i.e the angle between the horizon and the camera
|
||||
*/
|
||||
f = Math.tan(Math.PI * 0.5 - 0.5 * this.fov);
|
||||
|
||||
/**
|
||||
* Flat array storing particle data: [baseX, baseZ, baseY, r, g, b, a, size, perspectiveSize, halfSize, perspectiveDepthAlpha]
|
||||
* @type {Float32Array}
|
||||
*/
|
||||
#particleData = new Float32Array(0);
|
||||
#particleOffsetCount = this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Performance optimizations
|
||||
/**
|
||||
* @type {ImageData}
|
||||
*/
|
||||
// @ts-expect-error - Assigned in resize listener
|
||||
#buffer;
|
||||
|
||||
#gridWidth = 0;
|
||||
#gridDepth = 0;
|
||||
#fieldWidth = 0;
|
||||
#fieldHeight = 0;
|
||||
#fieldDepth = 0;
|
||||
|
||||
#relativeWidth = 0;
|
||||
#relativeHeight = 0;
|
||||
|
||||
// Frustum culling bounds
|
||||
#frustumLeft = 0;
|
||||
#frustumRight = 0;
|
||||
#frustumTop = 0;
|
||||
#frustumBottom = 0;
|
||||
#frustumNear = 0;
|
||||
#frustumFar = 0;
|
||||
|
||||
/**
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
#canvas;
|
||||
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
#ctx;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* @param {string | HTMLCanvasElement} target
|
||||
*/
|
||||
constructor(target) {
|
||||
const element = typeof target === "string" ? document.querySelector(target) : target;
|
||||
|
||||
if (!(element instanceof HTMLCanvasElement)) {
|
||||
throw new TypeError("Invalid canvas element");
|
||||
}
|
||||
|
||||
this.#canvas = element;
|
||||
|
||||
const ctx = this.#canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
desynchronized: true,
|
||||
});
|
||||
|
||||
if (!ctx) {
|
||||
throw new TypeError("Failed to get 2D context");
|
||||
}
|
||||
|
||||
this.#ctx = ctx;
|
||||
|
||||
// We apply a background color to the canvas element itself for a slight performance boost.
|
||||
this.#canvas.style.background = getComputedStyle(document.body).backgroundColor;
|
||||
// All containment rules are applied to the element to further improve performance.
|
||||
this.#canvas.style.contain = "strict";
|
||||
// Applying a null transform on the canvas forces the browser to use the GPU for rendering.
|
||||
this.#canvas.style.willChange = "transform";
|
||||
this.#canvas.style.transform = "translate3d(0, 0, 0)";
|
||||
this.#canvas.style.pointerEvents = "none";
|
||||
|
||||
window.addEventListener("resize", this.refresh);
|
||||
const colorSchemeChangeListener = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
colorSchemeChangeListener.addEventListener("change", this.refresh);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const container = this.#canvas.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
this.width = container.offsetWidth;
|
||||
this.height = container.offsetHeight;
|
||||
this.aspectRatio = this.width / this.height;
|
||||
|
||||
this.#relativeWidth = this.width * this.dpi;
|
||||
this.#relativeHeight = this.height * this.dpi;
|
||||
|
||||
this.#canvas.width = this.#relativeWidth;
|
||||
this.#canvas.height = this.#relativeHeight;
|
||||
|
||||
this.#canvas.style.width = this.width + "px";
|
||||
this.#canvas.style.height = this.height + "px";
|
||||
|
||||
this.#ctx.scale(this.dpi, this.dpi);
|
||||
|
||||
this.#ctx.fillStyle = getComputedStyle(document.body).backgroundColor;
|
||||
|
||||
this.#buffer = this.#ctx.createImageData(this.width * this.dpi, this.height * this.dpi);
|
||||
|
||||
this.#gridWidth = 300 * this.aspectRatio;
|
||||
this.#gridDepth = 300;
|
||||
|
||||
this.#fieldWidth = this.#gridWidth;
|
||||
this.#fieldHeight = this.waveHeight * this.aspectRatio;
|
||||
this.#fieldDepth = this.#gridDepth;
|
||||
|
||||
this.#calculateFrustumBounds();
|
||||
this.#generateParticles();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Frustum Culling
|
||||
|
||||
#calculateFrustumBounds() {
|
||||
const halfFov = this.fov / 2;
|
||||
const tanHalfFov = Math.tan(halfFov);
|
||||
|
||||
// Calculate frustum bounds at different depths
|
||||
this.#frustumNear = 1;
|
||||
this.#frustumFar = this.cameraZ + this.#gridDepth;
|
||||
|
||||
// At near plane
|
||||
const nearHeight = 2 * this.#frustumNear * tanHalfFov;
|
||||
const nearWidth = nearHeight * this.aspectRatio;
|
||||
|
||||
// Store half-widths and half-heights for faster testing
|
||||
this.#frustumLeft = -nearWidth / 2;
|
||||
this.#frustumRight = nearWidth / 2;
|
||||
this.#frustumTop = nearHeight / 2;
|
||||
this.#frustumBottom = -nearHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a 3D point is within the view frustum
|
||||
* @param {number} x - World X coordinate
|
||||
* @param {number} y - World Y coordinate
|
||||
* @param {number} z - World Z coordinate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isInFrustum(x, y, z) {
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
// Behind camera or too far
|
||||
if (projectedZ <= this.#frustumNear || projectedZ > this.#frustumFar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate frustum bounds at this depth
|
||||
const scale = projectedZ / this.#frustumNear;
|
||||
const left = this.#frustumLeft * scale;
|
||||
const right = this.#frustumRight * scale;
|
||||
const top = this.#frustumTop * scale;
|
||||
const bottom = this.#frustumBottom * scale;
|
||||
|
||||
// Test if point is within frustum bounds
|
||||
return x >= left && x <= right && y >= bottom && y <= top;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Particle Generation
|
||||
|
||||
#generateParticles() {
|
||||
const particleCount =
|
||||
Math.floor(this.#gridWidth / this.distance) *
|
||||
Math.floor(this.#gridDepth / this.distance);
|
||||
|
||||
this.#particleData = new Float32Array(particleCount * ParticleOffsets.PARTICLE_SIZE);
|
||||
this.#particleOffsetCount = this.#particleData.length / ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
let particleIndex = 0;
|
||||
|
||||
for (let x = 0; x < this.#gridWidth; x += this.distance) {
|
||||
const baseX = Math.round(-this.#gridWidth / 2 + x);
|
||||
|
||||
for (let z = 0; z < this.#gridDepth; z += this.distance) {
|
||||
const baseZ = -this.#gridDepth / 2 + z;
|
||||
const depthFactor = (z / this.#gridDepth) * 0.9;
|
||||
const horizonFactor = (x / this.#gridWidth) * 1;
|
||||
|
||||
// Pre-calculate perspective size based on fixed depth
|
||||
const projectedZ = baseZ + this.cameraZ;
|
||||
const baseSize = (this.height / 250) * this.pointSize * this.dpi;
|
||||
|
||||
// More exaggerated scaling with logarithmic falloff for horizon effect
|
||||
const normalizedDepth = Math.max(0, (projectedZ - 50) / 200);
|
||||
// Log scale for dramatic falloff
|
||||
const logScale = Math.log(1 + normalizedDepth * 4) / Math.log(6);
|
||||
// Invert so closer = larger
|
||||
const distanceFactor = Math.max(0.05, 1 - logScale);
|
||||
const perspectiveSize = baseSize * distanceFactor * 3;
|
||||
|
||||
const alpha = Math.max(Math.floor(z / this.#gridDepth), 0.85);
|
||||
const depthAlpha = Math.max(0.6, Math.pow(distanceFactor, 2));
|
||||
|
||||
const offset = particleIndex * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
// Store particle data in flat array
|
||||
this.#particleData[offset + ParticleOffsets.BASE_X] = baseX;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Z] = baseZ;
|
||||
this.#particleData[offset + ParticleOffsets.BASE_Y] = this.cameraZ * -0.4;
|
||||
this.#particleData[offset + ParticleOffsets.R] = Math.min(
|
||||
Math.floor(253 / (depthFactor * horizonFactor)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.G] = Math.min(
|
||||
Math.floor(75 / horizonFactor),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.B] = Math.min(
|
||||
Math.floor(45 / (depthFactor * horizonFactor * 2)),
|
||||
255,
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.A] = alpha;
|
||||
this.#particleData[offset + ParticleOffsets.SIZE] = baseSize;
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE] = perspectiveSize;
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE] = Math.max(
|
||||
1,
|
||||
Math.round(perspectiveSize / 1.5),
|
||||
);
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA] =
|
||||
depthAlpha ** 2;
|
||||
|
||||
particleIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Projection
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
* @returns {{x: number, y: number, z: number} | null}
|
||||
*/
|
||||
project3DTo2D(x, y, z) {
|
||||
const projectedX = (x * this.f) / this.aspectRatio;
|
||||
const projectedY = y * this.f;
|
||||
const projectedZ = z + this.cameraZ;
|
||||
|
||||
if (projectedZ <= 0) return null;
|
||||
|
||||
// Convert to screen coordinates
|
||||
// top of canvas is horizon (y=0), bottom is near
|
||||
|
||||
const screenX = ((projectedX / projectedZ) * this.width) / 2 + this.width / 2;
|
||||
const screenY =
|
||||
this.height / 2 - ((projectedY / projectedZ) * this.height) / 2 - this.cameraZ * 2;
|
||||
|
||||
return { x: screenX, y: screenY, z: projectedZ };
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} z
|
||||
* @param {number} time
|
||||
* @returns {number}
|
||||
*/
|
||||
calculateWaveY(x, z, time) {
|
||||
return (
|
||||
(Math.cos((x / this.#fieldWidth) * this.turbulence + time * this.speed) +
|
||||
Math.sin((z / this.#fieldDepth) * this.turbulence + time * this.speed)) *
|
||||
this.#fieldHeight
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a simple point particle (for distant objects)
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawPointParticle(x, y, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
if (centerX >= 0 && centerX < width && centerY >= 0 && centerY < height) {
|
||||
const index = (centerY * width + centerX) * 4;
|
||||
const alpha = Math.round(a * 255);
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
data[index] = Math.min(255, data[index] + (r * blendFactor) / 255);
|
||||
data[index + 1] = Math.min(255, data[index + 1] + (g * blendFactor) / 255);
|
||||
data[index + 2] = Math.min(255, data[index + 2] + (b * blendFactor) / 255);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an isosceles triangle particle
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} halfSize
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {number} a
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
#drawTriangleParticle(x, y, halfSize, r, g, b, a) {
|
||||
const data = this.#buffer.data;
|
||||
const centerX = Math.round(x * this.dpi);
|
||||
const centerY = Math.round(y * this.dpi);
|
||||
|
||||
const width = this.#relativeWidth;
|
||||
const height = this.#relativeHeight;
|
||||
|
||||
const alpha = Math.round(a * 255);
|
||||
|
||||
// Draw hollow isosceles triangle pointing up - just the outline
|
||||
for (let py = -halfSize; py <= halfSize; py++) {
|
||||
// Calculate max width at this height - steeper taper for more triangular shape
|
||||
// 0 at top, 1 at bottom
|
||||
const normalizedHeight = (py + halfSize) / (2 * halfSize);
|
||||
const triangleWidth = Math.round(halfSize * normalizedHeight);
|
||||
|
||||
for (let px = -triangleWidth; px <= triangleWidth; px++) {
|
||||
// Only draw if on the edge (left edge, right edge, or bottom edge)
|
||||
const isLeftEdge = px === -triangleWidth;
|
||||
const isRightEdge = px === triangleWidth;
|
||||
const isBottomEdge = py === halfSize;
|
||||
|
||||
if (isLeftEdge || isRightEdge || isBottomEdge) {
|
||||
const xPixel = centerX + px;
|
||||
const yPixel = centerY + py;
|
||||
|
||||
if (xPixel >= 0 && xPixel < width && yPixel >= 0 && yPixel < height) {
|
||||
const index = (yPixel * width + xPixel) * 4;
|
||||
const blendFactor = alpha * 1.25;
|
||||
|
||||
// Additive blending for glow effect
|
||||
data[index] = Math.min(255, data[index] + (r * blendFactor) / 255);
|
||||
data[index + 1] = Math.min(255, data[index + 1] + (g * blendFactor) / 255);
|
||||
data[index + 2] = Math.min(255, data[index + 2] + (b * blendFactor) / 255);
|
||||
data[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#render = (time = performance.now()) => {
|
||||
const timestamp = time / 6000;
|
||||
this.#buffer.data.fill(0);
|
||||
|
||||
for (let i = 0; i < this.#particleOffsetCount; i++) {
|
||||
const offset = i * ParticleOffsets.PARTICLE_SIZE;
|
||||
|
||||
const baseX = this.#particleData[offset + ParticleOffsets.BASE_X];
|
||||
const baseZ = this.#particleData[offset + ParticleOffsets.BASE_Z];
|
||||
const baseY = this.#particleData[offset + ParticleOffsets.BASE_Y];
|
||||
|
||||
const waveY = this.calculateWaveY(baseX, baseZ, timestamp);
|
||||
const worldY = baseY + waveY;
|
||||
|
||||
if (!this.#isInFrustum(baseX, worldY, baseZ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const projected = this.project3DTo2D(baseX, worldY, baseZ);
|
||||
|
||||
if (
|
||||
!projected ||
|
||||
projected.x < -50 ||
|
||||
projected.x > this.width + 50 ||
|
||||
projected.y < 0 ||
|
||||
projected.y > this.height + 50
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = Math.abs(projected.z - this.cameraZ);
|
||||
const perspectiveSize = this.#particleData[offset + ParticleOffsets.PERSPECTIVE_SIZE];
|
||||
|
||||
if (distance > this.lodThreshold || perspectiveSize < this.pointSizeCutoff) {
|
||||
this.#drawPointParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA],
|
||||
);
|
||||
} else {
|
||||
// Close enough for detailed triangle
|
||||
this.#drawTriangleParticle(
|
||||
projected.x,
|
||||
projected.y,
|
||||
this.#particleData[offset + ParticleOffsets.HALF_SIZE],
|
||||
this.#particleData[offset + ParticleOffsets.R],
|
||||
this.#particleData[offset + ParticleOffsets.G],
|
||||
this.#particleData[offset + ParticleOffsets.B],
|
||||
this.#particleData[offset + ParticleOffsets.PERSPECTIVE_DEPTH_ALPHA],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#ctx.putImageData(this.#buffer, 0, 0);
|
||||
this.#ctx.globalCompositeOperation = "soft-light";
|
||||
this.#ctx.fillRect(0, 0, this.#relativeWidth, this.#relativeHeight);
|
||||
this.#ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
this.#renderFrameID = requestAnimationFrame(this.#render);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public Methods
|
||||
|
||||
#renderFrameID = -1;
|
||||
|
||||
play = () => {
|
||||
this.refresh();
|
||||
|
||||
this.#render();
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
cancelAnimationFrame(this.#renderFrameID);
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,4 @@ var RobotsTxt []byte
|
||||
//go:embed security.txt
|
||||
var SecurityTxt []byte
|
||||
|
||||
var StaticDir = http.Dir("./web/dist/")
|
||||
|
||||
var StaticHandler = http.FileServer(StaticDir)
|
||||
var StaticHandler = http.FileServer(http.Dir("./web/dist/"))
|
||||
|
||||
@@ -1466,6 +1466,11 @@
|
||||
<source>No form found</source>
|
||||
<target>Nenalezen žádný formulář</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s45935843b1b5b496">
|
||||
<source>Form didn't return a promise for submitting</source>
|
||||
<target>Formulář nevrátil pro odeslání Promise</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s74475586afc1fb0f">
|
||||
<source>Select type</source>
|
||||
@@ -6311,7 +6316,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
</trans-unit>
|
||||
<trans-unit id="sbd19064fc3f405c1">
|
||||
<source>Check your Inbox for a verification email.</source>
|
||||
<target>Ověřovací email najdete ve Vaší poštovní schránce.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s8aff572e64b7936b">
|
||||
@@ -7500,7 +7504,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
</trans-unit>
|
||||
<trans-unit id="sb864dc36a463a155">
|
||||
<source>Ignore server certificate</source>
|
||||
<target>Ignorovat serverový certifikát</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s20366a8d1eaaca54">
|
||||
<source>Enable wallpaper</source>
|
||||
@@ -8631,6 +8634,10 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Shoduje se s IP adresou klienta události (přísné porovnávání, pro síťové porovnávání použijte zásadu výrazu).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Neplatný požadavek na aktualizaci.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Synchronizovat skupinu</target>
|
||||
@@ -9747,423 +9754,267 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
|
||||
</trans-unit>
|
||||
<trans-unit id="sb7af25ce6e30d61a">
|
||||
<source>The currently selected policy engine mode is <x id="0" equiv-text="${policyEngineMode.label}"/>:</source>
|
||||
<target>Aktuálně vybraný policy engine je <x id="0" equiv-text="${policyEngineMode.label}"/>:</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se1d2545eda4b1600">
|
||||
<source>Import Existing Certificate-Key Pair</source>
|
||||
<target>Importovat existující pár certifikátu a klíče</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb3d5c0a0501669df">
|
||||
<source>Generate New Certificate-Key Pair</source>
|
||||
<target>Vygenerovat nový pár certifikátu a klíče</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9686d31d28fcf7d">
|
||||
<source>Show field content</source>
|
||||
<target>Zobrazit obsah pole</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb1b05a7573ab618c">
|
||||
<source>Hide field content</source>
|
||||
<target>Skrýt obsah pole</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
<target>Znovu ověřit pomocí Plex</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0433d667ea6eec1a">
|
||||
<source>The name of an invitation must be a slug: only lower case letters, numbers, and the hyphen are permitted here.</source>
|
||||
<target>Název pozvánky musí být jednoslovný: jsou povolena pouze malá písmena, číslice a pomlčky.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e9d5ea88f02ae68">
|
||||
<source>Select the group of users which the alerts are sent to. </source>
|
||||
<target>Vyberte uživatelskou skupinu, které se budou zasílat upozornění.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se630f2ccd39bf9e6">
|
||||
<source>If no group is selected and 'Send notification to event user' is disabled the rule is disabled. </source>
|
||||
<target>Pokud není vybrána žádná skupina a 'Zasílat upozornění původci události' je vypnuto, pravidlo není aktivní.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47966b2a708694e2">
|
||||
<source>Send notification to event user</source>
|
||||
<target>Zasílat oznámení původci události</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd30f00ff2135589c">
|
||||
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
|
||||
<target>Pokud je povoleno, oznámení se kromě uživatelů ve skupině výše odešle také původci události. Uživatel, který událost vyvolal, je vždy prvním příjemcem; pokud má být oznámení doručeno pouze jednou, je třeba v transportu notifikací zapnout volbu ‘Poslat jednou’.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbd65aeeb8a3b9bbc">
|
||||
<source>Maximum registration attempts</source>
|
||||
<target>Maximální počet pokusů o registraci</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8495753cb15e8d8e">
|
||||
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
|
||||
<target>Maximální dovolený počet pokusů o registraci. Pokud je nastaven na 0, počet pokusů není omezen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sab4db6a3bd6abc1e">
|
||||
<source>This application does currently not have any application entitlements defined.</source>
|
||||
<target>Tato aplikace nemá momentálně definována žádná oprávnění.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7225aacf0eee94d2">
|
||||
<source>Authenticated as <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/></source>
|
||||
<target>Přihlášen jako <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa3ff409b84679db8">
|
||||
<source>Remember device</source>
|
||||
<target>Zapamatovat zařízení</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3b123cba0bf03f29">
|
||||
<source>If set to a duration above 0, a cookie will be stored for the duration specified which will allow authentik to know if the user is signing in from a new device.</source>
|
||||
<target>Pokud je nastaveno na hodnotu vyšší než 0, dojde k uložení cookie na stanovenou dobu, což umožní authentiku zjistit, že se uživatel přihlašuje z nového zařízení.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s059358bf402de018">
|
||||
<source>Element inside the form slot is not a Form</source>
|
||||
<target>Prvek uvnitř formulářového slotu není formulář.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8563c5a14e9a42f2">
|
||||
<source>New Password</source>
|
||||
<target>Nové heslo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s547b687213f48489">
|
||||
<source>Update <x id="0" equiv-text="${item.name || item.username}"/>'s password</source>
|
||||
<target>Aktualizovat heslo uživatele <x id="0" equiv-text="${item.name || item.username}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3c619d54c995d4ab">
|
||||
<source>Modify the payload sent to the provider.</source>
|
||||
<target>Upravit data posílaná poskytovateli.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12d6dde9b30c3093">
|
||||
<source>Dismiss</source>
|
||||
<target>Skrýt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdea479482318489d">
|
||||
<source>Status messages</source>
|
||||
<target>Stavové zprávy</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s05a4a4df6783d0cf">
|
||||
<source>Provider name</source>
|
||||
<target>Jméno poskytovatele</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s19714b5520312d9a">
|
||||
<source>Select an invalidation flow...</source>
|
||||
<target>Vyberte tok zneplatnění...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1a3d030efd11f28">
|
||||
<source>Open about dialog</source>
|
||||
<target>Otevřít dialog O programu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s95b96d7ead27527f">
|
||||
<source>Product name</source>
|
||||
<target>Název produktu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s68b6d404c3349b18">
|
||||
<source>Product version</source>
|
||||
<target>Verze produktu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se0d7715f3211d220">
|
||||
<source>Global navigation</source>
|
||||
<target>Globální navigace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd51d6a39208696cc">
|
||||
<source>Collapse <x id="0" equiv-text="${this.label}"/></source>
|
||||
<target>Sbalit <x id="0" equiv-text="${this.label}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scce64bcfb0a73ad7">
|
||||
<source>Expand <x id="0" equiv-text="${this.label}"/></source>
|
||||
<target>Rozbalit <x id="0" equiv-text="${this.label}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfe1fb536cab6c0f5">
|
||||
<source><x id="0" equiv-text="${this.label}"/> navigation</source>
|
||||
<target><x id="0" equiv-text="${this.label}"/> navigace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scbc2fd9f2bd26fdd">
|
||||
<source>Main content</source>
|
||||
<target>Hlavní obsah</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd2b106933823b754">
|
||||
<source>When using a Duo MFA, Access or Beyond plan, an Admin API application can be created. This will allow authentik to import devices automatically.</source>
|
||||
<target>Když používáte Duo MFA, Duo Access nebo Duo Beyond, můžete vytvořit aplikaci Admin API. Toto umožní authentiku automaticky importovat zařízení.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd5e9b70ffb7a8b46">
|
||||
<source>Skip to content</source>
|
||||
<target>Přeskočeit na hlavní obsah</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s334b3924d2bd5d55">
|
||||
<source>Kerberos Source</source>
|
||||
<target>Zdroj Kerberos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcdf61483337948a">
|
||||
<source>Successfully updated schedule.</source>
|
||||
<target>Plán byl úspěšně aktualizován.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd372fe5b712f6b30">
|
||||
<source>Crontab</source>
|
||||
<target>Crontab</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s95bc8722b2708f8b">
|
||||
<source>Paused</source>
|
||||
<target>Pozastavený</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se41af830054194bc">
|
||||
<source>Pause this schedule</source>
|
||||
<target>Pozastavit tento plán</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd091e43d5e99dea4">
|
||||
<source>Waiting to run</source>
|
||||
<target>Čeká na spuštění</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85994a70cd39166c">
|
||||
<source>Running</source>
|
||||
<target>Běží</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc7637275f670c938">
|
||||
<source>Queue</source>
|
||||
<target>V pořadí</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb91d1be10eb2c7da">
|
||||
<source>Last updated</source>
|
||||
<target>Poslední aktualizace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s911c2c952e64b223">
|
||||
<source>Show only standalone tasks</source>
|
||||
<target>Ukázat pouze samostatné úlohy</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s14053a4609676b8b">
|
||||
<source>Exclude successful tasks</source>
|
||||
<target>Vynechat úspěšné úlohy</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8ff646d0515aab2a">
|
||||
<source>Retry task</source>
|
||||
<target>Zopakovat úlohu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9b8dccb514a0e34c">
|
||||
<source>Schedule</source>
|
||||
<target>Plán</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf92789320708efed">
|
||||
<source>Next run</source>
|
||||
<target>Příští spuštění</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1aecc6c0d6cbf4ed">
|
||||
<source>Last status</source>
|
||||
<target>Poslední stav</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6089c283e28012fb">
|
||||
<source>Show only standalone schedules</source>
|
||||
<target>Ukázat pouze samostatné plány</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s48006fb6e0b1860a">
|
||||
<source>Run scheduled task now</source>
|
||||
<target>Spustit naplánovanou úlohu nyní</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6492593d534108e1">
|
||||
<source>Update Schedule</source>
|
||||
<target>Aktualizovat plán</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf2d616b20d62240d">
|
||||
<source>Schedules</source>
|
||||
<target>Pláy</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc797fd9076cc136d">
|
||||
<source>Tasks</source>
|
||||
<target>Úlohy</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3f63421908094590">
|
||||
<source>Current status</source>
|
||||
<target>Aktuální stav</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4412853e1655ebb3">
|
||||
<source>Sync is currently running.</source>
|
||||
<target>Probíhá synchronizace.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s08661004803c26b0">
|
||||
<source>Sync is not currently running.</source>
|
||||
<target>Synchronizace teď neprobíhá.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s81d16aa9ec62262c">
|
||||
<source>Last successful sync</source>
|
||||
<target>Poslední úspěšná synchronizace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd1eb8f8acdbcd1d3">
|
||||
<source>No successful sync found.</source>
|
||||
<target>Žádná úspěšná synchronizace nenalezena.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b0eeb3de1789c6e">
|
||||
<source>Last sync status</source>
|
||||
<target>Stav poslední synchronizace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sac44b0f4dc14c227">
|
||||
<source>Current execution logs</source>
|
||||
<target>Aktuální logy běhu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5e13dff03b580216">
|
||||
<source>Previous executions logs</source>
|
||||
<target>Předešlé logy běhu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6abb1cd87fe0114e">
|
||||
<source>Home</source>
|
||||
<target>Domů</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se58e6ed983bf34b0">
|
||||
<source>Collapse navigation</source>
|
||||
<target>Sbalit navigaci</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6ef25894ed00175">
|
||||
<source>Expand navigation</source>
|
||||
<target>Rozbalit navigaci</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s148b5e365440a7c1">
|
||||
<source>Table pagination</source>
|
||||
<target>Stránkování tabulky</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5d929ff1619ac0c9">
|
||||
<source>Search</source>
|
||||
<target>Hledat</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd2c2366d13599d8c">
|
||||
<source>Table actions</source>
|
||||
<target>Akce tabulky</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d195621e562d805">
|
||||
<source>Select row</source>
|
||||
<target>Vybrat řádek</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s572d21b6a41e24fa">
|
||||
<source>Table of <x id="0" equiv-text="${this.label}"/></source>
|
||||
<target>Tabulka <x id="0" equiv-text="${this.label}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa25b60b4fac481aa">
|
||||
<source>Table content</source>
|
||||
<target>Obsah tabulky</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5eba8fa19126f70a">
|
||||
<source>Learn more about the enterprise license.</source>
|
||||
<target>Dozvědět se více o podnikové licenci.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9db1679f3b234d4e">
|
||||
<source>Search for providers…</source>
|
||||
<target>Hledat poskytovatele...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76790480b7b28ad2">
|
||||
<source>Edit provider</source>
|
||||
<target>Upravit poskytovatele</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9839619155ed2cf6">
|
||||
<source>Default NameID Policy</source>
|
||||
<target>Výchozí NameID politika</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).</source>
|
||||
<target>Nastavte výchozí zásadu NameID použitou během přihlášení inicializovaných poskytovatelem identit a také když příchozí Tvrzení (assertion) zásadu NameID nespecifikuje (používá se také při uživatelském mapování NameID).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
<target>Jméno aplikace</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
<target>Zobrazované jméno v knihovně aplikací.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
<target>URI, kam se pošle "back-channel" odhlašovací zpráva, kdy se uživatel odhlási. Vyžadováno pro "back-chanel" funkcionalitu protokolu OpenID Connect.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
<target>Tato URI se volají na straně serveru při odhlášení uživatele, aby server předal OAuth2/OpenID klientům odhlašovací událost.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
<target>Back-Channel Logout URI</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
<target>např. Spolupráce, Komunikace, Interní, atd.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
<target>Chyba při načítání aplikace &quot;<x id="0" equiv-text="${this.applicationSlug}"/>&quot;.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s32fc592c4a264edd">
|
||||
<source>Account Recovery Max Attempts</source>
|
||||
<target>Max. počet pokusů o obnovu účtu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8341dba451980abe">
|
||||
<source>Account Recovery Cache Timeout</source>
|
||||
<target>Časový limit cache obnovy účtu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sede4ad68849ce0c5">
|
||||
<source>The time window used to count recent account recovery attempts.</source>
|
||||
<target>Časové okno použité pro počítání pokusů o obnovu přístupu k účtu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s669b18c6d2d9c95b">
|
||||
<source>None</source>
|
||||
<target>Žádný</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2de7890b100c4f4c">
|
||||
<source>Flags</source>
|
||||
<target>Příznaky</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0b34d32a602aee8">
|
||||
<source>Modify flags to opt into new authentik behaviours early.</source>
|
||||
<target>Upravte příznaky, abyste dříve získali přístup k novým chováním authentiku.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s503217f10ebdc63e">
|
||||
<source>Loading templates...</source>
|
||||
<target>Načítám šablony...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6891d230751dd7bf">
|
||||
<source>Template used for the verification email.</source>
|
||||
<target>Šablona použitá pro ověřovací email.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf1fc82d7ae3f4d81">
|
||||
<source>Email Subject Prefix</source>
|
||||
<target>Předpona předmětu emailu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
<target>Šablona emailu</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
<target>Došlo k neznámé chybě</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
<target>Pro více informací se podívejte do konzole prohlížeče.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
<target>Při odesílání formuláře došlo k chybě.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
<target>Toto pole je povinné.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
<target>Získat návrhy</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
<target>Hledat v tabulce</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
<target>Při aktualizaci poskytovatele došlo k chybě.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
<target>Při vytváření poskytovatele došlo k chybě.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
<target>Zosobňuji uživatele...</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
<target>Toto může trvat několik sekund.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
<target>Krátké vysvětlení, proč zosobňujete daného uživatele. Zobrazí se v auditovacích záznamech.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8635,6 +8635,10 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Entspricht der Client-IP des Events (strenger Abgleich, für Netzwerkanpassungen eine Expression Policy verwenden).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Ungültige Update-Anfrage.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Gruppe synchronisieren</target>
|
||||
@@ -10051,42 +10055,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -6817,6 +6817,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sa82f8948649d0989">
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
</trans-unit>
|
||||
@@ -7932,42 +7935,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8641,6 +8641,10 @@ Las vinculaciones a grupos/usuarios se verifican en función del usuario del eve
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Coincide con la IP del Cliente del Evento (coincidencia estricta, para la coincidencia de red utilicé una Política de Expresión).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Solicitud de actualización no válida.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Sincronizar Grupo</target>
|
||||
@@ -10099,42 +10103,6 @@ El valor de este campo se compara con el atributo de pertenencia del usuario.</t
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8639,6 +8639,10 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Correspondance de l'adresse IP du client de l'évènement (correspondante stricte, pour un correspondance sur le réseau utiliser une politique d'expression).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Requête de mise à jour invalide.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Synchroniser le groupe</target>
|
||||
@@ -10122,42 +10126,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8640,6 +8640,10 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Corrisponde all'IP client di evento (corrispondenza rigorosa, per la corrispondenza della rete utilizza una politica di espressione).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Richiesta di aggiornamento non valida.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Gruppo di sincronizzazione</target>
|
||||
@@ -10055,42 +10059,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8262,6 +8262,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sa82f8948649d0989">
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
</trans-unit>
|
||||
@@ -9378,42 +9381,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8168,6 +8168,9 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="sa82f8948649d0989">
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
</trans-unit>
|
||||
@@ -9284,42 +9287,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8583,6 +8583,9 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="sa82f8948649d0989">
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
</trans-unit>
|
||||
@@ -9699,42 +9702,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8588,6 +8588,10 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Ḿàţćĥēś Ēvēńţ'ś Ćĺĩēńţ ĨƤ (śţŕĩćţ ḿàţćĥĩńĝ, ƒōŕ ńēţŵōŕķ ḿàţćĥĩńĝ ũśē àń Ēxƥŕēśśĩōń Ƥōĺĩćŷ).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Ĩńvàĺĩď ũƥďàţē ŕēǫũēśţ.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Śŷńć Ĝŕōũƥ</target>
|
||||
@@ -9707,40 +9711,4 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
||||
@@ -8620,6 +8620,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sa82f8948649d0989">
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Группа синхронизации </target>
|
||||
@@ -9790,42 +9793,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8598,6 +8598,10 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
<target>Olayın İstemci IP'si ile eşleşir (katı eşleştirme, ağ eşleştirmesi için bir İfade Politikası kullanın).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
<target>Geçersiz güncelleme isteği.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
<target>Grubu Eşitle</target>
|
||||
@@ -9762,42 +9766,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
</trans-unit>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -5443,6 +5443,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sa82f8948649d0989">
|
||||
<source>Matches Event's Client IP (strict matching, for network matching use an Expression Policy).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6506b7c69456b0d6">
|
||||
<source>Invalid update request.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3819162efec77de1">
|
||||
<source>Sync Group</source>
|
||||
</trans-unit>
|
||||
@@ -6559,42 +6562,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s66401e0b2526acab">
|
||||
<source>Email Template</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdaecc3c29a27c5c5">
|
||||
<source>An unknown error occurred</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se84ba7f105934495">
|
||||
<source>Please check the browser console for more details.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s44872da71d5307c8">
|
||||
<source>There was an error submitting the form.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc57b150b53a3b9e6">
|
||||
<source>This field is required.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbe3756d568bb048e">
|
||||
<source>Query suggestions</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5638954badbd5b21">
|
||||
<source>Table Search</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfc238215103c6162">
|
||||
<source>An error occurred while updating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc055fffc26e97237">
|
||||
<source>An error occurred while creating the provider.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1fd02f07e4fa3312">
|
||||
<source>Impersonating user...</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s12928f97c99f88db">
|
||||
<source>This may take a few seconds.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbc4f2320a13d4dfd">
|
||||
<source>A brief explanation of why you are impersonating the user. This will be included in audit logs.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s23da7320dee28a60">
|
||||
<source>Device Code</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user