diff --git a/infrastructure/local/docker-compose.yaml b/infrastructure/local/docker-compose.yaml index ec3481b..42cc84d 100644 --- a/infrastructure/local/docker-compose.yaml +++ b/infrastructure/local/docker-compose.yaml @@ -44,10 +44,20 @@ services: - "8180:8080" volumes: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json - command: start-dev --import-realm + - ./keycloak/themes/skyroster:/opt/keycloak/themes/skyroster + # NOTE: --import-realm uses IGNORE_EXISTING strategy. Changes to realm-export.json + # are NOT re-applied on existing realms. To pick up changes locally, run: + # docker compose down -v && docker compose --profile up -d + command: > + start-dev + --import-realm + --spi-theme-cache-themes=false + --spi-theme-cache-templates=false + --spi-theme-static-max-age=-1 depends_on: postgres: condition: service_healthy + restart: on-failure:10 frontend: build: diff --git a/infrastructure/local/keycloak/realm-export.json b/infrastructure/local/keycloak/realm-export.json index 18e3ecc..249e5b7 100644 --- a/infrastructure/local/keycloak/realm-export.json +++ b/infrastructure/local/keycloak/realm-export.json @@ -2,6 +2,10 @@ "realm": "skyroster", "enabled": true, "sslRequired": "none", + "loginTheme": "skyroster", + "internationalizationEnabled": true, + "supportedLocales": ["pl", "en"], + "defaultLocale": "pl", "roles": { "realm": [ { "name": "operations_administrator", "composite": false }, diff --git a/infrastructure/local/keycloak/themes/skyroster/login/error.ftl b/infrastructure/local/keycloak/themes/skyroster/login/error.ftl new file mode 100644 index 0000000..ca05f6f --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/error.ftl @@ -0,0 +1,15 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + ${msg("errorTitle")} + <#elseif section = "form"> + + <#if client?? && client.baseUrl?has_content> + ${msg("backToLogin")} + <#else> + ${msg("backToLogin")} + + + diff --git a/infrastructure/local/keycloak/themes/skyroster/login/login.ftl b/infrastructure/local/keycloak/themes/skyroster/login/login.ftl new file mode 100644 index 0000000..bc2f4cc --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/login.ftl @@ -0,0 +1,59 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password'); section> + <#if section = "header"> + ${msg("loginAccountTitle")} + <#elseif section = "form"> +
+ +
+ + + + <#if messagesPerField.existsError('username','password')> + ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} + + +
+ +
+ + +
+ + <#if realm.rememberMe && !usernameHidden??> +
+ checked + /> + +
+ + + value="${auth.selectedCredential}"/> + + + +
+ + diff --git a/infrastructure/local/keycloak/themes/skyroster/login/messages/messages_en.properties b/infrastructure/local/keycloak/themes/skyroster/login/messages/messages_en.properties new file mode 100644 index 0000000..dd6d170 --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/messages/messages_en.properties @@ -0,0 +1,2 @@ +loginAccountTitle=Sign in to SkyRoster +backToLogin=Back to login diff --git a/infrastructure/local/keycloak/themes/skyroster/login/messages/messages_pl.properties b/infrastructure/local/keycloak/themes/skyroster/login/messages/messages_pl.properties new file mode 100644 index 0000000..741e244 --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/messages/messages_pl.properties @@ -0,0 +1,9 @@ +loginAccountTitle=Zaloguj się do SkyRoster +usernameOrEmail=Nazwa użytkownika +password=Hasło +doLogIn=Zaloguj się +rememberMe=Zapamiętaj mnie +errorTitle=Wystąpił błąd +backToLogin=Wróć do logowania +invalidUserMessage=Nieprawidłowa nazwa użytkownika lub hasło +accountDisabledMessage=Konto jest wyłączone, skontaktuj się z administratorem diff --git a/infrastructure/local/keycloak/themes/skyroster/login/resources/css/theme.css b/infrastructure/local/keycloak/themes/skyroster/login/resources/css/theme.css new file mode 100644 index 0000000..b22cee8 --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/resources/css/theme.css @@ -0,0 +1,242 @@ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/Inter-Regular.woff2") format("woff2"); +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("../fonts/Inter-SemiBold.woff2") format("woff2"); +} + +:root { + --sr-primary-500: #10b981; + --sr-primary-600: #059669; + --sr-primary-50: #ecfdf5; + + --sr-surface-0: #ffffff; + --sr-surface-50: #f8fafc; + --sr-surface-100: #f1f5f9; + --sr-surface-200: #e2e8f0; + --sr-surface-700: #334155; + --sr-surface-900: #0f172a; + + --sr-text: var(--sr-surface-900); + --sr-text-muted: var(--sr-surface-700); + --sr-border: var(--sr-surface-200); + --sr-danger: #ef4444; + + --sr-radius-input: 6px; + --sr-radius-card: 12px; + --sr-shadow-card: 0 10px 30px rgba(15, 23, 42, 0.08); + --sr-ring: rgba(16, 185, 129, 0.2); + --sr-ring-strong: rgba(16, 185, 129, 0.35); + + --sr-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +html, body { + margin: 0; + padding: 0; + font-family: var(--sr-font); + color: var(--sr-text); + background: linear-gradient(180deg, var(--sr-surface-50) 0%, var(--sr-surface-100) 100%); + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { box-sizing: border-box; } + +.sr-card-wrapper { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.sr-card { + width: 100%; + max-width: 420px; + background: var(--sr-surface-0); + border-radius: var(--sr-radius-card); + box-shadow: var(--sr-shadow-card); + padding: 32px; +} + +.sr-card__header { + margin-bottom: 24px; +} + +.sr-brand { + margin: 0; + font-size: 24px; + font-weight: 600; + color: var(--sr-text); + letter-spacing: -0.01em; +} + +.sr-card__subtitle { + margin: 4px 0 0; + color: var(--sr-text-muted); + font-size: 14px; +} + +.sr-card__body { + display: flex; + flex-direction: column; + gap: 16px; +} + +.sr-alert { + padding: 10px 12px; + border-radius: var(--sr-radius-input); + font-size: 14px; + margin-bottom: 16px; +} + +.sr-alert--error { + background: #fef2f2; + color: var(--sr-danger); + border: 1px solid #fecaca; +} + +.sr-alert--success { + background: var(--sr-primary-50); + color: var(--sr-primary-600); + border: 1px solid #a7f3d0; +} + +.sr-alert--warning { + background: #fffbeb; + color: #b45309; + border: 1px solid #fde68a; +} + +.sr-alert--info { + background: var(--sr-surface-50); + color: var(--sr-text-muted); + border: 1px solid var(--sr-border); +} + +@media (max-width: 480px) { + .sr-card-wrapper { padding: 16px; } + .sr-card { padding: 24px; } +} + +#kc-form-login { + display: flex; + flex-direction: column; + gap: 16px; +} + +.sr-field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.sr-field__error:empty { + display: none; +} + +.sr-label { + font-size: 13px; + font-weight: 600; + color: var(--sr-text); +} + +.sr-input { + width: 100%; + padding: 10px 12px; + font-size: 14px; + font-family: var(--sr-font); + color: var(--sr-text); + background: var(--sr-surface-0); + border: 1px solid var(--sr-border); + border-radius: var(--sr-radius-input); + transition: border-color 120ms, box-shadow 120ms; +} + +.sr-input:focus { + outline: none; + border-color: var(--sr-primary-500); + box-shadow: 0 0 0 3px var(--sr-ring); +} + +.sr-input[aria-invalid="true"] { + border-color: var(--sr-danger); +} + +.sr-field__error { + font-size: 12px; + color: var(--sr-danger); +} + +.sr-checkbox { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--sr-text-muted); +} + +.sr-checkbox input[type="checkbox"] { + accent-color: var(--sr-primary-500); + width: 16px; + height: 16px; +} + +.sr-button { + font-family: var(--sr-font); + font-size: 14px; + font-weight: 600; + border-radius: var(--sr-radius-input); + cursor: pointer; + transition: background-color 120ms, border-color 120ms, transform 60ms; + border: 1px solid transparent; +} + +.sr-button:active { transform: translateY(1px); } + +.sr-button--primary { + width: 100%; + height: 44px; + background: var(--sr-primary-500); + color: #ffffff; +} + +.sr-button--primary:hover { background: var(--sr-primary-600); } + +.sr-button--primary:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--sr-ring-strong); +} + +.sr-button--secondary { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + background: var(--sr-surface-0); + color: var(--sr-primary-600); + border: 1px solid var(--sr-border); + text-decoration: none; +} + +.sr-button--secondary:hover { + background: var(--sr-surface-50); + border-color: var(--sr-primary-500); +} + +.sr-button--secondary:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--sr-ring-strong); +} diff --git a/infrastructure/local/keycloak/themes/skyroster/login/resources/fonts/Inter-Regular.woff2 b/infrastructure/local/keycloak/themes/skyroster/login/resources/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..b8699af Binary files /dev/null and b/infrastructure/local/keycloak/themes/skyroster/login/resources/fonts/Inter-Regular.woff2 differ diff --git a/infrastructure/local/keycloak/themes/skyroster/login/resources/fonts/Inter-SemiBold.woff2 b/infrastructure/local/keycloak/themes/skyroster/login/resources/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..95c48b1 Binary files /dev/null and b/infrastructure/local/keycloak/themes/skyroster/login/resources/fonts/Inter-SemiBold.woff2 differ diff --git a/infrastructure/local/keycloak/themes/skyroster/login/template.ftl b/infrastructure/local/keycloak/themes/skyroster/login/template.ftl new file mode 100644 index 0000000..3739032 --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/template.ftl @@ -0,0 +1,42 @@ +<#macro registrationLayout displayInfo=false displayMessage=true displayRequiredFields=false> + + + + + + ${msg("loginTitle", (realm.displayName!realm.name))} + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + + +
+
+
+

SkyRoster

+

<#nested "header">

+
+ + <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)> +
+ ${kcSanitize(message.summary)?no_esc} +
+ + +
+ <#nested "form"> +
+ + <#if displayInfo> +
+ <#nested "info"> +
+ +
+
+ + + diff --git a/infrastructure/local/keycloak/themes/skyroster/login/theme.properties b/infrastructure/local/keycloak/themes/skyroster/login/theme.properties new file mode 100644 index 0000000..e2aa5e1 --- /dev/null +++ b/infrastructure/local/keycloak/themes/skyroster/login/theme.properties @@ -0,0 +1,4 @@ +parent=keycloak.v2 +import=common/keycloak +styles=css/theme.css +locales=en,pl diff --git a/infrastructure/local/postgres/init-databases.sh b/infrastructure/local/postgres/init-databases.sh old mode 100644 new mode 100755 index 2a704b0..863e970 --- a/infrastructure/local/postgres/init-databases.sh +++ b/infrastructure/local/postgres/init-databases.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL +psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" <<-EOSQL CREATE DATABASE keycloak; - GRANT ALL PRIVILEGES ON DATABASE keycloak TO $POSTGRES_USER; + GRANT ALL PRIVILEGES ON DATABASE keycloak TO "${POSTGRES_USER}"; EOSQL \ No newline at end of file