Compare commits
10 Commits
16d9b3fd35
...
4484b70c19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4484b70c19 | ||
|
|
7d31714908 | ||
|
|
40c36ef299 | ||
|
|
cad8f5bb91 | ||
|
|
5af94062d2 | ||
|
|
832be361c7 | ||
|
|
1ed5c020b1 | ||
|
|
b6dde137b4 | ||
|
|
74aec1f1c9 | ||
|
|
88723b5e5f |
156
ansible/deploy.yml
Normal file
156
ansible/deploy.yml
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
# =============================================================
|
||||
# ecosplay-auth deploy playbook (local execution on the server)
|
||||
#
|
||||
# Assumes:
|
||||
# - This repo is cloned at {{ deploy_dir }} (default /var/www/e-auth)
|
||||
# and the playbook is invoked from inside ansible/.
|
||||
# - Caddy is already installed on the server with the
|
||||
# caddy-dns/cloudflare plugin and loads per-site files from
|
||||
# /etc/caddy/sites/*.conf.
|
||||
# - The user running `ansible-playbook` has passwordless sudo.
|
||||
#
|
||||
# Usage:
|
||||
# cd /var/www/e-auth/ansible
|
||||
# ansible-playbook deploy.yml
|
||||
# =============================================================
|
||||
|
||||
- name: Deploy ecosplay-auth (Keycloak + Caddy vhost)
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
# Root of the repo (the parent of the ansible/ directory).
|
||||
deploy_dir: "{{ playbook_dir | dirname }}"
|
||||
|
||||
tasks:
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# System prerequisites
|
||||
# ---------------------------------------------------------
|
||||
- name: Install base packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- python3-apt
|
||||
- rsync
|
||||
update_cache: yes
|
||||
state: present
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Docker Engine + compose plugin (idempotent)
|
||||
# ---------------------------------------------------------
|
||||
- name: Ensure /etc/apt/keyrings exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Add Docker GPG key
|
||||
ansible.builtin.get_url:
|
||||
url: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
|
||||
dest: /etc/apt/keyrings/docker.asc
|
||||
mode: "0644"
|
||||
|
||||
- name: Add Docker apt repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: >-
|
||||
deb [arch={{ ansible_architecture |
|
||||
replace('x86_64', 'amd64') |
|
||||
replace('aarch64', 'arm64') }}
|
||||
signed-by=/etc/apt/keyrings/docker.asc]
|
||||
https://download.docker.com/linux/{{ ansible_distribution | lower }}
|
||||
{{ ansible_distribution_release }} stable
|
||||
state: present
|
||||
filename: docker
|
||||
update_cache: yes
|
||||
|
||||
- name: Install Docker Engine + compose plugin
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
|
||||
- name: Ensure Docker service is running
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Repo files (already present at {{ deploy_dir }})
|
||||
# ---------------------------------------------------------
|
||||
- name: Ensure deploy directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ deploy_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure init/sync.sh is executable
|
||||
ansible.builtin.file:
|
||||
path: "{{ deploy_dir }}/init/sync.sh"
|
||||
mode: "0755"
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Caddy vhost for auth.e-cosplay.fr
|
||||
# ---------------------------------------------------------
|
||||
- name: Ensure /etc/caddy/sites directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/caddy/sites
|
||||
state: directory
|
||||
mode: "0755"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Deploy Caddy vhost for {{ auth_domain }}
|
||||
ansible.builtin.template:
|
||||
src: e-auth.conf.j2
|
||||
dest: "{{ caddy_site_file }}"
|
||||
mode: "0644"
|
||||
owner: root
|
||||
group: root
|
||||
notify: Reload caddy
|
||||
|
||||
- name: Validate Caddy configuration
|
||||
ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile
|
||||
register: caddy_validate
|
||||
changed_when: false
|
||||
failed_when: caddy_validate.rc != 0
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Bring up the docker-compose stack
|
||||
# ---------------------------------------------------------
|
||||
- name: Pull docker images
|
||||
ansible.builtin.command: docker compose pull
|
||||
args:
|
||||
chdir: "{{ deploy_dir }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Start docker-compose stack
|
||||
ansible.builtin.command: docker compose up -d --remove-orphans
|
||||
args:
|
||||
chdir: "{{ deploy_dir }}"
|
||||
register: compose_up
|
||||
changed_when: >-
|
||||
'Started' in (compose_up.stderr | default(''))
|
||||
or 'Created' in (compose_up.stderr | default(''))
|
||||
or 'Recreated' in (compose_up.stderr | default(''))
|
||||
|
||||
- name: Show compose output
|
||||
ansible.builtin.debug:
|
||||
var: compose_up.stderr_lines
|
||||
when: compose_up.stderr_lines is defined
|
||||
|
||||
handlers:
|
||||
- name: Reload caddy
|
||||
ansible.builtin.systemd:
|
||||
name: caddy
|
||||
state: reloaded
|
||||
11
ansible/group_vars/all.yml
Normal file
11
ansible/group_vars/all.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
# ecosplay-auth deploy vars
|
||||
|
||||
deploy_dir: /var/www/e-auth
|
||||
auth_domain: auth.e-cosplay.fr
|
||||
keycloak_local_port: 9450
|
||||
|
||||
# Caddy (assumed already installed and configured to load per-site
|
||||
# files from /etc/caddy/sites/*.conf). TLS is handled by Caddy's
|
||||
# default ACME flow (HTTP-01 / TLS-ALPN), no Cloudflare DNS plugin.
|
||||
caddy_site_file: /etc/caddy/sites/e-auth.conf
|
||||
8
ansible/templates/e-auth.conf.j2
Normal file
8
ansible/templates/e-auth.conf.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
# Managed by Ansible - ecosplay-auth
|
||||
# Reverse proxy for {{ auth_domain }} -> local Keycloak container on :{{ keycloak_local_port }}
|
||||
|
||||
{{ auth_domain }} {
|
||||
encode gzip zstd
|
||||
|
||||
reverse_proxy 127.0.0.1:{{ keycloak_local_port }}
|
||||
}
|
||||
@@ -69,10 +69,12 @@ services:
|
||||
SMTP_PASSWORD: BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP
|
||||
LOGIN_THEME: ecosplay
|
||||
ECOSPLAY_GROUPS: "gp_asso gp_contest gp_mail gp_mailling gp_member gp_ndd gp_sign gp_ticket super_admin_asso superadmin"
|
||||
ADMIN_USER_USERNAME: jovann@siteconseil.fr
|
||||
ADMIN_USER_USERNAME: jovann@e-cosplay.fr
|
||||
ADMIN_USER_PASSWORD: Shoko1997@
|
||||
ADMIN_USER_FIRSTNAME: Jovann
|
||||
ADMIN_USER_LASTNAME: Serreau
|
||||
SYNC_BOT_CLIENT: sync-bot
|
||||
SYNC_BOT_SECRET: dev-sync-bot-9f3b2a7c1e8d4f6a0b5c2e1d7f8a4b3c
|
||||
volumes:
|
||||
- ./init/sync.sh:/opt/init/sync.sh:ro
|
||||
entrypoint: ["/bin/bash", "/opt/init/sync.sh"]
|
||||
|
||||
144
init/sync.sh
144
init/sync.sh
@@ -16,6 +16,7 @@
|
||||
# ECOSPLAY_GROUPS (space-separated list)
|
||||
# ADMIN_USER_USERNAME, ADMIN_USER_PASSWORD,
|
||||
# ADMIN_USER_FIRSTNAME, ADMIN_USER_LASTNAME
|
||||
# SYNC_BOT_CLIENT, SYNC_BOT_SECRET
|
||||
# =============================================================
|
||||
|
||||
set -euo pipefail
|
||||
@@ -29,16 +30,44 @@ warn() { printf ' \033[1;33m! %s\033[0m\n' "$*"; }
|
||||
# -------------------------------------------------------------
|
||||
# Wait for Keycloak and authenticate
|
||||
# -------------------------------------------------------------
|
||||
log "Waiting for Keycloak at ${KC_SERVER}"
|
||||
until $KC config credentials \
|
||||
# Two authentication paths:
|
||||
# 1. sync-bot service account (preferred, used after bootstrap).
|
||||
# 2. Default bootstrap admin (used on first run, before sync-bot
|
||||
# has been created and granted the admin role).
|
||||
# -------------------------------------------------------------
|
||||
KC_LOGIN_MODE=none
|
||||
|
||||
login_as_sync_bot() {
|
||||
[ -n "${SYNC_BOT_SECRET:-}" ] || return 1
|
||||
$KC config credentials \
|
||||
--server "$KC_SERVER" \
|
||||
--realm master \
|
||||
--client "$SYNC_BOT_CLIENT" \
|
||||
--secret "$SYNC_BOT_SECRET" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
login_as_admin() {
|
||||
$KC config credentials \
|
||||
--server "$KC_SERVER" \
|
||||
--realm master \
|
||||
--user "$KC_ADMIN" \
|
||||
--password "$KC_ADMIN_PASSWORD" >/dev/null 2>&1; do
|
||||
info "not ready yet, retrying in 5s..."
|
||||
--password "$KC_ADMIN_PASSWORD" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
log "Authenticating to Keycloak at ${KC_SERVER}"
|
||||
while true; do
|
||||
if login_as_sync_bot; then
|
||||
KC_LOGIN_MODE=sync-bot
|
||||
break
|
||||
fi
|
||||
if login_as_admin; then
|
||||
KC_LOGIN_MODE=bootstrap
|
||||
break
|
||||
fi
|
||||
info "Keycloak not ready / no valid credentials yet, retrying in 5s..."
|
||||
sleep 5
|
||||
done
|
||||
info "Keycloak ready."
|
||||
info "Authenticated (mode=$KC_LOGIN_MODE)"
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -144,6 +173,19 @@ set_client_uris() {
|
||||
info " client $2 URIs synced ($1)"
|
||||
}
|
||||
|
||||
set_client_pkce() {
|
||||
# $1=realm $2=clientId $3=method ("S256", "plain", or "" to disable)
|
||||
local cid
|
||||
cid=$(client_internal_id "$1" "$2")
|
||||
if [ -z "$cid" ]; then
|
||||
warn "client $2 not found in $1, skipping PKCE update"
|
||||
return
|
||||
fi
|
||||
$KC update "clients/$cid" -r "$1" \
|
||||
-s "attributes.\"pkce.code.challenge.method\"=$3" >/dev/null
|
||||
info " client $2 PKCE method set to '${3:-none}' ($1)"
|
||||
}
|
||||
|
||||
ensure_client() {
|
||||
# $1=realm $2=clientId $3=name $4=description $5=secret
|
||||
# $6=redirectUris(json) $7=webOrigins(json) $8=postLogoutUris(##-separated)
|
||||
@@ -171,6 +213,72 @@ ensure_client() {
|
||||
info "+ client $2 ($1)"
|
||||
}
|
||||
|
||||
ensure_sync_bot() {
|
||||
# Create (or keep in sync) the automation service client in master.
|
||||
# Grants its service account the master 'admin' realm role so that
|
||||
# sync.sh can authenticate with client_credentials after the default
|
||||
# admin user is disabled.
|
||||
local cid
|
||||
cid=$(client_internal_id master "$SYNC_BOT_CLIENT")
|
||||
if [ -z "$cid" ]; then
|
||||
$KC create clients -r master \
|
||||
-s "clientId=$SYNC_BOT_CLIENT" \
|
||||
-s "name=Sync Bot" \
|
||||
-s "description=Service account used by init/sync.sh for unattended config reconciliation" \
|
||||
-s 'protocol=openid-connect' \
|
||||
-s 'enabled=true' \
|
||||
-s 'publicClient=false' \
|
||||
-s "secret=$SYNC_BOT_SECRET" \
|
||||
-s 'standardFlowEnabled=false' \
|
||||
-s 'implicitFlowEnabled=false' \
|
||||
-s 'directAccessGrantsEnabled=false' \
|
||||
-s 'serviceAccountsEnabled=true' >/dev/null
|
||||
cid=$(client_internal_id master "$SYNC_BOT_CLIENT")
|
||||
info "+ service client $SYNC_BOT_CLIENT (master)"
|
||||
else
|
||||
$KC update "clients/$cid" -r master \
|
||||
-s 'enabled=true' \
|
||||
-s 'serviceAccountsEnabled=true' \
|
||||
-s 'standardFlowEnabled=false' \
|
||||
-s 'directAccessGrantsEnabled=false' \
|
||||
-s "secret=$SYNC_BOT_SECRET" >/dev/null
|
||||
info "= service client $SYNC_BOT_CLIENT (master)"
|
||||
fi
|
||||
# Grant 'admin' realm role to the auto-generated service account user
|
||||
$KC add-roles -r master \
|
||||
--uusername "service-account-${SYNC_BOT_CLIENT}" \
|
||||
--rolename admin >/dev/null 2>&1 || true
|
||||
info " service-account-${SYNC_BOT_CLIENT} -> realm role admin"
|
||||
}
|
||||
|
||||
disable_default_admin() {
|
||||
# Only disable once we're sure sync-bot can authenticate (otherwise
|
||||
# next run would be locked out).
|
||||
if ! login_as_sync_bot; then
|
||||
warn "sync-bot auth not working, refusing to disable default admin"
|
||||
login_as_admin >/dev/null 2>&1 || true
|
||||
return
|
||||
fi
|
||||
# Re-login as admin to perform the disable action (sync-bot's token
|
||||
# was used only for verification)
|
||||
login_as_admin >/dev/null 2>&1 || true
|
||||
|
||||
local admin_id current
|
||||
admin_id=$(user_id master "$KC_ADMIN")
|
||||
if [ -z "$admin_id" ]; then
|
||||
info "no '$KC_ADMIN' user in master, nothing to disable"
|
||||
return
|
||||
fi
|
||||
current=$($KC get "users/$admin_id" -r master --fields enabled 2>/dev/null \
|
||||
| sed -n 's/.*"enabled"[ ]*:[ ]*\(true\|false\).*/\1/p' | head -n1)
|
||||
if [ "$current" = "false" ]; then
|
||||
info "= default admin user '$KC_ADMIN' already disabled"
|
||||
return
|
||||
fi
|
||||
$KC update "users/$admin_id" -r master -s enabled=false >/dev/null
|
||||
info "+ default admin user '$KC_ADMIN' disabled"
|
||||
}
|
||||
|
||||
# =============================================================
|
||||
# Master realm: SMTP, theme, locale
|
||||
# =============================================================
|
||||
@@ -191,6 +299,12 @@ $KC update realms/master \
|
||||
-s "defaultLocale=fr"
|
||||
info "master realm updated"
|
||||
|
||||
# =============================================================
|
||||
# Master realm: automation service account (sync-bot)
|
||||
# =============================================================
|
||||
log "Ensuring sync-bot service client in master realm"
|
||||
ensure_sync_bot
|
||||
|
||||
# =============================================================
|
||||
# Master realm: global Keycloak admin user
|
||||
# =============================================================
|
||||
@@ -232,8 +346,28 @@ if realm_exists ecosplay; then
|
||||
'["https://ticket.e-cosplay.fr/api/auth/login/sso/validate","https://cos.local/api/auth/login/sso/validate","https://ticket.e-cosplay.fr/connection/sso/check","https://cos.local/connection/sso/check"]' \
|
||||
'["https://ticket.e-cosplay.fr","https://cos.local"]' \
|
||||
'https://ticket.e-cosplay.fr/*##https://cos.local/*'
|
||||
|
||||
log "Reconciling ecosplay_code client"
|
||||
ensure_client ecosplay ecosplay_code "E-Cosplay Code" \
|
||||
"Forge de code (Gitea) - login SSO via ecosplay_code provider (Gitea ne supporte pas PKCE)" \
|
||||
"change-me-in-admin-console" \
|
||||
'["https://code.e-cosplay.fr/user/oauth2/ecosplay_code/callback","https://cos.local/user/oauth2/ecosplay_code/callback"]' \
|
||||
'["https://code.e-cosplay.fr","https://cos.local"]' \
|
||||
'https://code.e-cosplay.fr/*##https://cos.local/*'
|
||||
set_client_uris ecosplay ecosplay_code \
|
||||
'["https://code.e-cosplay.fr/user/oauth2/ecosplay_code/callback","https://cos.local/user/oauth2/ecosplay_code/callback"]' \
|
||||
'["https://code.e-cosplay.fr","https://cos.local"]' \
|
||||
'https://code.e-cosplay.fr/*##https://cos.local/*'
|
||||
# Gitea does not support PKCE on OIDC sources, disable the requirement
|
||||
set_client_pkce ecosplay ecosplay_code ""
|
||||
else
|
||||
warn "ecosplay realm not found — will be imported on next boot"
|
||||
fi
|
||||
|
||||
# =============================================================
|
||||
# Disable default bootstrap admin (only after sync-bot is proven)
|
||||
# =============================================================
|
||||
log "Disabling default bootstrap admin user"
|
||||
disable_default_admin
|
||||
|
||||
log "Sync complete"
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
|
||||
"users": [
|
||||
{
|
||||
"username": "jovann@siteconseil.fr",
|
||||
"email": "jovann@siteconseil.fr",
|
||||
"username": "jovann@e-cosplay.fr",
|
||||
"email": "jovann@e-cosplay.fr",
|
||||
"firstName": "Jovann",
|
||||
"lastName": "Serreau",
|
||||
"enabled": true,
|
||||
@@ -117,6 +117,31 @@
|
||||
"pkce.code.challenge.method": "S256"
|
||||
}
|
||||
},
|
||||
{
|
||||
"clientId": "ecosplay_code",
|
||||
"name": "E-Cosplay Code",
|
||||
"description": "Forge de code (Gitea) - login SSO via ecosplay_code provider (Gitea ne supporte pas PKCE)",
|
||||
"enabled": true,
|
||||
"publicClient": false,
|
||||
"secret": "change-me-in-admin-console",
|
||||
"redirectUris": [
|
||||
"https://code.e-cosplay.fr/user/oauth2/ecosplay_code/callback",
|
||||
"https://cos.local/user/oauth2/ecosplay_code/callback"
|
||||
],
|
||||
"webOrigins": [
|
||||
"https://code.e-cosplay.fr",
|
||||
"https://cos.local"
|
||||
],
|
||||
"protocol": "openid-connect",
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"serviceAccountsEnabled": false,
|
||||
"frontchannelLogout": true,
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "https://code.e-cosplay.fr/*##https://cos.local/*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"clientId": "eticket",
|
||||
"name": "E-Ticket",
|
||||
|
||||
82
scripts/backup.sh
Executable file
82
scripts/backup.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================
|
||||
# E-Cosplay Keycloak backup
|
||||
# -------------------------------------------------------------
|
||||
# Dumps the Keycloak Postgres database to /backup/ as a
|
||||
# timestamped, gzip-compressed SQL file, then rotates old
|
||||
# dumps (keeps the latest N).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/backup.sh # default settings
|
||||
# BACKUP_DIR=/mnt/nas RETENTION=30 \
|
||||
# ./scripts/backup.sh # override via env
|
||||
#
|
||||
# Suggested cron (daily at 03:15):
|
||||
# 15 3 * * * /var/www/e-auth/scripts/backup.sh >>/var/log/e-auth-backup.log 2>&1
|
||||
#
|
||||
# Note: the repo itself (compose file, realm JSON, themes,
|
||||
# Ansible, sync.sh, etc.) lives in git and does not need to
|
||||
# be backed up here. Only the Postgres volume carries state
|
||||
# that is not reproducible from code (users, credentials,
|
||||
# TOTP secrets, sessions, brute-force counters, ...).
|
||||
# =============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR=${BACKUP_DIR:-/backup}
|
||||
RETENTION=${RETENTION:-14}
|
||||
PG_CONTAINER=${PG_CONTAINER:-ecosplay-auth-db}
|
||||
PG_USER=${PG_USER:-keycloak}
|
||||
PG_DB=${PG_DB:-keycloak}
|
||||
|
||||
TS=$(date +%F_%H-%M-%S)
|
||||
OUT="${BACKUP_DIR}/keycloak-${TS}.sql.gz"
|
||||
|
||||
log() { printf '[backup %s] %s\n' "$(date +%T)" "$*"; }
|
||||
fail() { printf '[backup %s] ERROR: %s\n' "$(date +%T)" "$*" >&2; exit 1; }
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Pre-flight checks
|
||||
# -------------------------------------------------------------
|
||||
command -v docker >/dev/null || fail "docker not found in PATH"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "${PG_CONTAINER}"; then
|
||||
fail "container '${PG_CONTAINER}' is not running"
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Dump
|
||||
# -------------------------------------------------------------
|
||||
log "Preparing ${BACKUP_DIR}"
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
chmod 700 "${BACKUP_DIR}"
|
||||
|
||||
log "Dumping database '${PG_DB}' from container '${PG_CONTAINER}'"
|
||||
if ! docker exec "${PG_CONTAINER}" \
|
||||
pg_dump -U "${PG_USER}" -d "${PG_DB}" \
|
||||
--no-owner --clean --if-exists \
|
||||
| gzip -9 > "${OUT}.part"; then
|
||||
rm -f "${OUT}.part"
|
||||
fail "pg_dump failed"
|
||||
fi
|
||||
|
||||
mv "${OUT}.part" "${OUT}"
|
||||
chmod 600 "${OUT}"
|
||||
|
||||
SIZE=$(du -h "${OUT}" | cut -f1)
|
||||
log "Wrote ${OUT} (${SIZE})"
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Rotate: keep only the latest ${RETENTION} backups
|
||||
# -------------------------------------------------------------
|
||||
log "Pruning backups older than the last ${RETENTION}"
|
||||
cd "${BACKUP_DIR}"
|
||||
# shellcheck disable=SC2010
|
||||
ls -1t keycloak-*.sql.gz 2>/dev/null \
|
||||
| tail -n +"$((RETENTION + 1))" \
|
||||
| while read -r old; do
|
||||
log " removing ${old}"
|
||||
rm -f "${old}"
|
||||
done
|
||||
|
||||
log "Done."
|
||||
277
themes/ecosplay/login/resources/css/brutalist.css
Normal file
277
themes/ecosplay/login/resources/css/brutalist.css
Normal file
@@ -0,0 +1,277 @@
|
||||
/* ============================================================
|
||||
E-Cosplay brutalist overrides for default Keycloak markup
|
||||
------------------------------------------------------------
|
||||
The shell (card, header, marquee, footer) is rendered by our
|
||||
template.ftl. Pages that we do NOT override (change password,
|
||||
OTP, verify email, required actions, etc.) reuse Keycloak's
|
||||
stock PatternFly/Bootstrap markup for their inner form. This
|
||||
file restyles those classes so everything inside the card
|
||||
matches the brutalist identity without having to override
|
||||
every single .ftl file.
|
||||
============================================================ */
|
||||
|
||||
/* Layout: kill Bootstrap grid side-padding that breaks our card */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.form-group > div[class*="col-"] {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
float: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.form-horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Labels ---------- */
|
||||
.pf-c-form__label,
|
||||
.pf-c-form__label-text,
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 900 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 11px;
|
||||
color: #111827 !important;
|
||||
margin-bottom: 0.5rem;
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
/* ---------- Inputs ---------- */
|
||||
.pf-c-form-control,
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="tel"],
|
||||
input[type="number"],
|
||||
input[type="url"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100% !important;
|
||||
padding: 14px 16px !important;
|
||||
background: #ffffff !important;
|
||||
color: #111827 !important;
|
||||
border: 4px solid #111827 !important;
|
||||
border-radius: 0 !important;
|
||||
font-weight: 700 !important;
|
||||
font-style: normal !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
transition: all 0.15s ease !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.pf-c-form-control:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="url"]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
background: #fef9c3 !important;
|
||||
box-shadow: 6px 6px 0 #4f46e5 !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.pf-c-form-control[aria-invalid="true"] {
|
||||
border-color: #dc2626 !important;
|
||||
background: #fee2e2 !important;
|
||||
}
|
||||
|
||||
/* ---------- Input group (password reveal button etc.) ---------- */
|
||||
.pf-c-input-group {
|
||||
display: flex !important;
|
||||
align-items: stretch !important;
|
||||
gap: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pf-c-input-group > .pf-c-form-control,
|
||||
.pf-c-input-group > input {
|
||||
flex: 1 1 auto !important;
|
||||
}
|
||||
|
||||
.pf-c-input-group > .pf-c-button.pf-m-control {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-width: 56px !important;
|
||||
padding: 0 18px !important;
|
||||
background: #111827 !important;
|
||||
color: #ffffff !important;
|
||||
border: 4px solid #111827 !important;
|
||||
border-left: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
font-weight: 900 !important;
|
||||
cursor: pointer !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
letter-spacing: 0 !important;
|
||||
font-style: normal !important;
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.pf-c-input-group > .pf-c-button.pf-m-control:hover {
|
||||
background: #4f46e5 !important;
|
||||
}
|
||||
|
||||
/* ---------- Buttons (primary / default / cancel) ---------- */
|
||||
.pf-c-button,
|
||||
input[type="submit"],
|
||||
button[type="submit"],
|
||||
button[type="button"].btn,
|
||||
a.btn {
|
||||
display: inline-block;
|
||||
padding: 16px 28px !important;
|
||||
font-weight: 900 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.12em !important;
|
||||
font-style: italic !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1 !important;
|
||||
border: 4px solid #111827 !important;
|
||||
border-radius: 0 !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.15s ease !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-primary,
|
||||
input.pf-m-primary,
|
||||
input[type="submit"].pf-m-primary,
|
||||
input[type="submit"].btn-primary,
|
||||
button.btn-primary {
|
||||
background: #4f46e5 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 8px 8px 0 rgba(0, 0, 0, 1) !important;
|
||||
}
|
||||
|
||||
.pf-c-button.pf-m-primary:hover,
|
||||
input[type="submit"].pf-m-primary:hover,
|
||||
button.btn-primary:hover {
|
||||
box-shadow: none !important;
|
||||
transform: translate(4px, 4px) !important;
|
||||
background: #4338ca !important;
|
||||
}
|
||||
|
||||
.pf-c-button.btn-default,
|
||||
.pf-c-button.btn-secondary,
|
||||
button.btn-default,
|
||||
button.btn-secondary,
|
||||
input[type="submit"].btn-default {
|
||||
background: #ffffff !important;
|
||||
color: #111827 !important;
|
||||
box-shadow: 8px 8px 0 rgba(0, 0, 0, 1) !important;
|
||||
}
|
||||
|
||||
.pf-c-button.btn-default:hover,
|
||||
button.btn-default:hover {
|
||||
background: #facc15 !important;
|
||||
box-shadow: none !important;
|
||||
transform: translate(4px, 4px) !important;
|
||||
}
|
||||
|
||||
/* Avoid the input-group eye button inheriting the primary style above */
|
||||
.pf-c-input-group .pf-c-button.pf-m-control {
|
||||
padding: 0 18px !important;
|
||||
font-size: 14px !important;
|
||||
letter-spacing: 0 !important;
|
||||
text-transform: none !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
/* ---------- Checkboxes ---------- */
|
||||
.checkbox {
|
||||
margin: 1rem 0 !important;
|
||||
}
|
||||
|
||||
.checkbox label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
cursor: pointer !important;
|
||||
font-size: 11px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
margin: 0 !important;
|
||||
border: 4px solid #111827 !important;
|
||||
accent-color: #4f46e5 !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
/* ---------- Button row ---------- */
|
||||
#kc-form-buttons {
|
||||
display: flex !important;
|
||||
gap: 12px !important;
|
||||
flex-wrap: wrap !important;
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
#kc-form-buttons > .pf-c-button,
|
||||
#kc-form-buttons > input[type="submit"],
|
||||
#kc-form-buttons > button {
|
||||
flex: 1 1 160px;
|
||||
}
|
||||
|
||||
/* ---------- Alerts / messages ---------- */
|
||||
.alert,
|
||||
.pf-c-alert {
|
||||
border: 4px solid #111827 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 6px 6px 0 rgba(0, 0, 0, 1) !important;
|
||||
padding: 14px 18px !important;
|
||||
font-weight: 800 !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.alert-success,
|
||||
.pf-c-alert.pf-m-success {
|
||||
background: #bbf7d0 !important;
|
||||
}
|
||||
.alert-warning,
|
||||
.pf-c-alert.pf-m-warning {
|
||||
background: #fde68a !important;
|
||||
}
|
||||
.alert-danger,
|
||||
.alert-error,
|
||||
.pf-c-alert.pf-m-danger {
|
||||
background: #fecaca !important;
|
||||
}
|
||||
.alert-info,
|
||||
.pf-c-alert.pf-m-info {
|
||||
background: #c7d2fe !important;
|
||||
}
|
||||
|
||||
/* ---------- Links ---------- */
|
||||
a {
|
||||
color: #4f46e5 !important;
|
||||
text-decoration: underline !important;
|
||||
text-decoration-thickness: 3px !important;
|
||||
text-underline-offset: 4px !important;
|
||||
font-weight: 800 !important;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #111827 !important;
|
||||
}
|
||||
|
||||
/* ---------- Headings inside the card ---------- */
|
||||
.pf-c-title,
|
||||
#kc-page-title,
|
||||
h1, h2, h3 {
|
||||
font-weight: 900 !important;
|
||||
text-transform: uppercase !important;
|
||||
font-style: italic !important;
|
||||
letter-spacing: -0.02em !important;
|
||||
color: #111827 !important;
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>${msg("loginTitle",(realm.displayName!''))}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="${url.resourcesPath}/css/brutalist.css">
|
||||
<style>
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0); }
|
||||
|
||||
Reference in New Issue
Block a user