fix(platform): isolate authentik admin styling

This commit is contained in:
Codex 2026-05-25 01:08:50 +03:00
parent e9def6672c
commit c1ac8b1a10
16 changed files with 670 additions and 61 deletions

View File

@ -61,17 +61,24 @@ https://ops.nodedc.ru -> Tasker / Operational Core
https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP endpoint https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP endpoint
``` ```
`id.nodedc.ru` is only the public OIDC/login host. Authentik Admin is deliberately kept off that public host; `/if/admin/*` returns `404` there.
Local/NAS check domains used during rollout: Local/NAS check domains used during rollout:
```text ```text
auth.nas.nodedc auth.nas.nodedc
auth-admin.nas.nodedc
172.22.0.222
launcher.nas.nodedc launcher.nas.nodedc
task.nas.nodedc task.nas.nodedc
auth.local.nodedc auth.local.nodedc
auth-admin.local.nodedc
launcher.local.nodedc launcher.local.nodedc
task.local.nodedc task.local.nodedc
``` ```
Use `http://auth-admin.nas.nodedc:18080/if/admin/` for technical Authentik Admin access from the NAS/local network. If a workstation does not resolve `auth-admin.nas.nodedc`, use `http://172.22.0.222:18080/if/admin/` as the local IP fallback. Both entrypoints keep Authentik access separate from the public `id.nodedc.ru` session and must not load NODE.DC auth-flow CSS.
### Platform compose project ### Platform compose project
Compose project: Compose project:
@ -278,7 +285,14 @@ The platform Authentik service exposes a stable identity-network alias in the Sy
nodedc-platform-authentik-server nodedc-platform-authentik-server
``` ```
When changing this alias or Authentik env wiring, recreate `authentik-server`, `authentik-worker`, and `launcher` together. A freshly recreated Authentik server can temporarily return `503 Service Unavailable` until the worker/bootstrap path is ready, so verify with retry from inside the launcher container. For Launcher-only BFF/frontend fixes, rebuild `nodedc/launcher:local` and recreate only `launcher`. When changing this alias or Authentik env wiring, recreate `authentik-server`, `authentik-worker`, and `launcher` together. A freshly recreated Authentik server can temporarily return `503 Service Unavailable` until the worker/bootstrap path is ready, so verify with retry from inside the launcher container. For Launcher-only BFF/frontend fixes, rebuild `nodedc/launcher:local` with `--no-cache` and recreate only `launcher`.
Authentik Brand custom CSS must stay empty in live DB. NODE.DC auth/login styling is injected only by templates on the public auth-flow host. After any Authentik template rollout run:
```bash
cd /volume1/docker/nodedc-platform/platform
sudo bash clear-authentik-brand-css.sh
```
Launcher image build is done from the Launcher repo when frontend/backend code changes: Launcher image build is done from the Launcher repo when frontend/backend code changes:

View File

@ -1,5 +1,6 @@
# domains # domains
AUTH_DOMAIN=auth.local.nodedc AUTH_DOMAIN=auth.local.nodedc
AUTH_ADMIN_DOMAIN=auth-admin.local.nodedc
LAUNCHER_DOMAIN=launcher.local.nodedc LAUNCHER_DOMAIN=launcher.local.nodedc
TASK_DOMAIN=task.local.nodedc TASK_DOMAIN=task.local.nodedc

View File

@ -36,10 +36,16 @@ Later phases should add reproducible configuration for:
## NODE.DC branded login ## NODE.DC branded login
`custom-templates/branding/nodedc-login.css` is mounted into Authentik at `/templates/branding/nodedc-login.css` and applied by `bootstrap-dev.py` through the native Authentik Brand `branding_custom_css` field. `custom-templates/branding/nodedc-login.css` is mounted into Authentik at `/templates/branding/nodedc-login.css`, but it must not be stored in the native Authentik Brand `branding_custom_css` field.
`custom-templates/base/header_js.html` keeps Authentik's native config script and adds a minimal NODE.DC field enhancement for the email clear control and password placeholder only. Authentik Brand custom CSS is global. Authentik also passes it into the Admin/User web component runtime, so broad login selectors can break list pages, toolbars, tables, and other Admin interface content. Keep `Brand.branding_custom_css` empty in local bootstrap and Synology deploy scripts.
`custom-templates/base/header_js.html` keeps Authentik's native config script and applies the NODE.DC favicon globally. It renders NODE.DC auth-screen CSS/DOM helpers only on `/if/flow/`, `/flows/`, and `/login/` paths, and excludes technical admin hosts such as `auth-admin.nas.nodedc` / `auth-admin.local.nodedc`. On public auth paths only, it also passes `nodedc-login.css` into `window.authentik.brand` so Authentik's Lit web components can style their shadow DOM. Admin and User paths receive an empty runtime brand CSS value.
`custom-templates/base/skeleton.html` leaves `<style data-id="brand-css">` empty outside flow/login paths as a second guard. `custom-templates/if/admin.html` tracks Authentik's upstream Admin shell while still including `base/header_js.html`, so module execution sees the sanitized brand config without rewriting Admin markup.
OAuth2 providers are assigned Authentik's `default-invalidation-flow` so application logout completes the IdP session and returns through the NODE.DC launcher route instead of showing the default Authentik application logout screen. OAuth2 providers are assigned Authentik's `default-invalidation-flow` so application logout completes the IdP session and returns through the NODE.DC launcher route instead of showing the default Authentik application logout screen.
The bootstrap admin must belong to the `authentik Admins` group, and that group must have `is_superuser=True`. Without this, `/if/admin/` can render the admin shell while model list pages appear empty because their API calls are denied.
This is intentionally not an HTML-rewriting proxy. Passwords, MFA, recovery, sessions and audit remain inside Authentik; Launcher and Task Manager stay OIDC clients. This is intentionally not an HTML-rewriting proxy. Passwords, MFA, recovery, sessions and audit remain inside Authentik; Launcher and Task Manager stay OIDC clients.

View File

@ -1,5 +1,4 @@
from os import environ from os import environ
from pathlib import Path
from django.db import transaction from django.db import transaction
@ -21,8 +20,6 @@ from authentik.providers.oauth2.models import (
from authentik.stages.identification.models import IdentificationStage from authentik.stages.identification.models import IdentificationStage
from authentik.stages.password.models import PasswordStage from authentik.stages.password.models import PasswordStage
BRANDING_CSS_PATH = Path("/templates/branding/nodedc-login.css")
GROUP_SPECS = [ GROUP_SPECS = [
("nodedc:superadmin", False), ("nodedc:superadmin", False),
("nodedc:launcher:admin", False), ("nodedc:launcher:admin", False),
@ -91,7 +88,10 @@ def ensure_groups():
def ensure_user_groups(groups): def ensure_user_groups(groups):
admin_email = environ.get("NODEDC_BOOTSTRAP_ADMIN_EMAIL", "").strip() admin_email = (
environ.get("NODEDC_BOOTSTRAP_ADMIN_EMAIL", "").strip()
or environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "").strip()
)
if not admin_email: if not admin_email:
return None return None
@ -105,12 +105,18 @@ def ensure_user_groups(groups):
user.email = admin_email user.email = admin_email
user.is_active = True user.is_active = True
user.type = "internal" user.type = "internal"
if environ.get("NODEDC_BOOTSTRAP_ADMIN_PASSWORD"): admin_password = (
user.set_password(environ["NODEDC_BOOTSTRAP_ADMIN_PASSWORD"]) environ.get("NODEDC_BOOTSTRAP_ADMIN_PASSWORD")
or environ.get("AUTHENTIK_BOOTSTRAP_PASSWORD")
)
if admin_password:
user.set_password(admin_password)
user.save() user.save()
authentik_admins = Group.objects.filter(name="authentik Admins").first() authentik_admins, _ = Group.objects.get_or_create(name="authentik Admins")
if 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) user.groups.add(authentik_admins)
for name in groups: for name in groups:
@ -174,12 +180,6 @@ def default_scope_mappings():
return mappings return mappings
def read_branding_css():
if BRANDING_CSS_PATH.exists():
return BRANDING_CSS_PATH.read_text(encoding="utf-8")
return ""
def ensure_nodedc_brand(): def ensure_nodedc_brand():
auth_domain = environ.get("AUTH_DOMAIN", "auth.local.nodedc").strip() or "auth.local.nodedc" auth_domain = environ.get("AUTH_DOMAIN", "auth.local.nodedc").strip() or "auth.local.nodedc"
authentication_flow = Flow.objects.get(slug="default-authentication-flow") authentication_flow = Flow.objects.get(slug="default-authentication-flow")
@ -214,7 +214,9 @@ def ensure_nodedc_brand():
brand.branding_title = "NODE.DC" brand.branding_title = "NODE.DC"
brand.branding_logo = "" brand.branding_logo = ""
brand.branding_favicon = "" brand.branding_favicon = ""
brand.branding_custom_css = read_branding_css() # Login styling is injected by flow templates only. Brand custom CSS is global
# and Authentik applies it to Admin/User web components as well.
brand.branding_custom_css = ""
brand.flow_authentication = authentication_flow brand.flow_authentication = authentication_flow
brand.flow_invalidation = invalidation_flow brand.flow_invalidation = invalidation_flow
brand.attributes = { brand.attributes = {

View File

@ -21,8 +21,38 @@
})(); })();
</script> </script>
{% with request_host=request.get_host %}
{% if request_host|slice:":11" != "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("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"> <style data-id="nodedc-auth-critical-loader">
:root { html.nodedc-auth-flow-page {
--ak-global--primary: rgb(195, 255, 102) !important; --ak-global--primary: rgb(195, 255, 102) !important;
--ak-global--background: #0e0f10 !important; --ak-global--background: #0e0f10 !important;
--ak-global--background-image: none !important; --ak-global--background-image: none !important;
@ -37,22 +67,22 @@
--nodedc-auth-bg: #0e0f10 !important; --nodedc-auth-bg: #0e0f10 !important;
} }
html, html.nodedc-auth-flow-page,
body, html.nodedc-auth-flow-page body,
html body .pf-c-login { html.nodedc-auth-flow-page body .pf-c-login {
background: #0e0f10 !important; background: #0e0f10 !important;
color-scheme: dark !important; color-scheme: dark !important;
} }
html body ak-flow-executor::part(main), html.nodedc-auth-flow-page body ak-flow-executor::part(main),
html body .pf-c-login__main, html.nodedc-auth-flow-page body .pf-c-login__main,
html body .pf-c-empty-state, html.nodedc-auth-flow-page body .pf-c-empty-state,
html body .pf-v5-c-empty-state, html.nodedc-auth-flow-page body .pf-v5-c-empty-state,
html body .pf-v6-c-empty-state, html.nodedc-auth-flow-page body .pf-v6-c-empty-state,
html body .pf-c-card, html.nodedc-auth-flow-page body .pf-c-card,
html body .pf-v5-c-card, html.nodedc-auth-flow-page body .pf-v5-c-card,
html body .pf-v6-c-card, html.nodedc-auth-flow-page body .pf-v6-c-card,
html body [class*="empty-state"] { html.nodedc-auth-flow-page body [class*="empty-state"] {
background: transparent !important; background: transparent !important;
box-shadow: none !important; box-shadow: none !important;
border: 0 !important; border: 0 !important;
@ -60,11 +90,11 @@
backdrop-filter: none !important; backdrop-filter: none !important;
} }
html body .pf-c-spinner, html.nodedc-auth-flow-page body .pf-c-spinner,
html body .pf-v5-c-spinner, html.nodedc-auth-flow-page body .pf-v5-c-spinner,
html body .pf-v6-c-spinner, html.nodedc-auth-flow-page body .pf-v6-c-spinner,
html body [role="progressbar"], html.nodedc-auth-flow-page body [role="progressbar"],
html body ak-spinner { html.nodedc-auth-flow-page body ak-spinner {
--pf-c-spinner--Color: rgb(195, 255, 102) !important; --pf-c-spinner--Color: rgb(195, 255, 102) !important;
--pf-v5-c-spinner--Color: rgb(195, 255, 102) !important; --pf-v5-c-spinner--Color: rgb(195, 255, 102) !important;
--pf-v6-c-spinner--Color: rgb(195, 255, 102) !important; --pf-v6-c-spinner--Color: rgb(195, 255, 102) !important;
@ -74,8 +104,8 @@
color: rgb(195, 255, 102) !important; color: rgb(195, 255, 102) !important;
} }
html body #ak-placeholder.ak-c-placeholder, html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder,
html body .ak-c-placeholder[slot="placeholder"] { html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] {
position: fixed !important; position: fixed !important;
top: 50% !important; top: 50% !important;
left: 50% !important; left: 50% !important;
@ -98,16 +128,16 @@
overflow: visible !important; overflow: visible !important;
} }
html body #ak-placeholder.ak-c-placeholder .pf-c-spinner, html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder .pf-c-spinner,
html body .ak-c-placeholder[slot="placeholder"] .pf-c-spinner, html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] .pf-c-spinner,
html body #ak-placeholder.ak-c-placeholder [role="progressbar"], html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder [role="progressbar"],
html body .ak-c-placeholder[slot="placeholder"] [role="progressbar"] { html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"] [role="progressbar"] {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
} }
html body #ak-placeholder.ak-c-placeholder::before, html.nodedc-auth-flow-page body #ak-placeholder.ak-c-placeholder::before,
html body .ak-c-placeholder[slot="placeholder"]::before { html.nodedc-auth-flow-page body .ak-c-placeholder[slot="placeholder"]::before {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -118,14 +148,14 @@
pointer-events: none; pointer-events: none;
} }
html body.nodedc-auth-loading ak-flow-executor::part(main), html.nodedc-auth-flow-page body.nodedc-auth-loading ak-flow-executor::part(main),
html body.nodedc-auth-loading .pf-c-login__main { html.nodedc-auth-flow-page body.nodedc-auth-loading .pf-c-login__main {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
pointer-events: none !important; pointer-events: none !important;
} }
html body.nodedc-auth-loading::before { html.nodedc-auth-flow-page body.nodedc-auth-loading::before {
content: ""; content: "";
position: fixed; position: fixed;
top: 50%; top: 50%;
@ -141,7 +171,7 @@
pointer-events: none; pointer-events: none;
} }
html body.nodedc-auth-card-ready::before { html.nodedc-auth-flow-page body.nodedc-auth-card-ready::before {
display: none !important; display: none !important;
} }
@ -165,14 +195,38 @@
} }
} }
</style> </style>
{% endif %}
{% endif %}
{% endwith %}
<script data-id="authentik-config"> <script data-id="authentik-config">
"use strict"; "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 = { window.authentik = {
locale: "ru", locale: "ru",
config: JSON.parse('{{ config_json|escapejs }}' || "{}"), config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
brand: JSON.parse('{{ brand_json|escapejs }}' || "{}"), brand: authentikBrand,
versionFamily: "{{ version_family }}", versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}", versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}", build: "{{ build }}",
@ -197,10 +251,31 @@
}); });
</script> </script>
{% with request_host=request.get_host %}
{% if request_host|slice:":11" != "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"> <script data-id="nodedc-auth-field-enhancements">
"use strict"; "use strict";
(function () { (function () {
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/")
);
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 = ` const logoSvg = `
<svg id="nodedc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220.82 54.55" aria-hidden="true"> <svg id="nodedc-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220.82 54.55" aria-hidden="true">
<defs> <defs>
@ -254,9 +329,11 @@
["Continue", "Продолжить"], ["Continue", "Продолжить"],
["Show password", "Показать пароль"], ["Show password", "Показать пароль"],
["Hide password", "Скрыть пароль"], ["Hide password", "Скрыть пароль"],
["Permission denied", "Доступ ограничен"], ["Permission denied", "Доступ запрещён"],
["Not you?", "Сменить пользователя"], ["Not you?", "Сменить пользователя"],
["Request has been denied.", "Доступ к модулю ограничен."], ["Request has been denied.", "Доступ запрещён."],
["The request has been denied.", "Доступ запрещён."],
["В произведении запроса было отказано.", "Доступ запрещён."],
["Go home", "Вернуться назад"], ["Go home", "Вернуться назад"],
["Response returned an error code", "Не удалось завершить операцию."], ["Response returned an error code", "Не удалось завершить операцию."],
]); ]);
@ -366,7 +443,11 @@
denied = [ denied = [
"Permission denied", "Permission denied",
"Доступ ограничен", "Доступ ограничен",
"Доступ запрещен",
"Доступ запрещён",
"Request has been denied.", "Request has been denied.",
"The request has been denied.",
"В произведении запроса было отказано.",
"Доступ к модулю ограничен.", "Доступ к модулю ограничен.",
].some((message) => text.includes(message)); ].some((message) => text.includes(message));
}); });
@ -387,8 +468,8 @@
} }
function applyPermissionDeniedLayout(root) { function applyPermissionDeniedLayout(root) {
const deniedTitleMessages = ["Permission denied", "Доступ ограничен"]; const deniedTitleMessages = ["Permission denied", "Доступ ограничен", "Доступ запрещен", "Доступ запрещён"];
const deniedSubtitle = "Доступ к модулю NODE.DC ограничен."; const deniedSubtitle = "Доступ запрещён.";
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) { acceptNode(node) {
const text = node.nodeValue?.trim() || ""; const text = node.nodeValue?.trim() || "";
@ -426,7 +507,19 @@
} }
function hidePermissionDeniedReason(root) { function hidePermissionDeniedReason(root) {
const deniedMessages = ["Request has been denied.", "Доступ к модулю ограничен."]; 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) => { root.querySelectorAll(".pf-c-alert, .pf-v5-c-alert, .pf-c-empty-state__body, [class*='alert']").forEach((element) => {
const text = element.textContent || ""; const text = element.textContent || "";
@ -547,12 +640,35 @@
enhancePasswordFields(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) { function enhanceSubmitHandoff(root) {
root.querySelectorAll("form").forEach((form) => { root.querySelectorAll("form").forEach((form) => {
if (form.dataset.nodedcSubmitBound === "true") return; if (form.dataset.nodedcSubmitBound === "true") return;
form.dataset.nodedcSubmitBound = "true"; form.dataset.nodedcSubmitBound = "true";
form.addEventListener("submit", () => { form.addEventListener("submit", () => {
syncFormFieldsBeforeSubmit(form);
document.body?.classList.add("nodedc-auth-submitting"); document.body?.classList.add("nodedc-auth-submitting");
}, true); }, true);
}); });
@ -823,3 +939,6 @@
}); });
})(); })();
</script> </script>
{% endif %}
{% endif %}
{% endwith %}

View File

@ -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:":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>

View File

@ -280,7 +280,7 @@ body.nodedc-auth-permission-denied .pf-c-login__main-header::after {
} }
body.nodedc-auth-permission-denied .pf-c-login__main-header::after { body.nodedc-auth-permission-denied .pf-c-login__main-header::after {
content: "Доступ к модулю NODE.DC ограничен." !important; 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,
@ -292,6 +292,16 @@ body.nodedc-auth-permission-denied h1.pf-c-title {
display: none !important; 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 { .pf-c-login__main-body {
padding: 0 !important; padding: 0 !important;
} }

View File

@ -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 %}

View File

@ -3,6 +3,9 @@
} }
http://{$AUTH_DOMAIN:auth.local.nodedc} { http://{$AUTH_DOMAIN:auth.local.nodedc} {
@auth_admin path /if/admin /if/admin/*
redir @auth_admin http://{$AUTH_ADMIN_DOMAIN:auth-admin.local.nodedc}{uri} 302
@auth_root path / @auth_root path /
redir @auth_root http://{$LAUNCHER_DOMAIN:launcher.local.nodedc}/ 302 redir @auth_root http://{$LAUNCHER_DOMAIN:launcher.local.nodedc}/ 302
@ -16,6 +19,17 @@ http://{$AUTH_DOMAIN:auth.local.nodedc} {
} }
} }
http://{$AUTH_ADMIN_DOMAIN: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 {scheme}
header_up X-Forwarded-For {remote_host}
}
}
http://{$LAUNCHER_DOMAIN:launcher.local.nodedc} { http://{$LAUNCHER_DOMAIN:launcher.local.nodedc} {
reverse_proxy {$LOCAL_LAUNCHER_UPSTREAM:host.docker.internal:5173} { reverse_proxy {$LOCAL_LAUNCHER_UPSTREAM:host.docker.internal:5173} {
header_up Host localhost:5173 header_up Host localhost:5173

View File

@ -2,6 +2,8 @@
# This intentionally uses high ports and does not touch existing nodedc-demo. # This intentionally uses high ports and does not touch existing nodedc-demo.
AUTH_DOMAIN=auth.nas.nodedc AUTH_DOMAIN=auth.nas.nodedc
AUTH_ADMIN_DOMAIN=auth-admin.nas.nodedc
AUTH_ADMIN_LAN_HOST=172.22.0.222
LAUNCHER_DOMAIN=launcher.nas.nodedc LAUNCHER_DOMAIN=launcher.nas.nodedc
TASK_DOMAIN=task.nas.nodedc TASK_DOMAIN=task.nas.nodedc
NODEDC_PUBLIC_HTTP_PORT=18080 NODEDC_PUBLIC_HTTP_PORT=18080

View File

@ -3,6 +3,9 @@
} }
http://{$AUTH_DOMAIN} { http://{$AUTH_DOMAIN} {
@auth_admin path /if/admin /if/admin/*
redir @auth_admin http://{$AUTH_ADMIN_DOMAIN:auth-admin.nas.nodedc}:{$NODEDC_PUBLIC_HTTP_PORT}{uri} 302
@auth_root path / @auth_root path /
redir @auth_root http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302 redir @auth_root http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302
@ -16,6 +19,17 @@ http://{$AUTH_DOMAIN} {
} }
} }
http://{$AUTH_ADMIN_DOMAIN:auth-admin.nas.nodedc}, http://{$AUTH_ADMIN_LAN_HOST:172.22.0.222} {
@admin_root path /
redir @admin_root /if/admin/ 302
reverse_proxy authentik-server:9000 {
header_up Host {http.request.host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-For {remote_host}
}
}
http://{$LAUNCHER_DOMAIN} { http://{$LAUNCHER_DOMAIN} {
reverse_proxy launcher:5173 { reverse_proxy launcher:5173 {
header_up Host {http.request.host} header_up Host {http.request.host}
@ -35,6 +49,9 @@ http://{$TASK_DOMAIN} {
} }
http://id.nodedc.ru { http://id.nodedc.ru {
@auth_admin path /if/admin /if/admin/*
respond @auth_admin 404
@auth_root path / @auth_root path /
redir @auth_root https://hub.nodedc.ru/ 302 redir @auth_root https://hub.nodedc.ru/ 302

View File

@ -19,6 +19,8 @@ https://ops.nodedc.ru -> Tasker / Operational Core
https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP
``` ```
`id.nodedc.ru` is the user-facing OIDC/login host. Authentik Admin is intentionally not exposed through this public host; `/if/admin/*` returns `404` there.
В `Caddyfile.http` эти домены проксируются через локальный HTTP edge, но upstream получает `X-Forwarded-Proto: https` и `X-Forwarded-Port: 443`. В `Caddyfile.http` эти домены проксируются через локальный HTTP edge, но upstream получает `X-Forwarded-Proto: https` и `X-Forwarded-Port: 443`.
## Локальные домены для первичной проверки ## Локальные домены для первичной проверки
@ -27,6 +29,7 @@ https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP
```text ```text
172.22.0.222 auth.nas.nodedc 172.22.0.222 auth.nas.nodedc
172.22.0.222 auth-admin.nas.nodedc
172.22.0.222 launcher.nas.nodedc 172.22.0.222 launcher.nas.nodedc
172.22.0.222 task.nas.nodedc 172.22.0.222 task.nas.nodedc
``` ```
@ -35,15 +38,19 @@ https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP
```text ```text
http://auth.nas.nodedc:18080 http://auth.nas.nodedc:18080
http://auth-admin.nas.nodedc:18080/if/admin/
http://172.22.0.222:18080/if/admin/
http://launcher.nas.nodedc:18080 http://launcher.nas.nodedc:18080
http://task.nas.nodedc:18080 http://task.nas.nodedc:18080
http://task.nas.nodedc:18090 http://task.nas.nodedc:18090
``` ```
`auth-admin.nas.nodedc` is the technical Authentik Admin entrypoint. `172.22.0.222:18080` is a local IP fallback for workstations without `auth-admin.nas.nodedc` DNS. Both keep Authentik access separate from the public `id.nodedc.ru` login entrypoint and do not load NODE.DC auth-flow CSS.
## Что входит ## Что входит
- `docker-compose.platform-http.yml` поднимает новый Authentik, Launcher и Caddy edge. - `docker-compose.platform-http.yml` поднимает новый Authentik, Launcher и Caddy edge.
- `Caddyfile.http` маршрутизирует локальные `auth/launcher/task.nas.nodedc` и внешние `id/hub/ops.nodedc.ru`. - `Caddyfile.http` маршрутизирует локальные `auth/auth-admin/launcher/task.nas.nodedc`, IP fallback `172.22.0.222` для Authentik Admin и внешние `id/hub/ops.nodedc.ru`.
- `deploy-current.sh` синхронизирует compose, Caddyfile и опционально Launcher source в NAS mount. Authentik templates синхронизируются только при явном `SYNC_AUTHENTIK_TEMPLATES=1`. - `deploy-current.sh` синхронизирует compose, Caddyfile и опционально Launcher source в NAS mount. Authentik templates синхронизируются только при явном `SYNC_AUTHENTIK_TEMPLATES=1`.
- `backup-current.sh` делает snapshot Launcher runtime/uploads/Auth templates/config и готовит команду `pg_dump` для Authentik Postgres. - `backup-current.sh` делает snapshot Launcher runtime/uploads/Auth templates/config и готовит команду `pg_dump` для Authentik Postgres.
- Tasker поднимается отдельным compose из `NODEDC_TASKMANAGER/plane-app/docker-compose.yaml` на порту `18090`. - Tasker поднимается отдельным compose из `NODEDC_TASKMANAGER/plane-app/docker-compose.yaml` на порту `18090`.
@ -134,7 +141,7 @@ TASKER_SYNC_SOURCE=1 ./infra/synology/deploy-current.sh
```bash ```bash
cd /volume1/docker/nodedc-platform/launcher/source cd /volume1/docker/nodedc-platform/launcher/source
sudo /usr/local/bin/docker build -t nodedc/launcher:local . sudo /usr/local/bin/docker build --no-cache -t nodedc/launcher:local .
cd /volume1/docker/nodedc-platform/platform cd /volume1/docker/nodedc-platform/platform
sudo /usr/local/bin/docker compose \ sudo /usr/local/bin/docker compose \
@ -143,7 +150,61 @@ sudo /usr/local/bin/docker compose \
up -d --force-recreate --no-deps launcher up -d --force-recreate --no-deps launcher
``` ```
После такого deploy проверить `healthz`, запись в launcher storage/uploads и сценарий пользователя без аппрува: сохранение аватара не должно показывать экран `Заявка ожидает подтверждения`. После такого deploy проверить `healthz`, запись в launcher storage/uploads и сценарий пользователя без аппрува: сохранение аватара не должно показывать экран `Заявка ожидает подтверждения`. Дополнительно проверить, что live bundle больше не содержит старый pending gate:
```bash
launcher_asset="$(
curl -k -sS --compressed -H 'Accept: text/html' https://hub.nodedc.ru/ \
| grep -aoE 'index-[A-Za-z0-9_-]+\.js' \
| head -n 1
)"
test -n "$launcher_asset"
if curl -k -sS --compressed "https://hub.nodedc.ru/assets/${launcher_asset}" \
| grep -aq 'Заявка ожидает подтверждения'; then
echo 'old pending gate still present'
exit 1
fi
echo 'launcher-pending-gate-ok'
```
## Authentik Admin и Brand CSS
NODE.DC auth-flow CSS must stay template-scoped. Do not store it in Authentik `Brand.branding_custom_css`: Authentik passes that CSS into Admin/User web component runtime and breaks native controls.
After syncing Authentik templates to NAS, recreate `reverse-proxy authentik-server authentik-worker launcher`, then clear existing global Brand CSS in the live DB:
```bash
cd /volume1/docker/nodedc-platform/platform
sudo bash clear-authentik-brand-css.sh
```
Verify:
```bash
id_admin_status="$(
curl -k -sS -o /dev/null -w '%{http_code}' https://id.nodedc.ru/if/admin/
)"
if [[ "$id_admin_status" != "404" ]]; then
echo "public id admin is not closed: status=${id_admin_status}"
exit 1
fi
echo 'public-id-admin-closed-ok'
auth_admin_page="$(
curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/admin/
)"
printf '%s' "$auth_admin_page" \
| grep -aE '<style data-id="brand-css"></style>|authentikBrand.branding_custom_css = ""'
auth_admin_flow="$(
curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/flow/default-authentication-flow/
)"
if printf '%s' "$auth_admin_flow" | grep -aq '<style data-id="nodedc-auth-login-css">'; then
echo 'admin host still has NODE.DC auth CSS'
exit 1
fi
echo 'auth-admin-css-ok'
```
## Backup текущего состояния ## Backup текущего состояния

View File

@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
DOCKER_BIN="${DOCKER_BIN:-/usr/local/bin/docker}"
PLATFORM_DIR="${PLATFORM_DIR:-/volume1/docker/nodedc-platform/platform}"
LAUNCHER_SOURCE_DIR="${LAUNCHER_SOURCE_DIR:-/volume1/docker/nodedc-platform/launcher/source}"
ENV_FILE="${ENV_FILE:-${PLATFORM_DIR}/.env.synology}"
COMPOSE_FILE="${COMPOSE_FILE:-${PLATFORM_DIR}/docker-compose.platform-http.yml}"
if [[ ! -x "${DOCKER_BIN}" ]]; then
DOCKER_BIN="$(command -v docker)"
fi
cd "${PLATFORM_DIR}"
echo "== storage permissions =="
mkdir -p ../launcher/server-storage ../launcher/uploads
chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads
chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads
echo "== launcher image build =="
cd "${LAUNCHER_SOURCE_DIR}"
"${DOCKER_BIN}" build --no-cache -t nodedc/launcher:local .
echo "== platform services recreate =="
cd "${PLATFORM_DIR}"
"${DOCKER_BIN}" compose \
--env-file "${ENV_FILE}" \
-f "${COMPOSE_FILE}" \
up -d --force-recreate --no-deps reverse-proxy authentik-server authentik-worker launcher
echo "== clear authentik global brand css =="
DOCKER_BIN="${DOCKER_BIN}" bash "${PLATFORM_DIR}/clear-authentik-brand-css.sh"
echo "== launcher storage check =="
"${DOCKER_BIN}" exec nodedc-platform-launcher-1 sh -lc \
'touch /app/server/storage/.write-test /app/server/storage/uploads/.write-test && rm /app/server/storage/.write-test /app/server/storage/uploads/.write-test && echo storage-ok'
echo "== launcher -> authentik api check =="
"${DOCKER_BIN}" exec nodedc-platform-launcher-1 sh -lc '
echo "$NODEDC_AUTHENTIK_BASE_URL"
getent hosts nodedc-platform-authentik-server
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if wget -qSO- \
--header "Authorization: Bearer $NODEDC_AUTHENTIK_SERVICE_TOKEN" \
"$NODEDC_AUTHENTIK_BASE_URL/api/v3/core/groups/?search=nodedc_admin" \
>/tmp/authentik-api-check.json \
2>/tmp/authentik-api-check.headers; then
head -n 25 /tmp/authentik-api-check.headers
exit 0
fi
head -n 25 /tmp/authentik-api-check.headers || true
echo "authentik-api-not-ready attempt=$attempt"
sleep 10
done
exit 1
'
echo "== auth flow check =="
auth_flow=""
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if auth_flow="$(
curl -k -fsS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/
)"; then
break
fi
echo "auth-flow-not-ready attempt=${attempt}"
sleep 10
done
printf '%s' "$auth_flow" \
| grep -aE 'syncFormFieldsBeforeSubmit|branding_custom_css = ""|Запросить доступ'
echo "== public admin closed check =="
id_admin_status="$(
curl -k -sS -o /dev/null -w '%{http_code}' https://id.nodedc.ru/if/admin/
)"
if [[ "$id_admin_status" != "404" ]]; then
echo "public id admin is not closed: status=${id_admin_status}"
exit 1
fi
echo "public-id-admin-closed-ok"
echo "== technical admin css check =="
auth_admin_page="$(
curl -k -fsS --compressed -H 'Host: auth-admin.nas.nodedc' http://127.0.0.1:18080/if/admin/
)"
printf '%s' "$auth_admin_page" \
| grep -aE '<style data-id="brand-css"></style>|authentikBrand.branding_custom_css = ""'
auth_admin_flow="$(
curl -k -fsS --compressed -H 'Host: auth-admin.nas.nodedc' http://127.0.0.1:18080/if/flow/default-authentication-flow/
)"
if printf '%s' "$auth_admin_flow" | grep -aq '<style data-id="nodedc-auth-login-css">'; then
echo "admin host still has NODE.DC auth CSS"
exit 1
fi
echo "auth-admin-css-ok"
echo "== launcher bundle check =="
launcher_asset="$(
curl -k -fsS --compressed -H 'Accept: text/html' https://hub.nodedc.ru/ \
| grep -aoE 'index-[A-Za-z0-9_-]+\.js' \
| head -n 1
)"
test -n "$launcher_asset"
if curl -k -fsS --compressed "https://hub.nodedc.ru/assets/${launcher_asset}" \
| grep -aq 'Заявка ожидает подтверждения'; then
echo "old pending gate still present"
exit 1
fi
echo "launcher-pending-gate-ok"
echo "runtime-apply-ok"

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
DOCKER_BIN="${DOCKER_BIN:-/usr/local/bin/docker}"
AUTHENTIK_CONTAINER="${AUTHENTIK_CONTAINER:-nodedc-platform-authentik-server-1}"
"${DOCKER_BIN}" exec "${AUTHENTIK_CONTAINER}" ak shell -c '
from authentik.brands.models import Brand
cleared = []
for brand in Brand.objects.all():
if brand.branding_custom_css:
brand.branding_custom_css = ""
brand.save(update_fields=["branding_custom_css"])
cleared.append(brand.domain)
print("cleared_brand_css=" + str(len(cleared)))
if cleared:
print("domains=" + ",".join(cleared))
'

View File

@ -30,6 +30,9 @@ rsync -av \
rsync -av \ rsync -av \
"${PLATFORM_REPO}/infra/synology/deploy-current.sh" \ "${PLATFORM_REPO}/infra/synology/deploy-current.sh" \
"${PLATFORM_REPO}/infra/synology/backup-current.sh" \ "${PLATFORM_REPO}/infra/synology/backup-current.sh" \
"${PLATFORM_REPO}/infra/synology/apply-current-runtime.sh" \
"${PLATFORM_REPO}/infra/synology/verify-current-runtime.sh" \
"${PLATFORM_REPO}/infra/synology/clear-authentik-brand-css.sh" \
"${NAS_ROOT}/platform/" "${NAS_ROOT}/platform/"
if [[ "${SYNC_AUTHENTIK_TEMPLATES}" == "1" ]]; then if [[ "${SYNC_AUTHENTIK_TEMPLATES}" == "1" ]]; then
@ -160,13 +163,23 @@ cat <<'EOF'
Synced files to NAS mount. Run on Synology to apply runtime changes: Synced files to NAS mount. Run on Synology to apply runtime changes:
cd /volume1/docker/nodedc-platform/platform
sudo bash apply-current-runtime.sh
If runtime was already applied and only verification is needed:
cd /volume1/docker/nodedc-platform/platform
sudo bash verify-current-runtime.sh
Manual equivalent:
cd /volume1/docker/nodedc-platform/platform cd /volume1/docker/nodedc-platform/platform
sudo mkdir -p ../launcher/server-storage ../launcher/uploads sudo mkdir -p ../launcher/server-storage ../launcher/uploads
sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads
sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads
cd /volume1/docker/nodedc-platform/launcher/source cd /volume1/docker/nodedc-platform/launcher/source
sudo /usr/local/bin/docker build -t nodedc/launcher:local . sudo /usr/local/bin/docker build --no-cache -t nodedc/launcher:local .
cd /volume1/docker/nodedc-platform/platform cd /volume1/docker/nodedc-platform/platform
sudo /usr/local/bin/docker compose \ sudo /usr/local/bin/docker compose \
@ -181,6 +194,12 @@ sudo /usr/local/bin/docker compose \
-f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \ -f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \
up -d --force-recreate --no-deps reverse-proxy authentik-server authentik-worker launcher up -d --force-recreate --no-deps reverse-proxy authentik-server authentik-worker launcher
After Authentik template rollout, clear global Brand custom CSS from the live DB.
NODE.DC login CSS must be template-scoped, not stored in Brand.branding_custom_css:
cd /volume1/docker/nodedc-platform/platform
sudo bash clear-authentik-brand-css.sh
Verify: Verify:
sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \ sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \
@ -201,7 +220,44 @@ sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc '
' '
curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \ curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \
| grep -aE 'hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ' | grep -aE 'syncFormFieldsBeforeSubmit|branding_custom_css = ""|hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ'
id_admin_status="$(
curl -k -sS -o /dev/null -w '%{http_code}' https://id.nodedc.ru/if/admin/
)"
if [[ "$id_admin_status" != "404" ]]; then
echo "public id admin is not closed: status=${id_admin_status}"
exit 1
fi
echo 'public-id-admin-closed-ok'
auth_admin_page="$(
curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/admin/
)"
printf '%s' "$auth_admin_page" \
| grep -aE '<style data-id="brand-css"></style>|authentikBrand.branding_custom_css = ""'
auth_admin_flow="$(
curl -k -fsS --compressed http://auth-admin.nas.nodedc:18080/if/flow/default-authentication-flow/
)"
if printf '%s' "$auth_admin_flow" | grep -aq '<style data-id="nodedc-auth-login-css">'; then
echo 'admin host still has NODE.DC auth CSS'
exit 1
fi
echo 'auth-admin-css-ok'
launcher_asset="$(
curl -k -sS --compressed -H 'Accept: text/html' https://hub.nodedc.ru/ \
| grep -aoE 'index-[A-Za-z0-9_-]+\.js' \
| head -n 1
)"
test -n "$launcher_asset"
if curl -k -sS --compressed "https://hub.nodedc.ru/assets/${launcher_asset}" \
| grep -aq 'Заявка ожидает подтверждения'; then
echo 'old pending gate still present'
exit 1
fi
echo 'launcher-pending-gate-ok'
Optional Tasker apply after TASKER_REPO sync: Optional Tasker apply after TASKER_REPO sync:

View File

@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
DOCKER_BIN="${DOCKER_BIN:-/usr/local/bin/docker}"
if [[ ! -x "${DOCKER_BIN}" ]]; then
DOCKER_BIN="$(command -v docker)"
fi
fetch_with_retry() {
local url="$1"
shift
local output=""
local attempt
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if output="$(curl -k -fsS --compressed "$@" "$url")"; then
printf '%s' "$output"
return 0
fi
echo "not-ready url=${url} attempt=${attempt}" >&2
sleep 10
done
return 1
}
echo "== containers =="
"${DOCKER_BIN}" ps --filter 'name=nodedc-platform' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
echo "== launcher storage check =="
"${DOCKER_BIN}" exec nodedc-platform-launcher-1 sh -lc \
'touch /app/server/storage/.write-test /app/server/storage/uploads/.write-test && rm /app/server/storage/.write-test /app/server/storage/uploads/.write-test && echo storage-ok'
echo "== launcher -> authentik api check =="
"${DOCKER_BIN}" exec nodedc-platform-launcher-1 sh -lc '
echo "$NODEDC_AUTHENTIK_BASE_URL"
getent hosts nodedc-platform-authentik-server
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if wget -qSO- \
--header "Authorization: Bearer $NODEDC_AUTHENTIK_SERVICE_TOKEN" \
"$NODEDC_AUTHENTIK_BASE_URL/api/v3/core/groups/?search=nodedc_admin" \
>/tmp/authentik-api-check.json \
2>/tmp/authentik-api-check.headers; then
head -n 25 /tmp/authentik-api-check.headers
exit 0
fi
head -n 25 /tmp/authentik-api-check.headers || true
echo "authentik-api-not-ready attempt=$attempt"
sleep 10
done
exit 1
'
echo "== auth flow check =="
auth_flow="$(fetch_with_retry https://id.nodedc.ru/if/flow/default-authentication-flow/)"
printf '%s' "$auth_flow" \
| grep -aE 'syncFormFieldsBeforeSubmit|branding_custom_css = ""|Запросить доступ'
echo "== public admin closed check =="
id_admin_status="$(
curl -k -sS -o /dev/null -w '%{http_code}' https://id.nodedc.ru/if/admin/
)"
if [[ "$id_admin_status" != "404" ]]; then
echo "public id admin is not closed: status=${id_admin_status}"
exit 1
fi
echo "public-id-admin-closed-ok"
echo "== technical admin css check =="
auth_admin_page="$(
fetch_with_retry http://127.0.0.1:18080/if/admin/ -H 'Host: auth-admin.nas.nodedc'
)"
printf '%s' "$auth_admin_page" \
| grep -aE '<style data-id="brand-css"></style>|authentikBrand.branding_custom_css = ""'
auth_admin_flow="$(
fetch_with_retry http://127.0.0.1:18080/if/flow/default-authentication-flow/ -H 'Host: auth-admin.nas.nodedc'
)"
if printf '%s' "$auth_admin_flow" | grep -aq '<style data-id="nodedc-auth-login-css">'; then
echo "admin host still has NODE.DC auth CSS"
exit 1
fi
echo "auth-admin-css-ok"
echo "== technical admin ip fallback check =="
ip_admin_page="$(
fetch_with_retry http://172.22.0.222:18080/if/admin/
)"
printf '%s' "$ip_admin_page" \
| grep -aE '<style data-id="brand-css"></style>|authentikBrand.branding_custom_css = ""'
echo "== launcher bundle check =="
launcher_html="$(fetch_with_retry https://hub.nodedc.ru/ -H 'Accept: text/html')"
launcher_asset="$(
printf '%s' "$launcher_html" \
| grep -aoE 'index-[A-Za-z0-9_-]+\.js' \
| head -n 1
)"
test -n "$launcher_asset"
echo "launcher_asset=${launcher_asset}"
if fetch_with_retry "https://hub.nodedc.ru/assets/${launcher_asset}" \
| grep -aq 'Заявка ожидает подтверждения'; then
echo "old pending gate still present"
exit 1
fi
echo "launcher-pending-gate-ok"
echo "runtime-verify-ok"