diff --git a/README.md b/README.md index 246620d..1c47b7d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ PORT=8210 npm run admin ``` -Админка будет доступна на `http://127.0.0.1:8210/admin/`, а подключенный сайт на `http://127.0.0.1:8210/`. +Админка будет доступна на `http://127.0.0.1:8210/`. Подключенный сайт хранится как управляемый проект, но не является корневым UI этого сервиса. ## Запуск с Authentik @@ -35,16 +35,15 @@ PORT=8210 npm run admin ```bash docker compose --env-file infra/.env -f infra/docker-compose.yml up -d --build -infra/scripts/bootstrap-authentik.sh ``` Адреса: -- CMS: `http://cms.local.nodedc:8210/admin/` +- CMS: `http://cms.local.nodedc:8210/` - Authentik login: `http://cms-auth.local.nodedc:8210/` - Authentik admin: `http://cms-auth-admin.local.nodedc:8210/if/admin/` -Bootstrap создает OIDC-приложение `dc-cms`, группы `dc-cms:*` и стартового администратора из `infra/.env`. После первичного входа временный пароль нужно заменить в Authentik. +Compose-сервис `authentik-bootstrap` создает OIDC-приложение `dc-cms`, группы `dc-cms:*` и стартового администратора из `infra/.env`. После первичного входа временный пароль нужно заменить в Authentik; следующие deploy не перетрут пароль, пока `CMS_BOOTSTRAP_FORCE_PASSWORD=false`. ## Synology @@ -63,11 +62,4 @@ DNS-записи должны вести на внешний IP NAS. В Synology Оба target-порта ведут в один Caddy контейнер внутри compose-проекта `dc-cms`; Caddy разводит запросы по Host. -Bootstrap Authentik on Synology: - -```bash -cd /volume1/docker/dc-cms/source/infra -sudo env CMS_ENV_FILE=.env.synology sh ./scripts/bootstrap-authentik.sh -``` - -Bootstrap выполняется так же, как в платформе NODE.DC: через `authentik-server ak shell < authentik/bootstrap-cms.py`. Ручной вход в worker, `VENV_PATH` и отдельный `apply_blueprint` не являются штатным сценарием. +На Synology bootstrap Authentik выполняется автоматически внутри compose-проекта при `nodedc-deploy apply`. Ручной вход в worker, `VENV_PATH`, отдельный `apply_blueprint` и shell-скрипты не являются штатным сценарием. diff --git a/infra/.env.example b/infra/.env.example index 61923e7..266bfd3 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -30,12 +30,13 @@ AUTHENTIK_BOOTSTRAP_EMAIL=dcctouch@gmail.com AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-temporary-password AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-random-token -# CMS bootstrap user. Used by infra/scripts/bootstrap-authentik.sh. +# CMS bootstrap user. Applied automatically by the authentik-bootstrap compose service. CMS_BOOTSTRAP_ADMIN_EMAIL=dcctouch@gmail.com CMS_BOOTSTRAP_ADMIN_PASSWORD=replace-with-temporary-password CMS_BOOTSTRAP_ADMIN_NAME=DCTOUCH CMS Admin +CMS_BOOTSTRAP_FORCE_PASSWORD=false -# CMS OIDC application. bootstrap-authentik.sh creates the matching provider. +# CMS OIDC application. The authentik-bootstrap compose service creates the matching provider. CMS_AUTH_ENABLED=true CMS_BASE_URL=http://cms.local.nodedc:8210 CMS_OIDC_ISSUER=http://cms-auth.local.nodedc:8210/application/o/dc-cms/ diff --git a/infra/.env.synology.example b/infra/.env.synology.example index 8f52637..2f872f7 100644 --- a/infra/.env.synology.example +++ b/infra/.env.synology.example @@ -31,12 +31,13 @@ AUTHENTIK_BOOTSTRAP_EMAIL=dcctouch@gmail.com AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-temporary-password AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-random-token -# CMS bootstrap user. Used by infra/scripts/bootstrap-authentik.sh. +# CMS bootstrap user. Applied automatically by the authentik-bootstrap compose service. CMS_BOOTSTRAP_ADMIN_EMAIL=dcctouch@gmail.com CMS_BOOTSTRAP_ADMIN_PASSWORD=replace-with-temporary-password CMS_BOOTSTRAP_ADMIN_NAME=DCTOUCH CMS Admin +CMS_BOOTSTRAP_FORCE_PASSWORD=false -# CMS OIDC application. bootstrap-authentik.sh creates the matching provider. +# CMS OIDC application. The authentik-bootstrap compose service creates the matching provider. CMS_AUTH_ENABLED=true CMS_BASE_URL=https://cms.dcserve.ru CMS_OIDC_ISSUER=https://auth.dcserve.ru/application/o/dc-cms/ diff --git a/infra/authentik/bootstrap-cms.py b/infra/authentik/bootstrap-cms.py index 8ab811c..25e7fe1 100644 --- a/infra/authentik/bootstrap-cms.py +++ b/infra/authentik/bootstrap-cms.py @@ -63,6 +63,13 @@ def optional_env(name, default=""): return environ.get(name, default).strip() +def bool_env(name, default=False): + value = environ.get(name, "") + if value == "": + return default + return value.strip().lower() in ("1", "true", "yes", "on") + + def ensure_group(name, is_superuser=False): group, _ = Group.objects.get_or_create(name=name) group.is_superuser = is_superuser @@ -88,6 +95,7 @@ def ensure_admin_user(groups): user = User.objects.filter(email__iexact=admin_email).first() or User.objects.filter( username=admin_email ).first() + created = user is None if user is None: user = User(username=admin_email, email=admin_email, name=admin_email, type="internal") @@ -101,7 +109,7 @@ def ensure_admin_user(groups): environ.get("CMS_BOOTSTRAP_ADMIN_PASSWORD", "") or environ.get("AUTHENTIK_BOOTSTRAP_PASSWORD", "") ) - if admin_password: + if admin_password and (created or bool_env("CMS_BOOTSTRAP_FORCE_PASSWORD")): user.set_password(admin_password) user.save() diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 3bf8ee0..5cba7b0 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -53,8 +53,8 @@ services: - ../projects:/app/projects - ${CMS_NODEDC_SITE_PATH:-../../NODEDC_SITE}:/sites/nodedc depends_on: - authentik-server: - condition: service_started + authentik-bootstrap: + condition: service_completed_successfully networks: - cms-edge @@ -132,6 +132,47 @@ services: networks: - cms-edge + authentik-bootstrap: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2} + command: + - ak + - shell + - -c + - exec(open('/bootstrap/bootstrap-cms.py', encoding='utf-8').read()) + restart: "no" + env_file: + - ${CMS_SERVICE_ENV_FILE:-.env} + environment: + AUTHENTIK_POSTGRESQL__HOST: postgresql-authentik + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?database password required} + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required} + AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED:-false} + CMS_AUTH_DOMAIN: ${CMS_AUTH_DOMAIN:-cms-auth.local.nodedc} + CMS_BOOTSTRAP_ADMIN_EMAIL: ${CMS_BOOTSTRAP_ADMIN_EMAIL:-} + CMS_BOOTSTRAP_ADMIN_PASSWORD: ${CMS_BOOTSTRAP_ADMIN_PASSWORD:-} + CMS_BOOTSTRAP_ADMIN_NAME: ${CMS_BOOTSTRAP_ADMIN_NAME:-DCTOUCH CMS Admin} + CMS_BOOTSTRAP_FORCE_PASSWORD: ${CMS_BOOTSTRAP_FORCE_PASSWORD:-false} + CMS_OIDC_CLIENT_ID: ${CMS_OIDC_CLIENT_ID:?cms oidc client id required} + CMS_OIDC_CLIENT_SECRET: ${CMS_OIDC_CLIENT_SECRET:?cms oidc client secret required} + CMS_OIDC_REDIRECT_URI: ${CMS_OIDC_REDIRECT_URI:?cms oidc redirect uri required} + CMS_OIDC_LOGGED_OUT_REDIRECT_URI: ${CMS_OIDC_LOGGED_OUT_REDIRECT_URI:?cms oidc logged out redirect uri required} + CMS_BASE_URL: ${CMS_BASE_URL:?cms base url required} + CMS_LOGOUT_URI: ${CMS_LOGOUT_URI:-} + depends_on: + authentik-server: + condition: service_healthy + authentik-worker: + condition: service_healthy + volumes: + - authentik-data:/data + - authentik-certs:/certs + - ./authentik/custom-templates:/templates:ro + - ./authentik/bootstrap-cms.py:/bootstrap/bootstrap-cms.py:ro + networks: + - cms-edge + networks: cms-edge: diff --git a/server/admin-server.mjs b/server/admin-server.mjs index 8ea90f6..9bbc1cf 100644 --- a/server/admin-server.mjs +++ b/server/admin-server.mjs @@ -330,9 +330,9 @@ function callbackUri(req) { function sanitizeReturnTo(value) { const rawValue = String(value || "").trim(); - if (!rawValue || rawValue.startsWith("//")) return "/admin/"; - if (/^https?:\/\//i.test(rawValue)) return "/admin/"; - return rawValue.startsWith("/") ? rawValue : "/admin/"; + if (!rawValue || rawValue.startsWith("//")) return "/"; + if (/^https?:\/\//i.test(rawValue)) return "/"; + return rawValue.startsWith("/") ? rawValue : "/"; } function sendRedirect(res, target, status = 302) { @@ -710,7 +710,7 @@ async function handleAuthRoute(req, res, url) { } if (req.method === "GET" && url.pathname === "/auth/logged-out") { - sendRedirect(res, authLoginUrl(req, "/admin/")); + sendRedirect(res, authLoginUrl(req, "/")); return true; } @@ -2026,11 +2026,12 @@ function parseMultipartUpload(req, bodyBuffer) { function safePublicPath(urlPath) { const withoutQuery = decodeURIComponent(urlPath.split("?")[0]); + const adminRootFiles = new Set(["/admin.css", "/admin.js", "/nodedc-logo.svg"]); const requestPath = - withoutQuery === "/" - ? "/index.html" - : withoutQuery === "/admin" || withoutQuery === "/admin/" - ? "/admin/index.html" + withoutQuery === "/" || withoutQuery === "/admin" || withoutQuery === "/admin/" + ? "/admin/index.html" + : adminRootFiles.has(withoutQuery) + ? `/admin${withoutQuery}` : withoutQuery; const normalized = normalize(requestPath).replace(/^(\.\.[/\\])+/, ""); const isAdminAsset = normalized === "/admin/index.html" || normalized.startsWith("/admin/"); @@ -2283,7 +2284,6 @@ const server = createServer(async (req, res) => { }); server.listen(port, host, () => { - console.log(`DC CMS admin: http://${host}:${port}/admin/`); - console.log(`Project site: http://${host}:${port}/`); + console.log(`DC CMS: http://${host}:${port}/`); console.log(`Project root: ${repoRoot}`); });