Serve CMS at root and bootstrap Authentik in compose

This commit is contained in:
DCCONSTRUCTIONS 2026-07-06 10:32:24 +03:00
parent 5004627476
commit 4b4963101a
6 changed files with 72 additions and 29 deletions

View File

@ -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-скрипты не являются штатным сценарием.

View File

@ -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/

View File

@ -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/

View File

@ -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()

View File

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

View File

@ -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}`);
});