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