Initial DC CMS extraction

This commit is contained in:
DCCONSTRUCTIONS
2026-07-06 00:33:27 +03:00
commit 43a96c9f7f
23 changed files with 13844 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# domains
CMS_DOMAIN=cms.local.nodedc
CMS_AUTH_DOMAIN=cms-auth.local.nodedc
CMS_AUTH_ADMIN_DOMAIN=cms-auth-admin.local.nodedc
CMS_PUBLIC_URL=http://cms.local.nodedc:8210
CMS_AUTH_PUBLIC_URL=http://cms-auth.local.nodedc:8210
CMS_AUTH_ADMIN_PUBLIC_URL=http://cms-auth-admin.local.nodedc:8210
CMS_HTTP_BIND=127.0.0.1:8210
CMS_AUTH_HTTP_BIND=127.0.0.1:8211
CMS_AUTH_ADMIN_HTTP_BIND=127.0.0.1:8212
CMS_EXTERNAL_SCHEME=http
# mounted NODE.DC site for local development
CMS_NODEDC_SITE_PATH=../../NODEDC_SITE
# authentik image
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
AUTHENTIK_TAG=2026.2.2
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS=127.0.0.0/8,172.16.0.0/12
# authentik database
PG_DB=authentik
PG_USER=authentik
PG_PASS=replace-with-random-secret
# authentik first-start bootstrap
AUTHENTIK_SECRET_KEY=replace-with-random-secret
AUTHENTIK_BOOTSTRAP_EMAIL=dcctouch@gmail.com
AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-temporary-password
AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-random-token
# CMS bootstrap user. Used by infra/scripts/bootstrap-authentik.sh.
CMS_BOOTSTRAP_ADMIN_EMAIL=dcctouch@gmail.com
CMS_BOOTSTRAP_ADMIN_PASSWORD=replace-with-temporary-password
CMS_BOOTSTRAP_ADMIN_NAME=DCTOUCH CMS Admin
# CMS OIDC application. bootstrap-authentik.sh creates the matching provider.
CMS_AUTH_ENABLED=true
CMS_BASE_URL=http://cms.local.nodedc:8210
CMS_OIDC_ISSUER=http://cms-auth.local.nodedc:8210/application/o/dc-cms/
CMS_OIDC_CLIENT_ID=dc-cms
CMS_OIDC_CLIENT_SECRET=replace-with-random-secret
CMS_OIDC_REDIRECT_URI=http://cms.local.nodedc:8210/auth/callback
CMS_OIDC_LOGGED_OUT_REDIRECT_URI=http://cms.local.nodedc:8210/auth/logged-out
CMS_LOGOUT_URI=http://cms.local.nodedc:8210/auth/logout
CMS_REQUIRED_GROUPS=dc-cms:owner,dc-cms:admin,dc-cms:editor,dc-cms:publisher
# CMS app session
CMS_SESSION_SECRET=replace-with-random-secret
COOKIE_DOMAIN=
COOKIE_SECURE=false
+52
View File
@@ -0,0 +1,52 @@
# public domains routed by Synology Reverse Proxy to 172.22.0.222:9918
CMS_DOMAIN=cms.dcserve.ru
CMS_AUTH_DOMAIN=auth.dcserve.ru
CMS_AUTH_ADMIN_DOMAIN=auth-admin.dcserve.ru
CMS_PUBLIC_URL=https://cms.dcserve.ru
CMS_AUTH_PUBLIC_URL=https://auth.dcserve.ru
CMS_AUTH_ADMIN_PUBLIC_URL=https://auth-admin.dcserve.ru
CMS_HTTP_BIND=172.22.0.222:9918
CMS_AUTH_HTTP_BIND=172.22.0.222:9919
CMS_AUTH_ADMIN_HTTP_BIND=127.0.0.1:9920
CMS_EXTERNAL_SCHEME=https
# mounted NODE.DC site root on Synology
CMS_NODEDC_SITE_PATH=/volume1/docker/dc-cms/sites/nodedc
# authentik image
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
AUTHENTIK_TAG=2026.2.2
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS=127.0.0.0/8,172.16.0.0/12
# authentik database
PG_DB=authentik
PG_USER=authentik
PG_PASS=replace-with-random-secret
# authentik first-start bootstrap
AUTHENTIK_SECRET_KEY=replace-with-random-secret
AUTHENTIK_BOOTSTRAP_EMAIL=dcctouch@gmail.com
AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-temporary-password
AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-random-token
# CMS bootstrap user. Used by infra/scripts/bootstrap-authentik.sh.
CMS_BOOTSTRAP_ADMIN_EMAIL=dcctouch@gmail.com
CMS_BOOTSTRAP_ADMIN_PASSWORD=replace-with-temporary-password
CMS_BOOTSTRAP_ADMIN_NAME=DCTOUCH CMS Admin
# CMS OIDC application. bootstrap-authentik.sh creates the matching provider.
CMS_AUTH_ENABLED=true
CMS_BASE_URL=https://cms.dcserve.ru
CMS_OIDC_ISSUER=https://auth.dcserve.ru/application/o/dc-cms/
CMS_OIDC_CLIENT_ID=dc-cms
CMS_OIDC_CLIENT_SECRET=replace-with-random-secret
CMS_OIDC_REDIRECT_URI=https://cms.dcserve.ru/auth/callback
CMS_OIDC_LOGGED_OUT_REDIRECT_URI=https://cms.dcserve.ru/auth/logged-out
CMS_LOGOUT_URI=https://cms.dcserve.ru/auth/logout
CMS_REQUIRED_GROUPS=dc-cms:owner,dc-cms:admin,dc-cms:editor,dc-cms:publisher
# CMS app session
CMS_SESSION_SECRET=replace-with-random-secret
COOKIE_DOMAIN=
COOKIE_SECURE=true
+320
View File
@@ -0,0 +1,320 @@
from os import environ
from django.db import transaction
from authentik.brands.models import Brand
from authentik.common.oauth.constants import SubModes
from authentik.core.models import Application, Group, User
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow, FlowStageBinding
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import (
ClientTypes,
IssuerMode,
OAuth2LogoutMethod,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,
ScopeMapping,
)
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.password.models import PasswordStage
GROUP_SPECS = [
("dc-cms:owner", False),
("dc-cms:admin", False),
("dc-cms:editor", False),
("dc-cms:publisher", False),
("dc-cms:site:nodedc:admin", False),
]
CMS_APP_SPEC = {
"slug": "dc-cms",
"name": "DC CMS",
"provider_name": "DC CMS OIDC",
"client_id_env": "CMS_OIDC_CLIENT_ID",
"client_secret_env": "CMS_OIDC_CLIENT_SECRET",
"redirect_uri_env": "CMS_OIDC_REDIRECT_URI",
"logged_out_redirect_uri_env": "CMS_OIDC_LOGGED_OUT_REDIRECT_URI",
"launch_url_env": "CMS_BASE_URL",
"launch_url": "http://cms.local.nodedc:8210",
"logout_uri_env": "CMS_LOGOUT_URI",
"logout_uri": "http://cms.local.nodedc:8210/auth/logout",
"groups": [
"dc-cms:owner",
"dc-cms:admin",
"dc-cms:editor",
"dc-cms:publisher",
"dc-cms:site:nodedc:admin",
],
"description": "Standalone CMS for NODE.DC websites and SEO knowledge bases.",
}
def required_env(name):
value = environ.get(name, "").strip()
if not value:
raise RuntimeError(f"{name} is required")
return value
def optional_env(name, default=""):
return environ.get(name, default).strip()
def ensure_group(name, is_superuser=False):
group, _ = Group.objects.get_or_create(name=name)
group.is_superuser = is_superuser
group.save()
return group
def ensure_groups():
groups = {}
for name, is_superuser in GROUP_SPECS:
groups[name] = ensure_group(name, is_superuser)
return groups
def ensure_admin_user(groups):
admin_email = (
environ.get("CMS_BOOTSTRAP_ADMIN_EMAIL", "").strip()
or environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "").strip()
)
if not admin_email:
return None
user = User.objects.filter(email__iexact=admin_email).first() or User.objects.filter(
username=admin_email
).first()
if user is None:
user = User(username=admin_email, email=admin_email, name=admin_email, type="internal")
user.username = admin_email
user.email = admin_email
user.name = environ.get("CMS_BOOTSTRAP_ADMIN_NAME", "DCTOUCH CMS Admin").strip() or admin_email
user.is_active = True
user.type = "internal"
admin_password = (
environ.get("CMS_BOOTSTRAP_ADMIN_PASSWORD", "")
or environ.get("AUTHENTIK_BOOTSTRAP_PASSWORD", "")
)
if admin_password:
user.set_password(admin_password)
user.save()
authentik_admins, _ = Group.objects.get_or_create(name="authentik Admins")
if not authentik_admins.is_superuser:
authentik_admins.is_superuser = True
authentik_admins.save(update_fields=["is_superuser"])
user.groups.add(authentik_admins)
for group in groups.values():
user.groups.add(group)
return user
def ensure_groups_scope_mapping():
mapping, _ = ScopeMapping.objects.get_or_create(
name="DC CMS OAuth Mapping: groups",
defaults={
"scope_name": "groups",
"description": "Adds Authentik group names to DC CMS OIDC tokens.",
"expression": 'return {"groups": [group.name for group in request.user.groups.all()]}',
},
)
mapping.scope_name = "groups"
mapping.description = "Adds Authentik group names to DC CMS OIDC tokens."
mapping.expression = 'return {"groups": [group.name for group in request.user.groups.all()]}'
mapping.save()
return mapping
def ensure_profile_scope_mapping():
expression = """
attributes = request.user.attributes or {}
display_name = request.user.name or request.user.username
name_parts = display_name.split(" ", 1)
avatar_url = attributes.get("picture") or attributes.get("avatar_url") or attributes.get("avatar")
return {
"name": display_name,
"given_name": name_parts[0] if name_parts else display_name,
"family_name": name_parts[1] if len(name_parts) > 1 else "",
"preferred_username": request.user.username,
"nickname": request.user.username,
"picture": avatar_url,
"avatar_url": avatar_url,
}
""".strip()
mapping, _ = ScopeMapping.objects.get_or_create(
name="DC CMS OAuth Mapping: profile context",
defaults={
"scope_name": "profile",
"description": "Adds normalized DC CMS profile claims to OIDC tokens.",
"expression": expression,
},
)
mapping.scope_name = "profile"
mapping.description = "Adds normalized DC CMS profile claims to OIDC tokens."
mapping.expression = expression
mapping.save()
return mapping
def default_scope_mappings():
scope_names = ["openid", "email", "offline_access"]
mappings = list(ScopeMapping.objects.filter(scope_name__in=scope_names))
mappings.append(ensure_profile_scope_mapping())
mappings.append(ensure_groups_scope_mapping())
return mappings
def ensure_cms_brand():
auth_domain = environ.get("CMS_AUTH_DOMAIN", "cms-auth.local.nodedc").strip() or "cms-auth.local.nodedc"
authentication_flow = Flow.objects.get(slug="default-authentication-flow")
invalidation_flow = Flow.objects.get(slug="default-invalidation-flow")
authentication_flow.name = "DC CMS authentication"
authentication_flow.title = "Управление сайтами."
authentication_flow.layout = "stacked"
authentication_flow.background = ""
authentication_flow.save()
identification_stage = IdentificationStage.objects.get(
name="default-authentication-identification"
)
password_stage = PasswordStage.objects.get(name="default-authentication-password")
password_stage.allow_show_password = True
password_stage.save()
identification_stage.user_fields = ["email"]
identification_stage.password_stage = password_stage
identification_stage.show_matched_user = False
identification_stage.enable_remember_me = False
identification_stage.save()
FlowStageBinding.objects.filter(target=authentication_flow, stage=password_stage).delete()
brand = Brand.objects.filter(domain=auth_domain).first()
if brand is None:
brand = Brand(domain=auth_domain)
Brand.objects.exclude(brand_uuid=brand.brand_uuid).update(default=False)
brand.default = True
brand.domain = auth_domain
brand.branding_title = "DC CMS"
brand.branding_logo = ""
brand.branding_favicon = ""
brand.branding_custom_css = ""
brand.flow_authentication = authentication_flow
brand.flow_invalidation = invalidation_flow
brand.attributes = {
**(brand.attributes or {}),
"settings": {
**((brand.attributes or {}).get("settings") or {}),
"locale": "ru",
"theme": {
**(((brand.attributes or {}).get("settings") or {}).get("theme") or {}),
"base": "dark",
},
},
}
brand.save()
return brand
def ensure_provider(spec, mappings):
authorization_flow = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
invalidation_flow = Flow.objects.get(slug="default-invalidation-flow")
signing_key = (
CertificateKeyPair.objects.filter(name="authentik Self-signed Certificate").first()
or CertificateKeyPair.objects.first()
)
if signing_key is None:
raise RuntimeError("No Authentik CertificateKeyPair exists for OIDC signing")
provider = OAuth2Provider.objects.filter(name=spec["provider_name"]).first()
if provider is None:
provider = OAuth2Provider(name=spec["provider_name"])
provider.name = spec["provider_name"]
provider.client_type = ClientTypes.CONFIDENTIAL
provider.client_id = required_env(spec["client_id_env"])
provider.client_secret = required_env(spec["client_secret_env"])
redirect_uri_values = [required_env(spec["redirect_uri_env"])]
logged_out_redirect_uri = optional_env(spec.get("logged_out_redirect_uri_env", ""), "")
if logged_out_redirect_uri:
redirect_uri_values.append(logged_out_redirect_uri)
provider.redirect_uris = [
RedirectURI(RedirectURIMatchingMode.STRICT, redirect_uri)
for redirect_uri in dict.fromkeys(redirect_uri_values)
]
provider.logout_uri = optional_env(spec.get("logout_uri_env", ""), spec["logout_uri"])
provider.logout_method = OAuth2LogoutMethod.FRONTCHANNEL
provider.include_claims_in_id_token = True
provider.sub_mode = SubModes.USER_UUID
provider.issuer_mode = IssuerMode.PER_PROVIDER
provider.authorization_flow = authorization_flow
provider.invalidation_flow = invalidation_flow
provider.signing_key = signing_key
provider.save()
provider.property_mappings.set(mappings)
return provider
def ensure_application(spec, provider, groups):
application = Application.objects.filter(slug=spec["slug"]).first()
if application is None:
application = Application(slug=spec["slug"], name=spec["name"])
application.name = spec["name"]
application.slug = spec["slug"]
application.group = "DC CMS"
application.provider = provider
application.meta_launch_url = optional_env(spec.get("launch_url_env", ""), spec["launch_url"])
application.meta_description = spec["description"]
application.meta_publisher = "DCTOUCH"
application.open_in_new_tab = False
application.policy_engine_mode = "any"
application.save()
PolicyBinding.objects.filter(target=application).exclude(
group__name__in=spec["groups"]
).delete()
for order, group_name in enumerate(spec["groups"]):
binding = PolicyBinding.objects.filter(target=application, group=groups[group_name]).first()
if binding is None:
binding = PolicyBinding(target=application, group=groups[group_name])
binding.enabled = True
binding.negate = False
binding.timeout = 30
binding.failure_result = False
binding.order = order
binding.save()
return application
@transaction.atomic
def main():
brand = ensure_cms_brand()
groups = ensure_groups()
user = ensure_admin_user(groups)
mappings = default_scope_mappings()
provider = ensure_provider(CMS_APP_SPEC, mappings)
application = ensure_application(CMS_APP_SPEC, provider, groups)
summary = {
"groups": list(groups),
"admin_user": user.email if user else None,
"brand": brand.domain,
"application": application.slug,
"provider": provider.name,
}
print(summary)
main()
@@ -0,0 +1 @@
@@ -0,0 +1,948 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<script data-id="nodedc-auth-favicon">
"use strict";
(function () {
const faviconHref = "data:image/svg+xml,%3Csvg%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%2032%2032%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cstyle%3E%0A%20%20%20%20.icon-fav-01%20%7B%20fill%3A%20%23333334%3B%20%7D%0A%20%20%20%20%40media%20(prefers-color-scheme%3A%20dark)%20%7B%20.icon-fav-01%20%7B%20fill%3A%20%23FEFEFE%3B%20%7D%20%7D%0A%20%20%20%20%40media%20(prefers-color-scheme%3A%20light)%20%7B%20.icon-fav-01%20%7B%20fill%3A%20%23000000%3B%20%7D%20%7D%0A%20%20%3C%2Fstyle%3E%0A%20%20%3Cpath%20class%3D%22icon-fav-01%22%20d%3D%22M17.8738%2015.9159L16.0002%2018.9718L14.1267%2015.9159H17.8738ZM23.6307%2012.7864H8.36914L15.9999%2025.2308L23.6307%2012.7864Z%22%2F%3E%0A%20%20%3Cpath%20class%3D%22icon-fav-01%22%20d%3D%22M10.9793%2019.4872L6.67269%2011.5918H25.5344L21.2281%2019.4872H25.0581L31.2614%208H0.738281L7.16244%2019.4872H10.9793Z%22%2F%3E%0A%3C%2Fsvg%3E";
const applyFavicon = () => {
if (!document.head) return;
document.querySelectorAll('link[rel~="icon"]').forEach((node) => node.remove());
const link = document.createElement("link");
link.rel = "icon";
link.type = "image/svg+xml";
link.sizes = "any";
link.href = faviconHref;
document.head.appendChild(link);
};
applyFavicon();
window.addEventListener("DOMContentLoaded", applyFavicon, { once: true });
})();
</script>
{% with request_host=request.get_host %}
{% if request_host|slice:":11" != "auth-admin." and request_host|slice:":15" != "cms-auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %}
{% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %}
<script data-id="nodedc-auth-flow-scope">
"use strict";
(function () {
const isAuthentikAdminHost =
window.location.hostname.startsWith("auth-admin.") ||
window.location.hostname.startsWith("cms-auth-admin.") ||
window.location.hostname.startsWith("id-admin.") ||
window.location.hostname === "172.22.0.222";
const isAuthExperiencePage =
!isAuthentikAdminHost &&
(
window.location.pathname.includes("/if/flow/") ||
window.location.pathname.includes("/flows/") ||
window.location.pathname.includes("/login/")
);
document.documentElement.classList.toggle("nodedc-auth-flow-page", isAuthExperiencePage);
window.addEventListener("DOMContentLoaded", function () {
document.body?.classList.toggle("nodedc-auth-flow-page", isAuthExperiencePage);
}, { once: true });
})();
</script>
<style data-id="nodedc-auth-login-css">
{% include "branding/nodedc-login.css" %}
</style>
<style data-id="nodedc-auth-critical-loader">
html.nodedc-auth-flow-page {
--ak-global--primary: #f4f4f5 !important;
--ak-global--background: #0e0f10 !important;
--ak-global--background-image: none !important;
--pf-global--primary-color--100: #f4f4f5 !important;
--pf-v5-global--primary-color--100: #f4f4f5 !important;
--pf-v6-global--primary-color--100: #f4f4f5 !important;
--pf-v4-global--palette--blue-300: #f4f4f5 !important;
--ak-global--palette--blue-300: #f4f4f5 !important;
--pf-global--active-color--100: #f4f4f5 !important;
--pf-global--link--Color: #f4f4f5 !important;
--nodedc-auth-primary: #f4f4f5 !important;
--nodedc-auth-bg: #0e0f10 !important;
}
html.nodedc-auth-flow-page,
html.nodedc-auth-flow-page body,
html.nodedc-auth-flow-page body .pf-c-login {
background: #0e0f10 !important;
color-scheme: dark !important;
}
html.nodedc-auth-flow-page body ak-flow-executor::part(main),
html.nodedc-auth-flow-page body .pf-c-login__main,
html.nodedc-auth-flow-page body .pf-c-empty-state,
html.nodedc-auth-flow-page body .pf-v5-c-empty-state,
html.nodedc-auth-flow-page body .pf-v6-c-empty-state,
html.nodedc-auth-flow-page body .pf-c-card,
html.nodedc-auth-flow-page body .pf-v5-c-card,
html.nodedc-auth-flow-page body .pf-v6-c-card,
html.nodedc-auth-flow-page body [class*="empty-state"] {
background: transparent !important;
box-shadow: none !important;
border: 0 !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
}
html.nodedc-auth-flow-page body .pf-c-spinner,
html.nodedc-auth-flow-page body .pf-v5-c-spinner,
html.nodedc-auth-flow-page body .pf-v6-c-spinner,
html.nodedc-auth-flow-page body [role="progressbar"],
html.nodedc-auth-flow-page body ak-spinner {
--pf-c-spinner--Color: #f4f4f5 !important;
--pf-v5-c-spinner--Color: #f4f4f5 !important;
--pf-v6-c-spinner--Color: #f4f4f5 !important;
--pf-c-spinner__clipper--after--BoxShadowColor: #f4f4f5 !important;
--pf-c-spinner__lead-ball--after--BackgroundColor: #f4f4f5 !important;
--pf-c-spinner__tail-ball--after--BackgroundColor: #f4f4f5 !important;
color: #f4f4f5 !important;
}
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder,
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] {
position: fixed !important;
top: 50% !important;
left: 50% !important;
display: block !important;
width: 2.5rem !important;
height: 2.5rem !important;
min-width: 2.5rem !important;
min-height: 2.5rem !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 999px !important;
background: transparent !important;
box-shadow: none !important;
opacity: 1 !important;
visibility: visible !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
transform: translate(-50%, -50%) !important;
overflow: visible !important;
}
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder .pf-c-spinner,
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] .pf-c-spinner,
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder [role="progressbar"],
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] [role="progressbar"] {
opacity: 0 !important;
visibility: hidden !important;
}
html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder::before,
html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"]::before {
content: "";
position: absolute;
inset: 0;
border: 2px solid rgba(255, 255, 255, 0.22);
border-top-color: #f4f4f5;
border-radius: 999px;
animation: nodedc-auth-placeholder-spin 0.72s linear infinite;
pointer-events: none;
}
html.nodedc-auth-flow-page body.nodedc-auth-loading ak-flow-executor::part(main),
html.nodedc-auth-flow-page body.nodedc-auth-loading .pf-c-login__main {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
html.nodedc-auth-flow-page body.nodedc-auth-loading::before {
content: "";
position: fixed;
top: 50%;
left: 50%;
z-index: 1200;
width: 2.5rem;
height: 2.5rem;
border: 2px solid rgba(255, 255, 255, 0.22);
border-top-color: #f4f4f5;
border-radius: 999px;
transform: translate(-50%, -50%);
animation: nodedc-auth-loader-spin 0.72s linear infinite;
pointer-events: none;
}
html.nodedc-auth-flow-page body.nodedc-auth-card-ready::before {
display: none !important;
}
@keyframes nodedc-auth-loader-spin {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes nodedc-auth-placeholder-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
{% endif %}
{% endif %}
{% endwith %}
<script data-id="authentik-config">
"use strict";
const isAuthentikAdminHost =
window.location.hostname.startsWith("auth-admin.") ||
window.location.hostname.startsWith("id-admin.") ||
window.location.hostname === "172.22.0.222";
const isAuthExperiencePage =
!isAuthentikAdminHost &&
(
window.location.pathname.includes("/if/flow/") ||
window.location.pathname.includes("/flows/") ||
window.location.pathname.includes("/login/")
);
const authentikBrand = JSON.parse('{{ brand_json|escapejs }}' || "{}");
if (isAuthExperiencePage) {
const nodedcAuthLoginCss = document.querySelector('style[data-id="nodedc-auth-login-css"]')?.textContent || "";
authentikBrand.branding_custom_css = nodedcAuthLoginCss;
authentikBrand.brandingCustomCss = nodedcAuthLoginCss;
} else {
authentikBrand.branding_custom_css = "";
authentikBrand.brandingCustomCss = "";
}
window.authentik = {
locale: "ru",
config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
brand: authentikBrand,
versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
api: {
base: "{{ base_url }}",
relBase: "{{ base_url_rel }}",
},
};
window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %}
window.dispatchEvent(
new CustomEvent("ak-message", {
bubbles: true,
composed: true,
detail: {
level: "{{ message.tags|escapejs }}",
message: "{{ message.message|escapejs }}",
},
}),
);
{% endfor %}
});
</script>
{% with request_host=request.get_host %}
{% if request_host|slice:":11" != "auth-admin." and request_host|slice:":15" != "cms-auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %}
{% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %}
<script data-id="nodedc-auth-field-enhancements">
"use strict";
(function () {
const isAuthentikAdminHost =
window.location.hostname.startsWith("auth-admin.") ||
window.location.hostname.startsWith("cms-auth-admin.") ||
window.location.hostname.startsWith("id-admin.") ||
window.location.hostname === "172.22.0.222";
const isAuthExperiencePage =
!isAuthentikAdminHost &&
(
window.location.pathname.includes("/if/flow/") ||
window.location.pathname.includes("/flows/") ||
window.location.pathname.includes("/login/")
);
if (!isAuthExperiencePage) return;
document.documentElement.classList.add("nodedc-auth-flow-page");
window.addEventListener("DOMContentLoaded", function () {
document.body?.classList.add("nodedc-auth-flow-page");
}, { once: true });
const logoSvg = `
<svg id="nodedc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220.82 54.55" aria-hidden="true">
<defs>
<style>
.cls-1{fill:#e2e1e1;}
.cls-2{fill:#dbdbdb;stroke:#dbdbdb;stroke-miterlimit:10;stroke-width:0.75px;}
</style>
</defs>
<path class="cls-1" d="M52.8,23.61,46.92,33.76,41.05,23.61H52.8m18-10.39H23.06L46.92,54.55Z"></path>
<polygon class="cls-1" points="31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13"></polygon>
<path class="cls-2" d="M116.35,18.49V1h1.27l10.34,15V1h1.33V18.49H128l-10.34-15v15Z"></path>
<path class="cls-2" d="M140.43,18.64c-4.79,0-8.16-3.72-8.16-8.89S135.64.86,140.43.86s8.17,3.72,8.17,8.89S145.25,18.64,140.43,18.64Zm0-1.25c4,0,6.79-3.17,6.79-7.64s-2.77-7.64-6.79-7.64-6.77,3.17-6.77,7.64S136.44,17.39,140.43,17.39Z"></path>
<path class="cls-2" d="M151.6,18.49V1h5.1c5.54,0,8.79,3.42,8.79,8.74s-3.25,8.74-8.79,8.74ZM153,17.24h3.75c4.77,0,7.42-2.92,7.42-7.49s-2.65-7.49-7.42-7.49H153Z"></path>
<path class="cls-2" d="M168.49,1h10.77V2.26h-9.42V8.93h7.89v1.25h-7.89v7.06h9.74v1.25H168.49Z"></path>
<path class="cls-2" d="M188.88,18.49V1H194c5.54,0,8.79,3.42,8.79,8.74s-3.25,8.74-8.79,8.74Zm1.35-1.25H194c4.77,0,7.41-2.92,7.41-7.49S198.75,2.26,194,2.26h-3.75Z"></path>
<path class="cls-2" d="M205.15,9.75c0-5.24,3.19-8.89,8.11-8.89a6.8,6.8,0,0,1,7.1,5.52h-1.43a5.54,5.54,0,0,0-5.74-4.27c-4.05,0-6.64,3.17-6.64,7.64s2.54,7.64,6.59,7.64a5.46,5.46,0,0,0,5.74-4.29h1.43c-.75,3.52-3.4,5.54-7.15,5.54C208.27,18.64,205.15,15.05,205.15,9.75Z"></path>
</svg>
`;
const clearIcon = `
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"></circle>
<path d="M9 9l6 6M15 9l-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
</svg>
`;
const genericLoginError = "Не удалось выполнить вход. Проверьте email и пароль или запросите доступ.";
const revokedLoginError = "Аккаунт больше не активен. Запросите доступ, если хотите подключиться снова.";
const genericFlowErrorMessages = [
"Response returned an error code",
"Response returned an error",
"Не удалось завершить операцию.",
];
const accountStatusCache = new Map();
const authTranslations = new Map([
["Failed to authenticate.", genericLoginError],
["Invalid credentials.", genericLoginError],
["Invalid password.", genericLoginError],
["Incorrect password.", genericLoginError],
["Authentication failed.", genericLoginError],
["This field is required.", "Заполните это поле."],
["Please enter your password", "Введите пароль"],
["Please enter your password.", "Введите пароль."],
["Please enter a valid email address.", "Введите корректную электронную почту."],
["Caps Lock is enabled.", "Включён Caps Lock."],
["Forgot username or password?", "Забыли пароль?"],
["Forgot password?", "Забыли пароль?"],
["Remember me on this device", "Запомнить на этом устройстве"],
["Email", "Эл. почта"],
["Email or Username", "Эл. почта"],
["Password", "Пароль"],
["Log in", "Войти"],
["Continue", "Продолжить"],
["Show password", "Показать пароль"],
["Hide password", "Скрыть пароль"],
["Permission denied", "Доступ запрещён"],
["Not you?", "Сменить пользователя"],
["Request has been denied.", "Доступ запрещён."],
["The request has been denied.", "Доступ запрещён."],
["В произведении запроса было отказано.", "Доступ запрещён."],
["Go home", "Вернуться назад"],
["Response returned an error code", "Не удалось завершить операцию."],
]);
function visitRoots(root, callback) {
callback(root);
root.querySelectorAll("*").forEach((element) => {
if (element.shadowRoot) {
visitRoots(element.shadowRoot, callback);
}
});
}
function ensureLogo() {
if (!document.body || document.querySelector("[data-nodedc-auth-logo='true']")) return;
document.body.classList.add("nodedc-auth-enhanced");
const logo = document.createElement("a");
logo.className = "nodedc-auth-logo";
logo.dataset.nodedcAuthLogo = "true";
logo.href = getLauncherBaseUrl();
logo.setAttribute("aria-label", "DC CMS");
logo.innerHTML = logoSvg;
document.body.appendChild(logo);
updateLogoLink(logo);
}
function getLauncherBaseUrl() {
const hostname = window.location.hostname;
const cmsHostnames = {
"cms-auth.local.nodedc": "cms.local.nodedc",
"cms-auth-admin.local.nodedc": "cms.local.nodedc",
"cms-id.local.nodedc": "cms.local.nodedc",
"auth.dcserve.ru": "cms.dcserve.ru",
"auth-admin.dcserve.ru": "cms.dcserve.ru",
};
const cmsHostname =
cmsHostnames[hostname] ||
(hostname.startsWith("cms-auth.")
? `cms.${hostname.slice(9)}`
: hostname.startsWith("auth.")
? `cms.${hostname.slice(5)}`
: hostname.startsWith("cms-id.")
? `cms.${hostname.slice(7)}`
: "cms.local.nodedc");
const port = window.location.port ? `:${window.location.port}` : "";
return `${window.location.protocol}//${cmsHostname}${port}/`;
}
function updateLogoLink(logo) {
fetch(new URL("/api/public/brand", getLauncherBaseUrl()).toString(), { cache: "no-store" })
.then((response) => (response.ok ? response.json() : null))
.then((payload) => {
if (payload?.logoLinkUrl) {
logo.href = payload.logoLinkUrl;
}
})
.catch(() => {});
}
function translateTextValue(value) {
if (!value) return value;
const trimmed = value.trim();
const translated = authTranslations.get(trimmed);
if (!translated) return value;
return value.replace(trimmed, translated);
}
function translateAuthText(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const parent = node.parentElement;
if (!parent || !node.nodeValue?.trim()) return NodeFilter.FILTER_REJECT;
if (["SCRIPT", "STYLE", "SVG", "PATH", "POLYGON"].includes(parent.tagName)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
textNodes.forEach((node) => {
const translated = translateTextValue(node.nodeValue);
if (translated !== node.nodeValue) {
node.nodeValue = translated;
}
});
root.querySelectorAll("[aria-label], [title], [placeholder], input[value], button").forEach((element) => {
["aria-label", "title", "placeholder"].forEach((attribute) => {
const value = element.getAttribute(attribute);
const translated = translateTextValue(value);
if (translated !== value) element.setAttribute(attribute, translated);
});
});
}
function hasPermissionDeniedScreen() {
let denied = false;
visitRoots(document, (root) => {
if (denied) return;
const text = root.textContent || "";
denied = [
"Permission denied",
"Доступ ограничен",
"Доступ запрещен",
"Доступ запрещён",
"Request has been denied.",
"The request has been denied.",
"В произведении запроса было отказано.",
"Доступ к модулю ограничен.",
].some((message) => text.includes(message));
});
return denied;
}
function syncPermissionDeniedState() {
document.body?.classList.toggle("nodedc-auth-permission-denied", hasPermissionDeniedScreen());
}
function findAncestor(element, predicate, maxDepth = 8) {
let current = element;
for (let depth = 0; current && depth < maxDepth; depth += 1) {
if (predicate(current)) return current;
current = current.parentElement;
}
return null;
}
function applyPermissionDeniedLayout(root) {
const deniedTitleMessages = ["Permission denied", "Доступ ограничен", "Доступ запрещен", "Доступ запрещён"];
const deniedSubtitle = "Доступ запрещён.";
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const text = node.nodeValue?.trim() || "";
return deniedTitleMessages.some((message) => text === message)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
textNodes.forEach((node) => {
const titleElement = findAncestor(
node.parentElement,
(element) =>
element.matches?.(".pf-c-title, .pf-v5-c-title, h1, h2") ||
element.getAttribute?.("slot") === "title",
);
const header =
titleElement?.closest?.(".pf-c-login__main-header") ||
findAncestor(titleElement || node.parentElement, (element) => element.matches?.(".pf-c-login__main-header"), 10) ||
root.querySelector?.(".pf-c-login__main-header");
if (titleElement) {
titleElement.style.display = "none";
}
if (header) {
header.setAttribute("data-nodedc-permission-denied", "true");
header.setAttribute("data-nodedc-denied-subtitle", deniedSubtitle);
}
});
}
function hidePermissionDeniedReason(root) {
const deniedMessages = [
"Request has been denied.",
"The request has been denied.",
"В произведении запроса было отказано.",
"Доступ к модулю ограничен.",
"Объяснение:",
"Reason:",
"Привязка политики",
"Policy binding",
"returned result",
"returned a result",
"'None' вернула результат",
];
root.querySelectorAll(".pf-c-alert, .pf-v5-c-alert, .pf-c-empty-state__body, [class*='alert']").forEach((element) => {
const text = element.textContent || "";
if (deniedMessages.some((message) => text.includes(message))) {
element.style.display = "none";
}
});
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const text = node.nodeValue?.trim() || "";
return deniedMessages.some((message) => text.includes(message))
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
textNodes.forEach((node) => {
let element = node.parentElement;
for (let depth = 0; element && depth < 6; depth += 1) {
const text = (element.textContent || "").trim();
if (deniedMessages.some((message) => text === message || text.endsWith(message)) && text.length <= 96) {
element.style.display = "none";
return;
}
element = element.parentElement;
}
if (node.parentElement) {
node.parentElement.style.display = "none";
}
});
}
function enhanceIdentifierField(root) {
const input = root.querySelector("#ak-identifier-input");
if (!input || input.dataset.nodedcClearBound === "true") return;
const fieldGroup = input.closest(".pf-c-form__group") || input.parentElement;
if (!fieldGroup) return;
input.dataset.nodedcClearBound = "true";
fieldGroup.style.position = "relative";
const button = document.createElement("button");
button.type = "button";
button.tabIndex = -1;
button.className = "nodedc-auth-clear-input";
button.dataset.nodedcClearInput = "true";
button.setAttribute("aria-label", "Очистить электронную почту");
button.innerHTML = clearIcon;
const update = () => {
button.dataset.visible = input.value ? "true" : "false";
};
button.addEventListener("click", (event) => {
event.preventDefault();
input.value = "";
input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
input.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
input.focus();
update();
});
input.addEventListener("input", update);
input.addEventListener("change", update);
fieldGroup.appendChild(button);
update();
}
function maskPasswordInput(input) {
if (!input || input.type !== "text") return;
input.type = "password";
const button = input.closest(".pf-c-input-group")?.querySelector(".pf-c-button.pf-m-control");
const icon = button?.querySelector("i");
button?.setAttribute("aria-label", "Показать пароль");
icon?.classList.remove("fa-eye-slash");
icon?.classList.add("fa-eye");
}
function maskVisiblePasswords(target) {
visitRoots(document, (root) => {
root.querySelectorAll("#ak-stage-identification-password, #ak-stage-password-input").forEach((input) => {
const inputGroup = input.closest(".pf-c-input-group");
if (inputGroup?.contains(target)) return;
maskPasswordInput(input);
});
});
}
function enhancePasswordFields(root) {
root.querySelectorAll("#ak-stage-identification-password, #ak-stage-password-input").forEach((input) => {
if (input.placeholder !== "Введите пароль") {
input.placeholder = "Введите пароль";
}
if (input.dataset.nodedcBlurBound === "true") return;
input.dataset.nodedcBlurBound = "true";
input.addEventListener("blur", () => {
window.setTimeout(() => {
const inputGroup = input.closest(".pf-c-input-group");
if (inputGroup?.contains(document.activeElement)) return;
maskPasswordInput(input);
}, 0);
});
});
}
function normalizePasswordFields(root) {
enhancePasswordFields(root);
}
function dispatchSyntheticInput(input) {
if (!input) return;
try {
input.dispatchEvent(new InputEvent("input", {
bubbles: true,
composed: true,
inputType: "insertReplacementText",
data: input.value || "",
}));
} catch {
input.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
}
input.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
}
function syncFormFieldsBeforeSubmit(form) {
form.querySelectorAll("#ak-identifier-input, #ak-stage-identification-password, #ak-stage-password-input").forEach((input) => {
dispatchSyntheticInput(input);
});
}
function enhanceSubmitHandoff(root) {
root.querySelectorAll("form").forEach((form) => {
if (form.dataset.nodedcSubmitBound === "true") return;
form.dataset.nodedcSubmitBound = "true";
form.addEventListener("submit", () => {
syncFormFieldsBeforeSubmit(form);
document.body?.classList.add("nodedc-auth-submitting");
}, true);
});
}
function ensureAccessRequestLink(root) {
const input = root.querySelector("#ak-identifier-input, #ak-stage-identification-password, #ak-stage-password-input");
const form = input?.closest("form") || root.querySelector("form");
if (!input || !form || form.querySelector("[data-nodedc-access-request='true']")) return;
const link = document.createElement("a");
link.className = "nodedc-auth-request-access";
link.dataset.nodedcAccessRequest = "true";
link.href = "mailto:dcctouch@gmail.com";
link.textContent = "Запросить доступ";
form.appendChild(link);
}
function normalizeEmail(value) {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function getLoginEmail(root) {
return normalizeEmail(root.querySelector("#ak-identifier-input")?.value);
}
function getLoginErrorMessages() {
return [
"Failed to authenticate.",
"Invalid credentials.",
"Invalid password.",
"Incorrect password.",
"Authentication failed.",
"Не удалось выполнить вход.",
"Неверная почта или пароль.",
"Неверный пароль.",
genericLoginError,
revokedLoginError,
];
}
function replaceLoginErrorText(root, message) {
const errorMessages = getLoginErrorMessages();
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const text = node.nodeValue?.trim() || "";
return errorMessages.some((errorMessage) => text.includes(errorMessage))
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
textNodes.forEach((node) => {
const currentValue = node.nodeValue || "";
const trimmed = currentValue.trim();
if (!trimmed) return;
node.nodeValue = currentValue.replace(trimmed, message);
});
}
function syncLoginErrorReason(root) {
if (!hasAuthError(root)) return;
replaceLoginErrorText(root, genericLoginError);
const email = getLoginEmail(root);
if (!email) return;
const cachedStatus = accountStatusCache.get(email);
if (cachedStatus === "revoked") {
replaceLoginErrorText(root, revokedLoginError);
return;
}
if (cachedStatus === "unknown" || cachedStatus === "loading") return;
accountStatusCache.set(email, "loading");
const statusUrl = new URL("/api/public/login-account-status", getLauncherBaseUrl());
statusUrl.searchParams.set("email", email);
fetch(statusUrl.toString(), { cache: "no-store" })
.then((response) => (response.ok ? response.json() : null))
.then((payload) => {
const status = payload?.status === "revoked" ? "revoked" : "unknown";
accountStatusCache.set(email, status);
if (status !== "revoked") return;
visitRoots(document, (candidateRoot) => {
replaceLoginErrorText(candidateRoot, revokedLoginError);
});
})
.catch(() => {
accountStatusCache.set(email, "unknown");
});
}
function hasAuthError(root) {
const hasErrorElement = root.querySelector(
".pf-c-alert, .pf-m-error, .pf-c-helper-text__item.pf-m-error, [aria-invalid='true']"
);
if (hasErrorElement) return true;
const text = root.textContent || "";
return getLoginErrorMessages().some((message) => text.includes(message));
}
function hasGenericFlowError(root) {
const text = root.textContent || "";
return genericFlowErrorMessages.some((message) => text.includes(message));
}
function hasLoadingOnlyScreen(root) {
const isFlowExecutorRoot = root.host?.tagName?.toLowerCase() === "ak-flow-executor";
const hasProgress =
root.querySelector?.(".pf-c-spinner, .pf-v5-c-spinner, [role='progressbar'], ak-spinner");
const isEmptyFlowExecutor = isFlowExecutorRoot && (root.textContent || "").trim() === "";
const hasInteractiveAuth =
root.querySelector?.("input, textarea, select, button[type='submit'], .pf-c-alert, .pf-v5-c-alert") ||
hasGenericFlowError(root);
return Boolean((hasProgress || isEmptyFlowExecutor) && !hasInteractiveAuth);
}
function syncLoadingState() {
let isLoadingOnly = false;
let hasAuthContent = false;
visitRoots(document, (root) => {
if (hasLoadingOnlyScreen(root)) {
isLoadingOnly = true;
}
if (
root.querySelector?.("input, textarea, select, button[type='submit'], .pf-c-alert, .pf-v5-c-alert") ||
hasGenericFlowError(root)
) {
hasAuthContent = true;
}
});
const shouldShowLoader = isLoadingOnly && !hasAuthContent;
document.body?.classList.toggle("nodedc-auth-loading", shouldShowLoader);
document.body?.classList.toggle("nodedc-auth-card-ready", hasAuthContent);
}
function restoreCardOnErrors(root) {
if (hasAuthError(root)) {
document.body?.classList.remove("nodedc-auth-submitting");
}
}
function isOidcLogoutFlow() {
const path = window.location.pathname;
const params = new URLSearchParams(window.location.search);
return (
path.includes("/end-session/") ||
path.includes("/if/flow/default-invalidation-flow") ||
params.has("post_logout_redirect_uri")
);
}
function getSafePostLogoutRedirect() {
const rawUrl = new URLSearchParams(window.location.search).get("post_logout_redirect_uri");
if (!rawUrl) return null;
try {
const url = new URL(rawUrl);
const allowedHosts = new Set([
"cms.local.nodedc",
"cms.nodedc.ru",
"dc-cms.local.nodedc",
"cms.dcserve.ru",
]);
if (!["http:", "https:"].includes(url.protocol)) return null;
if (!allowedHosts.has(url.hostname) && url.hostname !== new URL(getLauncherBaseUrl()).hostname) return null;
if (!url.pathname.startsWith("/auth/logged-out")) return null;
return url.toString();
} catch {
return null;
}
}
function buildLoggedOutRedirectUrl() {
return new URL("/auth/logged-out", getLauncherBaseUrl()).toString();
}
function redirectGenericFlowError(root) {
if (document.body?.dataset.nodedcFlowErrorRedirected === "true") return;
if (!window.location.pathname.includes("/if/flow/")) return;
if (!hasGenericFlowError(root)) return;
document.body.dataset.nodedcFlowErrorRedirected = "true";
document.body.classList.add("nodedc-auth-submitting");
window.setTimeout(() => {
window.location.replace(getSafePostLogoutRedirect() || buildLoggedOutRedirectUrl());
}, 120);
}
function redirectCompletedLogout(root) {
if (!isOidcLogoutFlow() || document.body?.dataset.nodedcLogoutRedirected === "true") return;
const redirectUrl = getSafePostLogoutRedirect();
if (!redirectUrl) return;
const isInvalidationFlow = window.location.pathname.includes("/if/flow/default-invalidation-flow");
const text = root.textContent || "";
const logoutComplete = [
"Logout successful",
"You've logged out",
"You have been logged out",
"Logged out",
"Вы вышли",
"Выход выполнен",
].some((message) => text.includes(message));
if (!logoutComplete && !isInvalidationFlow) return;
document.body.dataset.nodedcLogoutRedirected = "true";
document.body.classList.add("nodedc-auth-submitting");
window.setTimeout(() => {
window.location.replace(redirectUrl);
}, isInvalidationFlow ? 1600 : 150);
}
let scheduled = false;
function enhanceAuthFields() {
scheduled = false;
ensureLogo();
visitRoots(document, (root) => {
enhanceIdentifierField(root);
normalizePasswordFields(root);
enhanceSubmitHandoff(root);
ensureAccessRequestLink(root);
translateAuthText(root);
syncLoginErrorReason(root);
applyPermissionDeniedLayout(root);
hidePermissionDeniedReason(root);
restoreCardOnErrors(root);
redirectCompletedLogout(root);
redirectGenericFlowError(root);
});
syncPermissionDeniedState();
syncLoadingState();
}
function scheduleEnhancement() {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(enhanceAuthFields);
}
window.addEventListener("DOMContentLoaded", scheduleEnhancement);
window.addEventListener("load", scheduleEnhancement);
document.addEventListener("pointerdown", (event) => {
const target = event.composedPath?.()[0] || event.target;
maskVisiblePasswords(target);
}, true);
window.addEventListener("blur", () => maskVisiblePasswords(null));
new MutationObserver(scheduleEnhancement).observe(document.documentElement, {
childList: true,
subtree: true,
});
})();
</script>
{% endif %}
{% endif %}
{% endwith %}
@@ -0,0 +1,50 @@
{% load static %}
{% load i18n %}
{% load authentik_core %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html
lang="{{ LANGUAGE_CODE }}"
data-theme="{% if ui_theme == "dark" %}dark{% else %}light{% endif %}"
data-theme-choice="{% if ui_theme == "dark" %}dark{% elif ui_theme == "light" %}light{% else %}auto{% endif %}"
>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
<meta name="darkreader-lock">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
{% include "base/theme.html" %}
{% with request_host=request.get_host %}
{% if request_host|slice:":11" != "auth-admin." and request_host|slice:":15" != "cms-auth-admin." and request_host|slice:":9" != "id-admin." and request_host|slice:":12" != "172.22.0.222" %}
{% if request.path|slice:":9" == "/if/flow/" or request.path|slice:":7" == "/flows/" or request.path|slice:":7" == "/login/" %}
<style data-id="brand-css">{{ brand_css }}</style>
{% else %}
<style data-id="brand-css"></style>
{% endif %}
{% else %}
<style data-id="brand-css"></style>
{% endif %}
{% endwith %}
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
{% block head %}
{% endblock %}
{% for key, value in html_meta.items %}
<meta name="{{key}}" content="{{ value }}" />
{% endfor %}
</head>
<body>
{% block body %}
{% endblock %}
{% block scripts %}
{% endblock %}
</body>
</html>
@@ -0,0 +1,737 @@
:root,
:host {
--ak-global--primary: #f4f4f5;
--ak-global--primary-hover: #ffffff;
--ak-global--background: #0e0f10;
--ak-global--background-image: none !important;
--ak-c-login--MaxWidth: 28rem;
--ak-c-login--spacer: 2.2rem;
--ak-c-login__main--BackgroundColor: transparent;
--ak-c-login__main--Color: #f3f3f3;
--ak-c-login__main--BoxShadow: none;
--ak-c-login__footer--PaddingBlock: 0;
--pf-global--primary-color--100: #f4f4f5;
--pf-global--primary-color--200: #ffffff;
--pf-v4-global--palette--blue-300: #f4f4f5;
--ak-global--palette--blue-300: #f4f4f5;
--pf-global--active-color--100: #f4f4f5;
--pf-global--link--Color: #f4f4f5;
--pf-global--link--Color--hover: #ffffff;
--pf-global--BackgroundColor--100: #0e0f10;
--pf-global--BackgroundColor--light-100: transparent;
--pf-global--Color--100: #f3f3f3;
--pf-global--Color--200: #9d9da3;
--pf-global--BorderRadius--lg: 1.9rem;
--pf-global--BorderRadius--sm: 1.15rem;
--pf-v5-global--BorderRadius--lg: 1.9rem;
--pf-v5-global--BorderRadius--sm: 1.15rem;
--pf-v6-global--BorderRadius--large: 1.9rem;
--pf-c-form-control--BackgroundColor: rgba(255, 255, 255, 0.04);
--pf-c-form-control--BorderColor: transparent;
--pf-c-form-control--BorderBottomColor: transparent;
--pf-v5-c-form-control--BackgroundColor: rgba(255, 255, 255, 0.04);
--pf-v5-c-form-control--BorderColor: transparent;
--pf-v5-c-form-control--BorderBottomColor: transparent;
--nodedc-auth-bg: #0e0f10;
--nodedc-auth-card-bg: rgba(9, 9, 12, 0.84);
--nodedc-auth-primary: #f4f4f5;
--nodedc-auth-primary-hover: #ffffff;
--nodedc-auth-on-primary: rgb(11, 17, 23);
--nodedc-auth-text-primary: #e4e6e7;
--nodedc-auth-text-secondary: #cacdce;
--nodedc-auth-text-placeholder: #959a9d;
color-scheme: dark;
}
html,
body,
.pf-c-login {
background: var(--nodedc-auth-bg) !important;
color: var(--nodedc-auth-text-primary) !important;
font-family: Inter, "Inter var", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
font-synthesis: none;
-webkit-font-smoothing: antialiased;
text-rendering: geometricPrecision;
}
:host {
background: transparent !important;
color: inherit !important;
font-family: Inter, "Inter var", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
font-synthesis: none;
-webkit-font-smoothing: antialiased;
text-rendering: geometricPrecision;
}
:host(.pf-c-login) {
display: flex !important;
min-width: 100vw !important;
min-height: 100vh !important;
align-items: center !important;
justify-content: center !important;
background: var(--nodedc-auth-bg) !important;
color: var(--nodedc-auth-text-primary) !important;
}
body::before,
.pf-c-login::before {
background: none !important;
}
body:not(.nodedc-auth-enhanced)::after {
content: "";
display: block;
position: fixed;
top: 1.7875rem;
left: calc(1.9375rem - 2px);
width: 7.25rem;
height: 1.79rem;
background: center / contain no-repeat url("data:image/svg+xml,%3Csvg id='nodedc-logo' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220.82 54.55'%3E%3Cpath fill='%23e2e1e1' d='M52.8 23.61 46.92 33.76 41.05 23.61H52.8m18-10.39H23.06l23.86 41.33Z'/%3E%3Cpolygon fill='%23e2e1e1' points='31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13'/%3E%3Cpath fill='%23dbdbdb' d='M116.35 18.49V1h1.27l10.34 15V1h1.33v17.49H128l-10.34-15v15Zm24.08.15c-4.79 0-8.16-3.72-8.16-8.89S135.64.86 140.43.86s8.17 3.72 8.17 8.89-3.35 8.89-8.17 8.89Zm0-1.25c4 0 6.79-3.17 6.79-7.64s-2.77-7.64-6.79-7.64-6.77 3.17-6.77 7.64 2.78 7.64 6.77 7.64ZM151.6 18.49V1h5.1c5.54 0 8.79 3.42 8.79 8.74s-3.25 8.74-8.79 8.74Zm1.4-1.25h3.75c4.77 0 7.42-2.92 7.42-7.49s-2.65-7.49-7.42-7.49H153ZM168.49 1h10.77v1.26h-9.42v6.67h7.89v1.25h-7.89v7.06h9.74v1.25h-11.09Zm20.39 17.49V1H194c5.54 0 8.79 3.42 8.79 8.74s-3.25 8.74-8.79 8.74Zm1.35-1.25H194c4.77 0 7.41-2.92 7.41-7.49S198.75 2.26 194 2.26h-3.75Zm14.92-7.49c0-5.24 3.19-8.89 8.11-8.89a6.8 6.8 0 0 1 7.1 5.52h-1.43a5.54 5.54 0 0 0-5.74-4.27c-4.05 0-6.64 3.17-6.64 7.64s2.54 7.64 6.59 7.64a5.46 5.46 0 0 0 5.74-4.29h1.43c-.75 3.52-3.4 5.54-7.15 5.54-4.89 0-8.01-3.59-8.01-8.89Z'/%3E%3C/svg%3E");
image-rendering: auto;
z-index: 20;
pointer-events: none;
}
.nodedc-auth-logo {
position: fixed;
display: block;
top: 1.7875rem;
left: calc(1.9375rem - 2px);
width: 7.25rem;
height: 1.79rem;
z-index: 20;
line-height: 0;
text-decoration: none;
pointer-events: auto;
color: var(--nodedc-auth-text-primary);
}
.nodedc-auth-logo svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
shape-rendering: geometricPrecision;
text-rendering: geometricPrecision;
}
.pf-c-page__drawer,
.pf-c-drawer,
.pf-c-drawer__main,
.pf-c-drawer__content,
.pf-c-drawer__body {
background: transparent !important;
}
.pf-c-drawer__body {
display: flex !important;
min-height: 100vh !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
}
ak-flow-executor.pf-c-login::before,
:host(.pf-c-login)::before {
display: none !important;
}
ak-flow-executor.pf-c-login {
display: flex !important;
width: 100% !important;
min-height: 100vh !important;
position: relative !important;
align-items: center !important;
justify-content: center !important;
padding-right: 0 !important;
box-sizing: border-box !important;
}
ak-brand-links,
ak-flow-inspector,
ak-locale-context,
ak-locale-switcher,
ak-locale-select,
ak-flow-executor [slot="footer"],
.pf-c-login__footer,
.pf-c-login__header,
.pf-c-login__main-header.pf-c-brand,
.pf-c-form > p:first-child {
display: none !important;
}
ak-flow-executor::part(locale-select),
ak-flow-executor::part(footer),
ak-flow-executor::part(branding) {
display: none !important;
}
ak-flow-executor::part(main),
.pf-c-login__main {
position: fixed !important;
top: 50% !important;
left: 50% !important;
right: auto !important;
transform: translate(-50%, -50%) !important;
width: min(100%, 28rem) !important;
max-width: 28rem !important;
min-height: 0 !important;
margin: 0 !important;
padding: 2.2rem !important;
box-sizing: border-box !important;
overflow: hidden !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.9rem !important;
background: transparent !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
justify-content: flex-start !important;
}
body.nodedc-auth-card-ready ak-flow-executor::part(main),
body.nodedc-auth-card-ready .pf-c-login__main {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.015) 100%),
var(--nodedc-auth-card-bg) !important;
-webkit-backdrop-filter: blur(40px) !important;
backdrop-filter: blur(40px) !important;
}
ak-flow-card,
ak-stage-identification,
ak-stage-password {
display: block !important;
width: 100% !important;
background: transparent !important;
}
.pf-c-login__main-body,
.pf-c-form,
.pf-c-form__group,
.pf-c-form__group-control,
.pf-c-input-group,
.pf-c-input-group__item,
.pf-c-card,
.pf-c-card__body,
.pf-c-empty-state,
.pf-c-empty-state__content,
.pf-c-empty-state__body,
.pf-v5-c-empty-state,
.pf-v5-c-empty-state__content,
.pf-v5-c-empty-state__body,
.pf-v6-c-empty-state,
.pf-v6-c-empty-state__content,
.pf-v6-c-empty-state__body {
background: transparent !important;
box-shadow: none !important;
border: 0 !important;
}
.pf-c-login__main-header {
display: block !important;
width: 100% !important;
min-width: 0 !important;
padding: 0 0 1.75rem !important;
}
.pf-c-login__main-header .pf-c-title,
.pf-c-title.pf-m-3xl,
h1.pf-c-title {
display: block !important;
width: 100% !important;
max-width: 22rem !important;
margin: 0 !important;
color: var(--nodedc-auth-text-primary) !important;
font-size: 2rem !important;
line-height: 2.5rem !important;
font-weight: 650 !important;
letter-spacing: -0.045em !important;
text-wrap: balance !important;
white-space: normal !important;
word-break: normal !important;
overflow-wrap: normal !important;
hyphens: none !important;
overflow: visible !important;
line-clamp: unset !important;
-webkit-line-clamp: unset !important;
-webkit-box-orient: initial !important;
text-transform: none !important;
}
.pf-c-login__main-header::after {
content: "Вход в панель управления сайтами.";
display: block;
max-width: 22rem;
margin-top: 0.75rem;
color: var(--nodedc-auth-text-placeholder);
font-size: 1.9rem;
line-height: 2.5rem;
font-weight: 650;
letter-spacing: -0.045em;
text-transform: none;
}
.pf-c-login__main-header[data-nodedc-permission-denied="true"]::after,
body.nodedc-auth-permission-denied .pf-c-login__main-header::after {
content: attr(data-nodedc-denied-subtitle) !important;
margin-top: 0;
}
body.nodedc-auth-permission-denied .pf-c-login__main-header::after {
content: "Доступ запрещён." !important;
}
.pf-c-login__main-header[data-nodedc-permission-denied="true"] .pf-c-title,
.pf-c-login__main-header[data-nodedc-permission-denied="true"] .pf-c-title.pf-m-3xl,
.pf-c-login__main-header[data-nodedc-permission-denied="true"] h1.pf-c-title,
body.nodedc-auth-permission-denied .pf-c-login__main-header .pf-c-title,
body.nodedc-auth-permission-denied .pf-c-title.pf-m-3xl,
body.nodedc-auth-permission-denied h1.pf-c-title {
display: none !important;
}
body.nodedc-auth-permission-denied ak-flow-executor::part(main),
body.nodedc-auth-permission-denied .pf-c-login__main {
background: transparent !important;
border: 0 !important;
outline: 0 !important;
box-shadow: none !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
}
.pf-c-login__main-body {
padding: 0 !important;
}
.pf-c-form {
display: flex !important;
flex-direction: column !important;
gap: 1.05rem !important;
}
.pf-c-form__group,
ak-flow-input-password.pf-c-form__group {
margin: 0 !important;
}
.pf-c-form__label,
.pf-c-form__label-text,
label {
color: var(--nodedc-auth-text-placeholder) !important;
font-size: 0.75rem !important;
line-height: 1rem !important;
font-weight: 400 !important;
letter-spacing: -0.01em !important;
text-transform: none !important;
}
.pf-c-form__label-required {
display: none !important;
}
label[for="ak-identifier-input"],
label[for="ak-stage-identification-password"] {
font-size: 0 !important;
}
label[for="ak-identifier-input"] .pf-c-form__label-text,
label[for="ak-identifier-input"] .pf-c-form__label-required,
label[for="ak-stage-identification-password"] .pf-c-form__label-text,
label[for="ak-stage-identification-password"] .pf-c-form__label-required {
display: none !important;
}
label[for="ak-identifier-input"]::after {
content: "Эл. почта";
font-size: 0.75rem;
color: var(--nodedc-auth-text-placeholder);
font-weight: 400;
}
label[for="ak-stage-identification-password"]::after {
content: "Пароль";
font-size: 0.75rem;
color: var(--nodedc-auth-text-placeholder);
font-weight: 400;
}
.pf-c-form-control,
.pf-c-input-group .pf-c-form-control,
input.pf-c-form-control {
width: 100% !important;
min-height: 3rem !important;
padding: 0 1rem !important;
border: 0 !important;
border-bottom: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.15rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.03) !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
color: var(--nodedc-auth-text-primary) !important;
font-size: 0.875rem !important;
font-style: normal !important;
font-weight: 400 !important;
text-transform: none !important;
caret-color: var(--nodedc-auth-primary) !important;
}
.pf-c-form-control::placeholder,
input.pf-c-form-control::placeholder {
color: var(--nodedc-auth-text-placeholder) !important;
font-style: normal !important;
opacity: 1 !important;
}
.pf-c-form-control:focus,
input.pf-c-form-control:focus {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.03) !important;
border: 0 !important;
border-bottom: 0 !important;
outline: none !important;
box-shadow: none !important;
}
.pf-c-form-control:-webkit-autofill,
.pf-c-form-control:-webkit-autofill:hover,
.pf-c-form-control:-webkit-autofill:focus,
input.pf-c-form-control:-webkit-autofill,
input.pf-c-form-control:-webkit-autofill:hover,
input.pf-c-form-control:-webkit-autofill:focus {
border: 0 !important;
outline: none !important;
-webkit-text-fill-color: var(--nodedc-auth-text-primary) !important;
caret-color: var(--nodedc-auth-primary) !important;
transition: background-color 999999s ease-in-out 0s !important;
box-shadow: inset 0 0 0 1000px rgba(255, 255, 255, 0.03) !important;
-webkit-box-shadow: inset 0 0 0 1000px rgba(255, 255, 255, 0.03) !important;
}
.pf-c-input-group {
position: relative !important;
display: block !important;
border: 0 !important;
box-shadow: none !important;
}
.pf-c-form__group:has(#ak-identifier-input) {
position: relative !important;
}
.pf-c-input-group .pf-c-button.pf-m-control {
position: absolute !important;
top: 0.5rem !important;
right: 0.75rem !important;
display: grid !important;
width: 2rem !important;
min-width: 2rem !important;
height: 2rem !important;
place-items: center !important;
padding: 0 !important;
border: 0 !important;
border-radius: 999px !important;
background: transparent !important;
color: var(--nodedc-auth-text-placeholder) !important;
box-shadow: none !important;
text-decoration: none !important;
appearance: none !important;
-webkit-appearance: none !important;
}
.pf-c-input-group .pf-c-button.pf-m-control::before,
.pf-c-input-group .pf-c-button.pf-m-control::after,
.pf-c-input-group .pf-c-button.pf-m-control:hover::before,
.pf-c-input-group .pf-c-button.pf-m-control:hover::after,
.pf-c-input-group .pf-c-button.pf-m-control:focus::before,
.pf-c-input-group .pf-c-button.pf-m-control:focus::after {
display: none !important;
border: 0 !important;
box-shadow: none !important;
content: none !important;
}
.pf-c-input-group .pf-c-button.pf-m-control:hover,
.pf-c-input-group .pf-c-button.pf-m-control:focus,
.pf-c-input-group .pf-c-button.pf-m-control:active {
border: 0 !important;
outline: none !important;
box-shadow: none !important;
background: transparent !important;
text-decoration: none !important;
}
.pf-c-input-group .pf-c-button.pf-m-control i,
.nodedc-auth-clear-input svg {
width: 1.25rem !important;
height: 1.25rem !important;
color: var(--nodedc-auth-text-placeholder) !important;
opacity: 0.9 !important;
}
.pf-c-input-group input.pf-c-form-control {
padding-right: 3rem !important;
}
.nodedc-auth-clear-input {
position: absolute !important;
right: 0.75rem !important;
bottom: 0.5rem !important;
display: none !important;
width: 2rem !important;
height: 2rem !important;
padding: 0 !important;
place-items: center !important;
border: 0 !important;
border-radius: 999px !important;
background: transparent !important;
color: var(--nodedc-auth-text-placeholder) !important;
box-shadow: none !important;
cursor: pointer !important;
}
.nodedc-auth-clear-input[data-visible="true"] {
display: grid !important;
}
.pf-c-button.pf-m-primary,
button.pf-m-primary,
button[type="submit"],
.pf-c-form .pf-c-button.pf-m-primary {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
min-height: 3.25rem !important;
margin-top: 1.25rem !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: var(--nodedc-auth-primary) !important;
color: var(--nodedc-auth-on-primary) !important;
font-size: 0.95rem !important;
font-weight: 600 !important;
line-height: 1 !important;
letter-spacing: -0.02em !important;
text-align: center !important;
text-transform: none !important;
}
.pf-c-button.pf-m-primary:hover,
button.pf-m-primary:hover,
button[type="submit"]:hover {
background: var(--nodedc-auth-primary-hover) !important;
color: var(--nodedc-auth-on-primary) !important;
}
.nodedc-auth-request-access {
display: inline-flex !important;
justify-content: center !important;
width: 100% !important;
margin-top: -0.35rem !important;
color: var(--nodedc-auth-primary) !important;
font-size: 0.75rem !important;
line-height: 1rem !important;
font-weight: 650 !important;
letter-spacing: -0.01em !important;
text-decoration: none !important;
}
.nodedc-auth-request-access:hover,
.nodedc-auth-request-access:focus {
color: var(--nodedc-auth-primary-hover) !important;
text-decoration: none !important;
}
.pf-c-alert,
.pf-c-alert.pf-m-inline {
border: 0 !important;
border-radius: 1rem !important;
margin: 0 0 1.15rem !important;
padding: 0 !important;
background: transparent !important;
color: var(--nodedc-auth-primary) !important;
box-shadow: none !important;
}
.pf-c-alert__icon,
.pf-c-alert__title,
.pf-c-alert__description {
color: var(--nodedc-auth-primary) !important;
font-size: 0.75rem !important;
line-height: 1rem !important;
font-weight: 600 !important;
}
body.nodedc-auth-permission-denied .pf-c-alert,
body.nodedc-auth-permission-denied .pf-v5-c-alert,
body.nodedc-auth-permission-denied .pf-c-alert.pf-m-inline {
display: none !important;
}
a,
.pf-c-button.pf-m-link,
button.pf-m-link {
color: var(--nodedc-auth-primary) !important;
font-size: 0.75rem !important;
line-height: 1rem !important;
font-weight: 600 !important;
text-decoration: none !important;
}
body.nodedc-auth-submitting ak-flow-executor::part(main),
body.nodedc-auth-loading ak-flow-executor::part(main),
body.nodedc-auth-submitting .pf-c-login__main,
body.nodedc-auth-loading .pf-c-login__main,
body.nodedc-auth-submitting .pf-c-empty-state,
body.nodedc-auth-loading .pf-c-empty-state,
body.nodedc-auth-submitting .pf-c-spinner,
body.nodedc-auth-loading .pf-c-spinner,
body.nodedc-auth-submitting [role="progressbar"] {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
body.nodedc-auth-loading [role="progressbar"] {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
.pf-c-spinner,
.pf-v5-c-spinner,
[role="progressbar"],
ak-spinner {
--pf-c-spinner--Color: var(--nodedc-auth-primary) !important;
--pf-v5-c-spinner--Color: var(--nodedc-auth-primary) !important;
--pf-c-spinner__clipper--after--BoxShadowColor: var(--nodedc-auth-primary) !important;
--pf-c-spinner__lead-ball--after--BackgroundColor: var(--nodedc-auth-primary) !important;
--pf-c-spinner__tail-ball--after--BackgroundColor: var(--nodedc-auth-primary) !important;
color: var(--nodedc-auth-primary) !important;
}
#ak-placeholder.ak-c-placeholder,
.ak-c-placeholder[slot="placeholder"] {
position: fixed !important;
top: 50% !important;
left: 50% !important;
display: block !important;
width: 2.5rem !important;
height: 2.5rem !important;
min-width: 2.5rem !important;
min-height: 2.5rem !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 999px !important;
background: transparent !important;
box-shadow: none !important;
opacity: 1 !important;
visibility: visible !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
transform: translate(-50%, -50%) !important;
overflow: visible !important;
}
#ak-placeholder.ak-c-placeholder .pf-c-spinner,
.ak-c-placeholder[slot="placeholder"] .pf-c-spinner,
#ak-placeholder.ak-c-placeholder [role="progressbar"],
.ak-c-placeholder[slot="placeholder"] [role="progressbar"] {
opacity: 0 !important;
visibility: hidden !important;
}
#ak-placeholder.ak-c-placeholder::before,
.ak-c-placeholder[slot="placeholder"]::before {
content: "";
position: absolute;
inset: 0;
border: 2px solid rgba(255, 255, 255, 0.22);
border-top-color: var(--nodedc-auth-primary);
border-radius: 999px;
animation: nodedc-auth-placeholder-spin 0.72s linear infinite;
pointer-events: none;
}
body.nodedc-auth-submitting::before,
body.nodedc-auth-loading::before {
content: "";
position: fixed;
top: 50%;
left: 50%;
z-index: 1200;
width: 2.5rem;
height: 2.5rem;
border: 2px solid rgba(255, 255, 255, 0.22);
border-top-color: var(--nodedc-auth-primary);
border-radius: 999px;
transform: translate(-50%, -50%);
animation: nodedc-auth-loader-spin 0.72s linear infinite;
pointer-events: none;
}
@keyframes nodedc-auth-loader-spin {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes nodedc-auth-placeholder-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
ak-library-application,
ak-application-wizard,
ak-user-interface,
.pf-c-page__sidebar,
.pf-c-page__header,
.pf-c-nav,
.pf-c-brand {
color-scheme: dark;
}
@media (max-width: 640px) {
ak-flow-executor.pf-c-login::before,
:host(.pf-c-login)::before {
display: none !important;
}
body::after,
.nodedc-auth-logo {
top: 1.7875rem;
left: calc(1.9375rem - 2px);
width: 7.25rem;
height: 1.79rem;
}
ak-flow-executor::part(main),
.pf-c-login__main {
position: fixed !important;
top: 50% !important;
right: auto !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
width: calc(100vw - 2rem) !important;
padding: 1.5rem !important;
}
}
@@ -0,0 +1,16 @@
{% extends "base/skeleton.html" %}
{% load authentik_core %}
{% block head %}
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container alignment="bottom"></ak-message-container>
<ak-interface-admin>
{% include "base/placeholder.html" %}
</ak-interface-admin>
{% endblock %}
+144
View File
@@ -0,0 +1,144 @@
name: dc-cms
services:
reverse-proxy:
image: caddy:2-alpine
restart: unless-stopped
env_file:
- .env
ports:
- "${CMS_HTTP_BIND:-127.0.0.1:8210}:80"
- "${CMS_AUTH_HTTP_BIND:-127.0.0.1:8211}:80"
- "${CMS_AUTH_ADMIN_HTTP_BIND:-127.0.0.1:8212}:80"
volumes:
- ./reverse-proxy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
cms-app:
condition: service_started
authentik-server:
condition: service_started
networks:
cms-edge:
aliases:
- ${CMS_DOMAIN:-cms.local.nodedc}
- ${CMS_AUTH_DOMAIN:-cms-auth.local.nodedc}
- ${CMS_AUTH_ADMIN_DOMAIN:-cms-auth-admin.local.nodedc}
cms-app:
build:
context: ..
restart: unless-stopped
env_file:
- .env
environment:
HOST: 0.0.0.0
PORT: 8090
SITE_ROOT: /sites/nodedc
CMS_AUTH_ENABLED: ${CMS_AUTH_ENABLED:-true}
CMS_OIDC_ISSUER: ${CMS_OIDC_ISSUER:?cms oidc issuer required}
CMS_OIDC_CLIENT_ID: ${CMS_OIDC_CLIENT_ID:?cms oidc client id required}
CMS_OIDC_CLIENT_SECRET: ${CMS_OIDC_CLIENT_SECRET:?cms oidc client secret required}
CMS_OIDC_REDIRECT_URI: ${CMS_OIDC_REDIRECT_URI:?cms oidc redirect uri required}
CMS_OIDC_LOGGED_OUT_REDIRECT_URI: ${CMS_OIDC_LOGGED_OUT_REDIRECT_URI:?cms oidc logged out redirect uri required}
CMS_REQUIRED_GROUPS: ${CMS_REQUIRED_GROUPS:-dc-cms:owner,dc-cms:admin,dc-cms:editor,dc-cms:publisher}
CMS_SESSION_SECRET: ${CMS_SESSION_SECRET:?cms session secret required}
COOKIE_SECURE: ${COOKIE_SECURE:-false}
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
expose:
- "8090"
volumes:
- ../projects:/app/projects
- ${CMS_NODEDC_SITE_PATH:-../../NODEDC_SITE}:/sites/nodedc
depends_on:
authentik-server:
condition: service_started
networks:
- cms-edge
postgresql-authentik:
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
env_file:
- .env
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
volumes:
- authentik-database:/var/lib/postgresql/data
networks:
- cms-edge
authentik-server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
command: server
restart: unless-stopped
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?database password required}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: ${AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS:-127.0.0.0/8,172.16.0.0/12}
AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED:-false}
AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL:-}
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD:-}
AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN:-}
depends_on:
postgresql-authentik:
condition: service_healthy
expose:
- "9000"
- "9443"
shm_size: 512mb
volumes:
- authentik-data:/data
- ./authentik/custom-templates:/templates:ro
networks:
- cms-edge
authentik-worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
command: worker
restart: unless-stopped
user: root
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?database password required}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED:-false}
depends_on:
postgresql-authentik:
condition: service_healthy
shm_size: 512mb
volumes:
- authentik-data:/data
- authentik-certs:/certs
- ./authentik/custom-templates:/templates:ro
- ./authentik/bootstrap-cms.py:/bootstrap/bootstrap-cms.py:ro
networks:
- cms-edge
networks:
cms-edge:
volumes:
authentik-database:
authentik-data:
authentik-certs:
caddy-data:
caddy-config:
+39
View File
@@ -0,0 +1,39 @@
{
auto_https off
}
http://{$CMS_DOMAIN:cms.local.nodedc} {
reverse_proxy cms-app:8090 {
header_up Host {host}
header_up X-Forwarded-Proto {$CMS_EXTERNAL_SCHEME:http}
header_up X-Forwarded-For {remote_host}
}
}
http://{$CMS_AUTH_DOMAIN:cms-auth.local.nodedc} {
@auth_admin path /if/admin /if/admin/*
redir @auth_admin {$CMS_AUTH_ADMIN_PUBLIC_URL}{uri} 302
@auth_root path /
redir @auth_root {$CMS_PUBLIC_URL}/ 302
@auth_user_dashboard path /if/user /if/user/*
redir @auth_user_dashboard {$CMS_PUBLIC_URL}/ 302
reverse_proxy authentik-server:9000 {
header_up Host {host}
header_up X-Forwarded-Proto {$CMS_EXTERNAL_SCHEME:http}
header_up X-Forwarded-For {remote_host}
}
}
http://{$CMS_AUTH_ADMIN_DOMAIN:cms-auth-admin.local.nodedc} {
@admin_root path /
redir @admin_root /if/admin/ 302
reverse_proxy authentik-server:9000 {
header_up Host {host}
header_up X-Forwarded-Proto {$CMS_EXTERNAL_SCHEME:http}
header_up X-Forwarded-For {remote_host}
}
}
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${INFRA_DIR}"
docker compose --env-file .env -f docker-compose.yml exec -T authentik-worker python /bootstrap/bootstrap-cms.py