Compare commits
9 Commits
9d7f8167d3
...
c99ad80aa7
| Author | SHA1 | Date |
|---|---|---|
|
|
c99ad80aa7 | |
|
|
25d3004cef | |
|
|
d3af184096 | |
|
|
932b9bc7ec | |
|
|
61d373f076 | |
|
|
60e70bf86d | |
|
|
ffd29c4f0b | |
|
|
0e462012da | |
|
|
95072586b9 |
|
|
@ -22,6 +22,7 @@ Git repo:
|
|||
|
||||
Артефакты:
|
||||
|
||||
- `docs/CURRENT_INFRA_HANDOFF.md`
|
||||
- `docs/DISCOVERY_REPORT.md`
|
||||
- `docs/ARCHITECTURE.md`
|
||||
- `docs/AUTH_MODEL.md`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,456 @@
|
|||
# NODE.DC current infra handoff
|
||||
|
||||
Last updated: 2026-05-15.
|
||||
|
||||
This document is the fast context entrypoint for a new engineering chat. Read it first before touching deploy, Synology, Authentik, Launcher, or Tasker.
|
||||
|
||||
## Product shape
|
||||
|
||||
NODE.DC is intentionally modular. It is not a single monorepo with all products merged into one codebase.
|
||||
|
||||
Source-of-truth repositories:
|
||||
|
||||
| Module | Responsibility | Local source |
|
||||
| --- | --- | --- |
|
||||
| Platform | Infra, Authentik, reverse proxy, Synology deploy scripts, architecture docs | `/Users/dcconstructions/Downloads/mnt/NODEDC/platform` |
|
||||
| Launcher / Hub | NODE.DC control plane, user/admin UI, access requests, access matrix, Authentik sync | `/Users/dcconstructions/Downloads/mnt/data/nodedc_launcher` |
|
||||
| Tasker / Operational Core | Plane fork, tasks/workspaces/projects, standalone-capable product module | `/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER` |
|
||||
| Ops Agents Gateway | Standalone MCP/API router for Tasker operational agents | `/Users/dcconstructions/Downloads/mnt/data/NODEDC_TASKMANAGER_CODEXAPI` |
|
||||
|
||||
Current Git branches:
|
||||
|
||||
- Platform: `main`
|
||||
- Launcher: `main`
|
||||
- Tasker: `master`
|
||||
- Ops Agents Gateway: `main`
|
||||
|
||||
The modules communicate through HTTP/OIDC/internal APIs. They must remain independently buildable and deployable.
|
||||
|
||||
## Ownership boundaries
|
||||
|
||||
Do not collapse data into one shared database.
|
||||
|
||||
| Area | Owner | Runtime state |
|
||||
| --- | --- | --- |
|
||||
| Identity, SSO, OIDC providers | Authentik | Authentik PostgreSQL volume in `nodedc-platform` |
|
||||
| Clients, users, groups, Launcher access matrix | Launcher | Launcher JSON/runtime storage under `/volume1/docker/nodedc-platform/launcher` |
|
||||
| Workspaces, projects, issues, comments, files | Tasker | Tasker PostgreSQL + MinIO/RabbitMQ/Redis volumes under `nodedc-tasker` |
|
||||
|
||||
Launcher can project access into Tasker, but it must not become the owner of Tasker domain tables. Tasker can run standalone if NODE.DC integration env vars are disabled or pointed elsewhere.
|
||||
|
||||
## Synology topology
|
||||
|
||||
Current NAS root:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform
|
||||
```
|
||||
|
||||
Mounted on macOS as:
|
||||
|
||||
```text
|
||||
/Volumes/docker/nodedc-platform
|
||||
```
|
||||
|
||||
Public domains:
|
||||
|
||||
```text
|
||||
https://id.nodedc.ru -> Authentik
|
||||
https://hub.nodedc.ru -> Launcher / Hub
|
||||
https://ops.nodedc.ru -> Tasker / Operational Core
|
||||
https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP endpoint
|
||||
```
|
||||
|
||||
Local/NAS check domains used during rollout:
|
||||
|
||||
```text
|
||||
auth.nas.nodedc
|
||||
launcher.nas.nodedc
|
||||
task.nas.nodedc
|
||||
auth.local.nodedc
|
||||
launcher.local.nodedc
|
||||
task.local.nodedc
|
||||
```
|
||||
|
||||
### Platform compose project
|
||||
|
||||
Compose project:
|
||||
|
||||
```text
|
||||
nodedc-platform
|
||||
```
|
||||
|
||||
Compose file:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml
|
||||
```
|
||||
|
||||
Source copy:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/NODEDC/platform/infra/synology/docker-compose.platform-http.yml
|
||||
```
|
||||
|
||||
Main services:
|
||||
|
||||
- `reverse-proxy`: Caddy HTTP edge, publishes host port `18080`.
|
||||
- `launcher`: `nodedc/launcher:local`, serves Hub on container port `5173`.
|
||||
- `postgresql-authentik`: Authentik PostgreSQL.
|
||||
- `authentik-server`: Authentik web/server.
|
||||
- `authentik-worker`: Authentik worker.
|
||||
|
||||
Main Docker volumes:
|
||||
|
||||
- `authentik-database`
|
||||
- `authentik-data`
|
||||
- `authentik-certs`
|
||||
- `caddy-data`
|
||||
- `caddy-config`
|
||||
|
||||
Launcher bind-mounted runtime state:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform/launcher/server-storage -> /app/server/storage
|
||||
/volume1/docker/nodedc-platform/launcher/uploads -> /app/dist/storage/uploads
|
||||
/volume1/docker/nodedc-platform/launcher/uploads -> /app/public/storage/uploads
|
||||
```
|
||||
|
||||
These directories must be writable by container user `node` / UID `1000`.
|
||||
|
||||
### Tasker compose project
|
||||
|
||||
Compose project:
|
||||
|
||||
```text
|
||||
nodedc-tasker
|
||||
```
|
||||
|
||||
NAS runtime/deploy directory:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform/tasker/plane-app
|
||||
```
|
||||
|
||||
NAS source/deploy copy:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform/tasker/plane-src
|
||||
```
|
||||
|
||||
Repository source:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src
|
||||
```
|
||||
|
||||
Repository compose/runtime template:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-app/docker-compose.yaml
|
||||
```
|
||||
|
||||
Main services:
|
||||
|
||||
- `web`: Tasker frontend, image `nodedc/plane-frontend:ru`.
|
||||
- `admin`: Tasker admin frontend, image `nodedc/plane-admin:ru`.
|
||||
- `space`: public/space frontend, image `nodedc/plane-space:ru`.
|
||||
- `live`: live server, image `nodedc/plane-live:local`.
|
||||
- `api`: backend API, image `nodedc/plane-backend:local`.
|
||||
- `worker`, `beat-worker`, `migrator`: backend workers/migrations.
|
||||
- `plane-db`: PostgreSQL.
|
||||
- `plane-redis`: Valkey/Redis.
|
||||
- `plane-mq`: RabbitMQ.
|
||||
- `plane-minio`: uploads/files.
|
||||
- `proxy`: Tasker edge proxy, publishes `LISTEN_HTTP_PORT` / current NAS port `18090`.
|
||||
|
||||
Main Docker volumes under the `nodedc-tasker` project:
|
||||
|
||||
- `nodedc-tasker_pgdata`
|
||||
- `nodedc-tasker_redisdata`
|
||||
- `nodedc-tasker_rabbitmq_data`
|
||||
- `nodedc-tasker_uploads`
|
||||
- log and proxy volumes from `plane-app/docker-compose.yaml`
|
||||
|
||||
NAS-only runtime files currently exist and are intentionally not committed with secrets:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform/tasker/plane-app/.env.synology
|
||||
/volume1/docker/nodedc-platform/tasker/plane-app/docker-compose.synology.override.yml
|
||||
```
|
||||
|
||||
The current override only adds `extra_hosts` entries so Tasker backend containers can resolve `id.nodedc.ru`, `hub.nodedc.ru`, `ops.nodedc.ru`, local domains, NAS domains, and `host.docker.internal` through `host-gateway`.
|
||||
|
||||
### Ops Agents Gateway compose project
|
||||
|
||||
Compose project:
|
||||
|
||||
```text
|
||||
nodedc-ops-agents
|
||||
```
|
||||
|
||||
Recommended NAS runtime/deploy directory:
|
||||
|
||||
```text
|
||||
/volume1/docker/nodedc-platform/ops-agents
|
||||
```
|
||||
|
||||
Repository source:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/NODEDC_TASKMANAGER_CODEXAPI
|
||||
```
|
||||
|
||||
Public entrypoint:
|
||||
|
||||
```text
|
||||
https://ops-agents.nodedc.ru
|
||||
```
|
||||
|
||||
Synology reverse proxy should route:
|
||||
|
||||
```text
|
||||
HTTPS ops-agents.nodedc.ru:443 -> HTTP 127.0.0.1:18190
|
||||
```
|
||||
|
||||
Do not use host port `18090` for this module: `18090` is the Tasker proxy port. Gateway container port remains `4100`; host port is controlled by `HOST_PORT=18190`.
|
||||
|
||||
Main services:
|
||||
|
||||
- `agent-gateway`: NODE.DC Operational Agents Gateway, image built from source by compose.
|
||||
- `postgres`: Gateway-owned PostgreSQL database for agents, grants, tokens, idempotency, and audit.
|
||||
|
||||
Deployment checklist:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/NODEDC_TASKMANAGER_CODEXAPI/docs/SYNOLOGY_DEPLOY.md
|
||||
```
|
||||
|
||||
## Deploy model
|
||||
|
||||
NAS is a deploy target and runtime state holder. It is not the source of truth for code.
|
||||
|
||||
Normal direction:
|
||||
|
||||
```text
|
||||
Git repo -> build image / sync deploy files -> Synology compose recreate selected service
|
||||
```
|
||||
|
||||
Do not edit NAS copies as the long-term fix. If an emergency live edit is made on NAS, port it back into the relevant repo before continuing product work.
|
||||
|
||||
## Platform / Launcher deploy
|
||||
|
||||
From macOS with `/Volumes/docker` mounted:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
|
||||
NAS_ROOT=/Volumes/docker/nodedc-platform \
|
||||
LAUNCHER_REPO=/Users/dcconstructions/Downloads/mnt/data/nodedc_launcher \
|
||||
./infra/synology/deploy-current.sh
|
||||
```
|
||||
|
||||
The script syncs platform deploy files and prints the Docker commands that must be run on Synology, because Docker there requires interactive `sudo`.
|
||||
|
||||
Launcher image build is done from the Launcher repo when frontend/backend code changes:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/launcher/source
|
||||
sudo /usr/local/bin/docker build --no-cache -t nodedc/launcher:local .
|
||||
```
|
||||
|
||||
Launcher container recreate:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/platform
|
||||
sudo /usr/local/bin/docker compose \
|
||||
--env-file /volume1/docker/nodedc-platform/platform/.env.synology \
|
||||
-f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \
|
||||
up -d --force-recreate --no-deps launcher
|
||||
```
|
||||
|
||||
## Tasker deploy
|
||||
|
||||
Tasker rebuild script is in the Tasker repo and copied to NAS:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/rebuild-nas-legacy.sh
|
||||
```
|
||||
|
||||
Production-style frontend rebuild on NAS:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 sh rebuild-nas-legacy.sh
|
||||
```
|
||||
|
||||
Admin frontend rebuild:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||
BUILD_BACKEND=0 BUILD_WEB=0 BUILD_ADMIN=1 sh rebuild-nas-legacy.sh
|
||||
```
|
||||
|
||||
Combined web/admin rebuild:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=1 sh rebuild-nas-legacy.sh
|
||||
```
|
||||
|
||||
The script defaults production frontend links to:
|
||||
|
||||
```text
|
||||
VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru
|
||||
```
|
||||
|
||||
Local/staging can override:
|
||||
|
||||
```bash
|
||||
VITE_NODEDC_LAUNCHER_URL=http://launcher.local.nodedc \
|
||||
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 \
|
||||
sh rebuild-nas-legacy.sh
|
||||
```
|
||||
|
||||
Manual recreate command used on NAS:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/tasker/plane-app
|
||||
sudo /usr/local/bin/docker compose -p nodedc-tasker \
|
||||
--env-file .env.synology \
|
||||
-f docker-compose.yaml \
|
||||
-f docker-compose.synology.override.yml \
|
||||
up -d --no-build --force-recreate web
|
||||
```
|
||||
|
||||
Replace `web` with `admin`, `api`, or another service only when that service image was intentionally rebuilt.
|
||||
|
||||
Tasker backend must receive the Gateway internal URL/token before Codex/Ops Agent UI can create agents in production:
|
||||
|
||||
```env
|
||||
PLANE_NODEDC_AGENT_GATEWAY_URL=http://127.0.0.1:18190
|
||||
PLANE_NODEDC_AGENT_GATEWAY_TOKEN=<same value as NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN>
|
||||
```
|
||||
|
||||
After changing these env vars, rebuild/recreate Tasker backend services: `api`, `worker`, `beat-worker`, and `migrator` when migrations are expected.
|
||||
|
||||
## Ops Agents Gateway deploy
|
||||
|
||||
From the Gateway repo on Synology or from a synced source copy:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/ops-agents
|
||||
sudo /usr/local/bin/docker compose --env-file .env -f docker-compose.synology.yml up -d --build
|
||||
```
|
||||
|
||||
Required Synology `.env` values are documented in:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/NODEDC_TASKMANAGER_CODEXAPI/docs/SYNOLOGY_DEPLOY.md
|
||||
```
|
||||
|
||||
Minimum checks:
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:18190/healthz
|
||||
curl -fsS http://127.0.0.1:18190/readyz
|
||||
curl -fsS -i https://ops-agents.nodedc.ru/mcp | head
|
||||
```
|
||||
|
||||
## Backup rules
|
||||
|
||||
Platform backup script:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
|
||||
NAS_ROOT=/Volumes/docker/nodedc-platform ./infra/synology/backup-current.sh
|
||||
```
|
||||
|
||||
It creates filesystem backups and generates a Synology-side Authentik database dump command.
|
||||
|
||||
Tasker safety:
|
||||
|
||||
- Frontend-only Tasker rebuilds do not touch Tasker PostgreSQL or MinIO uploads.
|
||||
- Backend image rebuild is not enough to lose data by itself, but backend migrations can change DB schema.
|
||||
- Before backend migrations, backup `nodedc-tasker_pgdata` and `nodedc-tasker_uploads`.
|
||||
- Never run destructive volume commands on NAS production data.
|
||||
|
||||
Hard rules:
|
||||
|
||||
```text
|
||||
Never run docker compose down -v on production/staging data.
|
||||
Never delete nodedc-tasker_* volumes without a verified backup.
|
||||
Never use docker system prune blindly on the NAS.
|
||||
Never commit .env.synology or real secrets.
|
||||
```
|
||||
|
||||
## Local development model
|
||||
|
||||
The same codebase must support local and production-like runs. Differences must come from env vars and build args, not branches with hardcoded production URLs.
|
||||
|
||||
Local target domains:
|
||||
|
||||
```text
|
||||
127.0.0.1 auth.local.nodedc
|
||||
127.0.0.1 launcher.local.nodedc
|
||||
127.0.0.1 task.local.nodedc
|
||||
```
|
||||
|
||||
Platform local docs:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/NODEDC/platform/docs/DEPLOYMENT_LOCAL.md
|
||||
/Users/dcconstructions/Downloads/mnt/NODEDC/platform/infra/README.md
|
||||
```
|
||||
|
||||
Tasker local/prod integration env contract:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/docs_prod/NODEDC_TASKER_DEPLOY_MODEL.md
|
||||
```
|
||||
|
||||
Useful local Tasker frontend build checks:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src
|
||||
pnpm turbo run build --filter=web
|
||||
pnpm turbo run build --filter=admin
|
||||
```
|
||||
|
||||
Known current validation note: `admin` typecheck passes. Full `web` typecheck currently has pre-existing upstream/local Plane TypeScript debt unrelated to the NODE.DC logo/deploy baseline work.
|
||||
|
||||
## Current docs map
|
||||
|
||||
Read these in order:
|
||||
|
||||
1. This file: `/Users/dcconstructions/Downloads/mnt/NODEDC/platform/docs/CURRENT_INFRA_HANDOFF.md`
|
||||
2. Architecture: `/Users/dcconstructions/Downloads/mnt/NODEDC/platform/docs/ARCHITECTURE.md`
|
||||
3. Synology deploy: `/Users/dcconstructions/Downloads/mnt/NODEDC/platform/infra/synology/README.md`
|
||||
4. Local deploy: `/Users/dcconstructions/Downloads/mnt/NODEDC/platform/docs/DEPLOYMENT_LOCAL.md`
|
||||
5. Tasker deploy model: `/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/docs_prod/NODEDC_TASKER_DEPLOY_MODEL.md`
|
||||
6. Ops Agents Gateway deploy: `/Users/dcconstructions/Downloads/mnt/data/NODEDC_TASKMANAGER_CODEXAPI/docs/SYNOLOGY_DEPLOY.md`
|
||||
|
||||
## New-chat bootstrap prompt
|
||||
|
||||
Use this when starting a new engineering chat:
|
||||
|
||||
```text
|
||||
We are working on NODE.DC. First read:
|
||||
/Users/dcconstructions/Downloads/mnt/NODEDC/platform/docs/CURRENT_INFRA_HANDOFF.md
|
||||
|
||||
Respect the modular architecture:
|
||||
- Platform repo owns infra/Synology/Auth/reverse-proxy docs and scripts.
|
||||
- Launcher repo owns Hub/control-plane.
|
||||
- Tasker repo owns Plane fork and must remain standalone-capable.
|
||||
- Ops Agents Gateway repo owns MCP/API routing for external operational agents.
|
||||
- NAS is deploy target/runtime state, not source of truth.
|
||||
- Do not touch production volumes destructively.
|
||||
- Do not commit secrets or .env.synology.
|
||||
```
|
||||
|
||||
## Known follow-up hardening
|
||||
|
||||
- Add a sanitized committed Tasker `docker-compose.synology.override.example.yml`.
|
||||
- Add a Tasker volume backup script equivalent to Platform `backup-current.sh`.
|
||||
- Add an Ops Agents Gateway backup script for its Postgres volume/dump.
|
||||
- Add a single smoke-check script for public domains: `id.nodedc.ru`, `hub.nodedc.ru`, `ops.nodedc.ru`, `ops-agents.nodedc.ru`.
|
||||
- Decide whether release tags should be coordinated across the three repos after stable production milestones.
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
# DevOps Security Handoff
|
||||
|
||||
Актуализировано: 2026-05-12.
|
||||
|
||||
Документ фиксирует границу передачи NODE.DC platform DevOps-инженеру. Локальная разработка закрывает код, конфиги, preflight и smoke-сценарии, которые можно честно проверить без реального staging host. DevOps закрывает серверные домены, TLS, secrets, firewall и финальную staging acceptance.
|
||||
|
||||
## Что уже подготовлено локально
|
||||
|
||||
### Platform / Authentik / reverse proxy
|
||||
|
||||
Репозиторий:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/NODEDC/platform
|
||||
```
|
||||
|
||||
Готовые артефакты:
|
||||
|
||||
```text
|
||||
docs/STAGING_SECURITY_PLAN.md
|
||||
docs/SECURITY_CHECKLIST.md
|
||||
infra/docker-compose.staging.example.yml
|
||||
infra/reverse-proxy/Caddyfile.staging
|
||||
infra/.env.staging.example
|
||||
infra/scripts/check-staging-env.sh
|
||||
```
|
||||
|
||||
Локально проверено:
|
||||
|
||||
```bash
|
||||
cd platform/infra
|
||||
NODEDC_STAGING_ENV_FILE=.env.staging.example docker compose --env-file .env.staging.example -f docker-compose.staging.example.yml config
|
||||
docker run --rm --env-file .env.staging.example -v "$PWD/reverse-proxy/Caddyfile.staging:/etc/caddy/Caddyfile:ro" caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile
|
||||
./scripts/check-staging-env.sh .env.staging.example
|
||||
```
|
||||
|
||||
Ожидаемый результат: compose и Caddyfile валидны; preflight на `.env.staging.example` падает, потому что example содержит placeholder secrets.
|
||||
|
||||
### Launcher / control plane
|
||||
|
||||
Репозиторий:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/nodedc_launcher
|
||||
```
|
||||
|
||||
Подготовлено:
|
||||
|
||||
- control-plane snapshot перенесён из public static в server-only storage;
|
||||
- `/storage/launcher-data.json` закрыт;
|
||||
- `/api/storage/data` и `/api/storage/upload` требуют session;
|
||||
- `/api/apps` отдаёт каталог сервисов с флагами доступа; карточки сервисов видны всем authenticated users, но launch разрешён только при app access;
|
||||
- hard delete вызывает Tasker cleanup: sessions, identity links, workspace/project memberships, issue assignees;
|
||||
- internal API token отделён от OIDC client secret;
|
||||
- повторный accept уже принятого invite отклоняется.
|
||||
|
||||
Локально проверено:
|
||||
|
||||
```bash
|
||||
cd nodedc_launcher
|
||||
node --check server/dev-server.mjs
|
||||
npm run build
|
||||
npm test
|
||||
curl -i http://launcher.local.nodedc/api/me
|
||||
curl -i http://launcher.local.nodedc/api/apps
|
||||
curl -i http://launcher.local.nodedc/api/services/task-manager/launch
|
||||
```
|
||||
|
||||
Ожидаемый результат без session: `401`.
|
||||
|
||||
### Task Manager / Operational Core
|
||||
|
||||
Репозиторий:
|
||||
|
||||
```text
|
||||
/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER
|
||||
```
|
||||
|
||||
Готовые артефакты:
|
||||
|
||||
```text
|
||||
plane-app/plane.env.staging.example
|
||||
scripts/check-tasker-staging-env.sh
|
||||
```
|
||||
|
||||
Подготовлено:
|
||||
|
||||
- `PLANE_NODEDC_ACCESS_TOKEN` больше не fallback-ится на `PLANE_OIDC_CLIENT_SECRET`;
|
||||
- unlinked old sessions могут отзываться через `PLANE_NODEDC_ACCESS_ENFORCE_UNLINKED=1`;
|
||||
- internal logout умеет чистить ExternalIdentityLink, WorkspaceMember, ProjectMember, IssueAssignee;
|
||||
- self-host workspace invite снова создаёт pending request в Launcher;
|
||||
- launcher-managed workspace отклоняет self-service invite request;
|
||||
- Tasker proxy получает `TRUSTED_PROXIES` из env.
|
||||
|
||||
Локально проверено:
|
||||
|
||||
```bash
|
||||
cd NODEDC_TASKMANAGER
|
||||
docker compose --env-file plane-app/plane.env.staging.example -f plane-app/docker-compose.yaml config
|
||||
./scripts/check-tasker-staging-env.sh plane-app/plane.env.staging.example
|
||||
```
|
||||
|
||||
Ожидаемый результат: compose валиден; preflight на example падает из-за placeholder secrets.
|
||||
|
||||
## Что DevOps должен сделать на сервере
|
||||
|
||||
## Последний локальный smoke перед передачей
|
||||
|
||||
Выполнено 2026-05-12 после rebuild `nodedc/plane-backend:local` и `nodedc/plane-frontend:ru`, затем `./setup.sh stop && ./setup.sh start`.
|
||||
|
||||
Контейнеры Tasker:
|
||||
|
||||
```text
|
||||
admin, api, beat-worker, live, plane-db, plane-minio, plane-mq, plane-redis, proxy, space, web, worker — Up
|
||||
web/admin/space — healthy
|
||||
```
|
||||
|
||||
HTTP smoke:
|
||||
|
||||
```text
|
||||
http://auth.local.nodedc/ -> 302
|
||||
http://launcher.local.nodedc/healthz -> 200
|
||||
http://task.local.nodedc/ -> 200
|
||||
http://localhost:8090/ -> 200
|
||||
```
|
||||
|
||||
Unauth negative paths:
|
||||
|
||||
```text
|
||||
GET http://launcher.local.nodedc/api/me -> 401
|
||||
GET http://launcher.local.nodedc/api/apps -> 401
|
||||
GET http://launcher.local.nodedc/api/services/task-manager/launch -> 401
|
||||
POST http://launcher.local.nodedc/api/internal/access/check without token -> 401
|
||||
POST http://task.local.nodedc/api/internal/nodedc/logout/ without token -> 401
|
||||
```
|
||||
|
||||
Этот smoke закрывает локальную runtime-ready часть. Он не заменяет DevOps staging smoke на реальных HTTPS-доменах.
|
||||
|
||||
### 1. Подготовить DNS и host
|
||||
|
||||
Выбрать реальные домены:
|
||||
|
||||
```text
|
||||
auth.<staging-domain>
|
||||
launcher.<staging-domain>
|
||||
task.<staging-domain>
|
||||
```
|
||||
|
||||
Все DNS-записи должны указывать на staging host или ingress. Порты `80/tcp` и `443/tcp` должны быть доступны снаружи для TLS/ACME.
|
||||
|
||||
### 2. Создать реальные env-файлы
|
||||
|
||||
Platform:
|
||||
|
||||
```bash
|
||||
cd platform/infra
|
||||
cp .env.staging.example .env.staging
|
||||
```
|
||||
|
||||
Tasker:
|
||||
|
||||
```bash
|
||||
cd NODEDC_TASKMANAGER
|
||||
cp plane-app/plane.env.staging.example plane-app/plane.env.staging
|
||||
```
|
||||
|
||||
Заменить все `replace-with-*` на реальные значения. Нельзя использовать local/dev secrets, `change-me`, `local-dev`, `.local.nodedc`, `localhost`.
|
||||
|
||||
Обязательное правило:
|
||||
|
||||
- `NODEDC_INTERNAL_ACCESS_TOKEN` должен совпадать с `PLANE_NODEDC_ACCESS_TOKEN`;
|
||||
- `NODEDC_INTERNAL_ACCESS_TOKEN` не должен совпадать ни с одним OIDC client secret;
|
||||
- `LAUNCHER_OIDC_CLIENT_SECRET` и `PLANE_OIDC_CLIENT_SECRET` должны быть разными;
|
||||
- `COOKIE_SECURE=true`;
|
||||
- все public/OIDC/internal URLs должны быть `https://`.
|
||||
|
||||
### 3. Ограничить proxy trust
|
||||
|
||||
В staging нельзя оставлять broad ranges:
|
||||
|
||||
```text
|
||||
0.0.0.0/0
|
||||
::/0
|
||||
10.0.0.0/8
|
||||
172.16.0.0/12
|
||||
192.168.0.0/16
|
||||
127.0.0.0/8
|
||||
```
|
||||
|
||||
Нужно указать только фактический subnet reverse proxy / ingress:
|
||||
|
||||
```text
|
||||
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS=<edge-or-ingress-cidr>
|
||||
TRUSTED_PROXIES=<edge-or-ingress-cidr>
|
||||
```
|
||||
|
||||
### 4. Прогнать preflight
|
||||
|
||||
Platform:
|
||||
|
||||
```bash
|
||||
cd platform/infra
|
||||
./scripts/check-staging-env.sh .env.staging
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.example.yml config
|
||||
```
|
||||
|
||||
Tasker:
|
||||
|
||||
```bash
|
||||
cd NODEDC_TASKMANAGER
|
||||
./scripts/check-tasker-staging-env.sh plane-app/plane.env.staging
|
||||
docker compose --env-file plane-app/plane.env.staging -f plane-app/docker-compose.yaml config
|
||||
```
|
||||
|
||||
Preflight обязан пройти на реальных env-файлах. Если падает — staging не запускать.
|
||||
|
||||
### 5. Поднять runtime
|
||||
|
||||
Platform:
|
||||
|
||||
```bash
|
||||
cd platform/infra
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.example.yml up -d
|
||||
```
|
||||
|
||||
Tasker команда зависит от выбранной topology. Если используется текущий Plane runtime:
|
||||
|
||||
```bash
|
||||
cd NODEDC_TASKMANAGER
|
||||
docker compose --env-file plane-app/plane.env.staging -f plane-app/docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
Если Tasker стоит за platform reverse proxy, наружу публикуется только platform edge. Postgres, Redis/Valkey, MinIO, RabbitMQ, Authentik server/worker, Launcher BFF и Tasker API не публикуются напрямую наружу.
|
||||
|
||||
### 6. Bootstrap Authentik
|
||||
|
||||
Создать/обновить:
|
||||
|
||||
- отдельное Application/Provider для Launcher;
|
||||
- отдельное Application/Provider для Tasker;
|
||||
- отдельные OIDC client secrets;
|
||||
- группы `nodedc:superadmin`, `nodedc:launcher:admin`, `nodedc:launcher:user`, `nodedc:taskmanager:admin`, `nodedc:taskmanager:user`;
|
||||
- access policies для каждого приложения;
|
||||
- redirect/logout URI только на staging HTTPS domains.
|
||||
|
||||
### 7. Проверить финальный staging smoke
|
||||
|
||||
Минимальный smoke:
|
||||
|
||||
1. `https://auth...`, `https://launcher...`, `https://task...` открываются по HTTPS.
|
||||
2. HTTP делает redirect на HTTPS.
|
||||
3. Ответы содержат HSTS.
|
||||
4. Cookies выставляются как `Secure` и `HttpOnly`.
|
||||
5. Без login Launcher ведёт в Authentik.
|
||||
6. Active user видит все карточки сервисов, но открыть может только разрешённые сервисы.
|
||||
7. User без Task Manager app access видит карточку Task Manager в Launcher, не может открыть сервис и получает deny по прямому `https://task...`.
|
||||
8. Blocked/annulled user теряет Launcher и Tasker session после hard refresh.
|
||||
9. Self-host workspace invite создаёт pending request в Launcher.
|
||||
10. Launcher-managed workspace не принимает self-service invite request из Tasker.
|
||||
11. Hard delete удаляет active Tasker WorkspaceMember/ProjectMember/IssueAssignee.
|
||||
12. Повторный accept уже принятого invite отклоняется.
|
||||
13. Internal endpoints без token дают `401`.
|
||||
14. Audit содержит admin actions: approve/reject/access change/hard delete.
|
||||
15. Снаружи не доступны Postgres, Redis/Valkey, MinIO, RabbitMQ, Authentik server/worker, Launcher BFF и Tasker API-порты.
|
||||
|
||||
## Что не входит в текущую передачу
|
||||
|
||||
- billing;
|
||||
- тарифы;
|
||||
- email automation;
|
||||
- production HA/backup automation;
|
||||
- production monitoring/SIEM;
|
||||
- public self-service без ручного approve.
|
||||
|
||||
Эти темы не являются блокерами текущего закрытого demo release.
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
# Security Checklist
|
||||
|
||||
Staging path and runbook: `docs/STAGING_SECURITY_PLAN.md`.
|
||||
DevOps handoff: `docs/DEVOPS_SECURITY_HANDOFF.md`.
|
||||
Service catalog UX rules: `docs/SERVICE_CATALOG_UX_RULES.md`.
|
||||
|
||||
## Network
|
||||
|
||||
- [x] Local dev compose публикует только reverse proxy port.
|
||||
|
|
@ -26,6 +30,8 @@
|
|||
- [ ] Admin endpoints требуют `nodedc:superadmin` или `nodedc:launcher:admin`.
|
||||
- [ ] Все admin actions пишутся в audit log.
|
||||
- [ ] Удаление пользователя реализовано как deactivate/disable, не hard delete.
|
||||
- [x] Local runtime не отдает `storage/launcher-data.json` напрямую через public static route.
|
||||
- [x] Local runtime требует user session для `/api/storage/data`.
|
||||
|
||||
## Plane
|
||||
|
||||
|
|
@ -43,13 +49,20 @@
|
|||
- [ ] Access/refresh tokens не логируются.
|
||||
- [ ] Session cookies имеют `secure=true` в staging/production.
|
||||
- [ ] В production включены HTTPS и HSTS.
|
||||
- [x] Local internal API token отделен от `PLANE_OIDC_CLIENT_SECRET`.
|
||||
- [x] Launcher internal access API отклоняет запросы без token, с неверным token и со старым OIDC client secret.
|
||||
- [x] Tasker internal logout API отклоняет запросы без token и со старым OIDC client secret.
|
||||
|
||||
## Acceptance scenarios
|
||||
|
||||
- [ ] Без логина Launcher отправляет в Authentik.
|
||||
- [ ] Пользователь без `nodedc:taskmanager:access` не видит Task Manager в Launcher.
|
||||
- [ ] Пользователь без `nodedc:taskmanager:access` получает deny на прямой `task.local.nodedc`.
|
||||
- [x] Без логина Launcher отправляет в Authentik.
|
||||
- [ ] Пользователь без Task Manager app access видит карточку Task Manager в Launcher, но не может открыть сервис.
|
||||
- [x] Пользователь без Task Manager app access получает deny в Tasker access middleware при прямом доступе.
|
||||
- [ ] Пользователь с доступом открывает Task Manager.
|
||||
- [ ] Старый Plane admin после OIDC видит старые workspace/tasks/comments.
|
||||
- [ ] Деактивированный пользователь теряет доступ.
|
||||
- [ ] Admin action появляется в audit log.
|
||||
- [x] Hard-deleted/annulled user теряет stale Tasker identity link и старая unlinked-сессия отзывается middleware.
|
||||
- [x] Hard-deleted/annulled user удаляется из active Tasker workspace/project members и issue assignees.
|
||||
- [x] Self-host Tasker workspace invite создает pending request в Launcher, а launcher-managed workspace не принимает self-service invite request.
|
||||
- [x] Admin action появляется в audit log.
|
||||
- [x] Повторный accept уже принятого invite отклоняется.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# Service Catalog UX Rules
|
||||
|
||||
Актуализировано: 2026-05-12.
|
||||
|
||||
Этот документ фиксирует обязательное правило Launcher-витрины NODE.DC.
|
||||
|
||||
## Главное правило
|
||||
|
||||
Все authenticated users видят карточки всех сервисов, добавленных в каталог Launcher, независимо от текущего app access.
|
||||
|
||||
Видимость карточки сервиса не означает право запуска сервиса.
|
||||
|
||||
## Зачем это нужно
|
||||
|
||||
Launcher — это не только access gate, но и витрина платформы. Пользователь должен видеть, какие приложения существуют в NODE.DC, даже если доступ к конкретному приложению ещё не выдан.
|
||||
|
||||
Это нужно для:
|
||||
|
||||
- демонстрации состава платформы;
|
||||
- ручного запроса доступа;
|
||||
- будущих тарифов и paid access;
|
||||
- понимания, какие модули доступны в экосистеме.
|
||||
|
||||
## Как должен работать UI
|
||||
|
||||
Для сервиса без доступа:
|
||||
|
||||
- карточка сервиса остаётся видимой в нижней/сервисной панели;
|
||||
- описание, медиа и карточка доступны для чтения;
|
||||
- статус/chip показывает отсутствие доступа;
|
||||
- кнопка открытия сервиса disabled или ведёт в controlled request/access flow;
|
||||
- прямой запуск через `/api/services/:serviceSlug/launch` обязан возвращать deny.
|
||||
|
||||
Для сервиса с доступом:
|
||||
|
||||
- карточка сервиса видима;
|
||||
- кнопка открытия активна;
|
||||
- launch идёт только через Launcher BFF, а не прямым trust из UI.
|
||||
|
||||
## Realtime правило
|
||||
|
||||
Изменение доступа должно обновлять состояние карточки без hard refresh:
|
||||
|
||||
- выдали доступ — кнопка открытия становится активной;
|
||||
- сняли доступ — карточка остаётся видимой, но кнопка открытия выключается;
|
||||
- direct launch после снятия доступа остаётся запрещённым server-side.
|
||||
|
||||
Нельзя чинить безопасность через скрытие карточек. Безопасность обеспечивается server-side launch deny, Tasker middleware и internal access enforcement.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
- Фильтровать `/api/apps` только до доступных пользователю приложений, если это ломает витрину.
|
||||
- Убирать карточку сервиса из UI только потому, что у пользователя нет app access.
|
||||
- Считать скрытие карточки security control.
|
||||
- Ломать realtime-переключение кнопки `Открыть` при approve/revoke доступа.
|
||||
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
# Staging Security Plan
|
||||
|
||||
Актуализировано: 2026-05-12.
|
||||
|
||||
Этот документ фиксирует минимальный staging path для закрытого demo release NODE.DC platform. Billing, тарифы и email automation не входят в текущий scope: доступы, approve и передача ссылок остаются ручными через Launcher/root-admin.
|
||||
|
||||
## Целевые домены
|
||||
|
||||
Для staging нужны отдельные домены от локального `.local.nodedc`:
|
||||
|
||||
```text
|
||||
auth.staging.nodedc.example -> Authentik
|
||||
launcher.staging.nodedc.example -> Launcher BFF/web
|
||||
task.staging.nodedc.example -> Task Manager / Operational Core
|
||||
```
|
||||
|
||||
Финальные DNS-имена заменяются перед deploy. Все redirect URI в Authentik должны совпадать с этими доменами и использовать `https://`.
|
||||
|
||||
## Публичная поверхность
|
||||
|
||||
Снаружи публикуется только edge reverse proxy:
|
||||
|
||||
- `80/tcp` — только redirect на HTTPS;
|
||||
- `443/tcp` — HTTPS termination, HSTS, proxy headers;
|
||||
- Postgres, Redis/Valkey, MinIO, Authentik server/worker, Launcher BFF и Tasker backend не публикуются напрямую наружу.
|
||||
|
||||
Reverse proxy обязан прокидывать:
|
||||
|
||||
- `Host`;
|
||||
- `X-Forwarded-Proto`;
|
||||
- `X-Forwarded-For`;
|
||||
- WebSocket/HTTP upgrade для Tasker realtime/live.
|
||||
|
||||
## Runtime topology
|
||||
|
||||
Staging baseline добавлен как пример:
|
||||
|
||||
```text
|
||||
platform/infra/docker-compose.staging.example.yml
|
||||
platform/infra/reverse-proxy/Caddyfile.staging
|
||||
platform/infra/.env.staging.example
|
||||
NODEDC_TASKMANAGER/plane-app/plane.env.staging.example
|
||||
```
|
||||
|
||||
Запускать его нужно только после копирования `.env.staging.example` в ignored `.env.staging` и замены всех placeholder secrets:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform/infra
|
||||
./scripts/check-staging-env.sh .env.staging
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.example.yml config
|
||||
docker compose --env-file .env.staging -f docker-compose.staging.example.yml up -d
|
||||
```
|
||||
|
||||
Для Tasker staging env:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER
|
||||
./scripts/check-tasker-staging-env.sh plane-app/plane.env.staging
|
||||
docker compose --env-file plane-app/plane.env.staging -f plane-app/docker-compose.yaml config
|
||||
```
|
||||
|
||||
Можно проверить example compose без реальных secret так:
|
||||
|
||||
```bash
|
||||
NODEDC_STAGING_ENV_FILE=.env.staging.example \
|
||||
docker compose --env-file .env.staging.example -f docker-compose.staging.example.yml config
|
||||
```
|
||||
|
||||
Runtime правила:
|
||||
|
||||
- Authentik `server` и `worker` используют отдельный Postgres volume/network;
|
||||
- Authentik worker не монтирует `/var/run/docker.sock`;
|
||||
- Launcher хранит control-plane snapshot только в server-side storage;
|
||||
- Tasker остаётся отдельным приложением со своей БД, storage и доменной моделью;
|
||||
- Launcher не читает и не пишет Plane DB напрямую;
|
||||
- интеграция Launcher -> Tasker идёт через internal API с отдельным `NODEDC_INTERNAL_ACCESS_TOKEN`.
|
||||
|
||||
## Обязательные env
|
||||
|
||||
Значения с `change-me`, local token и dev secrets запрещены в staging.
|
||||
|
||||
```text
|
||||
COOKIE_SECURE=true
|
||||
COOKIE_DOMAIN=.staging.nodedc.example
|
||||
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN=<random-32-bytes-min>
|
||||
SESSION_SECRET=<random-32-bytes-min>
|
||||
AUTHENTIK_SECRET_KEY=<random-32-bytes-min>
|
||||
PG_PASS=<random-32-bytes-min>
|
||||
|
||||
LAUNCHER_OIDC_ISSUER=https://auth.staging.nodedc.example/application/o/launcher/
|
||||
LAUNCHER_OIDC_REDIRECT_URI=https://launcher.staging.nodedc.example/auth/callback
|
||||
LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI=https://launcher.staging.nodedc.example/auth/logged-out
|
||||
|
||||
PLANE_OIDC_ISSUER=https://auth.staging.nodedc.example/application/o/task-manager/
|
||||
PLANE_OIDC_REDIRECT_URI=https://task.staging.nodedc.example/auth/oidc/callback
|
||||
PLANE_NODEDC_ACCESS_ENFORCEMENT=1
|
||||
PLANE_NODEDC_ACCESS_ENFORCE_UNLINKED=1
|
||||
PLANE_NODEDC_ACCESS_TOKEN=<same-as-NODEDC_INTERNAL_ACCESS_TOKEN>
|
||||
```
|
||||
|
||||
OIDC client secrets для Launcher и Tasker должны быть разными. Internal API token не должен совпадать ни с одним OIDC client secret.
|
||||
|
||||
## Trusted proxies
|
||||
|
||||
Локальный default `127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128` допустим только для dev.
|
||||
|
||||
В staging `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS` и аналогичные параметры Tasker должны указывать только на фактический reverse-proxy subnet или ingress CIDR. Широкие private ranges без необходимости запрещены.
|
||||
|
||||
## Cookies и HTTPS
|
||||
|
||||
Для staging:
|
||||
|
||||
- TLS обязателен на всех публичных доменах;
|
||||
- HTTP должен делать permanent redirect на HTTPS;
|
||||
- `COOKIE_SECURE=true`;
|
||||
- session cookies остаются `HttpOnly`;
|
||||
- `SameSite=Lax` допустим для текущего top-level OIDC flow;
|
||||
- включить HSTS после проверки сертификатов и redirect URI.
|
||||
|
||||
## Backup перед staging
|
||||
|
||||
Перед первым staging deploy и перед destructive security smoke:
|
||||
|
||||
- dump Authentik Postgres;
|
||||
- dump Tasker/Plane Postgres;
|
||||
- архив Launcher `server/storage`;
|
||||
- архив Tasker uploads/MinIO data;
|
||||
- архив `.env`/secret references без публикации в git;
|
||||
- зафиксировать image tags Launcher/Tasker/Authentik.
|
||||
|
||||
Минимальный restore check: поднять копию DB/storage на isolated host или namespace и проверить login + открытие Tasker.
|
||||
|
||||
## Secrets rotation
|
||||
|
||||
Rotation policy до допуска внешних пользователей:
|
||||
|
||||
- `NODEDC_INTERNAL_ACCESS_TOKEN` — rotate при любом подозрении на утечку и перед production;
|
||||
- OIDC client secrets — rotate отдельно для Launcher и Tasker;
|
||||
- `SESSION_SECRET` — rotate с принудительным logout всех пользователей;
|
||||
- DB passwords — rotate через maintenance window;
|
||||
- старые secrets удаляются из `.env`, shell history и runtime logs.
|
||||
|
||||
## Acceptance перед людьми
|
||||
|
||||
Минимальный staging smoke:
|
||||
|
||||
1. Super sudo входит в Launcher через `https://launcher...`.
|
||||
2. Обычный active user видит все карточки сервисов, но открыть может только разрешённые сервисы.
|
||||
3. User без Task Manager app access видит карточку Task Manager в витрине, не может открыть сервис и получает deny по прямому `https://task...`.
|
||||
4. Blocked/annulled user теряет Launcher и Tasker session после hard refresh.
|
||||
5. Self-host workspace invite создаёт pending request в Launcher.
|
||||
6. Launcher-managed workspace не принимает self-service invite request из Tasker.
|
||||
7. Hard delete удаляет active WorkspaceMember/ProjectMember/IssueAssignee в Tasker.
|
||||
8. Audit сохраняет admin actions, approve/reject и hard delete.
|
||||
9. Повторный accept уже принятого invite отклоняется.
|
||||
10. Прямые backend/internal endpoints без token возвращают `401`.
|
||||
|
|
@ -42,5 +42,6 @@ PLANE_OIDC_REDIRECT_URI=http://task.local.nodedc/auth/oidc/callback
|
|||
|
||||
# security
|
||||
SESSION_SECRET=change-me-generate-with-infra-scripts-init-dev-env
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN=change-me-generate-with-infra-scripts-init-dev-env
|
||||
COOKIE_DOMAIN=.local.nodedc
|
||||
COOKIE_SECURE=false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# staging domains
|
||||
AUTH_DOMAIN=auth.staging.nodedc.example
|
||||
LAUNCHER_DOMAIN=launcher.staging.nodedc.example
|
||||
TASK_DOMAIN=task.staging.nodedc.example
|
||||
|
||||
# edge proxy
|
||||
ACME_EMAIL=admin@nodedc.example
|
||||
PLATFORM_HTTP_PORT=80
|
||||
PLATFORM_HTTPS_PORT=443
|
||||
PLATFORM_PROXY_IMAGE=caddy:2-alpine
|
||||
STAGING_LAUNCHER_UPSTREAM=launcher:5173
|
||||
STAGING_TASK_MANAGER_UPSTREAM=task-manager-proxy:80
|
||||
|
||||
# authentik image
|
||||
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
|
||||
AUTHENTIK_TAG=2026.2.2
|
||||
|
||||
# authentik database
|
||||
PG_DB=authentik
|
||||
PG_USER=authentik
|
||||
PG_PASS=replace-with-random-staging-secret
|
||||
|
||||
# authentik
|
||||
AUTHENTIK_SECRET_KEY=replace-with-random-staging-secret
|
||||
AUTHENTIK_ERROR_REPORTING__ENABLED=false
|
||||
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS=replace-with-reverse-proxy-subnet
|
||||
|
||||
# launcher oidc
|
||||
LAUNCHER_OIDC_ISSUER=https://auth.staging.nodedc.example/application/o/launcher/
|
||||
LAUNCHER_OIDC_CLIENT_ID=nodedc-launcher
|
||||
LAUNCHER_OIDC_CLIENT_SECRET=replace-with-random-staging-secret
|
||||
LAUNCHER_OIDC_REDIRECT_URI=https://launcher.staging.nodedc.example/auth/callback
|
||||
LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI=https://launcher.staging.nodedc.example/auth/logged-out
|
||||
|
||||
# plane oidc
|
||||
PLANE_OIDC_ISSUER=https://auth.staging.nodedc.example/application/o/task-manager/
|
||||
PLANE_OIDC_CLIENT_ID=nodedc-task-manager
|
||||
PLANE_OIDC_CLIENT_SECRET=replace-with-random-staging-secret
|
||||
PLANE_OIDC_REDIRECT_URI=https://task.staging.nodedc.example/auth/oidc/callback
|
||||
|
||||
# security
|
||||
SESSION_SECRET=replace-with-random-staging-secret
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN=replace-with-random-staging-secret
|
||||
COOKIE_DOMAIN=.staging.nodedc.example
|
||||
COOKIE_SECURE=true
|
||||
|
||||
# tasker downstream security
|
||||
PLANE_NODEDC_ACCESS_ENFORCEMENT=1
|
||||
PLANE_NODEDC_ACCESS_ENFORCE_UNLINKED=1
|
||||
PLANE_NODEDC_ACCESS_TOKEN=replace-with-same-value-as-NODEDC_INTERNAL_ACCESS_TOKEN
|
||||
|
|
@ -41,7 +41,9 @@ APP_SPECS = [
|
|||
"redirect_uri_env": "LAUNCHER_OIDC_REDIRECT_URI",
|
||||
"logged_out_redirect_uri_env": "LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI",
|
||||
"default_logged_out_redirect_uri": "http://launcher.local.nodedc/auth/logged-out",
|
||||
"launch_url_env": "LAUNCHER_BASE_URL",
|
||||
"launch_url": "http://launcher.local.nodedc",
|
||||
"logout_uri_env": "LAUNCHER_LOGOUT_URI",
|
||||
"logout_uri": "http://launcher.local.nodedc/logout",
|
||||
"groups": ["nodedc:superadmin", "nodedc:launcher:admin", "nodedc:launcher:user"],
|
||||
"description": "NODE.DC control plane launcher.",
|
||||
|
|
@ -53,7 +55,9 @@ APP_SPECS = [
|
|||
"client_id_env": "PLANE_OIDC_CLIENT_ID",
|
||||
"client_secret_env": "PLANE_OIDC_CLIENT_SECRET",
|
||||
"redirect_uri_env": "PLANE_OIDC_REDIRECT_URI",
|
||||
"launch_url_env": "TASK_BASE_URL",
|
||||
"launch_url": "http://task.local.nodedc",
|
||||
"logout_uri_env": "TASK_LOGOUT_URI",
|
||||
"logout_uri": "http://task.local.nodedc/logout",
|
||||
"groups": ["nodedc:superadmin", "nodedc:taskmanager:admin", "nodedc:taskmanager:user"],
|
||||
"description": "NODE.DC Plane-based task manager.",
|
||||
|
|
@ -260,7 +264,7 @@ def ensure_provider(spec, mappings):
|
|||
RedirectURI(RedirectURIMatchingMode.STRICT, redirect_uri)
|
||||
for redirect_uri in dict.fromkeys(redirect_uri_values)
|
||||
]
|
||||
provider.logout_uri = spec["logout_uri"]
|
||||
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
|
||||
|
|
@ -282,7 +286,7 @@ def ensure_application(spec, provider, groups):
|
|||
application.slug = spec["slug"]
|
||||
application.group = "NODE.DC"
|
||||
application.provider = provider
|
||||
application.meta_launch_url = spec["launch_url"]
|
||||
application.meta_launch_url = optional_env(spec.get("launch_url_env", ""), spec["launch_url"])
|
||||
application.meta_description = spec["description"]
|
||||
application.meta_publisher = "NODE.DC"
|
||||
application.open_in_new_tab = False
|
||||
|
|
|
|||
|
|
@ -266,7 +266,19 @@
|
|||
|
||||
function getLauncherBaseUrl() {
|
||||
const hostname = window.location.hostname;
|
||||
const launcherHostname = hostname.startsWith("auth.") ? `launcher.${hostname.slice(5)}` : "launcher.local.nodedc";
|
||||
const launcherHostnames = {
|
||||
"id.nodedc.ru": "hub.nodedc.ru",
|
||||
"auth.nodedc.ru": "hub.nodedc.ru",
|
||||
"id.notdc.ru": "launcher.notdc.ru",
|
||||
"auth.notdc.ru": "launcher.notdc.ru",
|
||||
};
|
||||
const launcherHostname =
|
||||
launcherHostnames[hostname] ||
|
||||
(hostname.startsWith("auth.")
|
||||
? `launcher.${hostname.slice(5)}`
|
||||
: hostname.startsWith("id.")
|
||||
? `hub.${hostname.slice(3)}`
|
||||
: "hub.nodedc.ru");
|
||||
const port = window.location.port ? `:${window.location.port}` : "";
|
||||
|
||||
return `${window.location.protocol}//${launcherHostname}${port}/`;
|
||||
|
|
@ -691,8 +703,8 @@
|
|||
try {
|
||||
const url = new URL(rawUrl);
|
||||
const allowedHosts = new Set([
|
||||
"launcher.local.nodedc",
|
||||
"launcher.local.notdc",
|
||||
"hub.nodedc.ru",
|
||||
"hub.notdc.ru",
|
||||
"launcher.notdc.ru",
|
||||
"platform.notdc.ru",
|
||||
"notdc.ru",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
name: nodedc-platform-staging
|
||||
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: ${PLATFORM_PROXY_IMAGE:-caddy:2-alpine}
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: ${NODEDC_STAGING_ENV_FILE:-.env.staging}
|
||||
required: true
|
||||
ports:
|
||||
- "${PLATFORM_HTTP_PORT:-80}:80"
|
||||
- "${PLATFORM_HTTPS_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./reverse-proxy/Caddyfile.staging:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
depends_on:
|
||||
authentik-server:
|
||||
condition: service_started
|
||||
networks:
|
||||
- edge
|
||||
- identity
|
||||
|
||||
postgresql-authentik:
|
||||
image: docker.io/library/postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: ${NODEDC_STAGING_ENV_FILE:-.env.staging}
|
||||
required: true
|
||||
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:
|
||||
- identity
|
||||
|
||||
authentik-server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
|
||||
command: server
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: ${NODEDC_STAGING_ENV_FILE:-.env.staging}
|
||||
required: true
|
||||
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}
|
||||
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: ${AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS:?trusted proxy CIDR required}
|
||||
depends_on:
|
||||
postgresql-authentik:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- "9000"
|
||||
- "9443"
|
||||
shm_size: 512mb
|
||||
volumes:
|
||||
- authentik-data:/data
|
||||
- ./authentik/custom-templates:/templates:ro
|
||||
networks:
|
||||
- identity
|
||||
|
||||
authentik-worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
|
||||
command: worker
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: ${NODEDC_STAGING_ENV_FILE:-.env.staging}
|
||||
required: true
|
||||
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
|
||||
networks:
|
||||
- identity
|
||||
|
||||
networks:
|
||||
edge:
|
||||
identity:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
authentik-database:
|
||||
authentik-data:
|
||||
authentik-certs:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
http://{$AUTH_DOMAIN} {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
|
||||
http://{$LAUNCHER_DOMAIN} {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
|
||||
http://{$TASK_DOMAIN} {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
|
||||
{$AUTH_DOMAIN} {
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
}
|
||||
|
||||
@auth_root path /
|
||||
redir @auth_root https://{$LAUNCHER_DOMAIN}/ 302
|
||||
|
||||
@auth_user_dashboard path /if/user /if/user/*
|
||||
redir @auth_user_dashboard https://{$LAUNCHER_DOMAIN}/ 302
|
||||
|
||||
reverse_proxy authentik-server:9000 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
{$LAUNCHER_DOMAIN} {
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
}
|
||||
|
||||
reverse_proxy {$STAGING_LAUNCHER_UPSTREAM} {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
{$TASK_DOMAIN} {
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
}
|
||||
|
||||
reverse_proxy {$STAGING_TASK_MANAGER_UPSTREAM} {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ ensure_env_value LAUNCHER_OIDC_CLIENT_ID nodedc-launcher
|
|||
ensure_env_value PLANE_OIDC_CLIENT_ID nodedc-task-manager
|
||||
ensure_env_value LAUNCHER_OIDC_CLIENT_SECRET "$(rand_hex 48)"
|
||||
ensure_env_value PLANE_OIDC_CLIENT_SECRET "$(rand_hex 48)"
|
||||
ensure_env_value NODEDC_INTERNAL_ACCESS_TOKEN "$(rand_hex 48)"
|
||||
ensure_env_value LAUNCHER_OIDC_REDIRECT_URI http://launcher.local.nodedc/auth/callback
|
||||
ensure_env_value PLANE_OIDC_REDIRECT_URI http://task.local.nodedc/auth/oidc/callback
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="${1:-"$ROOT_DIR/.env.staging"}"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "Missing staging env file: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
failures=0
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
failures=$((failures + 1))
|
||||
}
|
||||
|
||||
require_value() {
|
||||
local name="$1"
|
||||
local value="${!name:-}"
|
||||
|
||||
if [[ -z "$value" ]]; then
|
||||
fail "$name is required"
|
||||
fi
|
||||
}
|
||||
|
||||
require_secret() {
|
||||
local name="$1"
|
||||
local value="${!name:-}"
|
||||
|
||||
require_value "$name"
|
||||
|
||||
if [[ "$value" =~ change-me|local-dev|replace-with|example ]]; then
|
||||
fail "$name uses a placeholder/dev value"
|
||||
fi
|
||||
|
||||
if [[ ${#value} -lt 32 ]]; then
|
||||
fail "$name must be at least 32 characters"
|
||||
fi
|
||||
}
|
||||
|
||||
require_https_url() {
|
||||
local name="$1"
|
||||
local value="${!name:-}"
|
||||
|
||||
require_value "$name"
|
||||
|
||||
if [[ "$value" != https://* ]]; then
|
||||
fail "$name must use https://"
|
||||
fi
|
||||
}
|
||||
|
||||
require_staging_domain() {
|
||||
local name="$1"
|
||||
local value="${!name:-}"
|
||||
|
||||
require_value "$name"
|
||||
|
||||
if [[ "$value" == *.local.nodedc || "$value" == "localhost" || "$value" == 127.* ]]; then
|
||||
fail "$name must not use local/dev domain"
|
||||
fi
|
||||
}
|
||||
|
||||
require_value AUTH_DOMAIN
|
||||
require_value LAUNCHER_DOMAIN
|
||||
require_value TASK_DOMAIN
|
||||
require_value STAGING_LAUNCHER_UPSTREAM
|
||||
require_value STAGING_TASK_MANAGER_UPSTREAM
|
||||
require_value AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS
|
||||
|
||||
require_staging_domain AUTH_DOMAIN
|
||||
require_staging_domain LAUNCHER_DOMAIN
|
||||
require_staging_domain TASK_DOMAIN
|
||||
|
||||
require_https_url LAUNCHER_OIDC_ISSUER
|
||||
require_https_url LAUNCHER_OIDC_REDIRECT_URI
|
||||
require_https_url LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI
|
||||
require_https_url PLANE_OIDC_ISSUER
|
||||
require_https_url PLANE_OIDC_REDIRECT_URI
|
||||
|
||||
require_secret PG_PASS
|
||||
require_secret AUTHENTIK_SECRET_KEY
|
||||
require_secret SESSION_SECRET
|
||||
require_secret NODEDC_INTERNAL_ACCESS_TOKEN
|
||||
require_secret LAUNCHER_OIDC_CLIENT_SECRET
|
||||
require_secret PLANE_OIDC_CLIENT_SECRET
|
||||
|
||||
if [[ "${COOKIE_SECURE:-}" != "true" ]]; then
|
||||
fail "COOKIE_SECURE must be true"
|
||||
fi
|
||||
|
||||
if [[ "${COOKIE_DOMAIN:-}" == *.local.nodedc || "${COOKIE_DOMAIN:-}" == "localhost" ]]; then
|
||||
fail "COOKIE_DOMAIN must not use local/dev domain"
|
||||
fi
|
||||
|
||||
if [[ "${NODEDC_INTERNAL_ACCESS_TOKEN:-}" == "${LAUNCHER_OIDC_CLIENT_SECRET:-}" ]]; then
|
||||
fail "NODEDC_INTERNAL_ACCESS_TOKEN must differ from LAUNCHER_OIDC_CLIENT_SECRET"
|
||||
fi
|
||||
|
||||
if [[ "${NODEDC_INTERNAL_ACCESS_TOKEN:-}" == "${PLANE_OIDC_CLIENT_SECRET:-}" ]]; then
|
||||
fail "NODEDC_INTERNAL_ACCESS_TOKEN must differ from PLANE_OIDC_CLIENT_SECRET"
|
||||
fi
|
||||
|
||||
if [[ "${LAUNCHER_OIDC_CLIENT_SECRET:-}" == "${PLANE_OIDC_CLIENT_SECRET:-}" ]]; then
|
||||
fail "Launcher and Tasker OIDC client secrets must differ"
|
||||
fi
|
||||
|
||||
if [[ "${PLANE_NODEDC_ACCESS_TOKEN:-}" != "${NODEDC_INTERNAL_ACCESS_TOKEN:-}" ]]; then
|
||||
fail "PLANE_NODEDC_ACCESS_TOKEN must match NODEDC_INTERNAL_ACCESS_TOKEN"
|
||||
fi
|
||||
|
||||
if [[ "${PLANE_NODEDC_ACCESS_ENFORCEMENT:-}" != "1" ]]; then
|
||||
fail "PLANE_NODEDC_ACCESS_ENFORCEMENT must be 1"
|
||||
fi
|
||||
|
||||
if [[ "${PLANE_NODEDC_ACCESS_ENFORCE_UNLINKED:-}" != "1" ]]; then
|
||||
fail "PLANE_NODEDC_ACCESS_ENFORCE_UNLINKED must be 1"
|
||||
fi
|
||||
|
||||
case "${AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS:-}" in
|
||||
*"0.0.0.0/0"*|*"::/0"*|*"10.0.0.0/8"*|*"172.16.0.0/12"*|*"192.168.0.0/16"*|*"127.0.0.0/8"*)
|
||||
fail "AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS must be limited to the actual reverse-proxy/ingress subnet"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $failures -gt 0 ]]; then
|
||||
echo "Staging env check failed with $failures issue(s)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Staging env check passed: $ENV_FILE"
|
||||
|
|
@ -56,6 +56,7 @@ PLANE_OIDC_REDIRECT_URI=http://task.local.nodedc/auth/oidc/callback
|
|||
|
||||
# security
|
||||
SESSION_SECRET=$(rand 48)
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN=$(openssl rand -hex 48 | tr -d '\n')
|
||||
COOKIE_DOMAIN=.local.nodedc
|
||||
COOKIE_SECURE=false
|
||||
EOF
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# Parallel Synology HTTP deployment.
|
||||
# This intentionally uses high ports and does not touch existing nodedc-demo.
|
||||
|
||||
AUTH_DOMAIN=auth.nas.nodedc
|
||||
LAUNCHER_DOMAIN=launcher.nas.nodedc
|
||||
TASK_DOMAIN=task.nas.nodedc
|
||||
NODEDC_PUBLIC_HTTP_PORT=18080
|
||||
PLATFORM_HTTP_PORT=18080
|
||||
SYNOLOGY_TASK_MANAGER_UPSTREAM=host.docker.internal:18090
|
||||
|
||||
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
|
||||
|
||||
PG_DB=authentik
|
||||
PG_USER=authentik
|
||||
PG_PASS=replace-with-random-synology-secret
|
||||
AUTHENTIK_SECRET_KEY=replace-with-random-synology-secret
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@nodedc.local
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=replace-with-random-synology-secret
|
||||
AUTHENTIK_BOOTSTRAP_TOKEN=replace-with-random-synology-secret
|
||||
|
||||
LAUNCHER_BASE_URL=https://hub.nodedc.ru
|
||||
TASK_BASE_URL=https://ops.nodedc.ru
|
||||
TASK_LOGOUT_URI=https://ops.nodedc.ru/logout
|
||||
TASK_INTERNAL_LOGOUT_URL=https://ops.nodedc.ru/api/internal/nodedc/logout/
|
||||
|
||||
LAUNCHER_OIDC_ISSUER=https://id.nodedc.ru/application/o/launcher/
|
||||
LAUNCHER_OIDC_CLIENT_ID=nodedc-launcher
|
||||
LAUNCHER_OIDC_CLIENT_SECRET=replace-with-random-synology-secret
|
||||
LAUNCHER_OIDC_REDIRECT_URI=https://hub.nodedc.ru/auth/callback
|
||||
LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI=https://hub.nodedc.ru/auth/logged-out
|
||||
LAUNCHER_LOGOUT_URI=https://hub.nodedc.ru/logout
|
||||
LAUNCHER_COOKIE_DOMAIN=.nodedc.ru
|
||||
|
||||
PLANE_OIDC_ISSUER=https://id.nodedc.ru/application/o/task-manager/
|
||||
PLANE_OIDC_CLIENT_ID=nodedc-task-manager
|
||||
PLANE_OIDC_CLIENT_SECRET=replace-with-random-synology-secret
|
||||
PLANE_OIDC_REDIRECT_URI=https://ops.nodedc.ru/auth/oidc/callback
|
||||
|
||||
NODEDC_AUTHENTIK_BASE_URL=http://authentik-server:9000
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN=replace-with-random-synology-secret
|
||||
SESSION_SECRET=replace-with-random-synology-secret
|
||||
COOKIE_DOMAIN=.nas.nodedc
|
||||
COOKIE_SECURE=false
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
http://{$AUTH_DOMAIN} {
|
||||
@auth_root path /
|
||||
redir @auth_root http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302
|
||||
|
||||
@auth_user_dashboard path /if/user /if/user/*
|
||||
redir @auth_user_dashboard http://{$LAUNCHER_DOMAIN}:{$NODEDC_PUBLIC_HTTP_PORT}/ 302
|
||||
|
||||
reverse_proxy authentik-server:9000 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
http://{$LAUNCHER_DOMAIN} {
|
||||
reverse_proxy launcher:5173 {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
http://{$TASK_DOMAIN} {
|
||||
reverse_proxy {$SYNOLOGY_TASK_MANAGER_UPSTREAM} {
|
||||
header_up Host {http.request.host}
|
||||
header_up X-Forwarded-Host {http.request.host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-Port {$NODEDC_PUBLIC_HTTP_PORT}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
http://id.nodedc.ru {
|
||||
@auth_root path /
|
||||
redir @auth_root https://hub.nodedc.ru/ 302
|
||||
|
||||
@auth_user_dashboard path /if/user /if/user/*
|
||||
redir @auth_user_dashboard https://hub.nodedc.ru/ 302
|
||||
|
||||
reverse_proxy authentik-server:9000 {
|
||||
header_up Host id.nodedc.ru
|
||||
header_up X-Forwarded-Host id.nodedc.ru
|
||||
header_up X-Forwarded-Proto https
|
||||
header_up X-Forwarded-Port 443
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
http://hub.nodedc.ru {
|
||||
reverse_proxy launcher:5173 {
|
||||
header_up Host hub.nodedc.ru
|
||||
header_up X-Forwarded-Host hub.nodedc.ru
|
||||
header_up X-Forwarded-Proto https
|
||||
header_up X-Forwarded-Port 443
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
http://ops.nodedc.ru {
|
||||
reverse_proxy {$SYNOLOGY_TASK_MANAGER_UPSTREAM} {
|
||||
header_up Host ops.nodedc.ru
|
||||
header_up X-Forwarded-Host ops.nodedc.ru
|
||||
header_up X-Forwarded-Proto https
|
||||
header_up X-Forwarded-Port 443
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
# NODE.DC Synology deploy
|
||||
|
||||
Эта папка фиксирует текущий воспроизводимый NAS-deploy для `nodedc-platform` на Synology RS1221RP+.
|
||||
|
||||
## Правила
|
||||
|
||||
- Не выполнять `docker stop`, `docker restart`, `docker compose down`, `docker system prune` для старых проектов.
|
||||
- Новый compose project: `nodedc-platform`.
|
||||
- Новая папка на NAS: `/volume1/docker/nodedc-platform`.
|
||||
- Внутренний HTTP edge использует `18080`, Tasker upstream — `18090`, Ops Agents Gateway upstream — `18190`.
|
||||
- Старые порты `9000` и `5678` заняты старым `nodedc-demo` и не используются.
|
||||
|
||||
## Текущие внешние домены
|
||||
|
||||
```text
|
||||
https://id.nodedc.ru -> Authentik
|
||||
https://hub.nodedc.ru -> Launcher / Hub
|
||||
https://ops.nodedc.ru -> Tasker / Operational Core
|
||||
https://ops-agents.nodedc.ru -> Ops Agents Gateway / MCP
|
||||
```
|
||||
|
||||
В `Caddyfile.http` эти домены проксируются через локальный HTTP edge, но upstream получает `X-Forwarded-Proto: https` и `X-Forwarded-Port: 443`.
|
||||
|
||||
## Локальные домены для первичной проверки
|
||||
|
||||
На Mac для первичной проверки добавить в `/etc/hosts`:
|
||||
|
||||
```text
|
||||
172.22.0.222 auth.nas.nodedc
|
||||
172.22.0.222 launcher.nas.nodedc
|
||||
172.22.0.222 task.nas.nodedc
|
||||
```
|
||||
|
||||
Первичные URL:
|
||||
|
||||
```text
|
||||
http://auth.nas.nodedc:18080
|
||||
http://launcher.nas.nodedc:18080
|
||||
http://task.nas.nodedc:18080
|
||||
http://task.nas.nodedc:18090
|
||||
```
|
||||
|
||||
## Что входит
|
||||
|
||||
- `docker-compose.platform-http.yml` поднимает новый Authentik, Launcher и Caddy edge.
|
||||
- `Caddyfile.http` маршрутизирует локальные `auth/launcher/task.nas.nodedc` и внешние `id/hub/ops.nodedc.ru`.
|
||||
- `deploy-current.sh` синхронизирует compose, Caddyfile, Authentik templates и опционально Launcher source в NAS mount.
|
||||
- `backup-current.sh` делает snapshot Launcher runtime/uploads/Auth templates/config и готовит команду `pg_dump` для Authentik Postgres.
|
||||
- Tasker поднимается отдельным compose из `NODEDC_TASKMANAGER/plane-app/docker-compose.yaml` на порту `18090`.
|
||||
- Ops Agents Gateway поднимается отдельным compose из `NODEDC_TASKMANAGER_CODEXAPI/docker-compose.synology.yml` на `127.0.0.1:18190`; Synology reverse proxy должен вести `ops-agents.nodedc.ru` на этот порт, а не на `18090`.
|
||||
|
||||
## Синхронизация текущего состояния
|
||||
|
||||
С Mac, при смонтированном `/Volumes/docker`:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
|
||||
NAS_ROOT=/Volumes/docker/nodedc-platform \
|
||||
LAUNCHER_REPO=/Users/dcconstructions/Downloads/mnt/data/nodedc_launcher \
|
||||
./infra/synology/deploy-current.sh
|
||||
```
|
||||
|
||||
Скрипт не запускает Docker сам: на NAS `sudo` интерактивный, поэтому команды применения печатаются в конце.
|
||||
|
||||
## Backup текущего состояния
|
||||
|
||||
С Mac, при смонтированном `/Volumes/docker`:
|
||||
|
||||
```bash
|
||||
cd /Users/dcconstructions/Downloads/mnt/NODEDC/platform
|
||||
NAS_ROOT=/Volumes/docker/nodedc-platform ./infra/synology/backup-current.sh
|
||||
```
|
||||
|
||||
Файловый backup создаётся в `/Volumes/docker/nodedc-platform/backups/platform-current-*`.
|
||||
|
||||
Для Authentik Postgres dump нужно выполнить напечатанную команду на Synology, потому что Docker доступен через интерактивный `sudo`:
|
||||
|
||||
```bash
|
||||
bash /volume1/docker/nodedc-platform/backups/platform-current-YYYYMMDD-HHMMSS/run-authentik-db-dump-on-synology.sh
|
||||
```
|
||||
|
||||
## Что нужно перед запуском
|
||||
|
||||
- Собрать или загрузить `linux/amd64` images:
|
||||
- `nodedc/launcher:local`
|
||||
- `nodedc/plane-frontend:ru`
|
||||
- `nodedc/plane-admin:ru`
|
||||
- `nodedc/plane-space:ru`
|
||||
- `nodedc/plane-live:local`
|
||||
- `nodedc/plane-backend:local`
|
||||
- `nodedc/plane-proxy:ru`
|
||||
- Для Ops Agents Gateway отдельный registry image пока не обязателен: deploy из source repo выполняется через `docker compose --env-file .env -f docker-compose.synology.yml up -d --build`.
|
||||
- Создать `.env.synology` из `.env.synology.example` и заменить все `replace-with-*`.
|
||||
- Создать `plane.env.synology` для Tasker из `plane.env.staging.example`, но с HTTP URL на `*.nas.nodedc:18080` и портами `18090/18490`.
|
||||
|
||||
## Обязательные runtime-права
|
||||
|
||||
Launcher пишет runtime snapshot и uploads под пользователем `node` (`uid=1000`). После создания NAS-папок:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/platform
|
||||
sudo mkdir -p ../launcher/server-storage ../launcher/uploads
|
||||
sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads
|
||||
sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads
|
||||
```
|
||||
|
||||
Проверка внутри контейнера:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \
|
||||
'touch /app/server/storage/.write-test && rm /app/server/storage/.write-test && echo storage-ok'
|
||||
```
|
||||
|
||||
## Проверки после деплоя
|
||||
|
||||
```bash
|
||||
curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \
|
||||
| grep -aE 'hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ'
|
||||
```
|
||||
|
||||
В выводе должны быть `id.nodedc.ru -> hub.nodedc.ru` и не должно быть `launcher.local`.
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
NAS_ROOT="${NAS_ROOT:-/Volumes/docker/nodedc-platform}"
|
||||
NAS_PLATFORM_DIR="${NAS_PLATFORM_DIR:-/volume1/docker/nodedc-platform/platform}"
|
||||
BACKUP_ROOT="${BACKUP_ROOT:-${NAS_ROOT}/backups}"
|
||||
TIMESTAMP="${TIMESTAMP:-$(date +%Y%m%d-%H%M%S)}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-${BACKUP_ROOT}/platform-current-${TIMESTAMP}}"
|
||||
DOCKER_BIN="${DOCKER_BIN:-/usr/local/bin/docker}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-${NAS_PLATFORM_DIR}/docker-compose.platform-http.yml}"
|
||||
ENV_FILE="${ENV_FILE:-${NAS_PLATFORM_DIR}/.env.synology}"
|
||||
|
||||
mkdir -p "${BACKUP_DIR}/files/platform" \
|
||||
"${BACKUP_DIR}/files/launcher" \
|
||||
"${BACKUP_DIR}/files/authentik"
|
||||
|
||||
rsync_dir() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
|
||||
if [[ -e "${source}" ]]; then
|
||||
rsync -a --delete "${source}" "${destination}"
|
||||
else
|
||||
echo "skip missing: ${source}" | tee -a "${BACKUP_DIR}/warnings.log" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
rsync_file() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
|
||||
if [[ -f "${source}" ]]; then
|
||||
rsync -a "${source}" "${destination}"
|
||||
else
|
||||
echo "skip missing: ${source}" | tee -a "${BACKUP_DIR}/warnings.log" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
rsync_dir "${NAS_ROOT}/launcher/server-storage/" "${BACKUP_DIR}/files/launcher/server-storage/"
|
||||
rsync_dir "${NAS_ROOT}/launcher/uploads/" "${BACKUP_DIR}/files/launcher/uploads/"
|
||||
rsync_dir "${NAS_ROOT}/authentik/custom-templates/" "${BACKUP_DIR}/files/authentik/custom-templates/"
|
||||
rsync_dir "${NAS_ROOT}/platform/authentik/" "${BACKUP_DIR}/files/platform/authentik/"
|
||||
|
||||
rsync_file "${NAS_ROOT}/platform/.env.synology" "${BACKUP_DIR}/files/platform/"
|
||||
rsync_file "${NAS_ROOT}/platform/.env.synology.example" "${BACKUP_DIR}/files/platform/"
|
||||
rsync_file "${NAS_ROOT}/platform/docker-compose.platform-http.yml" "${BACKUP_DIR}/files/platform/"
|
||||
rsync_file "${NAS_ROOT}/platform/Caddyfile.http" "${BACKUP_DIR}/files/platform/"
|
||||
|
||||
cat > "${BACKUP_DIR}/manifest.txt" <<EOF
|
||||
NODE.DC platform current backup
|
||||
timestamp=${TIMESTAMP}
|
||||
nas_root=${NAS_ROOT}
|
||||
backup_dir=${BACKUP_DIR}
|
||||
|
||||
Contains:
|
||||
- Launcher runtime snapshot: launcher/server-storage
|
||||
- Launcher uploads: launcher/uploads
|
||||
- Authentik custom templates: authentik/custom-templates
|
||||
- Platform runtime config: platform/.env.synology, compose, Caddyfile
|
||||
|
||||
Secrets:
|
||||
- files/platform/.env.synology contains live secrets.
|
||||
- Keep this backup private.
|
||||
EOF
|
||||
|
||||
cat > "${BACKUP_DIR}/run-authentik-db-dump-on-synology.sh" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR="/volume1/docker/nodedc-platform/backups/$(basename "${BACKUP_DIR}")"
|
||||
cd "${NAS_PLATFORM_DIR}"
|
||||
|
||||
sudo "${DOCKER_BIN}" compose \\
|
||||
--env-file "${ENV_FILE}" \\
|
||||
-f "${COMPOSE_FILE}" \\
|
||||
exec -T postgresql-authentik \\
|
||||
sh -lc 'pg_dump -U "\${POSTGRES_USER:-authentik}" -d "\${POSTGRES_DB:-authentik}" --format=custom --no-owner --no-acl' \\
|
||||
> "\${BACKUP_DIR}/authentik-postgres.dump"
|
||||
|
||||
sudo "${DOCKER_BIN}" compose \\
|
||||
--env-file "${ENV_FILE}" \\
|
||||
-f "${COMPOSE_FILE}" \\
|
||||
exec -T postgresql-authentik \\
|
||||
sh -lc 'pg_restore --list /dev/stdin >/dev/null' \\
|
||||
< "\${BACKUP_DIR}/authentik-postgres.dump"
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "\${BACKUP_DIR}" && sha256sum authentik-postgres.dump > SHA256SUMS)
|
||||
else
|
||||
(cd "\${BACKUP_DIR}" && shasum -a 256 authentik-postgres.dump > SHA256SUMS)
|
||||
fi
|
||||
|
||||
echo "authentik-db-dump-ok: \${BACKUP_DIR}/authentik-postgres.dump"
|
||||
EOF
|
||||
chmod +x "${BACKUP_DIR}/run-authentik-db-dump-on-synology.sh"
|
||||
|
||||
find "${BACKUP_DIR}" -name @eaDir -prune -o -type d -exec chmod 700 {} \;
|
||||
find "${BACKUP_DIR}" -name @eaDir -prune -o -type f -exec chmod 600 {} \;
|
||||
chmod 700 "${BACKUP_DIR}/run-authentik-db-dump-on-synology.sh"
|
||||
|
||||
if [[ -x "${DOCKER_BIN}" && "${NAS_ROOT}" == /volume1/* ]]; then
|
||||
"${BACKUP_DIR}/run-authentik-db-dump-on-synology.sh"
|
||||
else
|
||||
cat <<EOF
|
||||
file-backup-ok: ${BACKUP_DIR}
|
||||
|
||||
DB dump was not run from this host.
|
||||
Run on Synology:
|
||||
|
||||
bash /volume1/docker/nodedc-platform/backups/$(basename "${BACKUP_DIR}")/run-authentik-db-dump-on-synology.sh
|
||||
EOF
|
||||
fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLATFORM_REPO="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
||||
NAS_ROOT="${NAS_ROOT:-/Volumes/docker/nodedc-platform}"
|
||||
LAUNCHER_REPO="${LAUNCHER_REPO:-}"
|
||||
|
||||
if [[ ! -d "${NAS_ROOT}" ]]; then
|
||||
echo "NAS_ROOT not found: ${NAS_ROOT}" >&2
|
||||
echo "Set NAS_ROOT=/path/to/nodedc-platform when the Synology share is mounted elsewhere." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${NAS_ROOT}/platform" "${NAS_ROOT}/authentik/custom-templates"
|
||||
|
||||
rsync -av \
|
||||
"${PLATFORM_REPO}/infra/synology/docker-compose.platform-http.yml" \
|
||||
"${NAS_ROOT}/platform/docker-compose.platform-http.yml"
|
||||
|
||||
rsync -av \
|
||||
"${PLATFORM_REPO}/infra/synology/Caddyfile.http" \
|
||||
"${NAS_ROOT}/platform/Caddyfile.http"
|
||||
|
||||
rsync -av \
|
||||
"${PLATFORM_REPO}/infra/synology/deploy-current.sh" \
|
||||
"${PLATFORM_REPO}/infra/synology/backup-current.sh" \
|
||||
"${NAS_ROOT}/platform/"
|
||||
|
||||
rsync -av --delete \
|
||||
"${PLATFORM_REPO}/infra/authentik/custom-templates/" \
|
||||
"${NAS_ROOT}/authentik/custom-templates/"
|
||||
|
||||
if [[ -n "${LAUNCHER_REPO}" ]]; then
|
||||
if [[ ! -d "${LAUNCHER_REPO}" ]]; then
|
||||
echo "LAUNCHER_REPO not found: ${LAUNCHER_REPO}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${NAS_ROOT}/launcher/source"
|
||||
rsync -av --delete \
|
||||
--exclude='.git/' \
|
||||
--exclude='node_modules/' \
|
||||
--exclude='dist/' \
|
||||
--exclude='server/storage/*' \
|
||||
--exclude='public/storage/uploads/*' \
|
||||
"${LAUNCHER_REPO}/" \
|
||||
"${NAS_ROOT}/launcher/source/"
|
||||
else
|
||||
echo "LAUNCHER_REPO is not set; launcher source was not synced."
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
Synced files to NAS mount. Run on Synology to apply runtime changes:
|
||||
|
||||
cd /volume1/docker/nodedc-platform/platform
|
||||
sudo mkdir -p ../launcher/server-storage ../launcher/uploads
|
||||
sudo chown -R 1000:1000 ../launcher/server-storage ../launcher/uploads
|
||||
sudo chmod -R u+rwX,g+rwX ../launcher/server-storage ../launcher/uploads
|
||||
|
||||
cd /volume1/docker/nodedc-platform/launcher/source
|
||||
sudo /usr/local/bin/docker build -t nodedc/launcher:local .
|
||||
|
||||
cd /volume1/docker/nodedc-platform/platform
|
||||
sudo /usr/local/bin/docker compose \
|
||||
--env-file /volume1/docker/nodedc-platform/platform/.env.synology \
|
||||
-f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \
|
||||
up -d --force-recreate launcher reverse-proxy authentik-server authentik-worker
|
||||
|
||||
Verify:
|
||||
|
||||
sudo /usr/local/bin/docker exec nodedc-platform-launcher-1 sh -lc \
|
||||
'touch /app/server/storage/.write-test && rm /app/server/storage/.write-test && echo storage-ok'
|
||||
|
||||
curl -k -sS --compressed https://id.nodedc.ru/if/flow/default-authentication-flow/ \
|
||||
| grep -aE 'hub.nodedc.ru|launcher.local|getLauncherBaseUrl|Запросить доступ'
|
||||
EOF
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
name: nodedc-platform
|
||||
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
ports:
|
||||
- "${PLATFORM_HTTP_PORT:-18080}:80"
|
||||
volumes:
|
||||
- ./Caddyfile.http:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
depends_on:
|
||||
authentik-server:
|
||||
condition: service_started
|
||||
launcher:
|
||||
condition: service_started
|
||||
extra_hosts:
|
||||
- "id.nodedc.ru:host-gateway"
|
||||
- "hub.nodedc.ru:host-gateway"
|
||||
- "ops.nodedc.ru:host-gateway"
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- edge
|
||||
- identity
|
||||
|
||||
launcher:
|
||||
image: nodedc/launcher:local
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 5173
|
||||
NODEDC_LAUNCHER_STORAGE_DIR: /app/server/storage
|
||||
expose:
|
||||
- "5173"
|
||||
volumes:
|
||||
- ../launcher/server-storage:/app/server/storage
|
||||
- ../launcher/uploads:/app/dist/storage/uploads
|
||||
- ../launcher/uploads:/app/public/storage/uploads
|
||||
extra_hosts:
|
||||
- "id.nodedc.ru:host-gateway"
|
||||
- "hub.nodedc.ru:host-gateway"
|
||||
- "ops.nodedc.ru:host-gateway"
|
||||
- "${AUTH_DOMAIN:-auth.nas.nodedc}:host-gateway"
|
||||
- "${LAUNCHER_DOMAIN:-launcher.nas.nodedc}:host-gateway"
|
||||
- "${TASK_DOMAIN:-task.nas.nodedc}:host-gateway"
|
||||
networks:
|
||||
- edge
|
||||
- identity
|
||||
|
||||
postgresql-authentik:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
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:
|
||||
- identity
|
||||
|
||||
authentik-server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
|
||||
command: server
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
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}
|
||||
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: ${AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS:-127.0.0.0/8,172.16.0.0/12}
|
||||
depends_on:
|
||||
postgresql-authentik:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- "9000"
|
||||
- "9443"
|
||||
shm_size: 512mb
|
||||
volumes:
|
||||
- authentik-data:/data
|
||||
- ../authentik/custom-templates:/templates:ro
|
||||
networks:
|
||||
- identity
|
||||
|
||||
authentik-worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.2}
|
||||
command: worker
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
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
|
||||
networks:
|
||||
- identity
|
||||
|
||||
networks:
|
||||
edge:
|
||||
identity:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
authentik-database:
|
||||
authentik-data:
|
||||
authentik-certs:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Tasker overlay: editable NODE.DC logo link
|
||||
|
||||
This overlay captures the Synology Tasker source hotfix that routes NODE.DC logo clicks through the Hub public brand config:
|
||||
|
||||
- Hub source of truth: `https://hub.nodedc.ru/api/public/brand`
|
||||
- Editable admin field: Hub admin → Platform → Misc → Logo link
|
||||
- Fallback for `*.nodedc.ru`: `https://hub.nodedc.ru/`
|
||||
|
||||
## Apply to Synology source
|
||||
|
||||
From the platform repo root on a machine that has `/Volumes/docker` mounted:
|
||||
|
||||
```bash
|
||||
rsync -av infra/synology/tasker-overlays/logo-link-brand-hotfix/files/ \
|
||||
/Volumes/docker/nodedc-platform/tasker/plane-src/
|
||||
```
|
||||
|
||||
Then deploy web only:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 sh rebuild-nas-legacy.sh
|
||||
```
|
||||
|
||||
Admin image can be rebuilt separately if needed:
|
||||
|
||||
```bash
|
||||
cd /volume1/docker/nodedc-platform/tasker/plane-src
|
||||
BUILD_BACKEND=0 BUILD_WEB=0 BUILD_ADMIN=1 sh rebuild-nas-legacy.sh
|
||||
```
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
node_modules
|
||||
.next
|
||||
.yarn
|
||||
.pnpm-store/
|
||||
|
||||
### NextJS ###
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
/coverage
|
||||
|
||||
# Next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# Production
|
||||
dist/
|
||||
out/
|
||||
build/
|
||||
.react-router/
|
||||
**/build/
|
||||
**/.react-router/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.history
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Turborepo
|
||||
.turbo
|
||||
|
||||
## Django ##
|
||||
venv
|
||||
.venv
|
||||
*.pyc
|
||||
staticfiles
|
||||
mediafiles
|
||||
.env
|
||||
.DS_Store
|
||||
logs/
|
||||
htmlcov/
|
||||
.coverage
|
||||
|
||||
node_modules/
|
||||
assets/dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
package-lock.json
|
||||
.vscode
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
# lock files
|
||||
package-lock.json
|
||||
|
||||
|
||||
|
||||
.secrets
|
||||
tmp/
|
||||
|
||||
## packages
|
||||
dist
|
||||
.temp/
|
||||
deploy/selfhost/plane-app/
|
||||
|
||||
## Storybook
|
||||
*storybook.log
|
||||
output.css
|
||||
|
||||
dev-editor
|
||||
# Redis
|
||||
*.rdb
|
||||
*.rdb.gz
|
||||
|
||||
storybook-static
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
temp/
|
||||
scripts/
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
FROM node:22-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH"
|
||||
ENV CI=1
|
||||
|
||||
RUN corepack enable pnpm
|
||||
|
||||
# =========================================================================== #
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
RUN pnpm add -g turbo@2.9.4
|
||||
|
||||
COPY . .
|
||||
|
||||
# Create a pruned workspace for just the admin app
|
||||
RUN turbo prune --scope=admin --docker
|
||||
|
||||
# =========================================================================== #
|
||||
|
||||
FROM base AS installer
|
||||
|
||||
# Build in production mode; we still install dev deps explicitly below
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Public envs required at build time (pick up via process.env)
|
||||
ARG VITE_API_BASE_URL=""
|
||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||
ARG VITE_API_BASE_PATH="/api"
|
||||
ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH
|
||||
|
||||
ARG VITE_ADMIN_BASE_URL=""
|
||||
ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL
|
||||
ARG VITE_ADMIN_BASE_PATH="/nodedcsudo"
|
||||
ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH
|
||||
|
||||
ARG VITE_SPACE_BASE_URL=""
|
||||
ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL
|
||||
ARG VITE_SPACE_BASE_PATH="/spaces"
|
||||
ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH
|
||||
|
||||
ARG VITE_LIVE_BASE_URL=""
|
||||
ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL
|
||||
ARG VITE_LIVE_BASE_PATH="/live"
|
||||
ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH
|
||||
|
||||
ARG VITE_WEB_BASE_URL=""
|
||||
ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
|
||||
ARG VITE_WEB_BASE_PATH=""
|
||||
ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH
|
||||
|
||||
ARG VITE_NODEDC_LAUNCHER_URL=""
|
||||
ENV VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL
|
||||
|
||||
ARG VITE_WEBSITE_URL="https://plane.so"
|
||||
ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
|
||||
ARG VITE_SUPPORT_EMAIL="support@plane.so"
|
||||
ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Copy full directory structure before fetch to ensure all package.json files are available
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
# Fetch dependencies to cache store, then install offline with dev deps
|
||||
RUN pnpm fetch --store-dir=/pnpm/store
|
||||
RUN CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
|
||||
|
||||
# Build only the admin package
|
||||
RUN pnpm turbo run build --filter=admin
|
||||
|
||||
# =========================================================================== #
|
||||
|
||||
FROM nginx:1.29-alpine AS production
|
||||
|
||||
COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/nodedcsudo
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
|
||||
export function AuthHeader() {
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
||||
<a href={logoLinkUrl}>
|
||||
<span className="tracking-normal text-16 font-semibold text-primary">NODE.DC</span>
|
||||
</a>
|
||||
<span className="rounded-full bg-white/6 px-3 py-1 text-11 font-medium text-secondary">
|
||||
Глобальное администрирование
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { BrainCog, ChevronDown, ExternalLink, Image, LogOut, Mail, Palette, Settings, UserCog2 } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { LockIcon, WorkspaceIcon } from "@plane/propel/icons";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
// assets
|
||||
import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
// local imports
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
const PRIMARY_NAVIGATION = [
|
||||
{ label: "Основное", href: "/general/", Icon: Settings },
|
||||
{ label: "Почта", href: "/email/", Icon: Mail },
|
||||
{ label: "Аутентификация", href: "/authentication/", Icon: LockIcon },
|
||||
{ label: "Воркспейсы", href: "/workspace/", Icon: WorkspaceIcon },
|
||||
];
|
||||
|
||||
const FEATURE_NAVIGATION = [
|
||||
{ label: "ИИ", href: "/ai/", Icon: BrainCog, description: "OpenAI модель и API-ключ" },
|
||||
{ label: "Изображения", href: "/image/", Icon: Image, description: "Внешние библиотеки изображений" },
|
||||
];
|
||||
|
||||
export const AdminHeader = observer(function AdminHeader() {
|
||||
const pathName = usePathname();
|
||||
const { currentUser, signOut } = useUser();
|
||||
const { resolvedTheme, setTheme } = useNextTheme();
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href));
|
||||
const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ";
|
||||
const avatarName = currentUser?.display_name || currentUser?.email || "DC";
|
||||
|
||||
const handleThemeSwitch = () => {
|
||||
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
return (
|
||||
<header className="nodedc-admin-header relative z-30 flex w-full flex-shrink-0 flex-col gap-4">
|
||||
<div className="nodedc-admin-header-top grid w-full items-center gap-4">
|
||||
<a href={logoLinkUrl} className="nodedc-admin-logo-link inline-flex w-fit items-center" aria-label="NODE.DC">
|
||||
<img src={NodeDcLogo} alt="NODE.DC" className="nodedc-admin-logo" />
|
||||
</a>
|
||||
|
||||
<nav className="nodedc-admin-top-nav justify-self-center" aria-label="Основная навигация God Mode">
|
||||
{PRIMARY_NAVIGATION.map((item) => {
|
||||
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||
return (
|
||||
<Link key={item.href} href={item.href} className="nodedc-admin-top-nav-item" data-active={isActive}>
|
||||
<item.Icon className="size-3.5 stroke-[1.7]" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="nodedc-admin-top-nav-item" data-active={isFeatureRoute}>
|
||||
<BrainCog className="size-3.5 stroke-[1.7]" />
|
||||
<span>Возможности</span>
|
||||
<ChevronDown className="size-3 stroke-[2]" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="nodedc-glass-popup-surface absolute top-full left-1/2 z-[120] mt-3 flex w-64 -translate-x-1/2 flex-col gap-1 p-2 outline-none">
|
||||
{FEATURE_NAVIGATION.map((item) => {
|
||||
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||||
return (
|
||||
<Menu.Item key={item.href}>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn("nodedc-admin-feature-menu-item", {
|
||||
"is-active": isActive,
|
||||
"is-hovered": active,
|
||||
})}
|
||||
>
|
||||
<span className="grid size-9 place-items-center rounded-full bg-white/6">
|
||||
<item.Icon className="size-4 stroke-[1.7]" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-13 font-medium text-primary">{item.label}</span>
|
||||
<span className="block truncate text-11 text-tertiary">{item.description}</span>
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
||||
<a href="/" className="nodedc-admin-top-nav-item">
|
||||
<ExternalLink className="size-3.5 stroke-[1.7]" />
|
||||
<span>В приложение</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative justify-self-end">
|
||||
<Menu.Button className="nodedc-admin-user-button">
|
||||
<span className="min-w-0 text-right">
|
||||
<span className="block max-w-40 truncate text-14 font-medium text-primary">{adminName}</span>
|
||||
</span>
|
||||
<span className="grid size-10 place-items-center rounded-full bg-white/7">
|
||||
{currentUser ? (
|
||||
<Avatar
|
||||
name={avatarName}
|
||||
src={getFileURL(currentUser.avatar_url)}
|
||||
size={32}
|
||||
shape="circle"
|
||||
className="!text-body-sm-medium"
|
||||
/>
|
||||
) : (
|
||||
<UserCog2 className="size-5 text-[rgb(var(--nodedc-card-active-rgb))]" />
|
||||
)}
|
||||
</span>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="nodedc-glass-popup-surface absolute top-full right-0 z-[120] mt-3 flex w-60 flex-col divide-y divide-white/6 p-2 text-12 outline-none">
|
||||
<div className="flex flex-col gap-1 px-2 pb-2">
|
||||
<span className="truncate text-13 font-medium text-primary">{adminName}</span>
|
||||
<span className="truncate text-11 text-tertiary">{currentUser?.email}</span>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Menu.Item as="button" type="button" className="nodedc-admin-menu-action" onClick={handleThemeSwitch}>
|
||||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||
{resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={signOut}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<Menu.Item as="button" type="submit" className="nodedc-admin-menu-action">
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Выйти
|
||||
</Menu.Item>
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export function buildNodeDCLauncherUrl(): string {
|
||||
const configuredUrl = process.env.VITE_NODEDC_LAUNCHER_URL;
|
||||
|
||||
if (configuredUrl) {
|
||||
return configuredUrl;
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return "http://launcher.local.nodedc/";
|
||||
}
|
||||
|
||||
const hostname = window.location.hostname.toLowerCase();
|
||||
|
||||
if (hostname.endsWith(".nodedc.ru")) {
|
||||
return "https://hub.nodedc.ru/";
|
||||
}
|
||||
|
||||
if (hostname.endsWith(".nas.nodedc")) {
|
||||
const port = window.location.port ? `:${window.location.port}` : "";
|
||||
return `${window.location.protocol}//launcher.nas.nodedc${port}/`;
|
||||
}
|
||||
|
||||
return "http://launcher.local.nodedc/";
|
||||
}
|
||||
|
||||
export function buildNodeDCBrandConfigUrl(): string {
|
||||
return new URL("/api/public/brand", buildNodeDCLauncherUrl()).toString();
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-brand";
|
||||
|
||||
type TNodeDCBrandPayload = {
|
||||
logoLinkUrl?: string | null;
|
||||
};
|
||||
|
||||
export function useNodeDCBrandLinkUrl() {
|
||||
const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" })
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((payload: TNodeDCBrandPayload | null) => {
|
||||
const configuredUrl = typeof payload?.logoLinkUrl === "string" ? payload.logoLinkUrl.trim() : "";
|
||||
if (isMounted && configuredUrl) setLogoLinkUrl(configuredUrl);
|
||||
return undefined;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC");
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return logoLinkUrl;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Shapes } from "lucide-react";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
|
||||
import { ExpandedToolbarLink, ExpandedToolbarToolButton, ToolbarNotificationsButton } from "./toolbar-controls";
|
||||
// types
|
||||
import type { TProjectShellToolbarLayoutProps } from "./types";
|
||||
|
||||
export const ExpandedProjectShellToolbarLayout = ({
|
||||
draftsItem,
|
||||
homeItem,
|
||||
isWorkspaceHome,
|
||||
notificationsCount,
|
||||
profileItem,
|
||||
stickiesItem,
|
||||
onOpenNotifications,
|
||||
}: TProjectShellToolbarLayoutProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toggleWidgetSettings } = useHome();
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
return (
|
||||
<div className={cn("nodedc-expanded-toolbar-shell", { "nodedc-home-top-toolbar": isWorkspaceHome })}>
|
||||
<div className="nodedc-expanded-toolbar">
|
||||
<div className="nodedc-expanded-toolbar-top">
|
||||
<div className="nodedc-expanded-toolbar-left">
|
||||
<a href={logoLinkUrl} className="nodedc-expanded-brand-link" aria-label="NODE.DC">
|
||||
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-center">
|
||||
<WorkspaceMenuRoot variant="expanded-toolbar" />
|
||||
<div className="nodedc-expanded-nav-group">
|
||||
<ExpandedToolbarLink item={homeItem} label="Главная" />
|
||||
<ProjectsToolbarMenu variant="expanded" />
|
||||
<ExpandedToolbarLink item={stickiesItem} label="Стикеры" />
|
||||
<ExpandedToolbarLink item={draftsItem} label="Черновики" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-right">
|
||||
<div className="nodedc-expanded-user-group">
|
||||
<ExpandedToolbarLink item={profileItem} label="Профиль" />
|
||||
<ToolbarNotificationsButton
|
||||
label={t("notification.label")}
|
||||
notificationsCount={notificationsCount}
|
||||
onClick={onOpenNotifications}
|
||||
variant="expanded"
|
||||
/>
|
||||
<UserMenuRoot variant="expanded-toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-expanded-toolbar-tools-row">
|
||||
<div className="nodedc-expanded-breadcrumbs-slot" data-nodedc-expanded-breadcrumbs-slot />
|
||||
{!isWorkspaceHome && (
|
||||
<div className="nodedc-expanded-main-tool-cluster">
|
||||
<TopNavPowerK variant="expanded-toolbar" />
|
||||
<div className="nodedc-expanded-header-filters-slot" data-nodedc-expanded-header-filters-slot />
|
||||
</div>
|
||||
)}
|
||||
<div className="nodedc-expanded-action-tool-cluster">
|
||||
<div className="nodedc-expanded-tool-slot" data-nodedc-voice-task-toolbar-slot />
|
||||
{isWorkspaceHome && (
|
||||
<ExpandedToolbarToolButton label={t("home.manage_widgets")} onClick={() => toggleWidgetSettings(true)}>
|
||||
<Shapes className="size-4" />
|
||||
</ExpandedToolbarToolButton>
|
||||
)}
|
||||
<div className="nodedc-expanded-primary-action-slot" data-nodedc-expanded-primary-action-slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
|
||||
const authContentMap = {
|
||||
[EAuthModes.SIGN_IN]: {
|
||||
pageTitle: "auth_actions.sign_up",
|
||||
text: "auth.common.new_to_plane",
|
||||
linkText: "auth_actions.sign_up",
|
||||
linkHref: "/sign-up",
|
||||
},
|
||||
[EAuthModes.SIGN_UP]: {
|
||||
pageTitle: "auth_actions.sign_in",
|
||||
text: "auth.common.already_have_an_account",
|
||||
linkText: "auth_actions.sign_in",
|
||||
linkHref: "/sign-in",
|
||||
},
|
||||
};
|
||||
|
||||
type AuthHeaderProps = {
|
||||
type: EAuthModes;
|
||||
};
|
||||
|
||||
export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
// store
|
||||
const { config } = useInstance();
|
||||
// derived values
|
||||
const enableSignUpConfig = config?.enable_signup ?? false;
|
||||
|
||||
return (
|
||||
<AuthHeaderBase
|
||||
pageTitle={t(authContentMap[type].pageTitle)}
|
||||
additionalAction={
|
||||
enableSignUpConfig && (
|
||||
<div className="flex flex-col items-end text-center text-13 font-medium text-tertiary sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className="text-body-sm-regular text-tertiary">{t(authContentMap[type].text)}</span>
|
||||
<Link
|
||||
data-ph-element={AUTH_TRACKER_ELEMENTS.NAVIGATE_TO_SIGN_UP}
|
||||
href={authContentMap[type].linkHref}
|
||||
className="nodedc-auth-link text-body-sm-semibold"
|
||||
>
|
||||
{t(authContentMap[type].linkText)}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type TAuthHeaderBase = {
|
||||
pageTitle: string;
|
||||
additionalAction?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthHeaderBase(props: TAuthHeaderBase) {
|
||||
const { pageTitle, additionalAction } = props;
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle + " - NODE.DC"} />
|
||||
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6 px-2 py-1">
|
||||
<a href={logoLinkUrl}>
|
||||
<PlaneLockup
|
||||
height={31}
|
||||
width={148}
|
||||
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
||||
/>
|
||||
</a>
|
||||
{additionalAction}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
import { GOD_MODE_URL } from "@plane/constants";
|
||||
// assets
|
||||
import GradientLogo from "@/app/assets/auth/gradient-logo.webp?url";
|
||||
import GradientBgLogo from "@/app/assets/auth/gradient-bg-logo.webp?url";
|
||||
import DefaultLayout from "@/layouts/default-layout";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
|
||||
export function InstanceNotReady() {
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<div className="relative z-10 flex h-screen w-screen overflow-hidden">
|
||||
<div className="flex h-full w-full flex-col items-center px-8 pt-6 pb-10">
|
||||
<div className="sticky top-0 flex w-full shrink-0 items-center justify-between gap-6">
|
||||
<a href={logoLinkUrl} aria-label="NODE.DC">
|
||||
<PlaneLockup
|
||||
height={40}
|
||||
width={190}
|
||||
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-7">
|
||||
<div className="nodedc-error-shell flex max-w-3xl flex-col items-center gap-11 text-center">
|
||||
<img
|
||||
src={GradientBgLogo}
|
||||
className="pointer-events-none absolute -top-24 -left-32 h-56 w-96 opacity-12"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img
|
||||
src={GradientBgLogo}
|
||||
className="pointer-events-none absolute -right-20 -bottom-16 h-56 w-96 opacity-12"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img src={GradientLogo} className="h-24 w-40 object-contain" alt="NODE.DC Logo" />
|
||||
<div className="flex max-w-124 flex-col items-center gap-3">
|
||||
<h1 className="text-h2-semibold text-primary">NODE.DC готов к запуску</h1>
|
||||
<p className="text-center text-body-md-regular text-secondary">
|
||||
Завершите настройку инстанса и создайте первое рабочее пространство, чтобы начать работу с
|
||||
проектами и рабочими элементами.
|
||||
</p>
|
||||
</div>
|
||||
<a href={GOD_MODE_URL} className="w-80">
|
||||
<Button variant="primary" className="nodedc-error-primary w-full" size="xl">
|
||||
Перейти к настройке
|
||||
</Button>
|
||||
</a>
|
||||
<a
|
||||
href="https://nodedc.dctouch.ru/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="nodedc-error-link text-13 font-medium"
|
||||
>
|
||||
Служба поддержки
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { InboxIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||
|
||||
type TNodeDCStandaloneShellProps = {
|
||||
children: ReactNode;
|
||||
notificationsCount?: number;
|
||||
onOpenNotifications?: () => void;
|
||||
showUserControls?: boolean;
|
||||
};
|
||||
|
||||
export const NodeDCStandaloneShell = (props: TNodeDCStandaloneShellProps) => {
|
||||
const { children, notificationsCount = 0, onOpenNotifications, showUserControls = false } = props;
|
||||
const { t } = useTranslation();
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen w-full overflow-hidden bg-[#050507] text-primary">
|
||||
<div className="pointer-events-none absolute inset-0 opacity-80">
|
||||
<div className="absolute top-[-18rem] left-[-12rem] h-[34rem] w-[34rem] rounded-full bg-[rgb(var(--nodedc-accent-rgb))]/10 blur-[120px]" />
|
||||
<div className="absolute right-[-14rem] bottom-[-18rem] h-[38rem] w-[38rem] rounded-full bg-white/7 blur-[140px]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(255,255,255,0.06),transparent_38%),linear-gradient(180deg,rgba(255,255,255,0.035),rgba(255,255,255,0))]" />
|
||||
</div>
|
||||
|
||||
<header className="nodedc-expanded-toolbar-shell absolute inset-x-0 top-0 z-[2]">
|
||||
<div className="nodedc-expanded-toolbar-top">
|
||||
<div className="nodedc-expanded-toolbar-left">
|
||||
<a href={logoLinkUrl} className="nodedc-expanded-brand-link" aria-label="NODE.DC">
|
||||
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="nodedc-expanded-toolbar-center" />
|
||||
<div className="nodedc-expanded-toolbar-right">
|
||||
{showUserControls && (
|
||||
<div className="nodedc-expanded-user-group">
|
||||
{onOpenNotifications && (
|
||||
<Tooltip tooltipContent={t("notification.label")} position="bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-toolbar-icon-button nodedc-expanded-notification-button relative flex items-center justify-center"
|
||||
data-active={false}
|
||||
aria-label={t("notification.label")}
|
||||
onClick={onOpenNotifications}
|
||||
>
|
||||
<span className="nodedc-toolbar-icon-active-dot">
|
||||
<InboxIcon className="size-5" />
|
||||
</span>
|
||||
{notificationsCount > 0 && (
|
||||
<span className="nodedc-toolbar-notification-dot absolute top-1.5 right-1.5 size-2 rounded-full bg-danger-primary" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<UserMenuRoot variant="expanded-toolbar" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="relative z-[1] flex min-h-screen w-full items-center justify-center px-5 py-10 pt-[calc(var(--nodedc-shell-height)+2.25rem)]">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
// hooks
|
||||
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
// local imports
|
||||
import { SwitchAccountDropdown } from "./switch-account-dropdown";
|
||||
|
||||
export const OnboardingHeader = observer(function OnboardingHeader() {
|
||||
const { data: user } = useUser();
|
||||
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||
|
||||
const userName = user?.display_name
|
||||
? user.display_name
|
||||
: user?.first_name
|
||||
? `${user.first_name} ${user.last_name ?? ""}`.trim()
|
||||
: user?.email;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 flex flex-col gap-4">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-t-lg bg-surface-1">
|
||||
<div className="h-full w-full bg-accent-primary" />
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between gap-6 px-6">
|
||||
<a href={logoLinkUrl} aria-label="NODE.DC">
|
||||
<PlaneLockup height={20} width={95} className="text-primary transition-opacity hover:opacity-90" />
|
||||
</a>
|
||||
<SwitchAccountDropdown fullName={userName} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-auth";
|
||||
|
||||
type TNodeDCBrandPayload = {
|
||||
logoLinkUrl?: string | null;
|
||||
};
|
||||
|
||||
export function useNodeDCBrandLinkUrl() {
|
||||
const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" })
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((payload: TNodeDCBrandPayload | null) => {
|
||||
const configuredUrl = typeof payload?.logoLinkUrl === "string" ? payload.logoLinkUrl.trim() : "";
|
||||
if (isMounted && configuredUrl) setLogoLinkUrl(configuredUrl);
|
||||
return undefined;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC");
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return logoLinkUrl;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
DOCKER=/usr/local/bin/docker
|
||||
SRC=/volume1/docker/nodedc-platform/tasker/plane-src
|
||||
APP=/volume1/docker/nodedc-platform/tasker/plane-app
|
||||
BUILD_BACKEND=${BUILD_BACKEND:-1}
|
||||
BUILD_WEB=${BUILD_WEB:-1}
|
||||
BUILD_ADMIN=${BUILD_ADMIN:-0}
|
||||
RECREATE_SERVICES=""
|
||||
|
||||
printf "== sudo session ==\n"
|
||||
sudo -v
|
||||
|
||||
if [ "$BUILD_BACKEND" = "1" ]; then
|
||||
printf "== backend image: nodedc/plane-backend:local ==\n"
|
||||
cd "$SRC/apps/api"
|
||||
sudo DOCKER_BUILDKIT=0 "$DOCKER" build \
|
||||
-t nodedc/plane-backend:local \
|
||||
-f Dockerfile.api .
|
||||
RECREATE_SERVICES="$RECREATE_SERVICES api worker beat-worker"
|
||||
else
|
||||
printf "== skip backend image ==\n"
|
||||
fi
|
||||
|
||||
if [ "$BUILD_WEB" = "1" ]; then
|
||||
printf "== frontend image: nodedc/plane-frontend:ru ==\n"
|
||||
cd "$SRC"
|
||||
sudo DOCKER_BUILDKIT=0 "$DOCKER" build \
|
||||
--build-arg VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru \
|
||||
--build-arg VITE_NODEDC_OIDC_LOGIN_ENABLED=1 \
|
||||
-t nodedc/plane-frontend:ru \
|
||||
-f apps/web/Dockerfile.web.nas-legacy .
|
||||
RECREATE_SERVICES="$RECREATE_SERVICES web"
|
||||
else
|
||||
printf "== skip frontend image ==\n"
|
||||
fi
|
||||
|
||||
if [ "$BUILD_ADMIN" = "1" ]; then
|
||||
printf "== admin image: nodedc/plane-admin:ru ==\n"
|
||||
cd "$SRC"
|
||||
sudo DOCKER_BUILDKIT=1 "$DOCKER" build \
|
||||
--build-arg VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru \
|
||||
-t nodedc/plane-admin:ru \
|
||||
-f apps/admin/Dockerfile.admin .
|
||||
RECREATE_SERVICES="$RECREATE_SERVICES admin"
|
||||
else
|
||||
printf "== skip admin image ==\n"
|
||||
fi
|
||||
|
||||
if [ -z "$RECREATE_SERVICES" ]; then
|
||||
printf "== nothing to recreate ==\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "== recreate tasker services ==\n"
|
||||
cd "$APP"
|
||||
sudo "$DOCKER" compose -p nodedc-tasker \
|
||||
--env-file .env.synology \
|
||||
-f docker-compose.yaml \
|
||||
-f docker-compose.synology.override.yml \
|
||||
up -d --no-build --force-recreate $RECREATE_SERVICES
|
||||
|
||||
printf "== containers ==\n"
|
||||
sudo "$DOCKER" ps \
|
||||
--filter "name=nodedc-tasker" \
|
||||
--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
printf "== smoke: logo ==\n"
|
||||
curl -k -sSI --resolve ops.nodedc.ru:443:127.0.0.1 \
|
||||
https://ops.nodedc.ru/nodedc-logo.svg \
|
||||
| grep -Ei "HTTP/|content-type" || true
|
||||
|
||||
printf "== smoke: websocket ==\n"
|
||||
curl -k -i --http1.1 --resolve ops.nodedc.ru:443:127.0.0.1 \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
-H "Sec-WebSocket-Version: 13" \
|
||||
https://ops.nodedc.ru/live/nodedc/stream \
|
||||
--max-time 35 \
|
||||
2>/dev/null | sed -n "1,25p" || true
|
||||
|
||||
printf "== done ==\n"
|
||||
Loading…
Reference in New Issue