From bac4f62bce2e623b878a7c2506cbdac57944ee18 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 4 May 2026 21:17:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20Branded=20Authentik=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/AUTH_BRANDED_LOGIN_RFC.md | 77 +++ docs/AUTH_MODEL.md | 2 + infra/authentik/README.md | 10 + infra/authentik/bootstrap-dev.py | 70 ++- .../custom-templates/base/header_js.html | 286 +++++++++ .../branding/nodedc-login.css | 553 ++++++++++++++++++ infra/reverse-proxy/Caddyfile | 6 + infra/scripts/bootstrap-authentik-dev.sh | 1 + 8 files changed, 1003 insertions(+), 2 deletions(-) create mode 100644 docs/AUTH_BRANDED_LOGIN_RFC.md create mode 100644 infra/authentik/custom-templates/base/header_js.html create mode 100644 infra/authentik/custom-templates/branding/nodedc-login.css diff --git a/docs/AUTH_BRANDED_LOGIN_RFC.md b/docs/AUTH_BRANDED_LOGIN_RFC.md new file mode 100644 index 0000000..b5097a6 --- /dev/null +++ b/docs/AUTH_BRANDED_LOGIN_RFC.md @@ -0,0 +1,77 @@ +# NODE.DC branded login RFC + +## Цель + +Пользователь должен видеть единое окно входа NODE.DC в визуальной логике текущего Plane login: темный фон, компактная карточка, NODE.DC logo, крупный заголовок, email/password, яркая CTA-кнопка. + +При этом Authentik остается внутренним identity provider и продолжает владеть паролями, MFA, recovery, lockout, session, OAuth/OIDC и audit events. + +## Жесткие запреты + +Запрещенные варианты реализации: + +- reverse proxy, который переписывает HTML Authentik на лету; +- frontend-only форма в Launcher, которая принимает email/password; +- BFF endpoint в Launcher, который принимает пароль пользователя и пересылает его в Authentik; +- OAuth password grant / ROPC; +- хранение паролей, refresh tokens или Authentik session cookies в frontend/localStorage; +- обход Authentik CSRF, MFA, recovery, lockout, enrollment и audit flow. + +Эти варианты расширяют trusted boundary платформы: Launcher становится обработчиком паролей, а это ломает разделение ответственности и усложняет аудит. + +## Выбранный безопасный путь + +Базовый production-safe путь: Authentik-native branded flow. + +- UI кастомизируется через Brand/Flow внутри Authentik. +- `branding_custom_css` задает NODE.DC look без HTML-прокси. +- Password stage, Identification stage, User Login stage, MFA/recovery остаются штатными Authentik stages. +- Identification stage связан со штатным Password stage, поэтому email и password показываются в одном Authentik challenge без передачи пароля в Launcher. +- Launcher и Task Manager остаются OIDC clients и не получают пароль пользователя. +- Direct links на Launcher/Task Manager продолжают идти через Authorization Code Flow + PKCE. +- OAuth2 provider end-session использует штатный Authentik invalidation flow с UserLogoutStage, чтобы пользователь не попадал на Authentik application logout dashboard. + +Если Brand/CSS не даст pixel-level соответствие Plane login, следующий допустимый уровень — кастомный Authentik template/flow executor внутри Authentik deployment. Это тоже остается IdP-side custom UI, но требует отдельной security review при каждом upgrade Authentik. + +## Runtime prototype + +Локальный bootstrap настраивает Brand для `auth.local.nodedc`: + +- title: `NODE.DC`; +- default authentication flow: `default-authentication-flow`; +- flow title: `Работайте во всех измерениях.`; +- default locale: `ru`; +- theme: `dark`; +- dark NODE.DC CSS из `/templates/branding/nodedc-login.css`; +- без изменения password/MFA/recovery mechanics. + +Файлы: + +- `infra/authentik/bootstrap-dev.py` — idempotent Brand/Flow bootstrap; +- `infra/authentik/custom-templates/branding/nodedc-login.css` — native Authentik Brand CSS. + +## Security acceptance + +Перед переводом в production нужно проверить: + +- пароль не проходит через Launcher/BFF/network logs вне Authentik; +- CSRF token и Authentik cookies остаются штатными; +- MFA/passkeys/recovery/enrollment работают после стилизации; +- failed login/lockout/audit events остаются в Authentik; +- logout/SLO закрывает Launcher и app sessions; +- прямой заход `task.local.nodedc` и `launcher.local.nodedc` возвращает пользователя после login; +- UI не содержит слова `authentik` в пользовательском flow; +- кастомизация переживает restart контейнеров и повторный bootstrap; +- fallback admin URL к Authentik остается доступен только как служебный контур. + +## Upgrade policy + +Brand/CSS считается безопасным maintenance layer. Template override считается повышенным риском: перед обновлением Authentik нужно прогонять visual/security regression, потому что DOM web components может измениться. + +## Источники + +- Authentik Flows: https://docs.goauthentik.io/docs/add-secure-apps/flows-stages/flow/ +- Authentik Stages: https://docs.goauthentik.io/docs/flow/stages +- Password stage: https://docs.goauthentik.io/add-secure-apps/flows-stages/stages/password/ +- Custom CSS: https://docs.goauthentik.io/brands/custom-css/ +- Single Logout: https://docs.goauthentik.io/add-secure-apps/providers/single-logout/ diff --git a/docs/AUTH_MODEL.md b/docs/AUTH_MODEL.md index 6615db7..34ac6bc 100644 --- a/docs/AUTH_MODEL.md +++ b/docs/AUTH_MODEL.md @@ -48,6 +48,8 @@ Production login должен быть NODE.DC-branded: Запрещено делать быстрый frontend-only password form, который обходит IdP protections, хранит секреты в браузере или ломает MFA/recovery/audit. +Текущее безопасное решение зафиксировано в `docs/AUTH_BRANDED_LOGIN_RFC.md`: сначала используем Authentik-native Brand/CSS/Flow customization. Reverse proxy HTML-rewrite, password form в Launcher и пересылка пароля через BFF запрещены. + ## Required claims Минимальный normalized user object: diff --git a/infra/authentik/README.md b/infra/authentik/README.md index afd622d..c90f739 100644 --- a/infra/authentik/README.md +++ b/infra/authentik/README.md @@ -33,3 +33,13 @@ Later phases should add reproducible configuration for: - groups and policies; - admin service token scope; - exports or blueprints for repeatable setup. + +## NODE.DC branded login + +`custom-templates/branding/nodedc-login.css` is mounted into Authentik at `/templates/branding/nodedc-login.css` and applied by `bootstrap-dev.py` through the native Authentik Brand `branding_custom_css` field. + +`custom-templates/base/header_js.html` keeps Authentik's native config script and adds a minimal NODE.DC field enhancement for the email clear control and password placeholder only. + +OAuth2 providers are assigned Authentik's `default-invalidation-flow` so application logout completes the IdP session and returns through the NODE.DC launcher route instead of showing the default Authentik application logout screen. + +This is intentionally not an HTML-rewriting proxy. Passwords, MFA, recovery, sessions and audit remain inside Authentik; Launcher and Task Manager stay OIDC clients. diff --git a/infra/authentik/bootstrap-dev.py b/infra/authentik/bootstrap-dev.py index f011631..f6cdd0b 100644 --- a/infra/authentik/bootstrap-dev.py +++ b/infra/authentik/bootstrap-dev.py @@ -1,11 +1,13 @@ from os import environ +from pathlib import Path from django.db import transaction +from authentik.brands.models import Brand from authentik.common.oauth.constants import SubModes from authentik.core.models import Application, Group, User from authentik.crypto.models import CertificateKeyPair -from authentik.flows.models import Flow +from authentik.flows.models import Flow, FlowStageBinding from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( ClientTypes, @@ -16,6 +18,10 @@ from authentik.providers.oauth2.models import ( RedirectURIMatchingMode, ScopeMapping, ) +from authentik.stages.identification.models import IdentificationStage +from authentik.stages.password.models import PasswordStage + +BRANDING_CSS_PATH = Path("/templates/branding/nodedc-login.css") GROUP_SPECS = [ ("nodedc:superadmin", False), @@ -158,9 +164,67 @@ def default_scope_mappings(): return mappings +def read_branding_css(): + if BRANDING_CSS_PATH.exists(): + return BRANDING_CSS_PATH.read_text(encoding="utf-8") + return "" + + +def ensure_nodedc_brand(): + auth_domain = environ.get("AUTH_DOMAIN", "auth.local.nodedc").strip() or "auth.local.nodedc" + authentication_flow = Flow.objects.get(slug="default-authentication-flow") + invalidation_flow = Flow.objects.get(slug="default-invalidation-flow") + + authentication_flow.name = "NODE.DC authentication" + authentication_flow.title = "Работайте во всех измерениях." + authentication_flow.layout = "stacked" + authentication_flow.background = "" + authentication_flow.save() + + identification_stage = IdentificationStage.objects.get( + name="default-authentication-identification" + ) + password_stage = PasswordStage.objects.get(name="default-authentication-password") + password_stage.allow_show_password = True + password_stage.save() + identification_stage.user_fields = ["email"] + identification_stage.password_stage = password_stage + identification_stage.show_matched_user = False + identification_stage.enable_remember_me = False + identification_stage.save() + FlowStageBinding.objects.filter(target=authentication_flow, stage=password_stage).delete() + + brand = Brand.objects.filter(domain=auth_domain).first() + if brand is None: + brand = Brand(domain=auth_domain) + + Brand.objects.exclude(brand_uuid=brand.brand_uuid).update(default=False) + brand.default = True + brand.domain = auth_domain + brand.branding_title = "NODE.DC" + brand.branding_logo = "" + brand.branding_favicon = "" + brand.branding_custom_css = read_branding_css() + brand.flow_authentication = authentication_flow + brand.flow_invalidation = invalidation_flow + brand.attributes = { + **(brand.attributes or {}), + "settings": { + **((brand.attributes or {}).get("settings") or {}), + "locale": "ru", + "theme": { + **(((brand.attributes or {}).get("settings") or {}).get("theme") or {}), + "base": "dark", + }, + }, + } + brand.save() + return brand + + def ensure_provider(spec, mappings): authorization_flow = Flow.objects.get(slug="default-provider-authorization-implicit-consent") - invalidation_flow = Flow.objects.get(slug="default-provider-invalidation-flow") + invalidation_flow = Flow.objects.get(slug="default-invalidation-flow") signing_key = ( CertificateKeyPair.objects.filter(name="authentik Self-signed Certificate").first() or CertificateKeyPair.objects.first() @@ -228,6 +292,7 @@ def ensure_application(spec, provider, groups): @transaction.atomic def main(): + brand = ensure_nodedc_brand() groups = ensure_groups() user = ensure_user_groups(groups) mappings = default_scope_mappings() @@ -243,6 +308,7 @@ def main(): summary = { "groups": list(groups), "admin_user": user.email if user else None, + "brand": brand.domain, "applications": [application.slug for application in applications], "providers": [provider.name for provider in providers], } diff --git a/infra/authentik/custom-templates/base/header_js.html b/infra/authentik/custom-templates/base/header_js.html new file mode 100644 index 0000000..b1a3400 --- /dev/null +++ b/infra/authentik/custom-templates/base/header_js.html @@ -0,0 +1,286 @@ +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} + + + + diff --git a/infra/authentik/custom-templates/branding/nodedc-login.css b/infra/authentik/custom-templates/branding/nodedc-login.css new file mode 100644 index 0000000..6db65ff --- /dev/null +++ b/infra/authentik/custom-templates/branding/nodedc-login.css @@ -0,0 +1,553 @@ +:root, +:host { + --ak-global--primary: rgb(195, 255, 102); + --ak-global--primary-hover: rgb(218, 255, 139); + --ak-global--background: #0e0f10; + --ak-global--background-image: none !important; + --ak-c-login--MaxWidth: 28rem; + --ak-c-login--spacer: 2.2rem; + --ak-c-login__main--BackgroundColor: transparent; + --ak-c-login__main--Color: #f3f3f3; + --ak-c-login__main--BoxShadow: none; + --ak-c-login__footer--PaddingBlock: 0; + --pf-global--primary-color--100: rgb(195, 255, 102); + --pf-global--primary-color--200: rgb(218, 255, 139); + --pf-global--link--Color: rgb(195, 255, 102); + --pf-global--link--Color--hover: rgb(218, 255, 139); + --pf-global--BackgroundColor--100: #0e0f10; + --pf-global--BackgroundColor--light-100: transparent; + --pf-global--Color--100: #f3f3f3; + --pf-global--Color--200: #9d9da3; + --pf-global--BorderRadius--lg: 1.9rem; + --pf-global--BorderRadius--sm: 1.15rem; + --pf-v5-global--BorderRadius--lg: 1.9rem; + --pf-v5-global--BorderRadius--sm: 1.15rem; + --pf-v6-global--BorderRadius--large: 1.9rem; + --pf-c-form-control--BackgroundColor: rgba(255, 255, 255, 0.04); + --pf-c-form-control--BorderColor: transparent; + --pf-c-form-control--BorderBottomColor: transparent; + --pf-v5-c-form-control--BackgroundColor: rgba(255, 255, 255, 0.04); + --pf-v5-c-form-control--BorderColor: transparent; + --pf-v5-c-form-control--BorderBottomColor: transparent; + --nodedc-auth-bg: #0e0f10; + --nodedc-auth-card-bg: rgba(9, 9, 12, 0.84); + --nodedc-auth-primary: rgb(195, 255, 102); + --nodedc-auth-primary-hover: rgb(218, 255, 139); + --nodedc-auth-on-primary: rgb(11, 17, 23); + --nodedc-auth-text-primary: #e4e6e7; + --nodedc-auth-text-secondary: #cacdce; + --nodedc-auth-text-placeholder: #959a9d; + color-scheme: dark; +} + +html, +body, +.pf-c-login { + background: var(--nodedc-auth-bg) !important; + color: var(--nodedc-auth-text-primary) !important; + font-family: Inter, "Inter var", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; + font-synthesis: none; + -webkit-font-smoothing: antialiased; + text-rendering: geometricPrecision; +} + +:host { + background: transparent !important; + color: inherit !important; + font-family: Inter, "Inter var", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; + font-synthesis: none; + -webkit-font-smoothing: antialiased; + text-rendering: geometricPrecision; +} + +:host(.pf-c-login) { + display: flex !important; + min-width: 100vw !important; + min-height: 100vh !important; + align-items: center !important; + justify-content: center !important; + background: var(--nodedc-auth-bg) !important; + color: var(--nodedc-auth-text-primary) !important; +} + +body::before, +.pf-c-login::before { + background: none !important; +} + +body:not(.nodedc-auth-enhanced)::after { + content: ""; + display: block; + position: fixed; + top: 3.4rem; + left: 2.9rem; + width: 152px; + height: 38px; + background: center / contain no-repeat url("data:image/svg+xml,%3Csvg id='nodedc-logo' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220.82 54.55'%3E%3Cpath fill='%23e2e1e1' d='M52.8 23.61 46.92 33.76 41.05 23.61H52.8m18-10.39H23.06l23.86 41.33Z'/%3E%3Cpolygon fill='%23e2e1e1' points='31.28 33.13 18.11 10.34 75.73 10.34 62.59 33.13 74.28 33.13 93.22 0 0 0 19.61 33.13 31.28 33.13'/%3E%3Cpath fill='%23dbdbdb' d='M116.35 18.49V1h1.27l10.34 15V1h1.33v17.49H128l-10.34-15v15Zm24.08.15c-4.79 0-8.16-3.72-8.16-8.89S135.64.86 140.43.86s8.17 3.72 8.17 8.89-3.35 8.89-8.17 8.89Zm0-1.25c4 0 6.79-3.17 6.79-7.64s-2.77-7.64-6.79-7.64-6.77 3.17-6.77 7.64 2.78 7.64 6.77 7.64ZM151.6 18.49V1h5.1c5.54 0 8.79 3.42 8.79 8.74s-3.25 8.74-8.79 8.74Zm1.4-1.25h3.75c4.77 0 7.42-2.92 7.42-7.49s-2.65-7.49-7.42-7.49H153ZM168.49 1h10.77v1.26h-9.42v6.67h7.89v1.25h-7.89v7.06h9.74v1.25h-11.09Zm20.39 17.49V1H194c5.54 0 8.79 3.42 8.79 8.74s-3.25 8.74-8.79 8.74Zm1.35-1.25H194c4.77 0 7.41-2.92 7.41-7.49S198.75 2.26 194 2.26h-3.75Zm14.92-7.49c0-5.24 3.19-8.89 8.11-8.89a6.8 6.8 0 0 1 7.1 5.52h-1.43a5.54 5.54 0 0 0-5.74-4.27c-4.05 0-6.64 3.17-6.64 7.64s2.54 7.64 6.59 7.64a5.46 5.46 0 0 0 5.74-4.29h1.43c-.75 3.52-3.4 5.54-7.15 5.54-4.89 0-8.01-3.59-8.01-8.89Z'/%3E%3C/svg%3E"); + image-rendering: auto; + z-index: 20; + pointer-events: none; +} + +.nodedc-auth-logo { + position: fixed; + top: 3.4rem; + left: 2.9rem; + width: 152px; + height: 38px; + z-index: 20; + pointer-events: none; + color: var(--nodedc-auth-text-primary); +} + +.nodedc-auth-logo svg { + display: block; + width: 100%; + height: 100%; + overflow: visible; + shape-rendering: geometricPrecision; + text-rendering: geometricPrecision; +} + +.pf-c-page__drawer, +.pf-c-drawer, +.pf-c-drawer__main, +.pf-c-drawer__content, +.pf-c-drawer__body { + background: transparent !important; +} + +.pf-c-drawer__body { + display: flex !important; + min-height: 100vh !important; + align-items: center !important; + justify-content: center !important; + padding: 0 !important; +} + +ak-flow-executor.pf-c-login::before, +:host(.pf-c-login)::before { + display: none !important; +} + +ak-flow-executor.pf-c-login { + display: flex !important; + width: 100% !important; + min-height: 100vh !important; + position: relative !important; + align-items: center !important; + justify-content: center !important; + padding-right: 0 !important; + box-sizing: border-box !important; +} + +ak-brand-links, +ak-flow-inspector, +ak-locale-context, +ak-locale-switcher, +ak-locale-select, +ak-flow-executor [slot="footer"], +.pf-c-login__footer, +.pf-c-login__header, +.pf-c-login__main-header.pf-c-brand, +.pf-c-form > p:first-child { + display: none !important; +} + +ak-flow-executor::part(locale-select), +ak-flow-executor::part(footer), +ak-flow-executor::part(branding) { + display: none !important; +} + +ak-flow-executor::part(main), +.pf-c-login__main { + position: fixed !important; + top: 50% !important; + left: 50% !important; + right: auto !important; + transform: translate(-50%, -50%) !important; + width: min(100%, 28rem) !important; + max-width: 28rem !important; + min-height: 0 !important; + margin: 0 !important; + padding: 2.2rem !important; + box-sizing: border-box !important; + overflow: hidden !important; + border: 0 !important; + outline: none !important; + box-shadow: none !important; + border-radius: 1.9rem !important; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.015) 100%), + var(--nodedc-auth-card-bg) !important; + -webkit-backdrop-filter: blur(40px); + backdrop-filter: blur(40px); + justify-content: flex-start !important; +} + +ak-flow-card, +ak-stage-identification, +ak-stage-password { + display: block !important; + width: 100% !important; + background: transparent !important; +} + +.pf-c-login__main-body, +.pf-c-form, +.pf-c-form__group, +.pf-c-form__group-control, +.pf-c-input-group, +.pf-c-input-group__item, +.pf-c-card, +.pf-c-card__body { + background: transparent !important; + box-shadow: none !important; + border: 0 !important; +} + +.pf-c-login__main-header { + display: block !important; + width: 100% !important; + min-width: 0 !important; + padding: 0 0 1.75rem !important; +} + +.pf-c-login__main-header .pf-c-title, +.pf-c-title.pf-m-3xl, +h1.pf-c-title { + display: block !important; + width: 100% !important; + max-width: 22rem !important; + margin: 0 !important; + color: var(--nodedc-auth-text-primary) !important; + font-size: 2rem !important; + line-height: 2.5rem !important; + font-weight: 650 !important; + letter-spacing: -0.045em !important; + text-wrap: balance !important; + white-space: normal !important; + word-break: normal !important; + overflow-wrap: normal !important; + hyphens: none !important; + overflow: visible !important; + line-clamp: unset !important; + -webkit-line-clamp: unset !important; + -webkit-box-orient: initial !important; + text-transform: none !important; +} + +.pf-c-login__main-header::after { + content: "С возвращением в NODE.DC."; + display: block; + max-width: 22rem; + margin-top: 0.75rem; + color: var(--nodedc-auth-text-placeholder); + font-size: 1.9rem; + line-height: 2.5rem; + font-weight: 650; + letter-spacing: -0.045em; + text-transform: none; +} + +.pf-c-login__main-body { + padding: 0 !important; +} + +.pf-c-form { + display: flex !important; + flex-direction: column !important; + gap: 1.05rem !important; +} + +.pf-c-form__group, +ak-flow-input-password.pf-c-form__group { + margin: 0 !important; +} + +.pf-c-form__label, +.pf-c-form__label-text, +label { + color: var(--nodedc-auth-text-placeholder) !important; + font-size: 0.75rem !important; + line-height: 1rem !important; + font-weight: 400 !important; + letter-spacing: -0.01em !important; + text-transform: none !important; +} + +.pf-c-form__label-required { + display: none !important; +} + +label[for="ak-identifier-input"], +label[for="ak-stage-identification-password"] { + font-size: 0 !important; +} + +label[for="ak-identifier-input"] .pf-c-form__label-text, +label[for="ak-identifier-input"] .pf-c-form__label-required, +label[for="ak-stage-identification-password"] .pf-c-form__label-text, +label[for="ak-stage-identification-password"] .pf-c-form__label-required { + display: none !important; +} + +label[for="ak-identifier-input"]::after { + content: "Эл. почта"; + font-size: 0.75rem; + color: var(--nodedc-auth-text-placeholder); + font-weight: 400; +} + +label[for="ak-stage-identification-password"]::after { + content: "Пароль"; + font-size: 0.75rem; + color: var(--nodedc-auth-text-placeholder); + font-weight: 400; +} + +.pf-c-form-control, +.pf-c-input-group .pf-c-form-control, +input.pf-c-form-control { + width: 100% !important; + min-height: 3rem !important; + padding: 0 1rem !important; + border: 0 !important; + border-bottom: 0 !important; + outline: none !important; + box-shadow: none !important; + border-radius: 1.15rem !important; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), + rgba(255, 255, 255, 0.03) !important; + -webkit-backdrop-filter: blur(18px); + backdrop-filter: blur(18px); + color: var(--nodedc-auth-text-primary) !important; + font-size: 0.875rem !important; + font-style: normal !important; + font-weight: 400 !important; + text-transform: none !important; + caret-color: var(--nodedc-auth-primary) !important; +} + +.pf-c-form-control::placeholder, +input.pf-c-form-control::placeholder { + color: var(--nodedc-auth-text-placeholder) !important; + font-style: normal !important; + opacity: 1 !important; +} + +.pf-c-form-control:focus, +input.pf-c-form-control:focus { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), + rgba(255, 255, 255, 0.03) !important; + border: 0 !important; + border-bottom: 0 !important; + outline: none !important; + box-shadow: none !important; +} + +.pf-c-form-control:-webkit-autofill, +.pf-c-form-control:-webkit-autofill:hover, +.pf-c-form-control:-webkit-autofill:focus, +input.pf-c-form-control:-webkit-autofill, +input.pf-c-form-control:-webkit-autofill:hover, +input.pf-c-form-control:-webkit-autofill:focus { + border: 0 !important; + outline: none !important; + -webkit-text-fill-color: var(--nodedc-auth-text-primary) !important; + caret-color: var(--nodedc-auth-primary) !important; + transition: background-color 999999s ease-in-out 0s !important; + box-shadow: inset 0 0 0 1000px rgba(255, 255, 255, 0.03) !important; + -webkit-box-shadow: inset 0 0 0 1000px rgba(255, 255, 255, 0.03) !important; +} + +.pf-c-input-group { + position: relative !important; + display: block !important; + border: 0 !important; + box-shadow: none !important; +} + +.pf-c-form__group:has(#ak-identifier-input) { + position: relative !important; +} + +.pf-c-input-group .pf-c-button.pf-m-control { + position: absolute !important; + top: 0.5rem !important; + right: 0.75rem !important; + display: grid !important; + width: 2rem !important; + min-width: 2rem !important; + height: 2rem !important; + place-items: center !important; + padding: 0 !important; + border: 0 !important; + border-radius: 999px !important; + background: transparent !important; + color: var(--nodedc-auth-text-placeholder) !important; + box-shadow: none !important; + text-decoration: none !important; + appearance: none !important; + -webkit-appearance: none !important; +} + +.pf-c-input-group .pf-c-button.pf-m-control::before, +.pf-c-input-group .pf-c-button.pf-m-control::after, +.pf-c-input-group .pf-c-button.pf-m-control:hover::before, +.pf-c-input-group .pf-c-button.pf-m-control:hover::after, +.pf-c-input-group .pf-c-button.pf-m-control:focus::before, +.pf-c-input-group .pf-c-button.pf-m-control:focus::after { + display: none !important; + border: 0 !important; + box-shadow: none !important; + content: none !important; +} + +.pf-c-input-group .pf-c-button.pf-m-control:hover, +.pf-c-input-group .pf-c-button.pf-m-control:focus, +.pf-c-input-group .pf-c-button.pf-m-control:active { + border: 0 !important; + outline: none !important; + box-shadow: none !important; + background: transparent !important; + text-decoration: none !important; +} + +.pf-c-input-group .pf-c-button.pf-m-control i, +.nodedc-auth-clear-input svg { + width: 1.25rem !important; + height: 1.25rem !important; + color: var(--nodedc-auth-text-placeholder) !important; + opacity: 0.9 !important; +} + +.pf-c-input-group input.pf-c-form-control { + padding-right: 3rem !important; +} + +.nodedc-auth-clear-input { + position: absolute !important; + right: 0.75rem !important; + bottom: 0.5rem !important; + display: none !important; + width: 2rem !important; + height: 2rem !important; + padding: 0 !important; + place-items: center !important; + border: 0 !important; + border-radius: 999px !important; + background: transparent !important; + color: var(--nodedc-auth-text-placeholder) !important; + box-shadow: none !important; + cursor: pointer !important; +} + +.nodedc-auth-clear-input[data-visible="true"] { + display: grid !important; +} + +.pf-c-button.pf-m-primary, +button.pf-m-primary, +button[type="submit"], +.pf-c-form .pf-c-button.pf-m-primary { + width: 100% !important; + min-height: 3.25rem !important; + margin-top: 1.25rem !important; + border: 0 !important; + outline: none !important; + box-shadow: none !important; + border-radius: 1.25rem !important; + background: var(--nodedc-auth-primary) !important; + color: var(--nodedc-auth-on-primary) !important; + font-size: 0.95rem !important; + font-weight: 600 !important; + letter-spacing: -0.02em !important; + text-transform: none !important; +} + +.pf-c-button.pf-m-primary:hover, +button.pf-m-primary:hover, +button[type="submit"]:hover { + background: var(--nodedc-auth-primary-hover) !important; + color: var(--nodedc-auth-on-primary) !important; +} + +.pf-c-alert, +.pf-c-alert.pf-m-inline { + border: 0 !important; + border-radius: 1rem !important; + margin: 0 0 1.15rem !important; + padding: 0 !important; + background: transparent !important; + color: var(--nodedc-auth-primary) !important; + box-shadow: none !important; +} + +.pf-c-alert__icon, +.pf-c-alert__title, +.pf-c-alert__description { + color: var(--nodedc-auth-primary) !important; + font-size: 0.75rem !important; + line-height: 1rem !important; + font-weight: 600 !important; +} + +a, +.pf-c-button.pf-m-link, +button.pf-m-link { + color: var(--nodedc-auth-primary) !important; + font-size: 0.75rem !important; + line-height: 1rem !important; + font-weight: 600 !important; + text-decoration: none !important; +} + +body.nodedc-auth-submitting ak-flow-executor::part(main), +body.nodedc-auth-submitting .pf-c-login__main, +body.nodedc-auth-submitting .pf-c-empty-state, +body.nodedc-auth-submitting .pf-c-spinner, +body.nodedc-auth-submitting [role="progressbar"] { + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; +} + +ak-library-application, +ak-application-wizard, +ak-user-interface, +.pf-c-page__sidebar, +.pf-c-page__header, +.pf-c-nav, +.pf-c-brand { + color-scheme: dark; +} + +@media (max-width: 640px) { + ak-flow-executor.pf-c-login::before, + :host(.pf-c-login)::before { + display: none !important; + } + + body::after, + .nodedc-auth-logo { + top: 2rem; + left: 1.5rem; + width: 132px; + height: 29px; + } + + ak-flow-executor::part(main), + .pf-c-login__main { + position: fixed !important; + top: 50% !important; + right: auto !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + width: calc(100vw - 2rem) !important; + padding: 1.5rem !important; + } +} diff --git a/infra/reverse-proxy/Caddyfile b/infra/reverse-proxy/Caddyfile index d1f9994..c4cb849 100644 --- a/infra/reverse-proxy/Caddyfile +++ b/infra/reverse-proxy/Caddyfile @@ -3,6 +3,12 @@ } http://{$AUTH_DOMAIN:auth.local.nodedc} { + @auth_root path / + redir @auth_root http://{$LAUNCHER_DOMAIN:launcher.local.nodedc}/ 302 + + @auth_user_dashboard path /if/user /if/user/* + redir @auth_user_dashboard http://{$LAUNCHER_DOMAIN:launcher.local.nodedc}/ 302 + reverse_proxy authentik-server:9000 { header_up Host {host} header_up X-Forwarded-Proto {scheme} diff --git a/infra/scripts/bootstrap-authentik-dev.sh b/infra/scripts/bootstrap-authentik-dev.sh index 0c0ebfc..c72e8e3 100755 --- a/infra/scripts/bootstrap-authentik-dev.sh +++ b/infra/scripts/bootstrap-authentik-dev.sh @@ -67,6 +67,7 @@ set +a cd "$ROOT_DIR" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T \ + -e AUTH_DOMAIN="${AUTH_DOMAIN:-auth.local.nodedc}" \ -e NODEDC_BOOTSTRAP_ADMIN_EMAIL="${NODEDC_BOOTSTRAP_ADMIN_EMAIL:-}" \ -e NODEDC_BOOTSTRAP_ADMIN_PASSWORD="${NODEDC_BOOTSTRAP_ADMIN_PASSWORD:-}" \ -e LAUNCHER_OIDC_CLIENT_ID="$LAUNCHER_OIDC_CLIENT_ID" \