fix(platform): isolate authentik admin styling
This commit is contained in:
parent
e9def6672c
commit
c1ac8b1a10
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 текущего состояния
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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))
|
||||||
|
'
|
||||||
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue