diff --git a/.env.example b/.env.example index 788480d082655da6a5a747ef986be7c9fc43c3b5..d393f37d03b2dbccaa3610e46e65d00d37abecbd 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # shellcheck disable HOME_DOMAIN=local.test ADMIN_EMAIL= -USER_ID="501" -GROUP_ID="20" +USER_ID="1000" +GROUP_ID="1000" APP_ROOT=./.apps STORE_ROOT=/Volumes @@ -11,16 +11,25 @@ MEDIA_ROOT=/Volumes/media MAILER_ADDRESS=smtp.example.com MAILER_USERNAME=me@paperclover.net -MAILER_PASSWORD=yolo +MAILER_PASSWORD= + +KEYCLOAK_ADMIN_PASSWORD= +FORWARD_AUTH_KEY= POSTGRES_PASSWORD= POSTGRES_PASSWORD_AUTHELIA= POSTGRES_PASSWORD_FORGEJO= POSTGRES_PASSWORD_DAWARICH= POSTGRES_PASSWORD_JELLYFIN= +POSTGRES_PASSWORD_LLDAP= CLOVER_SOT_KEY= +ANUBIS_PRIVATE_KEY= + FORGEJO_SERVER_LFS_JWT_SECRET= FORGEJO_OAUTH2_JWT_SECRET= -FORGEJO_SECURITY_INTERNAL_TOKEN= +FORGEJO_OPENID_SECRET= +FORGEJO_SECURITY_SECRET_KEY= +FORGEJO_SECURITY_INTERNAL_TOKEN=(jwt) + diff --git a/compose.yaml b/compose.yaml index edef565d1b165e4f8671bcfa9856dcb3aa2ac6a2..a9cbd5e94367bd0eb73e89b46e070ff352c93892 100755 --- a/compose.yaml +++ b/compose.yaml @@ -1,8 +1,8 @@ services: # web server - caddy: + caddy: # port 80, 443 container_name: caddy - image: caddy:latest + image: caddy:2.11 ports: - 80:80 - 443:443 @@ -12,46 +12,110 @@ services: - "./config:/etc/caddy:ro" - "${APP_ROOT}/caddy:/data/caddy:rw" environment: - - HOME_DOMAIN + HOME_DOMAIN: "${HOME_DOMAIN}" develop: watch: - path: ./config/Caddyfile action: restart # SSO - authelia: - image: "authelia/authelia" - container_name: authelia + keycloak: # port 80, 443 + container_name: keycloak + build: + context: ./config/keycloak + dockerfile: Dockerfile + pull_policy: build volumes: - - "./config/authelia:/config" - restart: "unless-stopped" + - "${APP_ROOT}/keycloak:/shared:rw" + - "./config/keycloak/theme:/opt/keycloak/themes/snow:ro" + - "./config/font:/opt/keycloak/themes/snow/login/resources/font:ro" secrets: - - "authelia-jwt-secret" - - "authelia-session-secret" - - "authelia-encryption-key" + - keycloak-crt + - keycloak-key environment: - PUID: "$USER_ID" - PGID: "$GROUP_ID" - TZ: America/Los_Angelas - AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: "/run/secrets/authelia-jwt-secret" - AUTHELIA_SESSION_SECRET_FILE: "/run/secrets/authelia-session-secret" - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: "/run/secrets/authelia-encryption-key" - AUTHELIA_STORAGE_POSTGRES_ADDRESS: postgres:5432 - AUTHELIA_STORAGE_POSTGRES_DATABASE: authelia - AUTHELIA_STORAGE_POSTGRES_USERNAME: authelia - AUTHELIA_STORAGE_POSTGRES_PASSWORD: "${POSTGRES_PASSWORD_AUTHELIA}" - AUTHELIA_NOTIFIER_SMTP_ADDRESS: "smtp://${MAILER_ADDRESS}" - AUTHELIA_NOTIFIER_SMTP_USERNAME: "${MAILER_USERNAME}" - AUTHELIA_NOTIFIER_SMTP_PASSWORD: "${MAILER_PASSWORD}" + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD:?}" + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: "${POSTGRES_PASSWORD_KEYCLOAK:?}" + KC_HOSTNAME: "auth.${HOME_DOMAIN:?}" + KC_PROXY_HEADERS: xforwarded + KC_HEALTH_ENABLED: true + KC_HTTP_ENABLED: true + KC_HTTP_MANAGEMENT_SCHEME: http + ADMIN_EMAIL: "${ADMIN_EMAIL:?}" + MAILER_ADDRESS: "${MAILER_ADDRESS:?}" + MAILER_USERNAME: "${MAILER_USERNAME:?}" + MAILER_PASSWORD: "${MAILER_PASSWORD:?}" + HOME_DOMAIN: "${HOME_DOMAIN:?}" + KC_HTTP_PORT: 80 + KC_HTTPS_PORT: 443 + restart: unless-stopped + read_only: false + depends_on: + caddy: + condition: service_started + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: + - "CMD-SHELL" + - "test -f /shared/forward-auth" + interval: 10s + timeout: 5s + retries: 3 + start_period: 120s + forward-auth: # port 80 + container_name: forward-auth + build: + context: config/keycloak/forward-auth + dockerfile: Dockerfile + pull_policy: build + restart: unless-stopped + user: "0:0" + environment: + OAUTH2_PROXY_CLIENT_ID: "forward-auth" + OAUTH2_PROXY_COOKIE_SECRET: "${FORWARD_AUTH_KEY:?}" + OAUTH2_PROXY_OIDC_ISSUER_URL: "https://auth.${HOME_DOMAIN:?}/realms/master" + OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256" + OAUTH2_PROXY_PROVIDER: "keycloak-oidc" + OAUTH2_PROXY_EMAIL_DOMAINS: "*" + OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:80" + OAUTH2_PROXY_PINING_PATH: "/ping" + OAUTH2_PROXY_READY_PATH: "/ready" + OAUTH2_PROXY_PROXY_PREFIX: "/snow.oauth2" + OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "TRUE" + OAUTH2_PROXY_REVERSE_PROXY: "TRUE" + OAUTH2_PROXY_WHITELIST_DOMAINS: "*.${HOME_DOMAIN:?}" + OAUTH2_PROXY_SET_XAUTHREQUEST: "TRUE" + OAUTH2_PROXY_PASS_USER_HEADERS: "TRUE" + OAUTH2_PROXY_BACKEND_LOGOUT_URL: "https://auth.${HOME_DOMAIN:?}/realms/master/protocol/openid-connect/logout?id_token_hint={id_token}" + OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "TRUE" + HOME_DOMAIN: "${HOME_DOMAIN:?}" + depends_on: + keycloak: + condition: service_healthy + volumes: + - "${APP_ROOT}/keycloak/forward-auth:/shared/forward-auth:ro" # file manager - copyparty: # port 3923 + copyparty: # port 80 image: copyparty/ac container_name: copyparty user: "$USER_ID:$GROUP_ID" volumes: - "${STORE_ROOT}:/w" - "${APP_ROOT}/copyparty:/cfg" - - ./config/copyparty.conf:/cfg/copyparty.conf + - ./config/copyparty.conf:/cfg/copyparty.conf:ro restart: unless-stopped + labels: + net.paperclover.list.name: Copyparty File Storage + net.paperclover.list.domain: nas + net.paperclover.list.priority: 103 + depends_on: + forward-auth: + condition: service_started # paper clover's database service clover-source-of-truth: build: @@ -75,7 +139,10 @@ services: interval: 10s timeout: 5s retries: 3 - start_period: 40s + start_period: 10s + depends_on: + caddy: + condition: service_healthy # git hosting forgejo: # port 3000 container_name: forgejo @@ -101,14 +168,25 @@ services: - "/etc/localtime:/etc/localtime:ro" - ./config/forgejo:/custom:ro - ./config/font:/custom/public/assets/font:ro + labels: + net.paperclover.list.name: Clover Git Forge + net.paperclover.list.domain: git + net.paperclover.list.priority: 105 + depends_on: + keycloak: + condition: service_healthy + # forgejo-anubis: + # condition: service_healthy forgejo-anubis: container_name: forgejo-anubis image: ghcr.io/techarohq/anubis:latest - user: "$USER_ID:$GROUP_ID" + user: "${USER_ID:?}:${GROUP_ID:?}" restart: unless-stopped environment: BIND: :80 TARGET: http://forgejo:3000 + COOKIE_DOMAIN: "${HOME_DOMAIN:?}" + ED25519_PRIVATE_KEY_HEX: "${ANUBIS_PRIVATE_KEY:?}" forgejo-runner: container_name: forgejo-runner image: code.forgejo.org/forgejo/runner:12 @@ -151,6 +229,11 @@ services: - "./config/jellyfin:/config/custom:ro" - "./config/jellyfin/branding.xml:/config/config/branding.xml:ro" - "./config/font:/usr/local/share/fonts/custom:ro" + labels: + net.paperclover.list.name: Cloverfin + net.paperclover.list.domain: jelly + net.paperclover.list.priority: 110 + net.paperclover.list.access: media navidrome: # port 4533 container_name: navidrome image: deluan/navidrome:latest @@ -163,6 +246,11 @@ services: - ND_SCANNER_GROUPALBUMRELEASES=1 - ND_BASEURL=https://nd.${HOME_DOMAIN}/ - ND_PID_ALBUM=folder + labels: + net.paperclover.list.name: Navidrome Music + net.paperclover.list.domain: nd + net.paperclover.list.priority: 109 + net.paperclover.list.access: media # media aquisition qbittorrent: # webui on port 23938 platform: linux/amd64 # not available for aarch64 @@ -199,6 +287,11 @@ services: - "${APP_ROOT}/qbittorrent:/config" - "./config/openvpn:/config/openvpn:ro" - "/etc/localtime:/etc/localtime:ro" + labels: + net.paperclover.list.name: qBittorrent + net.paperclover.list.domain: qbt + net.paperclover.list.priority: 52 + net.paperclover.list.access: media-manage # jackett: # port 9117 # container_name: jackett # environment: @@ -209,6 +302,11 @@ services: # restart: unless-stopped # volumes: # - "${APP_ROOT}/jackett:/config/Jackett" + # labels: + # net.paperclover.list.name: Jackett API Adapter + # net.paperclover.list.domain: jkt + # net.paperclover.list.priority: 40 + # net.paperclover.list.access: media-manage # sonarr: # port 8989 # container_name: sonarr # environment: @@ -221,6 +319,11 @@ services: # - "${TORRENT_TEMP:-${APP_ROOT}/qbittorrent/tmp}:/data/tmp" # - "${MEDIA_ROOT}:/data/media" # restart: unless-stopped + # labels: + # net.paperclover.list.name: Sonarr TV PVR + # net.paperclover.list.domain: jkt + # net.paperclover.list.priority: 51 + # net.paperclover.list.access: media-manage # radarr: # port 7878 # container_name: radarr # environment: @@ -233,6 +336,11 @@ services: # - "${TORRENT_TEMP:-${APP_ROOT}/qbittorrent/tmp}:/data/tmp" # - "${MEDIA_ROOT}:/data/media" # restart: unless-stopped + # labels: + # net.paperclover.list.name: Radarr Movie Organizer + # net.paperclover.list.domain: jkt + # net.paperclover.list.priority: 50 + # net.paperclover.list.access: media-manage # language models opencode: # port 4096 container_name: opencode @@ -246,6 +354,11 @@ services: - "${APP_ROOT}/opencode/.config:/home/clover/.config" - "${APP_ROOT}/opencode/.local:/home/clover/.local" - "${APP_ROOT}/opencode/code:/home/clover/code" + labels: + net.paperclover.list.name: OpenCode + net.paperclover.list.domain: opencode + net.paperclover.list.priority: 70 + net.paperclover.list.access: personal # databases postgres: container_name: postgres @@ -260,33 +373,41 @@ services: user: "$USER_ID:$GROUP_ID" environment: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" - POSTGRES_PASSWORD_FORGEJO: "${POSTGRES_PASSWORD_FORGEJO}" - POSTGRES_PASSWORD_AUTHELIA: "${POSTGRES_PASSWORD_AUTHELIA}" - POSTGRES_PASSWORD_DAWARICH: "${POSTGRES_PASSWORD_DAWARICH}" - POSTGRES_PASSWORD_JELLYFIN: "${POSTGRES_PASSWORD_JELLYFIN}" + POSTGRES_PASSWORD_FORGEJO: "${POSTGRES_PASSWORD_FORGEJO:?}" + POSTGRES_PASSWORD_KEYCLOAK: "${POSTGRES_PASSWORD_KEYCLOAK:?}" + POSTGRES_PASSWORD_DAWARICH: "${POSTGRES_PASSWORD_DAWARICH:?}" + POSTGRES_PASSWORD_JELLYFIN: "${POSTGRES_PASSWORD_JELLYFIN:?}" volumes: - "${APP_ROOT}/pg_data:/var/lib/postgresql/data/" healthcheck: - test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"] + test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"] interval: 10s timeout: 5s retries: 5 start_period: 10s pgadmin: # port 80 container_name: pgadmin + entrypoint: ["/bin/sh", "/init/pgadmin-init.sh"] image: "docker.io/dpage/pgadmin4:latest" - user: "5050:5050" + user: "0:0" environment: PGADMIN_DEFAULT_EMAIL: "pgadmin4@pgadmin.org" PGADMIN_DEFAULT_PASSWORD: "secret" PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" PGADMIN_CONFIG_SERVER_MODE: "False" + POSTGRES_PASSWORD: "$POSTGRES_PASSWORD" volumes: - "${APP_ROOT}/pg_admin:/var/lib/pgadmin" + - "./config/postgres:/init" restart: unless-stopped depends_on: postgres: condition: service_healthy + labels: + net.paperclover.list.name: pgAdmin + net.paperclover.list.domain: pg + net.paperclover.list.priority: 15 + net.paperclover.list.access: personal redis: # port 6379 container_name: redis image: redis:8 @@ -294,12 +415,23 @@ services: restart: unless-stopped volumes: - "${APP_ROOT}/redis:/data" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 1s + timeout: 3s + retries: 30 + start_period: 5s redis-insight: # port 5540 container_name: redis-insight image: redis/redisinsight:latest environment: RI_REDIS_HOST: redis restart: unless-stopped + labels: + net.paperclover.list.name: Redis Insight + net.paperclover.list.domain: redis + net.paperclover.list.priority: 14 + net.paperclover.list.access: personal # location history dawarich-app: # port 30161 image: freikin/dawarich:latest @@ -340,6 +472,11 @@ services: limits: cpus: "0.50" memory: 4G + labels: + net.paperclover.list.name: Dawarich Location History + net.paperclover.list.domain: dawarich + net.paperclover.list.priority: 30 + net.paperclover.list.access: personal dawarich-sidekiq: image: freikin/dawarich:latest container_name: dawarich-sidekiq @@ -387,12 +524,21 @@ services: - 22000:22000/udp - 21027:21027/udp restart: unless-stopped + labels: + net.paperclover.list.name: Syncthing + net.paperclover.list.domain: sync + net.paperclover.list.priority: 31 + net.paperclover.list.access: personal # administration openspeedtest: # port 3000 restart: unless-stopped container_name: openspeedtest image: openspeedtest/latest user: "$USER_ID:$GROUP_ID" + labels: + net.paperclover.list.name: Open Speed Test + net.paperclover.list.domain: speedtest + net.paperclover.list.priority: 10 technitium-dns: image: technitium/dns-server container_name: technitium-dns @@ -411,13 +557,16 @@ services: restart: unless-stopped sysctls: - net.ipv4.ip_local_port_range=1024 65000 + labels: + net.paperclover.list.name: DNS + net.paperclover.list.domain: dns + net.paperclover.list.priority: 9 + net.paperclover.list.access: personal secrets: openvpn-credentials: file: ./secrets/openvpn-credentials.txt - authelia-jwt-secret: - file: ./secrets/authelia-jwt-secret.txt - authelia-session-secret: - file: ./secrets/authelia-session-secret.txt - authelia-encryption-key: - file: ./secrets/authelia-encryption-key.txt + keycloak-crt: + file: ./secrets/keycloak.crt + keycloak-key: + file: ./secrets/keycloak.key diff --git a/config/Caddyfile b/config/Caddyfile index f1e2126799b927e7c5b452e519f1c459d944103b..a342c6934fb5289067b01ccf8a620a0c555852d3 100644 --- a/config/Caddyfile +++ b/config/Caddyfile @@ -1,99 +1,133 @@ -local.test, *.local.test { - tls internal -} +# (auth_require) { +# @not_websocket not header Connection Upgrade +# handle /snow.oauth2/* { +# reverse_proxy "http://forward-auth" { +# header_up X-Real-IP {remote_host} +# header_up X-Forwarded-Uri {uri} +# } +# } +# +# handle { +# forward_auth @not_websocket "http://forward-auth" { +# uri /snow.oauth2/auth +# header_up X-Real-IP {remote_host} +# copy_headers X-Forwarded-User X-Forwarded-Groups X-Forwarded-Email +# @error status 401 +# handle_response @error { +# redir * /snow.oauth2/sign_in?rd={scheme}://{host}{uri} +# } +# } +# +# reverse_proxy {args[:]} +# } +# } +(auth-optional) { + handle /snow.oauth2/* { + reverse_proxy "http://forward-auth" { + header_up X-Real-IP {remote_host} + header_up X-Forwarded-Uri {uri} + } + } -(auth) { - @not_websocket not header Connection Upgrade - forward_auth @not_websocket authelia:9091 { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + @not_websocket not header Connection Upgrade + reverse_proxy @not_websocket "http://forward-auth" { + method GET + rewrite /snow.oauth2/auth + + header_up X-Forwarded-Method {method} + header_up X-Forwarded-Uri {uri} + + @good status 2xx + handle_response @good { + request_header User-Id {rp.header.X-Auth-Request-User} + request_header User-Groups {rp.header.X-Auth-Request-Groups} + request_header User-Email {rp.header.X-Auth-Request-Email} + request_header User-Name {rp.header.X-Auth-Request-Preferred-Username} + } + @bad status 4xx + handle_response @bad { + request_header User-Id "" + request_header User-Groups "" + request_header User-Email "" + request_header User-Name "" + } } + + reverse_proxy {args[:]} } -# jelly.{$HOME_DOMAIN} -import ./jellyfin/Caddyfile +# services are sorted alphabetically auth.{$HOME_DOMAIN} { - reverse_proxy "authelia:9091" + reverse_proxy "http://keycloak" } dawarich.{$HOME_DOMAIN} { - reverse_proxy "dawarich:30161" + reverse_proxy "http://dawarich:30161" } db.{$HOME_DOMAIN} { - reverse_proxy "clover-source-of-truth:80" -} -nas.{$HOME_DOMAIN} { - forward_auth authelia:9091 { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name - } - route { - @login path /login - redir @login https://auth.{$HOME_DOMAIN}?rd=https%3A%2F%2Fnas.{$HOME_DOMAIN}{?rd} - - @logout path /logout - redir @logout https://auth.{$HOME_DOMAIN}/logout?rd=https://nas.{$HOME_DOMAIN} - - reverse_proxy copyparty:3923 - } + reverse_proxy "http://clover-source-of-truth" } git.{$HOME_DOMAIN} { - reverse_proxy "forgejo-anubis:80" { + reverse_proxy "http://forgejo-anubis" { header_up X-Real-Ip {remote_host} header_up X-Http-Version {http.request.proto} } } +import ./jellyfin/Caddyfile +# jkt.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "http://jackett:9117" { +# header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" +# } +# } music.{$HOME_DOMAIN} { - reverse_proxy "navidrome:4533" -} -qbt.{$HOME_DOMAIN} { - import auth - reverse_proxy "qbittorrent:23938" -} -snr.{$HOME_DOMAIN} { - import auth - reverse_proxy "sonarr:8989" { - header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" - } -} -rdr.{$HOME_DOMAIN} { - import auth - reverse_proxy "radarr:7878" { - header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" - } -} -jkt.{$HOME_DOMAIN} { - import auth - reverse_proxy "jackett:9117" { - header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" - } + reverse_proxy "http://navidrome:4533" +} +nas.{$HOME_DOMAIN} { + import auth-optional "http://copyparty" } +# opencode.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "http://opencode" +# } +# pg.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "http://pgadmin" +# } +# qbt.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "qbittorrent:23938" +# } +# redis.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "redis-insight:5540" +# } +# rdr.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "radarr:7878" { +# header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" +# } +# } +# snr.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "sonarr:8989" { +# header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" +# } +# } speedtest.{$HOME_DOMAIN} { reverse_proxy "openspeedtest:3000" } -opencode.{$HOME_DOMAIN} { - import auth - reverse_proxy "opencode:3923" -} -pg.{$HOME_DOMAIN} { - import auth - reverse_proxy "pgadmin:80" -} -redis.{$HOME_DOMAIN} { - import auth - reverse_proxy "redis-insight:5540" -} -sync.{$HOME_DOMAIN} { - import auth - reverse_proxy "syncthing:8384" -} +# sync.{$HOME_DOMAIN} { +# import auth +# reverse_proxy "syncthing:8384" +# } # redirections paperclover.dev { - redir "https://git.paperclover.net{uri}" - tls { - on_demand - } + redir "https://git.paperclover.net{uri}" + tls { + on_demand + } } :80, :443 { @@ -105,11 +139,13 @@ paperclover.dev { } } - handle_path /generate_204 { - respond "" 204 - } handle { respond "the 'Not Found' page was not found: {http.request.host}" 404 } } +local.test, *.local.test { + tls internal +} + + diff --git a/config/authelia/configuration.yml b/config/authelia/configuration.yml deleted file mode 100644 index ef1ee0a4397ca9f450b06b837ea95877961976f1..0000000000000000000000000000000000000000 --- a/config/authelia/configuration.yml +++ /dev/null @@ -1,46 +0,0 @@ -storage: - postgres: - address: "tcp://postgres:5432" - database: "authelia" - schema: "public" - username: "authelia" - timeout: "5s" -authentication_backend: - refresh_interval: "5m" - file: - watch: true - path: "/config/users.yaml" - search: - email: true - case_insensitive: true -totp: - issuer: "local.test" -session: - name: "authelia_session" - same_site: "lax" - inactivity: "5m" - expiration: "1h" - remember_me: "3M" - cookies: - - name: "clover-authelia" - domain: "local.test" - authelia_url: "https://auth.local.test" -notifier: - disable_startup_check: true - smtp: - address: "smtp://127.0.0.1:25" - timeout: "5s" - username: "test" - password: "password" - sender: "clover bot " - identifier: "localhost" - subject: "[Authelia] {title}" -access_control: - default_policy: one_factor - rules: - - domain: "nas.local.test" - policy: bypass # optional -regulation: - max_retries: 10 - find_time: "2 minutes" - ban_time: "5 minutes" diff --git a/config/authelia/users.yaml b/config/authelia/users.yaml deleted file mode 100644 index de8efcecb5f335738d87acdee0556758717b5519..0000000000000000000000000000000000000000 --- a/config/authelia/users.yaml +++ /dev/null @@ -1,11 +0,0 @@ -users: - snow: - disabled: false - displayname: "snow" - password: "$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/" - email: "account@paperclover.net" - fish: - disabled: false - displayname: "fish" - password: "$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/" - email: "julia@paperclover.net" diff --git a/config/copyparty.conf b/config/copyparty.conf index 34021c99f163ec8f2519364465fc03a1ce71c606..dc7a25db1e399cc38f5a8322c7e7b4f8296c3d11 100644 --- a/config/copyparty.conf +++ b/config/copyparty.conf @@ -2,19 +2,21 @@ # e2dsa # enable file indexing and filesystem scanning # e2ts # enable multimedia indexing ansi # enable colors in log messages + p: 80 # port shr: /shr # set the virtual path for shares df: 16 # stop accepting uploads if less than 16 GB free disk space ver # show copyparty version in the controlpanel theme: 2 # monokai - name: clover nas # change the server-name that's displayed in the browser + name: clover's nas stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow) - # authelia - xff-src: lan # accept X-Forwarded-For from `lan` - idp-h-usr: remote-user - idp-h-grp: remote-groups - idp-login: /login?rd={dst} # caddy redirect to avoid hardcoding a domain here - idp-logout: /logout + # keycloak + xff-src: lan # accept X-Forwarded-For from `lan` + rproxy: 1 + idp-h-usr: user-name + idp-h-grp: user-groups + idp-login: /snow.oauth2/sign_in?rd={dst} # caddy redirect to avoid hardcoding a domain here + idp-logout: /snow.oauth2/sign_out idp-login-t: with sso (snow sign on) [/] # webroot diff --git a/config/jellyfin/Caddyfile b/config/jellyfin/Caddyfile index b4f7dabab533ab98199e0d0232f04dbb81ed73bd..c4048cb73d420d2f4ecf668bbd60af1d5958544f 100644 --- a/config/jellyfin/Caddyfile +++ b/config/jellyfin/Caddyfile @@ -3,69 +3,64 @@ jelly.{$HOME_DOMAIN} { header Content-Type "text/css" respond 200 { body ` - @import url("/original{http.request.uri.path}") layer(base); + @import url("/original{http.request.uri.path}") layer(base); @import url("/original/web/themes/dark/theme.css") layer(basetheme); @import url("https://jellyfin.catppuccin.com/theme.css") layer(cat); @import url("https://jellyfin.catppuccin.com/catppuccin-macchiato.css"); @import url("https://jellyfin.catppuccin.com/catppuccin-latte.css") (prefers-color-scheme: light); @font-face { - font-family: 'Name Sans'; - src: url('/web/extra/font/ATNameSansVariable-Regular.woff2') format('woff2') tech(variations); - font-weight: 100 900; - font-style: normal; - font-display: swap; - font-stretch: 75% 125%; - } + font-family: 'Name Sans'; + src: url('/web/extra/font/ATNameSansVariable-Regular.woff2') format('woff2') tech(variations); + font-weight: 100 900; + font-style: normal; + font-display: swap; + font-stretch: 75% 125%;} @font-face { - font-family: 'Name Sans'; - src: url('/web/extra/font/ATNameSansVariable-Italic.woff2') format('woff2') tech(variations); - font-weight: 100 900; - font-style: italic; - font-display: swap; - font-stretch: 75% 125%; - } + font-family: 'Name Sans'; + src: url('/web/extra/font/ATNameSansVariable-Italic.woff2') format('woff2') tech(variations); + font-weight: 100 900; + font-style: italic; + font-display: swap; + font-stretch: 75% 125%;} body { font-family: "Name Sans", sans-serif;} .preload, .touch-menu-la, .mainDrawer-scrollContainer { - background-color: var(--main-background); - color: var(--main-text); - } + background-color: var(--main-background); + color: var(--main-text);} + :root { - --main-color: var(--sapphire); - --jf-font-button: 500 0.875rem / 1.75 "Name Sans", sans-serif; - --jf-font-h1: 300 1.8rem / 1.167 "Name Sans", sans-serif; - --jf-font-h2: 300 1.5rem / 1.2 "Name Sans", sans-serif; - --jf-font-h3: 400 1.17rem / 1.167 "Name Sans", sans-serif; - --jf-font-h4: 400 2.125rem / 1.235 "Name Sans", sans-serif; - --jf-font-h5: 400 1.5rem / 1.334 "Name Sans", sans-serif; - --jf-font-h6: 500 1.25rem / 1.6 "Name Sans", sans-serif; - --jf-font-subtitle1: 400 1rem / 1.75 "Name Sans", sans-serif; - --jf-font-subtitle2: 500 0.875rem / 1.57 "Name Sans", sans-serif; - --jf-font-body1: 400 1rem / 1.5 "Name Sans", sans-serif; - --jf-font-body2: 400 0.875rem / 1.43 "Name Sans", sans-serif; - --jf-font-caption: 400 0.75rem / 1.66 "Name Sans", sans-serif; - --jf-font-overline: 400 0.75rem / 2.66 "Name Sans", sans-serif; - --jf-font-inherit: inherit inherit / inherit inherit; - } + --main-color: var(--sapphire); + --jf-font-button: 500 0.875rem / 1.75 "Name Sans", sans-serif; + --jf-font-h1: 300 1.8rem / 1.167 "Name Sans", sans-serif; + --jf-font-h2: 300 1.5rem / 1.2 "Name Sans", sans-serif; + --jf-font-h3: 400 1.17rem / 1.167 "Name Sans", sans-serif; + --jf-font-h4: 400 2.125rem / 1.235 "Name Sans", sans-serif; + --jf-font-h5: 400 1.5rem / 1.334 "Name Sans", sans-serif; + --jf-font-h6: 500 1.25rem / 1.6 "Name Sans", sans-serif; + --jf-font-subtitle1: 400 1rem / 1.75 "Name Sans", sans-serif; + --jf-font-subtitle2: 500 0.875rem / 1.57 "Name Sans", sans-serif; + --jf-font-body1: 400 1rem / 1.5 "Name Sans", sans-serif; + --jf-font-body2: 400 0.875rem / 1.43 "Name Sans", sans-serif; + --jf-font-caption: 400 0.75rem / 1.66 "Name Sans", sans-serif; + --jf-font-overline: 400 0.75rem / 2.66 "Name Sans", sans-serif; + --jf-font-inherit: inherit inherit / inherit inherit;} - .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner.png)!important; } - .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon.png)!important; } - .splashLogo { background-image: url(/web/extra/cloverfin-icon.png)!important; } + .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner.png)!important;} + .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon.png)!important;} + .splashLogo { background-image: url(/web/extra/cloverfin-icon.png)!important;} @media (min-device-width: 992px) { - .splashLogo { background-image: url(/web/extra/cloverfin-banner.png)!important; } - } + .splashLogo { background-image: url(/web/extra/cloverfin-banner.png)!important;}} @media (prefers-color-scheme: light) { - .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important;} - .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important;} - .splashLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important; } - } + .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important;} + .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important;} + .splashLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important;}} @media (min-device-width: 992px) and (prefers-color-scheme: light) { - .splashLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important; } - } + .splashLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important;}} + ` } } diff --git a/config/keycloak/.gitignore b/config/keycloak/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40c65e139cfa909786ff270aa4dd6b86e6409fcf --- /dev/null +++ b/config/keycloak/.gitignore @@ -0,0 +1,2 @@ +.venv +.python-version diff --git a/config/keycloak/Dockerfile b/config/keycloak/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..50681b516773b69eb634edae5aeaf4fc3f55e4ef --- /dev/null +++ b/config/keycloak/Dockerfile @@ -0,0 +1,20 @@ +FROM registry.access.redhat.com/ubi9/python-311 AS python + +USER root +RUN pip3 install --no-cache-dir --target=/install python-keycloak requests + +FROM quay.io/keycloak/keycloak:latest + +USER root +COPY --from=python /usr/bin/python3.11 /usr/bin/python3 +COPY --from=python /usr/lib64/libpython3.11.so.1.0 /usr/lib64/ +COPY --from=python /usr/lib64/libcrypto.so.3 /usr/lib64/ +COPY --from=python /usr/lib64/libssl.so.3 /usr/lib64/ +COPY --from=python /usr/lib64/python3.11 /usr/lib64/python3.11 +COPY --from=python /install /usr/lib64/python3.11/site-packages +RUN ln -s /usr/lib64/libpython3.11.so.1.0 /usr/lib64/libpython3.11.so + +COPY init.py /opt/keycloak/init.py +COPY entry.sh /opt/keycloak/entry.sh + +ENTRYPOINT ["/opt/keycloak/entry.sh"] diff --git a/config/keycloak/entry.sh b/config/keycloak/entry.sh new file mode 100755 index 0000000000000000000000000000000000000000..f1e0edd6ee1b8bd016ba668e3f88be0f4ee77c12 --- /dev/null +++ b/config/keycloak/entry.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +THEME_DEV_ARGS="" +# THEME_DEV_ARGS='--spi-theme--static-max-age=-1 --spi-theme--cache-themes=false --spi-theme--cache-templates=false' + +/opt/keycloak/bin/kc.sh start --verbose \ + --https-certificate-file=/run/secrets/keycloak-crt \ + --https-certificate-key-file=/run/secrets/keycloak-key \ + $THEME_DEV_ARGS & + +PID="$!" + +/opt/keycloak/init.py + +wait "$PID" diff --git a/config/keycloak/forward-auth/Dockerfile b/config/keycloak/forward-auth/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e72d3006e21f88f23126036e95323c1a907abd53 --- /dev/null +++ b/config/keycloak/forward-auth/Dockerfile @@ -0,0 +1,5 @@ +FROM busybox:musl AS busybox +FROM quay.io/oauth2-proxy/oauth2-proxy:v7.13.0 +COPY ./entry.sh /init/entry.sh +COPY --from=busybox /bin /bin +ENTRYPOINT "/init/entry.sh" diff --git a/config/keycloak/forward-auth/entry.sh b/config/keycloak/forward-auth/entry.sh new file mode 100755 index 0000000000000000000000000000000000000000..d8b5dca1fbc277f23f2f4c02cad16cece8ffe661 --- /dev/null +++ b/config/keycloak/forward-auth/entry.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +CADDY_IP="$(nslookup caddy | awk '/^Address: / { print $2 }')" +echo "${CADDY_IP} auth.${HOME_DOMAIN}" >>/etc/hosts +rm -f /etc/ssl/certs/caddy.crt +ln -s /caddy/pki/authorities/root.crt /etc/ssl/certs/caddy.crt + +export OAUTH2_PROXY_CLIENT_SECRET="$(cat /shared/forward-auth)" + +exec /bin/oauth2-proxy diff --git a/config/keycloak/init.py b/config/keycloak/init.py new file mode 100755 index 0000000000000000000000000000000000000000..aceb961c72bfbed5dd77790adf303dbe7599931c --- /dev/null +++ b/config/keycloak/init.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# declarative configuration for keycloak +# pyright: reportUnknownMemberType=false +# pyright: reportUnknownVariableType=false +# pyright: reportUnknownArgumentType=false +import logging +import os +import requests +import sys +import time +from keycloak import KeycloakAdmin, KeycloakOpenIDConnection +from pathlib import Path + + +def env(key: str): + val = os.environ.get(key) + if not val: + raise Exception(f"Missing ${key}") + return val + + +groups_to_create = ["snow", "media", "media-manage", "git", "file"] +realm_config = { + "realm": "master", + "displayName": "snow sign on", + "displayNameHtml": '
snow sign on
', + "editUsernameAllowed": True, + "resetPasswordAllowed": True, + "rememberMe": True, + "loginWithEmailAllowed": True, + "registrationAllowed": False, + "webAuthnPolicyRpEntityName": "snow sign on", + "webAuthnPolicyPasswordlessPasskeysEnabled": True, + "loginTheme": "snow", + "smtpServer": { + "from": f"noreply.auth@{env('HOME_DOMAIN')}", + "fromDisplayName": "clover bot", + "replyTo": f"me@{env('HOME_DOMAIN')}", + "replyToDisplayName": "clover caruso", + "envelopeFrom": "", + "host": env("MAILER_ADDRESS"), + "port": "587", + "ssl": "true", + "starttls": "true", + "auth": "true", + "user": env("MAILER_USERNAME"), + "authType": "basic", + "password": env("MAILER_PASSWORD"), + }, +} + + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def wait_for_keycloak(timeout: int = 120) -> bool: + start_time = time.time() + + while time.time() - start_time < timeout: + try: + response = requests.get(f"http://localhost:9000/health/ready", timeout=5) + if response.status_code == 200: + return True + except requests.exceptions.RequestException: + pass + + time.sleep(2) + + logger.error(f"Timeout waiting for Keycloak to be ready after {timeout} seconds") + return False + + +def configure(): + admin_user = env("KC_BOOTSTRAP_ADMIN_USERNAME") + admin_pass = env("KC_BOOTSTRAP_ADMIN_PASSWORD") + connection = KeycloakOpenIDConnection( + server_url="http://localhost:80", + username=admin_user, + password=admin_pass, + realm_name="master", + client_id="admin-cli", + ) + kc_admin = KeycloakAdmin(connection=connection) + + _ = kc_admin.update_realm("master", realm_config) + + # admin + admin_email = env("ADMIN_EMAIL") + users = kc_admin.get_users({"username": "snow"}) + if not users: + snow_user_id = kc_admin.create_user( + { + "username": "snow", + "firstName": "Snow", + "email": admin_email, + "enabled": True, + "emailVerified": True, + "requiredActions": [ + "UPDATE_PASSWORD", + "webauthn-register-passwordless", + ], + } + ) + _ = kc_admin.set_user_password(snow_user_id, admin_pass, temporary=True) + + roles = kc_admin.get_realm_roles() + admin_role = next((role for role in roles if role["name"] == "admin"), None) + if not admin_role: + raise Exception("Missing admin role") + + _ = kc_admin.assign_realm_roles(snow_user_id, [admin_role]) + logger.info("Admin role assigned to snow :3") + + # groups + existing_groups = kc_admin.get_groups() + existing_group_names = {group["name"] for group in existing_groups} + + for group_name in groups_to_create: + if group_name not in existing_group_names: + logger.info(f"Creating group: {group_name}") + _ = kc_admin.create_group({"name": group_name}) + else: + logger.info(f"Group {group_name} already exists, skipping") + + client_id = kc_admin.create_client( + { + "protocol": "openid-connect", + "clientId": "forward-auth", + "name": "Forward-Auth", + "description": "", + "publicClient": False, + "authorizationServicesEnabled": False, + "serviceAccountsEnabled": False, + "implicitFlowEnabled": False, + "directAccessGrantsEnabled": False, + "standardFlowEnabled": True, + "frontchannelLogout": True, + "attributes": { + "saml_idp_initiated_sso_url_name": "", + "standard.token.exchange.enabled": False, + "oauth2.device.authorization.grant.enabled": False, + "oidc.ciba.grant.enabled": False, + "pkce.code.challenge.method": "", + "dpop.bound.access.tokens": "false", + "post.logout.redirect.uris": "*", + }, + "alwaysDisplayInConsole": False, + "rootUrl": "", + "baseUrl": "", + "redirectUris": ["*"], + }, + skip_exists=True, + ) + client_data = kc_admin.get_client(client_id) + fw_secret_path = Path("/shared/forward-auth") + fw_secret_path.parent.mkdir(parents=True, exist_ok=True) + _ = fw_secret_path.write_text(client_data["secret"]) + + +def main(): + if not wait_for_keycloak(): + sys.exit(1) + + configure() + logger.info("initial data upserted <3") + + +if __name__ == "__main__": + main() diff --git a/config/keycloak/pyproject.toml b/config/keycloak/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..9474f8367a0113acbfa20b009061228eb323bb1f --- /dev/null +++ b/config/keycloak/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "keycloak" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "python-keycloak>=6.0.0", + "requests>=2.32.5", +] diff --git a/config/keycloak/theme/login/resources/css/styles.css b/config/keycloak/theme/login/resources/css/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..ea3ac145c7b15be4e3295be70a6184add9480c62 --- /dev/null +++ b/config/keycloak/theme/login/resources/css/styles.css @@ -0,0 +1,360 @@ +@font-face { + font-family: "Name Mono"; + src: url("../font/ATNameMono-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Name Sans"; + src: url("../font/nsv.woff2") format("woff2") tech(variations); + font-weight: 100 900; + font-style: normal; + font-display: swap; + font-stretch: 75% 125%; +} + +@font-face { + font-family: "Name Sans"; + src: url("../font/nsvi.woff2") format("woff2") tech(variations); + font-weight: 100 900; + font-style: italic; + font-display: swap; + font-stretch: 75% 125%; +} + +/* reset */ +html, +body { + height: 100%; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: unset; + font-weight: unset; +} + +:where( + html, + body, + p, + ol, + ul, + li, + dl, + dt, + dd, + blockquote, + figure, + fieldset, + legend, + textarea, + pre, + iframe, + hr, + h1, + h2, + h3, + h4, + h5, + h6 +) { + margin: 0; + padding: 0; +} + +*, :after, :before { + box-sizing: border-box; + font: unset; +} + +/* root */ +body { + color-scheme: light dark; + background-color: #f9feff; + background-image: url(../img/miku-light.png); + background-size: auto 100%; + background-position: right top; + background-repeat: no-repeat; + color: black; + --text: black; + font-family: "Name Sans", sans-serif; + + --header: light-dark(#1a46cd, #938cff); + --primary: light-dark(#00238f, #938cff); +} +@media (prefers-color-scheme: dark) { + body { + background-image: url(../img/miku-dark.png); + background-color: #2f4b67; + color: white; + --text: white; + } +} +@media (max-width: 1313px) { + body { + background-position: right calc(-100px + 50%) top; + } +} + +/* sidebar */ +.pf-v5-c-login__main { + max-width: 30rem; + padding: 2rem; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + --bg: light-dark(rgba(30, 30, 30, 0.1), rgba(0, 0, 0, 0.3)); + background-color: var(--bg); + border-style: solid; + border-right-width: 4px; + border-color: var(--bg); + backdrop-filter: blur(4px); + overflow-y: auto; +} +.kc-logo-text { + font-size: 3rem; + text-align: center; + color: var(--header); +} +#kc-page-title, #kc-info-wrapper { + text-align: center; + color: rgb(from var(--text) r g b / 0.8); +} +#kc-page-title { + margin-bottom: 1rem; +} +#kc-info-wrapper { + margin-top: 1rem; +} +#kc-form-options { + margin-bottom: 1rem; +} +a { + color: var(--header); + text-decoration: dotted underline; +} +a:hover { + text-decoration: underline; + background-color: rgb(from var(--header) r g b / 0.2); +} + +/* branding */ +.kc-logo-text::before { + display: block; + content: " "; + width: 30%; + aspect-ratio: 1; + margin: auto; + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDIwTDguNzUgMTcuNUw2IDE4TTEwIDRMOC43NSA2LjVMNiA2TTE0IDIwTDE1LjI1IDE3LjVMMTggMThNMTQgNEwxNS4yNSA2LjVMMTggNk0xNyAyMUwxNCAxNU0xNCAxNUgxME0xNCAxNUwxNS41IDEyTTEwIDE1TDcgMjFNMTAgMTVMOC41IDEyTTE3IDNMMTQgOU0xNCA5TDE1LjUgMTJNMTQgOUgxME0xNS41IDEySDIyTTIgMTJIOC41TTguNSAxMkwxMCA5TTEwIDlMNyAzTTIwIDEwTDE4LjUgMTJMMjAgMTRNNCAxMEw1LjUgMTJMNCAxNCIgc3Ryb2tlPSJ1cmwoI3BhaW50MF9saW5lYXJfNTQ4XzE5KSIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTEwIDIwTDguNzUgMTcuNUw2IDE4TTEwIDRMOC43NSA2LjVMNiA2TTE0IDIwTDE1LjI1IDE3LjVMMTggMThNMTQgNEwxNS4yNSA2LjVMMTggNk0xNyAyMUwxNCAxNU0xNCAxNUgxME0xNCAxNUwxNS41IDEyTTEwIDE1TDcgMjFNMTAgMTVMOC41IDEyTTE3IDNMMTQgOU0xNCA5TDE1LjUgMTJNMTQgOUgxME0xNS41IDEySDIyTTIgMTJIOC41TTguNSAxMkwxMCA5TTEwIDlMNyAzTTIwIDEwTDE4LjUgMTJMMjAgMTRNNCAxMEw1LjUgMTJMNCAxNCIgc3Ryb2tlPSJ1cmwoI3BhaW50MV9saW5lYXJfNTQ4XzE5KSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl81NDhfMTkiIHgxPSI0IiB5MT0iLTIiIHgyPSIxOC41IiB5Mj0iMjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzIyM0Q5OSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxNTQzOTIiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDFfbGluZWFyXzU0OF8xOSIgeDE9IjEwIiB5MT0iLTMiIHgyPSIxOCIgeTI9IjI3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNGMkUzRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRjJGOEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==); + background-size: cover; +} +#kc-page-title, +#kc-info-wrapper, +.checkbox, +a, +#kc-form-buttons, +.pf-v5-c-helper-text__item-text, +.pf-v5-c-alert__title, +input[type="submit"], +input[type="button"], +button { + text-transform: lowercase; +} + +/* text input */ +.pf-v5-c-form__group { + margin-bottom: 1rem; +} +.pf-v5-c-form__group input:not([type="checkbox"]) { + width: 100%; +} +.pf-v5-c-form__label { + margin-bottom: 0.25rem; + text-transform: lowercase; + user-select: none; + display: block; +} +.pf-v5-c-input-group { + display: flex; + border-radius: 8px; +} +.pf-v5-c-form-control { + background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15)); + padding: 2px; + appearance: none; + border: 2px solid var(--text); + font-size: inherit; + color: var(--text); + text-indent: 8px; + height: 38px; + border-radius: 8px; + flex: 1; +} +.pf-v5-c-form-control:has(+ button) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.pf-v5-c-form-control + button { + appearance: none; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + width: 38px; + border: 2px solid var(--text); + border-left: none; + background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15)); + transition: background-color 0.1s linear; +} +.pf-v5-c-form-control + button:hover { + background-color: light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2)); +} +.pf-v5-c-form__group:has(input:focus-visible) + > :is(input, .pf-v5-c-input-group) { + outline: 2px solid rgb(from var(--primary) r g b / 0.5); +} +.pf-v5-c-form__group:has(input:focus-visible) * { + border-color: var(--primary); + outline: none; +} +.pf-v5-c-form__group:has(input:focus-visible) .pf-v5-c-form__label { + color: var(--header); +} +.pf-v5-c-helper-text__item-text { + display: block; + margin-top: 0.5rem; +} +.pf-m-error { + color: light-dark(#c80000, #f56666); +} + +/* checkbox */ +.checkbox input { + display: none; +} +.checkbox label { + display: flex; + align-items: center; + user-select: none; + cursor: pointer; +} +.checkbox label:before { + content: " "; + display: block; + width: 24px; + height: 24px; + margin-right: 0.5rem; + margin-left: -2px; + background-color: var(--text); + + mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1pY29uIGx1Y2lkZS1zcXVhcmUiPjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgeD0iMyIgeT0iMyIgcng9IjIiLz48L3N2Zz4=); + mask-size: cover; +} +.checkbox label:has(:checked):before { + background-color: var(--primary); + mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1jaGVjay1iaWctaWNvbiBsdWNpZGUtc3F1YXJlLWNoZWNrLWJpZyI+PHBhdGggZD0iTTIxIDEwLjY1NlYxOWEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMlY1YTIgMiAwIDAgMSAyLTJoMTIuMzQ0Ii8+PHBhdGggZD0ibTkgMTEgMyAzTDIyIDQiLz48L3N2Zz4=); +} +.checkbox label:has(:checked) { + color: var(--primary); +} + +/* buttons */ +.pf-v5-c-button.pf-m-primary { + background-color: var(--primary); + display: block; + border: none; + height: 30px; + border-radius: 8px; + color: white; +} +.pf-v5-c-button.pf-m-block { + display: block; + width: 100%; +} + +/* alert */ +.pf-v5-c-alert { + border-radius: 8px; + padding: 8px; + align-items: center; + margin-bottom: 1rem; + text-align: center; + text-wrap: balance; +} +.alert-error { + background-color: light-dark(#ff010182, #f56666a1); +} +.alert-warning, .alert-info { + background-color: rgb(from var(--primary) r g b / 0.5); +} + +/* icons */ +[data-password-toggle] { + display: grid; + align-items: center; + justify-content: center; +} +[data-password-toggle] i { + display: block; + width: 24px; + height: 24px; + background-color: var(--text); + mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV5ZS1pY29uIGx1Y2lkZS1leWUiPjxwYXRoIGQ9Ik0yLjA2MiAxMi4zNDhhMSAxIDAgMCAxIDAtLjY5NiAxMC43NSAxMC43NSAwIDAgMSAxOS44NzYgMCAxIDEgMCAwIDEgMCAuNjk2IDEwLjc1IDEwLjc1IDAgMCAxLTE5Ljg3NiAwIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIvPjwvc3ZnPg==); +} +input[type="text"] + [data-password-toggle] i { + mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV5ZS1vZmYtaWNvbiBsdWNpZGUtZXllLW9mZiI+PHBhdGggZD0iTTEwLjczMyA1LjA3NmExMC43NDQgMTAuNzQ0IDAgMCAxIDExLjIwNSA2LjU3NSAxIDEgMCAwIDEgMCAuNjk2IDEwLjc0NyAxMC43NDcgMCAwIDEtMS40NDQgMi40OSIvPjxwYXRoIGQ9Ik0xNC4wODQgMTQuMTU4YTMgMyAwIDAgMS00LjI0Mi00LjI0MiIvPjxwYXRoIGQ9Ik0xNy40NzkgMTcuNDk5YTEwLjc1IDEwLjc1IDAgMCAxLTE1LjQxNy01LjE1MSAxIDEgMCAwIDEgMC0uNjk2IDEwLjc1IDEwLjc1IDAgMCAxIDQuNDQ2LTUuMTQzIi8+PHBhdGggZD0ibTIgMiAyMCAyMCIvPjwvc3ZnPg==); +} + +@media (max-width: 799px) { + body { + background-position: left 65% bottom 60%; + background-size: auto 150%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + } + body:before { + display: block; + content: ""; + flex: 1.5; + } + .pf-v5-c-login__main { + flex: 1 1 40%; + max-width: 25rem; + min-width: 80%; + border-width: 4px; + border-bottom-width: 0; + border-top-left-radius: 32px; + border-top-right-radius: 32px; + corner-shape: superellipse(0); + justify-content: space-between; + --bg: light-dark(rgba(255, 255, 255, 0.7), rgba(0.2, 0, 0.2, 0.6)); + } + .pf-v5-c-login__main:after { + display: block; + content: ""; + } + .kc-logo-text::before { + position: absolute; + left: 50%; + width: 100px; + transform: translate(-50%, calc(-50% - 35px)); + } +} + +#credit { + position: fixed; + bottom: 2px; + right: 2px; + font-size: 14px; +} +#credit a:not(:hover) { + opacity: 0.5; +} diff --git a/config/keycloak/theme/login/resources/font/.gitignore b/config/keycloak/theme/login/resources/font/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/config/keycloak/theme/login/resources/img/license.txt b/config/keycloak/theme/login/resources/img/license.txt new file mode 100644 index 0000000000000000000000000000000000000000..e3dfb05a4dc66f6de256016ed332ea9d27d56d04 --- /dev/null +++ b/config/keycloak/theme/login/resources/img/license.txt @@ -0,0 +1,2 @@ +https://safebooru.org/index.php?page=post&s=view&id=4405346 +dark mode by paper clover diff --git a/config/keycloak/theme/login/resources/img/miku-dark.png b/config/keycloak/theme/login/resources/img/miku-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..635e025142cb94b89f97ac79c6b460ca2c8294f4 Binary files /dev/null and b/config/keycloak/theme/login/resources/img/miku-dark.png differ diff --git a/config/keycloak/theme/login/resources/img/miku-light.png b/config/keycloak/theme/login/resources/img/miku-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5dc5ef1e1b250627c999b4fbe296af62cc2dea82 Binary files /dev/null and b/config/keycloak/theme/login/resources/img/miku-light.png differ diff --git a/config/keycloak/theme/login/resources/js/stars.js b/config/keycloak/theme/login/resources/js/stars.js new file mode 100644 index 0000000000000000000000000000000000000000..20327f1eba4fe0d989631e63dc3c3aef94e0d35d --- /dev/null +++ b/config/keycloak/theme/login/resources/js/stars.js @@ -0,0 +1,105 @@ +const shader = ` + // Heavily modified off of https://www.shadertoy.com/view/3sKGWw + // big thanks to these resources as well: + // https://youtu.be/3CycKKJiwis + // https://www.shadertoy.com/view/4djSRW + // https://www.shadertoy.com/view/3s3GDn + + precision mediump float; + uniform vec2 u_resolution; + uniform float u_time; + uniform bool u_mask; + + float random(vec2 p){ + vec3 p3 = fract(vec3(p.xyx) * .1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + + vec2 random2(vec2 p){ + vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); + p3 += dot(p3, p3.yzx+33.33); + return fract((p3.xx+p3.yz)*p3.zy); + } + + void main() { + float glow; + vec2 uv; + vec2 fl; + vec2 local_uv; + vec2 seed; + vec2 centre; + vec2 cell; + float t = u_time * 0.00015; + float blinkTime = u_time * 0.0068; + float rotationAngle = -t * 21.0; + mat2 rotation = mat2(cos(rotationAngle), -sin(rotationAngle), + sin(rotationAngle), cos(rotationAngle)); + vec3 col = vec3(0); + + centre = vec2(cos(t), sin(t)) * 5.0 + 0.5; + + // Get uv from the fragment coordinates, rotation and depth + uv = (centre - (gl_FragCoord.xy - u_resolution / 2.0) / (1600.0 / 25.0)) * rotation; + fl = floor(uv); + // The local cell coordinates. uv-fl == frac(uv) + local_uv = uv - fl - 0.5; + + // For a 3x3 group of cells around the fragment, find the distance from the points + // of each to the current fragment and draw an accumulative glow accordingly + // The local cell is (0, 0) + for(float j = -1.0; j <= 1.0; j++){ + for(float k = -1.0; k <= 1.0; k++){ + cell = vec2(j, k); + seed = 128.0 + (fl + cell); + glow = + ((0.6 + 0.5 * sin((128.0 * random(seed)) * blinkTime))) * 25.0 + * pow(0.005 / length(local_uv - (cell + 0.9 * (random2(seed) - 0.5))), 2.0); + col += vec3(0.2 * glow) + 0.1 * glow; + } + } + + col = 1.0 - exp(-col); + col = pow(col, vec3(0.45)); + + if (u_mask) { + float mask = smoothstep(100.0, 300.0, length(vec2(gl_FragCoord.x - u_resolution.x/2.0, gl_FragCoord.y - 200.0))); + col *= mask; + } + + gl_FragColor = vec4(col * (u_mask ? 0.6 : 0.25), 0); + } +`; + +function createShader(gl, type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (success) { + return shader; + } + + // eslint-disable-next-line no-console + console.error(gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + throw new Error("Failed to compile shader"); +} + +function createProgram(gl, vertexShader, fragmentShader) { + const program = gl.createProgram(); + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + const success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (success) { + return program; + } + + // eslint-disable-next-line no-console + console.error(gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + throw new Error("Failed to link program"); +} diff --git a/config/keycloak/theme/login/resources/js/webauthnRegister.js b/config/keycloak/theme/login/resources/js/webauthnRegister.js new file mode 100644 index 0000000000000000000000000000000000000000..eb98092ae45cd3931051e1bab579762534b847cd --- /dev/null +++ b/config/keycloak/theme/login/resources/js/webauthnRegister.js @@ -0,0 +1,159 @@ +import { base64url } from "rfc4648"; + +export async function registerByWebAuthn(input) { + // Check if WebAuthn is supported by this browser + if (!window.PublicKeyCredential) { + returnFailure(input.errmsg); + return; + } + + const publicKey = { + challenge: base64url.parse(input.challenge, { loose: true }), + rp: { id: input.rpId, name: input.rpEntityName }, + user: { + id: base64url.parse(input.userid, { loose: true }), + name: input.username, + displayName: input.username, + }, + pubKeyCredParams: getPubKeyCredParams(input.signatureAlgorithms), + }; + + if (input.attestationConveyancePreference !== "not specified") { + publicKey.attestation = input.attestationConveyancePreference; + } + + const authenticatorSelection = {}; + let isAuthenticatorSelectionSpecified = false; + + if (input.authenticatorAttachment !== "not specified") { + authenticatorSelection.authenticatorAttachment = + input.authenticatorAttachment; + isAuthenticatorSelectionSpecified = true; + } + + if (input.requireResidentKey !== "not specified") { + if (input.requireResidentKey === "Yes") { + authenticatorSelection.requireResidentKey = true; + } else { + authenticatorSelection.requireResidentKey = false; + } + isAuthenticatorSelectionSpecified = true; + } + + if (input.userVerificationRequirement !== "not specified") { + authenticatorSelection.userVerification = input.userVerificationRequirement; + isAuthenticatorSelectionSpecified = true; + } + + if (isAuthenticatorSelectionSpecified) { + publicKey.authenticatorSelection = authenticatorSelection; + } + + if (input.createTimeout !== 0) { + publicKey.timeout = input.createTimeout * 1000; + } + + const excludeCredentials = getExcludeCredentials(input.excludeCredentialIds); + if (excludeCredentials.length > 0) { + publicKey.excludeCredentials = excludeCredentials; + } + + try { + const result = await doRegister(publicKey); + returnSuccess(result, input.initLabel, input.initLabelPrompt); + } catch (error) { + returnFailure(error); + } +} + +function doRegister(publicKey) { + return navigator.credentials.create({ publicKey }); +} + +function getPubKeyCredParams(signatureAlgorithmsList) { + const pubKeyCredParams = []; + if (signatureAlgorithmsList.length === 0) { + pubKeyCredParams.push({ type: "public-key", alg: -7 }); + return pubKeyCredParams; + } + + for (const entry of signatureAlgorithmsList) { + pubKeyCredParams.push({ + type: "public-key", + alg: entry, + }); + } + + return pubKeyCredParams; +} + +function getExcludeCredentials(excludeCredentialIds) { + const excludeCredentials = []; + if (excludeCredentialIds === "") { + return excludeCredentials; + } + + for (const entry of excludeCredentialIds.split(",")) { + excludeCredentials.push({ + type: "public-key", + id: base64url.parse(entry, { loose: true }), + }); + } + + return excludeCredentials; +} + +function getTransportsAsString(transportsList) { + if (!Array.isArray(transportsList)) { + return ""; + } + + return transportsList.join(); +} + +function returnSuccess(result, initLabel, initLabelPrompt) { + document.getElementById("clientDataJSON").value = base64url.stringify( + new Uint8Array(result.response.clientDataJSON), + { pad: false }, + ); + document.getElementById("attestationObject").value = base64url.stringify( + new Uint8Array(result.response.attestationObject), + { pad: false }, + ); + document.getElementById("publicKeyCredentialId").value = base64url.stringify( + new Uint8Array(result.rawId), + { pad: false }, + ); + + if (typeof result.response.getTransports === "function") { + const transports = result.response.getTransports(); + if (transports) { + document.getElementById("transports").value = getTransportsAsString( + transports, + ); + } + } else { + console.log( + "Your browser is not able to recognize supported transport media for the authenticator.", + ); + } + + // let labelResult = window.prompt(initLabelPrompt, initLabel); + // if (labelResult === null) { + // labelResult = initLabel; + // } + document.getElementById("authenticatorLabel").value = "Passkey " + + new Date().toString(); + + document.getElementById("register").requestSubmit(); +} + +function returnFailure(err) { + if (err.name === "NotAllowedError") { + document.getElementById("error").value = + "the operation was cancelled / browser does not support passkey"; + } else { + document.getElementById("error").value = err; + } + document.getElementById("register").requestSubmit(); +} diff --git a/config/keycloak/theme/login/template.ftl b/config/keycloak/theme/login/template.ftl new file mode 100644 index 0000000000000000000000000000000000000000..712a4f97ea88328c90c22b552a9774e5637a5fa6 --- /dev/null +++ b/config/keycloak/theme/login/template.ftl @@ -0,0 +1,209 @@ +<#import "footer.ftl" as loginFooter> +<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false> + + dir="${(locale.rtl)?then('rtl','ltr')}"> + + + + + + + <#if properties.meta?has_content> + <#list properties.meta?split(' ') as meta> + + + + sso (snow sign on) + + <#if properties.stylesCommon?has_content> + <#list properties.stylesCommon?split(' ') as style> + + + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + + + + <#if scripts??> + <#list scripts as script> + + + + + + <#if authenticationSession??> + + + + + +
+
+
${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}
+
+
+
+ <#if realm.internationalizationEnabled && locale.supported?size gt 1> +
+
+ +
+
+ + <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+

<#nested "header">

+
+
+ <#else> +

<#nested "header">

+ + <#else> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+ <#nested "show-username"> +
+ + + + +
+
+
+ <#else> + <#nested "show-username"> +
+ + + + +
+ + +
+
+
+ + <#-- App-initiated actions should not see warning messages about the need to complete the action --> + <#-- during login. --> + <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)> +
+
+ <#if message.type = 'success'> + <#if message.type = 'warning'> + <#if message.type = 'error'> + <#if message.type = 'info'> +
+ ${kcSanitize(message.summary)?no_esc} +
+ + + <#nested "form"> + + <#if auth?has_content && auth.showTryAnotherWayLink()> +
+ +
+ + + <#nested "socialProviders"> + + <#if displayInfo> +
+
+ <#nested "info"> +
+
+ +
+
+ + + <@loginFooter.content/> +
+
+
+ art by 紺屋 +
+ + + diff --git a/config/keycloak/theme/login/theme.properties b/config/keycloak/theme/login/theme.properties new file mode 100644 index 0000000000000000000000000000000000000000..4782b6590d5e7fb87a10c56a54cc7aa7b9cdb0f1 --- /dev/null +++ b/config/keycloak/theme/login/theme.properties @@ -0,0 +1,128 @@ +parent=base +import=common/keycloak + +styles=css/styles.css +stylesCommon= + +darkMode=false + +kcFormGroupClass=pf-v5-c-form__group +kcFormGroupLabelClass=pf-v5-c-form__group-label pf-v5-u-pb-xs +kcFormLabelClass=pf-v5-c-form__label +kcFormLabelTextClass=pf-v5-c-form__label-text + +kcLabelClass=pf-v5-c-form__label +kcInputClass=pf-v5-c-form-control +kcInputGroup=pf-v5-c-input-group +kcFormHelperTextClass=pf-v5-c-form__helper-text +kcInputHelperTextClass=pf-v5-c-helper-text pf-v5-u-display-flex pf-v5-u-justify-content-space-between +kcInputHelperTextItemClass=pf-v5-c-helper-text__item +kcInputHelperTextItemTextClass=pf-v5-c-helper-text__item-text +kcInputGroupItemClass=pf-v5-c-input-group__item +kcFill=pf-m-fill +kcError=pf-m-error +kcLoginFooterBand=pf-v5-c-login__main-footer-band +kcLoginFooterBandItem=pf-v5-c-login__main-footer-band-item + +kcCheckboxClass=pf-v5-c-check +kcCheckboxInputClass=pf-v5-c-check__input +kcCheckboxLabelClass=pf-v5-c-check__label +kcCheckboxLabelRequiredClass=pf-v5-c-check__label-required + +kcInputRequiredClass=pf-v5-c-form__label-required +kcInputErrorMessageClass=pf-v5-c-helper-text__item-text pf-m-error kc-feedback-text +kcFormControlUtilClass=pf-v5-c-form-control__utilities +kcInputErrorIconStatusClass=pf-v5-c-form-control__icon pf-m-status +kcInputErrorIconClass=fas fa-exclamation-circle +kcAlertClass=pf-v5-c-alert pf-m-inline pf-v5-u-mb-md +kcAlertIconClass=pf-v5-c-alert__icon +kcAlertTitleClass=pf-v5-c-alert__title +kcAlertDescriptionClass=pf-v5-c-alert__description +kcFormPasswordVisibilityButtonClass=pf-v5-c-button pf-m-control +kcFormControlToggleIcon=pf-v5-c-form-control__toggle-icon +kcFormActionGroupClass=pf-v5-c-form__actions pf-v5-u-pt-xs +kcFormReadOnlyClass=pf-m-readonly + +kcPanelClass=pf-v5-c-panel pf-m-raised +kcPanelMainClass=pf-v5-c-panel__main +kcPanelMainBodyClass=pf-v5-c-panel__main-body +kcListClass=pf-v5-c-list + +kcButtonClass=pf-v5-c-button +kcButtonPrimaryClass=pf-v5-c-button pf-m-primary +kcButtonSecondaryClass=pf-v5-c-button pf-m-secondary +kcButtonBlockClass=pf-m-block +kcButtonLinkClass=pf-v5-c-button pf-m-link +kcCommonLogoIdP=pf-v5-c-login__main-footer-links-item +kcFormSocialAccountListClass=pf-v5-c-login__main-body pf-v5-u-pl-0 pf-v5-u-pr-0 +kcFormSocialAccountListItemClass=pf-v5-u-pb-sm +kcFormSocialAccountNameClass=pf-v5-u-m-auto +kcFormSocialAccountListButtonClass=pf-v5-c-button pf-m-secondary pf-m-block pf-v5-u-display-flex pf-v5-u-align-items-center pf-v5-u-justify-content-space-between +kcFormSocialAccountListButtonDisabledClass=pf-m-aria-disabled +kcFormSocialAccountListGridClass=pf-v5-l-grid pf-m-gutter pf-m-all-6-col-on-xl pf-m-all-6-col-on-sm +kcFormSocialAccountGridItem=pf-v5-l-grid__item + +kcLoginClass=pf-v5-c-login__main +kcFormClass=pf-v5-c-form pf-v5-u-w-100 +kcFormCardClass=card-pf + +kcResetFlowIcon=pf-icon fas fa-share-square + +kcSelectAuthListClass=pf-v5-c-data-list select-auth-container +kcSelectAuthListItemWrapperClass=pf-v5-c-data-list__item pf-m-clickable +kcSelectAuthListItemClass=pf-v5-c-data-list__item-row select-auth-box-parent +kcSelectAuthListItemHeadingClass=pf-v5-u-font-family-heading select-auth-box-headline +kcSelectAuthListItemBodyClass=pf-v5-c-data-list__cell pf-m-no-fill pf-v5-u-pt-md pf-v5-u-pb-md +kcSelectAuthListItemIconClass=pf-v5-c-data-list__cell pf-m-icon pf-v5-u-display-flex pf-v5-u-pt-0 pf-v5-u-align-items-center +kcSelectAuthListItemFillClass=pf-v5-c-data-list__item-action +kcSelectAuthListItemDescriptionClass=pf-v5-c-data-list__cell pf-m-no-fill select-auth-box-desc + +kcRecoveryCodesWarning=pf-v5-c-alert pf-m-warning pf-m-inline pf-v5-u-mb-md kc-recovery-codes-warning +kcLogin=pf-v5-c-login +kcLoginContainer=pf-v5-c-login__container +kcLoginMain=pf-v5-c-login__main +kcLoginMainHeader=pf-v5-c-login__main-header +kcLoginMainFooter=pf-v5-c-login__main-footer +kcLoginMainFooterBand=pf-v5-c-login__main-footer-band +kcLoginMainFooterBandItem=pf-v5-c-login__main-footer-band-item +kcLoginMainFooterHelperText=pf-v5-u-font-size-sm pf-v5-u-color-200 +kcLoginMainTitle=pf-v5-c-title pf-m-3xl +kcLoginMainHeaderUtilities=pf-v5-c-login__main-header-utilities +kcLoginMainBody=pf-v5-c-login__main-body + +kcContentWrapperClass=pf-v5-u-mb-md-on-md +kcWebAuthnDefaultIcon=pf-v5-c-icon pf-m-lg +kcMarginTopClass=pf-v5-u-mt-md-on-md + +kcLoginOTPListClass=pf-v5-c-tile +kcLoginOTPListItemHeaderClass=pf-v5-c-tile__header pf-m-stacked +kcLoginOTPListItemIconBodyClass=pf-v5-c-tile__icon +kcLoginOTPListItemIconClass=fa fa-mobile +kcLoginOTPListItemTitleClass=pf-v5-c-tile__title +kcLoginOTPListSelectedClass=pf-m-selected + +kcCopyIconClass=fas fa-copy +kcCheckIconClass=fas fa-check +kcAngleRightIconClass=fas fa-angle-right +kcAngleDownIconClass=fas fa-angle-down +kcExpandedClass=pf-m-expanded + +kcCodeClipboardCopyClass=pf-v5-c-clipboard-copy +kcCodeClipboardCopyGroupClass=pf-v5-c-clipboard-copy__group +kcCodeClipboardCopyContentClass=pf-v5-c-clipboard-copy__expandable-content + +kcDarkModeClass=pf-v5-theme-dark + +kcHtmlClass=login-pf +kcLogoIdP-facebook= +kcLogoIdP-google= +kcLogoIdP-github= +kcLogoIdP-linkedin= +kcLogoIdP-instagram= +kcLogoIdP-microsoft= +kcLogoIdP-bitbucket= +kcLogoIdP-gitlab= +kcLogoIdP-paypal= +kcLogoIdP-stackoverflow= +kcLogoIdP-twitter= +kcLogoIdP-openshift-v4= diff --git a/config/keycloak/theme/login/webauthn-error.ftl b/config/keycloak/theme/login/webauthn-error.ftl new file mode 100644 index 0000000000000000000000000000000000000000..2c31dab3728fdb22390524faca66e52df59bc343 --- /dev/null +++ b/config/keycloak/theme/login/webauthn-error.ftl @@ -0,0 +1,36 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=true; section> + <#if section = "header"> + ${kcSanitize(msg("webauthn-error-title"))?no_esc} + <#elseif section = "form"> + + + +
+ + +
+ + + + <#if isAppInitiatedAction??> +
+ +
+ + + + \ No newline at end of file diff --git a/config/keycloak/theme/login/webauthn-register.ftl b/config/keycloak/theme/login/webauthn-register.ftl new file mode 100644 index 0000000000000000000000000000000000000000..fa9261e623f40989c3d6209fd717bbaef6d1bfdc --- /dev/null +++ b/config/keycloak/theme/login/webauthn-register.ftl @@ -0,0 +1,72 @@ +<#import "template.ftl" as layout> +<#import "password-commons.ftl" as passwordCommons> + +<@layout.registrationLayout; section> + <#if section = "title"> + title + <#elseif section = "header"> +
+
+ +
+ + you will be prompted to create a passkey. please contact clover if + this doesn't work in your browser. + +
+ <#elseif section = "form"> + +
+
+ + + + + + +
+
+ + + + + + <#if !isSetRetry?has_content && isAppInitiatedAction?has_content> +
+ +
+ + + + diff --git a/config/keycloak/uv.lock b/config/keycloak/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..24046cab064d363850d2846c924f9fddf21439c4 --- /dev/null +++ b/config/keycloak/uv.lock @@ -0,0 +1,380 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "async-property" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/12/900eb34b3af75c11b69d6b78b74ec0fd1ba489376eceb3785f787d1a0a1d/async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380", size = 16523, upload-time = "2023-07-03T17:21:55.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7", size = 9546, upload-time = "2023-07-03T17:21:54.293Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jwcrypto" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/db/870e5d5fb311b0bcf049630b5ba3abca2d339fd5e13ba175b4c13b456d08/jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039", size = 87168, upload-time = "2024-03-06T19:58:31.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520, upload-time = "2024-03-06T19:58:29.765Z" }, +] + +[[package]] +name = "keycloak" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "python-keycloak" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "python-keycloak", specifier = ">=6.0.0" }, + { name = "requests", specifier = ">=2.32.5" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "python-keycloak" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "async-property" }, + { name = "deprecation" }, + { name = "httpx" }, + { name = "jwcrypto" }, + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/95/df72437403eba9f96902ff3b3049a3e06d0f0e47e5bd71ab1b364d440cef/python_keycloak-6.0.0.tar.gz", hash = "sha256:87271d381639f4179a9add88afbb3dbbe9689509cf240d1cf883e44beff4d8f9", size = 71107, upload-time = "2025-12-28T17:57:52.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/7e/f51774cb30db61f04dc0920f27b3a2c50ac05c8819dc4086436decf95010/python_keycloak-6.0.0-py3-none-any.whl", hash = "sha256:6f6697a0a894d6ded077aa86e12aa41433a0afffa27aa0b9e38395dd69a94009", size = 80167, upload-time = "2025-12-28T17:57:50.869Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] diff --git a/config/postgres/init.sh b/config/postgres/init.sh index 2f983ac5f344d7fcc766fc1134589a3073a27d7e..a1c1f068988c3e796d6f9d50ad038da120c3c414 100755 --- a/config/postgres/init.sh +++ b/config/postgres/init.sh @@ -7,6 +7,18 @@ until pg_isready -U postgres; do sleep 1 done +psql -v ON_ERROR_STOP=1 -U postgres postgres <<-SQL + DO \$\$ + BEGIN + IF EXISTS (SELECT FROM pg_user WHERE usename = 'postgres') THEN + ALTER USER "postgres" WITH PASSWORD '$POSTGRES_PASSWORD'; + ELSE + CREATE USER "postgres" WITH PASSWORD '$POSTGRES_PASSWORD'; + END IF; + END + \$\$; +SQL + ensure_db_user() { printf "Configuring Database %s\n" "$1" user="$1" @@ -30,7 +42,7 @@ SQL } ensure_db_user "clover" "$POSTGRES_PASSWORD" -ensure_db_user "authelia" "$POSTGRES_PASSWORD_AUTHELIA" +ensure_db_user "keycloak" "$POSTGRES_PASSWORD_KEYCLOAK" ensure_db_user "dawarich" "$POSTGRES_PASSWORD_DAWARICH" ensure_db_user "forgejo" "$POSTGRES_PASSWORD_FORGEJO" ensure_db_user "jellyfin" "$POSTGRES_PASSWORD_JELLYFIN" diff --git a/config/postgres/pgadmin-init.sh b/config/postgres/pgadmin-init.sh new file mode 100755 index 0000000000000000000000000000000000000000..14c25cbe6f38a5fbe2a8e13dbe8cd968e2dde163 --- /dev/null +++ b/config/postgres/pgadmin-init.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +export PGADMIN_SERVER_JSON_FILE=/init/pgadmin.json + +mkdir -p /var/lib/pgadmin/storage/pgadmin4_pgadmin.org +echo "postgres:5432:*:postgres:$POSTGRES_PASSWORD" >/var/lib/pgadmin/storage/pgadmin4_pgadmin.org/pgpass + +chmod 600 /var/lib/pgadmin/storage/*/pgpass +chown -R 5050:5050 /var/lib/pgadmin/storage/* + +/entrypoint.sh python3 diff --git a/config/postgres/pgadmin.json b/config/postgres/pgadmin.json new file mode 100644 index 0000000000000000000000000000000000000000..35df975eceaec989c73755375ff61a03975fa3ac --- /dev/null +++ b/config/postgres/pgadmin.json @@ -0,0 +1,13 @@ +{ + "Servers": { + "1": { + "Name": "Clover Home", + "Group": "Servers", + "Host": "postgres", + "Port": 5432, + "MaintenanceDB": "postgres", + "Username": "postgres", + "PassFile": "/var/lib/pgadmin/storage/pgadmin4_pgadmin.org/pgpass" + } + } +} diff --git a/local-dns.sh b/local-dns.sh index 2b5d6ad889fe82f990f982f9e13e06cd1bf00cd6..b54e5cd073e7c2389262f188ad82d006f05afea8 100644 --- a/local-dns.sh +++ b/local-dns.sh @@ -14,3 +14,6 @@ while ! [ -f .apps/caddy/pki/authorities/local/root.crt ]; do done sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain .apps/caddy/pki/authorities/local/root.crt + +echo +echo "If using OrbStack, you may need to restart it now" diff --git a/postgres-rescue.sh b/postgres-rescue.sh new file mode 100755 index 0000000000000000000000000000000000000000..ae3a79fd45007b44be60580a8a51af021a3cc3e7 --- /dev/null +++ b/postgres-rescue.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +. ./.env +cd "$(dirname "${BASH_SOURCE[0]}")" + +docker build -t postgres-rescue "config/postgres" --build-arg USER_ID="$USER_ID" --build-arg GROUP_ID="$GROUP_ID" + +docker run -it --rm \ + --user "$(id -u):$(id -g)" \ + -v "${APP_ROOT}/pg_data:/var/lib/postgresql/data" \ + --entrypoint /bin/bash \ + postgres-rescue diff --git a/readme.md b/readme.md index 7de3b548a041c05ed2b84b81b6dd9de7a0a7c8fb..4ed47285b4b17297dee0fe61e4e7d74597f0bb64 100644 --- a/readme.md +++ b/readme.md @@ -16,9 +16,6 @@ cp .env.example .env # fill secrets vi .env -vi secrets/authelia-encryption-key.txt -vi secrets/authelia-session-secret.txt -vi secrets/authelia-jwt-secret.txt vi secrets/openvpn-credentials.txt docker compose up -d diff --git a/secrets/.gitignore b/secrets/.gitignore index 2211df63dd2831aa0cfc38ba1ebc95e3c4620894..593bcf0e80e687e778500a232ded11e332aff821 100644 --- a/secrets/.gitignore +++ b/secrets/.gitignore @@ -1 +1,2 @@ -*.txt +!.gitignore +*