From 61b856254b06dd172f240d28cfa8e365bab1a4eb Mon Sep 17 00:00:00 2001 From: vigdorov Date: Wed, 14 Jan 2026 01:38:39 +0300 Subject: [PATCH] add custom theme --- .drone.yml | 62 +++ k8s/backend-deployment.yaml | 2 + keycloak-theme/Dockerfile | 4 + keycloak-theme/README.md | 102 +++++ keycloak-theme/team-planner/login/login.ftl | 82 ++++ .../login/messages/messages_ru.properties | 15 + .../login/resources/css/login.css | 356 ++++++++++++++++++ .../team-planner/login/theme.properties | 5 + ...33d5db6370b6de345e990751aa1f1da65ad675.png | Bin 0 -> 4253 bytes tests/playwright-report/index.html | 85 +++++ tests/test-results/.last-run.json | 6 +- .../test-failed-1.png | Bin 0 -> 4253 bytes 12 files changed, 717 insertions(+), 2 deletions(-) create mode 100644 keycloak-theme/Dockerfile create mode 100644 keycloak-theme/README.md create mode 100644 keycloak-theme/team-planner/login/login.ftl create mode 100644 keycloak-theme/team-planner/login/messages/messages_ru.properties create mode 100644 keycloak-theme/team-planner/login/resources/css/login.css create mode 100644 keycloak-theme/team-planner/login/theme.properties create mode 100644 tests/playwright-report/data/7a33d5db6370b6de345e990751aa1f1da65ad675.png create mode 100644 tests/playwright-report/index.html create mode 100644 tests/test-results/auth.setup.ts-authenticate-setup/test-failed-1.png diff --git a/.drone.yml b/.drone.yml index b79f433..e1cfb9f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -70,6 +70,32 @@ steps: from_secret: HARBOR_PASSWORD no_push_metadata: true +# --- Сборка Keycloak темы --- +- name: build-keycloak-theme + image: plugins/kaniko + when: + changeset: + includes: + - keycloak-theme/** + - .drone.yml + excludes: + - keycloak-theme/README.md + - keycloak-theme/**/*.md + settings: + registry: registry.vigdorov.ru + repo: registry.vigdorov.ru/library/keycloak-team-planner + dockerfile: keycloak-theme/Dockerfile + context: keycloak-theme + tags: + - ${DRONE_COMMIT_SHA:0:7} + - "26.5.0" + - latest + username: + from_secret: HARBOR_USER + password: + from_secret: HARBOR_PASSWORD + no_push_metadata: true + # ============================================================ # ДЕПЛОЙ (только после завершения ОБЕИХ сборок) # ============================================================ @@ -194,6 +220,42 @@ steps: fi - echo "✅ Frontend deployed to PROD (image:$IMAGE_TAG)" +# --- Развертывание Keycloak темы --- +- name: deploy-keycloak-theme + image: alpine/k8s:1.28.2 + depends_on: + - build-keycloak-theme + when: + changeset: + includes: + - keycloak-theme/** + - .drone.yml + excludes: + - keycloak-theme/README.md + - keycloak-theme/**/*.md + environment: + KUBE_CONFIG_CONTENT: + from_secret: KUBE_CONFIG + commands: + - mkdir -p ~/.kube + - echo "$KUBE_CONFIG_CONTENT" > ~/.kube/config + - chmod 600 ~/.kube/config + - sed -i "s|https://127.0.0.1:6443|https://10.10.10.100:6443|g" ~/.kube/config + - export KEYCLOAK_NAMESPACE="auth" + - export IMAGE_TAG="${DRONE_COMMIT_SHA:0:7}" + - export KEYCLOAK_IMAGE="registry.vigdorov.ru/library/keycloak-team-planner:$IMAGE_TAG" + - kubectl cluster-info + - kubectl set image statefulset/keycloak-keycloakx keycloak-keycloakx=$KEYCLOAK_IMAGE -n $KEYCLOAK_NAMESPACE + - echo "📋 Waiting for rollout..." + - | + if ! kubectl rollout status statefulset/keycloak-keycloakx -n $KEYCLOAK_NAMESPACE --timeout=180s; then + echo "❌ Rollout failed! Collecting diagnostics..." + kubectl get pods -n $KEYCLOAK_NAMESPACE -l app.kubernetes.io/name=keycloakx -o wide + kubectl describe statefulset keycloak-keycloakx -n $KEYCLOAK_NAMESPACE + exit 1 + fi + - echo "✅ Keycloak theme deployed (image:$IMAGE_TAG)" + --- kind: pipeline type: kubernetes diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml index 568390c..ffe1599 100644 --- a/k8s/backend-deployment.yaml +++ b/k8s/backend-deployment.yaml @@ -43,6 +43,8 @@ spec: secretKeyRef: name: team-planner-secrets key: db-password + - name: KEYCLOAK_REALM_URL + value: "https://auth.vigdorov.ru/realms/team-planner" resources: requests: memory: "256Mi" diff --git a/keycloak-theme/Dockerfile b/keycloak-theme/Dockerfile new file mode 100644 index 0000000..357fb93 --- /dev/null +++ b/keycloak-theme/Dockerfile @@ -0,0 +1,4 @@ +FROM quay.io/keycloak/keycloak:26.5.0 + +# Копируем кастомную тему +COPY team-planner /opt/keycloak/themes/team-planner diff --git a/keycloak-theme/README.md b/keycloak-theme/README.md new file mode 100644 index 0000000..d53c9c4 --- /dev/null +++ b/keycloak-theme/README.md @@ -0,0 +1,102 @@ +# Team Planner Keycloak Theme + +Кастомная тема для Keycloak, стилизованная под основное приложение Team Planner (Material UI). + +## Структура + +``` +team-planner/ +├── login/ +│ ├── theme.properties # Настройки темы +│ ├── login.ftl # Шаблон страницы входа +│ ├── resources/ +│ │ └── css/ +│ │ └── login.css # Стили MUI +│ └── messages/ +│ └── messages_ru.properties # Русские переводы +``` + +## Установка + +### Вариант 1: Volume mount (рекомендуется для dev) + +```yaml +# docker-compose.yml +services: + keycloak: + image: quay.io/keycloak/keycloak:latest + volumes: + - ./keycloak-theme/team-planner:/opt/keycloak/themes/team-planner +``` + +### Вариант 2: Dockerfile (рекомендуется для production) + +```dockerfile +FROM quay.io/keycloak/keycloak:latest + +COPY team-planner /opt/keycloak/themes/team-planner +``` + +### Вариант 3: Kubernetes ConfigMap + +```bash +# Создать ConfigMap из директории темы +kubectl create configmap keycloak-theme \ + --from-file=team-planner/login/ \ + -n keycloak + +# Примонтировать в deployment +``` + +## Активация темы + +1. Войдите в Keycloak Admin Console +2. Выберите realm `team-planner` +3. Перейдите в **Realm Settings** → **Themes** +4. В поле **Login theme** выберите `team-planner` +5. Нажмите **Save** + +Или через Keycloak CLI: + +```bash +/opt/keycloak/bin/kcadm.sh update realms/team-planner \ + -s loginTheme=team-planner \ + --server http://localhost:8080 \ + --realm master \ + --user admin +``` + +## Разработка + +Для разработки темы: + +1. Запустите Keycloak с примонтированной темой +2. Включите кэширование темы в dev режиме: + ``` + KC_SPI_THEME_STATIC_MAX_AGE=-1 + KC_SPI_THEME_CACHE_THEMES=false + KC_SPI_THEME_CACHE_TEMPLATES=false + ``` +3. Изменения в CSS/FTL применяются после обновления страницы + +## Кастомизация + +### Цвета + +Основные цвета определены в CSS переменных в `login.css`: + +```css +:root { + --primary-color: #1976d2; /* MUI primary */ + --primary-hover: #1565c0; + --text-primary: rgba(0, 0, 0, 0.87); + --text-secondary: rgba(0, 0, 0, 0.6); + --background: #f5f5f5; + --surface: #ffffff; +} +``` + +### Тексты + +Русские переводы в `messages/messages_ru.properties`. +Для других языков создайте `messages_XX.properties`. diff --git a/keycloak-theme/team-planner/login/login.ftl b/keycloak-theme/team-planner/login/login.ftl new file mode 100644 index 0000000..644d7a0 --- /dev/null +++ b/keycloak-theme/team-planner/login/login.ftl @@ -0,0 +1,82 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section> + <#if section = "header"> +
+

Team Planner

+

Приложение для управления бэклогом идей команды

+
+ <#elseif section = "form"> +
+
+ <#if realm.password> +
+
+ + + <#if messagesPerField.existsError('username','password')> + + ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} + + +
+ +
+ + +
+ +
+ <#if realm.rememberMe && !usernameHidden??> +
+ checked> + +
+ + <#if realm.resetPasswordAllowed> + ${msg("doForgotPassword")} + +
+ +
+ value="${auth.selectedCredential}"/> + +
+
+ +
+
+ <#elseif section = "info"> +
+ <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+ ${msg("noAccount")} ${msg("doRegister")} +
+ +

+ Для получения доступа обратитесь к Николаю Вигдорову +

+
+ <#elseif section = "socialProviders"> + <#if realm.password && social.providers??> +
+

Или войти через

+ +
+ + + diff --git a/keycloak-theme/team-planner/login/messages/messages_ru.properties b/keycloak-theme/team-planner/login/messages/messages_ru.properties new file mode 100644 index 0000000..12fc21b --- /dev/null +++ b/keycloak-theme/team-planner/login/messages/messages_ru.properties @@ -0,0 +1,15 @@ +# Russian translations for Team Planner theme +loginAccountTitle=Вход в Team Planner +loginTitle=Вход +loginTitleHtml=Вход +usernameOrEmail=Имя пользователя или email +username=Имя пользователя +email=Email +password=Пароль +rememberMe=Запомнить меня +doLogIn=Войти +doRegister=Зарегистрироваться +doForgotPassword=Забыли пароль? +noAccount=Нет аккаунта? +invalidUserMessage=Неверное имя пользователя или пароль. +invalidPasswordMessage=Неверное имя пользователя или пароль. diff --git a/keycloak-theme/team-planner/login/resources/css/login.css b/keycloak-theme/team-planner/login/resources/css/login.css new file mode 100644 index 0000000..76c3503 --- /dev/null +++ b/keycloak-theme/team-planner/login/resources/css/login.css @@ -0,0 +1,356 @@ +/* Team Planner - Keycloak Login Theme */ +/* Стилизация под MUI (Material Design) */ + +:root { + --primary-color: #1976d2; + --primary-hover: #1565c0; + --primary-light: #42a5f5; + --text-primary: rgba(0, 0, 0, 0.87); + --text-secondary: rgba(0, 0, 0, 0.6); + --background: #f5f5f5; + --surface: #ffffff; + --error: #d32f2f; + --border-radius: 4px; + --shadow: 0px 3px 3px -2px rgba(0,0,0,0.2), 0px 3px 4px 0px rgba(0,0,0,0.14), 0px 1px 8px 0px rgba(0,0,0,0.12); +} + +/* Reset и базовые стили */ +* { + box-sizing: border-box; +} + +body { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + background-color: var(--background); + margin: 0; + padding: 0; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +/* Скрываем стандартный header Keycloak */ +#kc-header { + display: none; +} + +#kc-header-wrapper { + display: none; +} + +/* Основной контейнер */ +.login-pf { + background-color: var(--background); +} + +#kc-content { + width: 100%; + max-width: 400px; + margin: 0 auto; +} + +#kc-content-wrapper { + width: 100%; +} + +/* Карточка логина */ +#kc-form-wrapper, +.card-pf { + background: var(--surface); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + padding: 48px; + width: 100%; + max-width: 400px; + margin: 20px; +} + +/* Заголовок */ +#kc-page-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + text-align: center; + margin-bottom: 8px; +} + +/* Подзаголовок - скрываем дефолтный */ +#kc-locale { + display: none; +} + +/* Кастомный заголовок */ +#kc-form-login::before { + content: "Team Planner"; + display: block; + font-size: 1.75rem; + font-weight: 700; + color: var(--text-primary); + text-align: center; + margin-bottom: 8px; +} + +#kc-form-login::after { + content: "Приложение для управления бэклогом идей команды"; + display: block; + font-size: 0.875rem; + color: var(--text-secondary); + text-align: center; + margin-bottom: 32px; +} + +/* Группы форм */ +.form-group { + margin-bottom: 24px; +} + +/* Лейблы */ +.form-group label, +.control-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 8px; +} + +/* Инпуты */ +.form-control, +input[type="text"], +input[type="password"], +input[type="email"] { + width: 100%; + padding: 16.5px 14px; + font-size: 1rem; + font-family: inherit; + color: var(--text-primary); + background-color: var(--surface); + border: 1px solid rgba(0, 0, 0, 0.23); + border-radius: var(--border-radius); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.form-control:hover, +input[type="text"]:hover, +input[type="password"]:hover, +input[type="email"]:hover { + border-color: var(--text-primary); +} + +.form-control:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus { + border-color: var(--primary-color); + border-width: 2px; + padding: 15.5px 13px; + box-shadow: none; +} + +/* Кнопка входа */ +#kc-login, +.btn-primary, +input[type="submit"] { + width: 100%; + padding: 12px 24px; + font-size: 0.9375rem; + font-weight: 500; + font-family: inherit; + text-transform: uppercase; + letter-spacing: 0.02857em; + color: #ffffff; + background-color: var(--primary-color); + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: background-color 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12); +} + +#kc-login:hover, +.btn-primary:hover, +input[type="submit"]:hover { + background-color: var(--primary-hover); + box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12); +} + +#kc-login:active, +.btn-primary:active, +input[type="submit"]:active { + box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2), 0px 8px 10px 1px rgba(0,0,0,0.14), 0px 3px 14px 2px rgba(0,0,0,0.12); +} + +/* Ссылки */ +a { + color: var(--primary-color); + text-decoration: none; + font-size: 0.875rem; +} + +a:hover { + text-decoration: underline; +} + +/* Ссылка "Забыли пароль?" */ +#kc-form-options { + margin-top: 16px; + text-align: center; +} + +/* Чекбокс "Запомнить меня" */ +.checkbox { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 24px; +} + +.checkbox input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--primary-color); + cursor: pointer; +} + +.checkbox label { + margin: 0; + font-size: 0.875rem; + color: var(--text-secondary); + cursor: pointer; +} + +/* Сообщения об ошибках */ +.alert, +.alert-error, +.kc-feedback-text { + background-color: #fdeded; + color: var(--error); + border: 1px solid #f5c6cb; + border-radius: var(--border-radius); + padding: 12px 16px; + margin-bottom: 24px; + font-size: 0.875rem; +} + +.alert-success { + background-color: #edf7ed; + color: #1e4620; + border-color: #c3e6cb; +} + +.alert-warning { + background-color: #fff4e5; + color: #663c00; + border-color: #ffeeba; +} + +/* Информационное сообщение внизу */ +#kc-registration { + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid rgba(0, 0, 0, 0.12); + text-align: center; +} + +#kc-registration span { + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Социальные провайдеры */ +#kc-social-providers { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid rgba(0, 0, 0, 0.12); +} + +#kc-social-providers h4 { + font-size: 0.875rem; + color: var(--text-secondary); + text-align: center; + margin-bottom: 16px; +} + +.social-provider-button { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + margin-bottom: 8px; + font-size: 0.875rem; + color: var(--text-primary); + background-color: var(--surface); + border: 1px solid rgba(0, 0, 0, 0.23); + border-radius: var(--border-radius); + cursor: pointer; + transition: background-color 0.2s ease; +} + +.social-provider-button:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +/* Footer - добавляем информацию о контакте */ +#kc-info-wrapper { + margin-top: 24px; + text-align: center; +} + +#kc-info-wrapper::after { + content: "Для получения доступа обратитесь к Николаю Вигдорову"; + display: block; + font-size: 0.875rem; + color: var(--text-secondary); + margin-top: 16px; +} + +/* Адаптивность */ +@media (max-width: 480px) { + #kc-form-wrapper, + .card-pf { + margin: 10px; + padding: 32px 24px; + } + + #kc-form-login::before { + font-size: 1.5rem; + } +} + +/* Скрываем ненужные элементы */ +.pf-c-alert__icon, +.login-pf-page .login-pf-page-header, +#kc-attempted-username { + display: none; +} + +/* Фикс для Keycloak 21+ */ +.pf-c-login__main { + background: var(--surface); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + padding: 48px; + max-width: 400px; + margin: 20px auto; +} + +.pf-c-form-control { + width: 100%; + padding: 16.5px 14px; + font-size: 1rem; + border: 1px solid rgba(0, 0, 0, 0.23); + border-radius: var(--border-radius); +} + +.pf-c-button.pf-m-primary { + width: 100%; + padding: 12px 24px; + font-size: 0.9375rem; + background-color: var(--primary-color); + border: none; + border-radius: var(--border-radius); +} diff --git a/keycloak-theme/team-planner/login/theme.properties b/keycloak-theme/team-planner/login/theme.properties new file mode 100644 index 0000000..e523564 --- /dev/null +++ b/keycloak-theme/team-planner/login/theme.properties @@ -0,0 +1,5 @@ +# Team Planner Keycloak Theme +parent=keycloak +import=common/keycloak + +styles=css/login.css diff --git a/tests/playwright-report/data/7a33d5db6370b6de345e990751aa1f1da65ad675.png b/tests/playwright-report/data/7a33d5db6370b6de345e990751aa1f1da65ad675.png new file mode 100644 index 0000000000000000000000000000000000000000..6d360f6bba60307ddce12a4bda5ae0e2ff9278b8 GIT binary patch literal 4253 zcmeAS@N?(olHy`uVBq!ia0y~yUeX7 q@D_FkhX4QX9*X@7G?5KtA~VB;)qHl1Z#nXSA`G6celF{r5}E*b2*WS{ literal 0 HcmV?d00001 diff --git a/tests/playwright-report/index.html b/tests/playwright-report/index.html new file mode 100644 index 0000000..1db288e --- /dev/null +++ b/tests/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/tests/test-results/.last-run.json b/tests/test-results/.last-run.json index cbcc1fb..75e7388 100644 --- a/tests/test-results/.last-run.json +++ b/tests/test-results/.last-run.json @@ -1,4 +1,6 @@ { - "status": "passed", - "failedTests": [] + "status": "failed", + "failedTests": [ + "17e3fe6f4d9d8bd79c6b-60f085b113a677673906" + ] } \ No newline at end of file diff --git a/tests/test-results/auth.setup.ts-authenticate-setup/test-failed-1.png b/tests/test-results/auth.setup.ts-authenticate-setup/test-failed-1.png new file mode 100644 index 0000000000000000000000000000000000000000..6d360f6bba60307ddce12a4bda5ae0e2ff9278b8 GIT binary patch literal 4253 zcmeAS@N?(olHy`uVBq!ia0y~yUeX7 q@D_FkhX4QX9*X@7G?5KtA~VB;)qHl1Z#nXSA`G6celF{r5}E*b2*WS{ literal 0 HcmV?d00001