# Caddy config.
#
# - gendsgn.ru — main production site, auto-TLS via Let's Encrypt.
# - www.gendsgn.ru — 301 redirect to apex (canonical URL).
# - :80 — fallback for raw IP access. Closed by same auth gate (pilot phase).
#
# `handle /api/*` (NOT `handle_path`) — we keep the /api prefix because
# the FastAPI router is mounted at /api/v1/*.
#
# Auth gate: basic_auth for pilot phase (2026-05-23).
# Users managed via caddy/users.caddy.snippet (git history = audit trail).
# Public exclusions: /health (liveness probe), /preview/* (static mockups).
#
# #2558: с 2026-07 basic_auth гейтит ТОЛЬКО Site Finder (`/`, `/api/*`,
# `/analytics` и т.д.). `/trade-in/*` (+ `/sale-share` redirect) вынесены ВЫШЕ
# import'а — у trade-in своя авторизация (форма входа + opaque session-cookie,
# см. #2552) поверх RBAC (`tradein-mvp/backend/app/core/rbac.py`). Site Finder
# всё ещё легаси-пилотный basic_auth (roles.yaml dual-mode остаётся живым для
# него — НЕ трогать caddy/users.caddy.snippet).
#
# IMPORTANT: route { } block is required to preserve directive order.
# Without route { }, Caddy executes directives in hard-coded default order
# (basic_auth runs before handle), making /health and /preview/* exclusions
# ineffective. With route { }, handlers are matched top-to-bottom as written.

# #2213: global `log_credentials` УБРАН. Он заставлял Caddy писать raw
# Authorization header (Base64 `user:password`, тривиально обратимый) в ОБА лог-файла
# (gendsgn.ru.log + auth_audit.log). Даже при root-only доступе и коротком retention
# это plaintext-пароли пилотов на диске — недопустимый риск против скромной выгоды.
# Trade-off: glitchtip-auth-forwarder больше не извлекает `attempted_username` из 401
# (тег деградирует в "(none)" — forwarder это уже обрабатывает gracefully, не падает).
# Событие basic_auth 401 (remote_ip / uri / method) по-прежнему уходит в GlitchTip.

gendsgn.ru {
    encode zstd gzip

    log {
        output file /var/log/caddy/gendsgn.ru.log {
            roll_size 50MiB
            roll_keep 5
            roll_keep_for 720h
        }
        format json
    }

    # Отдельный лог только для auth-событий.
    # Forwarder (ops/glitchtip-auth-forwarder) читает именно этот файл.
    # Retention 7 дней (меньше чем main log) — содержит plain Base64 credentials.
    log auth_audit {
        output file /var/log/caddy/auth_audit.log {
            roll_size 10MiB
            roll_keep 3
            roll_keep_for 168h
        }
        format json
    }

    route {
        # /health и /preview/* — public, без auth, short-circuit.
        handle /health {
            reverse_proxy backend:8000
        }

        # Static HTML mockups для review (audit alternatives).
        # Public access — без auth (по запросу 2026-05-17).
        handle_path /preview/* {
            root * /srv/preview
            file_server browse
        }

        # Trade-In UI preview — public CI surface (#801). Рендерит mock-фикстуру
        # «денежного экрана» без бэкенда → axe/lighthouse гоняются без креды.
        # Реальных клиентских данных нет (статичная фикстура) → безопасно публично.
        # ДО auth-import: route матчит сверху вниз, handle short-circuit'ит.
        # Без strip — Next.js basePath=/trade-in ждёт префикс в URL (как @tradein).
        # ui-preview + его статика (_next/static — CSS/JS бандлы, без секретов).
        # Оба ДО auth-import, иначе ассеты страницы уходят в @tradein (под auth) → 401 → без CSS.
        @uipreview path /trade-in/ui-preview/* /trade-in/_next/static/*
        handle @uipreview {
            reverse_proxy tradein-frontend:3000 {
                # #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и
                # @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент
                # мог бы прислать свой X-Authenticated-User. Сейчас инертно
                # (страница статична, у tradein-frontend нет секрета для
                # X-Internal-Auth-Secret), но убираем ради единообразия периметра,
                # а не полагаясь на то, что downstream ничего не делает с заголовком.
                header_up -X-Authenticated-User
            }
        }

        # #2558: Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker
        # stack, подключен через gendesign_shared network. Секция ЦЕЛИКОМ ДО
        # `import caddy/users.caddy.snippet` ниже — /trade-in имеет собственную
        # авторизацию (форма входа + opaque session-cookie, #2552; RBAC-проверка
        # роли внутри tradein-backend, `app/core/rbac.py`), Site Finder basic_auth
        # ей больше не нужен и не должен применяться (short-circuit сверху вниз,
        # как /health и /preview/* выше).
        #
        # X-Authenticated-User — ЯВНОЕ УДАЛЕНИЕ (`header_up -X-Authenticated-User`),
        # НЕ `header_up X-Authenticated-User {http.auth.user.id}`. Причина: этот
        # блок больше не идёт ПОСЛЕ basic_auth, поэтому `{http.auth.user.id}`
        # никогда не резолвится авторизованным юзером на этом пути.
        # Проверено эмпирически (echo-стенд на образе caddy:2, `caddy adapt`):
        # старая Set-форма (`header_up X-Authenticated-User {http.auth.user.id}`)
        # НЕ пропустила бы клиентский заголовок насквозь и НЕ оставила бы поле
        # пустым — Caddy подставляет НЕРАЗРЕШЁННЫЙ плейсхолдер как ЛИТЕРАЛЬНУЮ
        # строку (`ReplaceKnown`), т.е. upstream получил бы буквально
        # `X-Authenticated-User: {http.auth.user.id}`. Для backend (auth_mode=
        # "dual", `app/core/config.py`) это НЕ подмена личности — legacy path
        # (`rbac.py:186`) сделал бы `get_role("{http.auth.user.id}")`, юзер не
        # найден в roles.yaml → 403 для всех. Т.е. старая форма была бы не
        # security-дырой, а fail-closed-but-сломанной (все trade-in запросы без
        # session-cookie получали бы 403 вместо ожидаемого 401/редиректа на логин).
        # `-Field` остаётся правильным выбором не потому что Set был бы дырой, а
        # потому что это ЕДИНСТВЕННАЯ форма с явно задокументированной семантикой
        # "удалить заголовок" (Caddyfile reverse_proxy directive: `-<field>` =
        # delete) — корректное поведение не должно зависеть от того, как именно
        # Caddy трактует нерезолвленный/пустой плейсхолдер в Set-операции.
        # X-Internal-Auth-Secret НЕ трогаем — #2213-секрет всегда перезаписывается
        # из env (Set-операция с непустым значением, никак не связана с auth-гейтом
        # basic_auth), это единственное, что теперь отсекает подделку заголовков
        # изнутри gendesign_shared network для legacy dual-mode пути.
        handle /trade-in/api/* {
            # `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api;
            # FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только
            # префикса basePath /trade-in (Next.js basePath leak).
            uri strip_prefix /trade-in
            reverse_proxy tradein-backend:8000 {
                header_up -X-Authenticated-User
                header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
            }
        }

        # gendsgn.ru/sale-share — короткий адрес standalone-продукта «Поиск домов».
        # Next basePath=/trade-in → редиректим на канонический /trade-in/sale-share
        # (тот же tradein-frontend контейнер; query-string сохраняется). True vanity-URL
        # в адресной строке требует отдельного Next-app с basePath=/sale-share.
        # #2558: перенесён ВЫШЕ auth-import вместе с trade-in — редирект ведёт на
        # /trade-in/sale-share, для которого теперь нет Caddy basic_auth (как и
        # для остального /trade-in). Это НЕ делает страницу публичной: она всё
        # ещё за собственной авторизацией trade-in — `RouteGuard` во фронте
        # (`app/layout.tsx`, «Доступ: pilot + admin») и сессия для
        # `/api/v1/buildings/sale-share*` на бэке; без валидной сессии юзер
        # получит редирект на /login, а не контент. Смысл переноса — не открыть
        # страницу всем, а убрать несогласованность: короткий URL не должен быть
        # строже (Caddy basic_auth) целевого адреса, к которому и так уже нет
        # basic_auth-барьера (только собственный login trade-in).
        @saleshare path /sale-share /sale-share/
        handle @saleshare {
            redir /trade-in/sale-share permanent
        }

        # Matcher `path /trade-in /trade-in/*` ловит И /trade-in (без слеша),
        # И /trade-in/ + /trade-in/anything. Без обоих случаев `handle /trade-in/*`
        # пропускал /trade-in без слеша → попадал в общий frontend → пустой ответ.
        @tradein path /trade-in /trade-in/*
        handle @tradein {
            # Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
            reverse_proxy tradein-frontend:3000 {
                # См. комментарий над /trade-in/api/* выше — та же логика (явное
                # удаление вместо Set с пустым {http.auth.user.id}).
                header_up -X-Authenticated-User
                header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
            }
        }

        # Auth gate — с #2558 применяется ТОЛЬКО к Site Finder (handle /api/* и
        # handle {} ниже). Trade-In уже отработал и short-circuit'нул выше.
        import caddy/users.caddy.snippet

        handle /api/* {
            reverse_proxy backend:8000 {
                header_up X-Authenticated-User {http.auth.user.id}
            }
        }

        handle {
            reverse_proxy frontend:3000 {
                header_up X-Authenticated-User {http.auth.user.id}
            }
        }
    }
}

www.gendsgn.ru {
    redir https://gendsgn.ru{uri} permanent
}

# Obsidian Self-hosted LiveSync (CouchDB backend).
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
#
# DNS: A-record obsidian.gendsgn.ru → IP VPS.
# Клиенты Obsidian + Self-hosted LiveSync plugin указывают на этот URL.
obsidian.gendsgn.ru {
    encode zstd gzip

    reverse_proxy couchdb:5984 {
        # Большие документы (vault attachments / images) — увеличиваем timeout
        transport http {
            response_header_timeout 120s
        }
    }
}

# GlitchTip — self-hosted error tracking (Sentry-compatible).
# DNS: A-record errors.gendsgn.ru → IP VPS.
errors.gendsgn.ru {
    encode zstd gzip

    reverse_proxy glitchtip-web:8080

    log {
        output file /var/log/caddy/errors.gendsgn.ru.log
    }
}

# Uptime Kuma — self-hosted uptime monitoring + public status page (#75 B6-1).
# DNS: A-record status.gendsgn.ru → IP VPS (добавить перед деплоем стека).
# Контейнер из docker-compose.uptime.yml (project gendesign-uptime) на shared
# gendesign_shared network. Если стек не запущен — Caddy отдаёт 502 ТОЛЬКО на
# этом домене, main-сайт не страдает (как obsidian.gendsgn.ru).
#
# ВНИМАНИЕ: status-page НАМЕРЕННО публичен (trust-building для пилотов, issue #75).
# Admin-панель Kuma (/dashboard, /manage-*) защищена собственным логином Kuma —
# НЕ кладём её за caddy/users.caddy.snippet, иначе double-auth сломает setup.
status.gendsgn.ru {
    encode zstd gzip

    reverse_proxy uptime-kuma:3001

    log {
        output file /var/log/caddy/status.gendsgn.ru.log
    }
}

# Forgejo — self-hosted git (migration 2026-05-16).
# DNS: A-record git.gendsgn.ru → IP VPS.
# Forgejo container из forgejo-migration/docker-compose.yml на shared
# gendesign_default network. HTTP port 3000 (default Forgejo).
# Был добавлен вручную при migration, потерян при первом auto-deploy после
# изменения Caddyfile (deploy.yml делает git reset --hard). См. fix issue.
git.gendsgn.ru {
    encode zstd gzip

    reverse_proxy forgejo:3000

    log {
        output file /var/log/caddy/git.gendsgn.ru.log
    }
}

# Plain HTTP by IP — closed by same auth gate (prevent bypass via direct IP / SSH tunnel).
# Caddy issues no TLS here (no hostname). /health remains public.
:80 {
    encode zstd gzip

    route {
        # /health — public, без auth (GHA deploy smoke check, liveness probe).
        handle /health {
            reverse_proxy backend:8000
        }

        # Auth gate (same snippet as gendsgn.ru).
        import caddy/users.caddy.snippet

        handle /api/* {
            reverse_proxy backend:8000 {
                header_up X-Authenticated-User {http.auth.user.id}
            }
        }

        handle {
            reverse_proxy frontend:3000 {
                header_up X-Authenticated-User {http.auth.user.id}
            }
        }
    }
}
# Test deploy flow 2026-05-15T21:43:32Z
