authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-02 01:00:15-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-05 02:16:01-08:00
log914005dab4ce28f6ba6f52c236f3c621549630ce
treee783f54310637f193eae649546902d99d36b15b5
parent4f9fc9b6a085ffbb5f78850f91023a01ed4c410f
signaturelock-open Commit is signed but in an unrecognized format.

feat: sso (snow sign on)


33 files changed, 2106 insertions(+), 234 deletions(-)

.env.example+13-4
......@@ -1,8 +1,8 @@
11# shellcheck disable
22HOME_DOMAIN=local.test
33ADMIN_EMAIL=
4USER_ID="501"
5GROUP_ID="20"
4USER_ID="1000"
5GROUP_ID="1000"
66
77APP_ROOT=./.apps
88STORE_ROOT=/Volumes
......@@ -11,16 +11,25 @@ MEDIA_ROOT=/Volumes/media
1111
1212MAILER_ADDRESS=smtp.example.com
1313MAILER_USERNAME=me@paperclover.net
14MAILER_PASSWORD=yolo
14MAILER_PASSWORD=
15
16KEYCLOAK_ADMIN_PASSWORD=
17FORWARD_AUTH_KEY=
1518
1619POSTGRES_PASSWORD=
1720POSTGRES_PASSWORD_AUTHELIA=
1821POSTGRES_PASSWORD_FORGEJO=
1922POSTGRES_PASSWORD_DAWARICH=
2023POSTGRES_PASSWORD_JELLYFIN=
24POSTGRES_PASSWORD_LLDAP=
2125
2226CLOVER_SOT_KEY=
2327
28ANUBIS_PRIVATE_KEY=
29
2430FORGEJO_SERVER_LFS_JWT_SECRET=
2531FORGEJO_OAUTH2_JWT_SECRET=
26FORGEJO_SECURITY_INTERNAL_TOKEN=
32FORGEJO_OPENID_SECRET=
33FORGEJO_SECURITY_SECRET_KEY=
34FORGEJO_SECURITY_INTERNAL_TOKEN=(jwt)
35
compose.yaml+189-40
......@@ -1,8 +1,8 @@
11services:
22 # web server
3 caddy:
3 caddy: # port 80, 443
44 container_name: caddy
5 image: caddy:latest
5 image: caddy:2.11
66 ports:
77 - 80:80
88 - 443:443
......@@ -12,46 +12,110 @@ services:
1212 - "./config:/etc/caddy:ro"
1313 - "${APP_ROOT}/caddy:/data/caddy:rw"
1414 environment:
15 - HOME_DOMAIN
15 HOME_DOMAIN: "${HOME_DOMAIN}"
1616 develop:
1717 watch:
1818 - path: ./config/Caddyfile
1919 action: restart
2020 # SSO
21 authelia:
22 image: "authelia/authelia"
23 container_name: authelia
21 keycloak: # port 80, 443
22 container_name: keycloak
23 build:
24 context: ./config/keycloak
25 dockerfile: Dockerfile
26 pull_policy: build
2427 volumes:
25 - "./config/authelia:/config"
26 restart: "unless-stopped"
28 - "${APP_ROOT}/keycloak:/shared:rw"
29 - "./config/keycloak/theme:/opt/keycloak/themes/snow:ro"
30 - "./config/font:/opt/keycloak/themes/snow/login/resources/font:ro"
2731 secrets:
28 - "authelia-jwt-secret"
29 - "authelia-session-secret"
30 - "authelia-encryption-key"
32 - keycloak-crt
33 - keycloak-key
3134 environment:
32 PUID: "$USER_ID"
33 PGID: "$GROUP_ID"
34 TZ: America/Los_Angelas
35 AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: "/run/secrets/authelia-jwt-secret"
36 AUTHELIA_SESSION_SECRET_FILE: "/run/secrets/authelia-session-secret"
37 AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: "/run/secrets/authelia-encryption-key"
38 AUTHELIA_STORAGE_POSTGRES_ADDRESS: postgres:5432
39 AUTHELIA_STORAGE_POSTGRES_DATABASE: authelia
40 AUTHELIA_STORAGE_POSTGRES_USERNAME: authelia
41 AUTHELIA_STORAGE_POSTGRES_PASSWORD: "${POSTGRES_PASSWORD_AUTHELIA}"
42 AUTHELIA_NOTIFIER_SMTP_ADDRESS: "smtp://${MAILER_ADDRESS}"
43 AUTHELIA_NOTIFIER_SMTP_USERNAME: "${MAILER_USERNAME}"
44 AUTHELIA_NOTIFIER_SMTP_PASSWORD: "${MAILER_PASSWORD}"
35 KC_BOOTSTRAP_ADMIN_USERNAME: admin
36 KC_BOOTSTRAP_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD:?}"
37 KC_DB: postgres
38 KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
39 KC_DB_USERNAME: keycloak
40 KC_DB_PASSWORD: "${POSTGRES_PASSWORD_KEYCLOAK:?}"
41 KC_HOSTNAME: "auth.${HOME_DOMAIN:?}"
42 KC_PROXY_HEADERS: xforwarded
43 KC_HEALTH_ENABLED: true
44 KC_HTTP_ENABLED: true
45 KC_HTTP_MANAGEMENT_SCHEME: http
46 ADMIN_EMAIL: "${ADMIN_EMAIL:?}"
47 MAILER_ADDRESS: "${MAILER_ADDRESS:?}"
48 MAILER_USERNAME: "${MAILER_USERNAME:?}"
49 MAILER_PASSWORD: "${MAILER_PASSWORD:?}"
50 HOME_DOMAIN: "${HOME_DOMAIN:?}"
51 KC_HTTP_PORT: 80
52 KC_HTTPS_PORT: 443
53 restart: unless-stopped
54 read_only: false
55 depends_on:
56 caddy:
57 condition: service_started
58 postgres:
59 condition: service_healthy
60 redis:
61 condition: service_healthy
62 healthcheck:
63 test:
64 - "CMD-SHELL"
65 - "test -f /shared/forward-auth"
66 interval: 10s
67 timeout: 5s
68 retries: 3
69 start_period: 120s
70 forward-auth: # port 80
71 container_name: forward-auth
72 build:
73 context: config/keycloak/forward-auth
74 dockerfile: Dockerfile
75 pull_policy: build
76 restart: unless-stopped
77 user: "0:0"
78 environment:
79 OAUTH2_PROXY_CLIENT_ID: "forward-auth"
80 OAUTH2_PROXY_COOKIE_SECRET: "${FORWARD_AUTH_KEY:?}"
81 OAUTH2_PROXY_OIDC_ISSUER_URL: "https://auth.${HOME_DOMAIN:?}/realms/master"
82 OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256"
83 OAUTH2_PROXY_PROVIDER: "keycloak-oidc"
84 OAUTH2_PROXY_EMAIL_DOMAINS: "*"
85 OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:80"
86 OAUTH2_PROXY_PINING_PATH: "/ping"
87 OAUTH2_PROXY_READY_PATH: "/ready"
88 OAUTH2_PROXY_PROXY_PREFIX: "/snow.oauth2"
89 OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "TRUE"
90 OAUTH2_PROXY_REVERSE_PROXY: "TRUE"
91 OAUTH2_PROXY_WHITELIST_DOMAINS: "*.${HOME_DOMAIN:?}"
92 OAUTH2_PROXY_SET_XAUTHREQUEST: "TRUE"
93 OAUTH2_PROXY_PASS_USER_HEADERS: "TRUE"
94 OAUTH2_PROXY_BACKEND_LOGOUT_URL: "https://auth.${HOME_DOMAIN:?}/realms/master/protocol/openid-connect/logout?id_token_hint={id_token}"
95 OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "TRUE"
96 HOME_DOMAIN: "${HOME_DOMAIN:?}"
97 depends_on:
98 keycloak:
99 condition: service_healthy
100 volumes:
101 - "${APP_ROOT}/keycloak/forward-auth:/shared/forward-auth:ro"
45102 # file manager
46 copyparty: # port 3923
103 copyparty: # port 80
47104 image: copyparty/ac
48105 container_name: copyparty
49106 user: "$USER_ID:$GROUP_ID"
50107 volumes:
51108 - "${STORE_ROOT}:/w"
52109 - "${APP_ROOT}/copyparty:/cfg"
53 - ./config/copyparty.conf:/cfg/copyparty.conf
110 - ./config/copyparty.conf:/cfg/copyparty.conf:ro
54111 restart: unless-stopped
112 labels:
113 net.paperclover.list.name: Copyparty File Storage
114 net.paperclover.list.domain: nas
115 net.paperclover.list.priority: 103
116 depends_on:
117 forward-auth:
118 condition: service_started
55119 # paper clover's database service
56120 clover-source-of-truth:
57121 build:
......@@ -75,7 +139,10 @@ services:
75139 interval: 10s
76140 timeout: 5s
77141 retries: 3
78 start_period: 40s
142 start_period: 10s
143 depends_on:
144 caddy:
145 condition: service_healthy
79146 # git hosting
80147 forgejo: # port 3000
81148 container_name: forgejo
......@@ -101,14 +168,25 @@ services:
101168 - "/etc/localtime:/etc/localtime:ro"
102169 - ./config/forgejo:/custom:ro
103170 - ./config/font:/custom/public/assets/font:ro
171 labels:
172 net.paperclover.list.name: Clover Git Forge
173 net.paperclover.list.domain: git
174 net.paperclover.list.priority: 105
175 depends_on:
176 keycloak:
177 condition: service_healthy
178 # forgejo-anubis:
179 # condition: service_healthy
104180 forgejo-anubis:
105181 container_name: forgejo-anubis
106182 image: ghcr.io/techarohq/anubis:latest
107 user: "$USER_ID:$GROUP_ID"
183 user: "${USER_ID:?}:${GROUP_ID:?}"
108184 restart: unless-stopped
109185 environment:
110186 BIND: :80
111187 TARGET: http://forgejo:3000
188 COOKIE_DOMAIN: "${HOME_DOMAIN:?}"
189 ED25519_PRIVATE_KEY_HEX: "${ANUBIS_PRIVATE_KEY:?}"
112190 forgejo-runner:
113191 container_name: forgejo-runner
114192 image: code.forgejo.org/forgejo/runner:12
......@@ -151,6 +229,11 @@ services:
151229 - "./config/jellyfin:/config/custom:ro"
152230 - "./config/jellyfin/branding.xml:/config/config/branding.xml:ro"
153231 - "./config/font:/usr/local/share/fonts/custom:ro"
232 labels:
233 net.paperclover.list.name: Cloverfin
234 net.paperclover.list.domain: jelly
235 net.paperclover.list.priority: 110
236 net.paperclover.list.access: media
154237 navidrome: # port 4533
155238 container_name: navidrome
156239 image: deluan/navidrome:latest
......@@ -163,6 +246,11 @@ services:
163246 - ND_SCANNER_GROUPALBUMRELEASES=1
164247 - ND_BASEURL=https://nd.${HOME_DOMAIN}/
165248 - ND_PID_ALBUM=folder
249 labels:
250 net.paperclover.list.name: Navidrome Music
251 net.paperclover.list.domain: nd
252 net.paperclover.list.priority: 109
253 net.paperclover.list.access: media
166254 # media aquisition
167255 qbittorrent: # webui on port 23938
168256 platform: linux/amd64 # not available for aarch64
......@@ -199,6 +287,11 @@ services:
199287 - "${APP_ROOT}/qbittorrent:/config"
200288 - "./config/openvpn:/config/openvpn:ro"
201289 - "/etc/localtime:/etc/localtime:ro"
290 labels:
291 net.paperclover.list.name: qBittorrent
292 net.paperclover.list.domain: qbt
293 net.paperclover.list.priority: 52
294 net.paperclover.list.access: media-manage
202295 # jackett: # port 9117
203296 # container_name: jackett
204297 # environment:
......@@ -209,6 +302,11 @@ services:
209302 # restart: unless-stopped
210303 # volumes:
211304 # - "${APP_ROOT}/jackett:/config/Jackett"
305 # labels:
306 # net.paperclover.list.name: Jackett API Adapter
307 # net.paperclover.list.domain: jkt
308 # net.paperclover.list.priority: 40
309 # net.paperclover.list.access: media-manage
212310 # sonarr: # port 8989
213311 # container_name: sonarr
214312 # environment:
......@@ -221,6 +319,11 @@ services:
221319 # - "${TORRENT_TEMP:-${APP_ROOT}/qbittorrent/tmp}:/data/tmp"
222320 # - "${MEDIA_ROOT}:/data/media"
223321 # restart: unless-stopped
322 # labels:
323 # net.paperclover.list.name: Sonarr TV PVR
324 # net.paperclover.list.domain: jkt
325 # net.paperclover.list.priority: 51
326 # net.paperclover.list.access: media-manage
224327 # radarr: # port 7878
225328 # container_name: radarr
226329 # environment:
......@@ -233,6 +336,11 @@ services:
233336 # - "${TORRENT_TEMP:-${APP_ROOT}/qbittorrent/tmp}:/data/tmp"
234337 # - "${MEDIA_ROOT}:/data/media"
235338 # restart: unless-stopped
339 # labels:
340 # net.paperclover.list.name: Radarr Movie Organizer
341 # net.paperclover.list.domain: jkt
342 # net.paperclover.list.priority: 50
343 # net.paperclover.list.access: media-manage
236344 # language models
237345 opencode: # port 4096
238346 container_name: opencode
......@@ -246,6 +354,11 @@ services:
246354 - "${APP_ROOT}/opencode/.config:/home/clover/.config"
247355 - "${APP_ROOT}/opencode/.local:/home/clover/.local"
248356 - "${APP_ROOT}/opencode/code:/home/clover/code"
357 labels:
358 net.paperclover.list.name: OpenCode
359 net.paperclover.list.domain: opencode
360 net.paperclover.list.priority: 70
361 net.paperclover.list.access: personal
249362 # databases
250363 postgres:
251364 container_name: postgres
......@@ -260,33 +373,41 @@ services:
260373 user: "$USER_ID:$GROUP_ID"
261374 environment:
262375 POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
263 POSTGRES_PASSWORD_FORGEJO: "${POSTGRES_PASSWORD_FORGEJO}"
264 POSTGRES_PASSWORD_AUTHELIA: "${POSTGRES_PASSWORD_AUTHELIA}"
265 POSTGRES_PASSWORD_DAWARICH: "${POSTGRES_PASSWORD_DAWARICH}"
266 POSTGRES_PASSWORD_JELLYFIN: "${POSTGRES_PASSWORD_JELLYFIN}"
376 POSTGRES_PASSWORD_FORGEJO: "${POSTGRES_PASSWORD_FORGEJO:?}"
377 POSTGRES_PASSWORD_KEYCLOAK: "${POSTGRES_PASSWORD_KEYCLOAK:?}"
378 POSTGRES_PASSWORD_DAWARICH: "${POSTGRES_PASSWORD_DAWARICH:?}"
379 POSTGRES_PASSWORD_JELLYFIN: "${POSTGRES_PASSWORD_JELLYFIN:?}"
267380 volumes:
268381 - "${APP_ROOT}/pg_data:/var/lib/postgresql/data/"
269382 healthcheck:
270 test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"]
383 test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
271384 interval: 10s
272385 timeout: 5s
273386 retries: 5
274387 start_period: 10s
275388 pgadmin: # port 80
276389 container_name: pgadmin
390 entrypoint: ["/bin/sh", "/init/pgadmin-init.sh"]
277391 image: "docker.io/dpage/pgadmin4:latest"
278 user: "5050:5050"
392 user: "0:0"
279393 environment:
280394 PGADMIN_DEFAULT_EMAIL: "pgadmin4@pgadmin.org"
281395 PGADMIN_DEFAULT_PASSWORD: "secret"
282396 PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
283397 PGADMIN_CONFIG_SERVER_MODE: "False"
398 POSTGRES_PASSWORD: "$POSTGRES_PASSWORD"
284399 volumes:
285400 - "${APP_ROOT}/pg_admin:/var/lib/pgadmin"
401 - "./config/postgres:/init"
286402 restart: unless-stopped
287403 depends_on:
288404 postgres:
289405 condition: service_healthy
406 labels:
407 net.paperclover.list.name: pgAdmin
408 net.paperclover.list.domain: pg
409 net.paperclover.list.priority: 15
410 net.paperclover.list.access: personal
290411 redis: # port 6379
291412 container_name: redis
292413 image: redis:8
......@@ -294,12 +415,23 @@ services:
294415 restart: unless-stopped
295416 volumes:
296417 - "${APP_ROOT}/redis:/data"
418 healthcheck:
419 test: ["CMD", "redis-cli", "ping"]
420 interval: 1s
421 timeout: 3s
422 retries: 30
423 start_period: 5s
297424 redis-insight: # port 5540
298425 container_name: redis-insight
299426 image: redis/redisinsight:latest
300427 environment:
301428 RI_REDIS_HOST: redis
302429 restart: unless-stopped
430 labels:
431 net.paperclover.list.name: Redis Insight
432 net.paperclover.list.domain: redis
433 net.paperclover.list.priority: 14
434 net.paperclover.list.access: personal
303435 # location history
304436 dawarich-app: # port 30161
305437 image: freikin/dawarich:latest
......@@ -340,6 +472,11 @@ services:
340472 limits:
341473 cpus: "0.50"
342474 memory: 4G
475 labels:
476 net.paperclover.list.name: Dawarich Location History
477 net.paperclover.list.domain: dawarich
478 net.paperclover.list.priority: 30
479 net.paperclover.list.access: personal
343480 dawarich-sidekiq:
344481 image: freikin/dawarich:latest
345482 container_name: dawarich-sidekiq
......@@ -387,12 +524,21 @@ services:
387524 - 22000:22000/udp
388525 - 21027:21027/udp
389526 restart: unless-stopped
527 labels:
528 net.paperclover.list.name: Syncthing
529 net.paperclover.list.domain: sync
530 net.paperclover.list.priority: 31
531 net.paperclover.list.access: personal
390532 # administration
391533 openspeedtest: # port 3000
392534 restart: unless-stopped
393535 container_name: openspeedtest
394536 image: openspeedtest/latest
395537 user: "$USER_ID:$GROUP_ID"
538 labels:
539 net.paperclover.list.name: Open Speed Test
540 net.paperclover.list.domain: speedtest
541 net.paperclover.list.priority: 10
396542 technitium-dns:
397543 image: technitium/dns-server
398544 container_name: technitium-dns
......@@ -411,13 +557,16 @@ services:
411557 restart: unless-stopped
412558 sysctls:
413559 - net.ipv4.ip_local_port_range=1024 65000
560 labels:
561 net.paperclover.list.name: DNS
562 net.paperclover.list.domain: dns
563 net.paperclover.list.priority: 9
564 net.paperclover.list.access: personal
414565
415566secrets:
416567 openvpn-credentials:
417568 file: ./secrets/openvpn-credentials.txt
418 authelia-jwt-secret:
419 file: ./secrets/authelia-jwt-secret.txt
420 authelia-session-secret:
421 file: ./secrets/authelia-session-secret.txt
422 authelia-encryption-key:
423 file: ./secrets/authelia-encryption-key.txt
569 keycloak-crt:
570 file: ./secrets/keycloak.crt
571 keycloak-key:
572 file: ./secrets/keycloak.key
config/Caddyfile+112-76
......@@ -1,99 +1,133 @@
1local.test, *.local.test {
2 tls internal
3}
1# (auth_require) {
2# @not_websocket not header Connection Upgrade
3# handle /snow.oauth2/* {
4# reverse_proxy "http://forward-auth" {
5# header_up X-Real-IP {remote_host}
6# header_up X-Forwarded-Uri {uri}
7# }
8# }
9#
10# handle {
11# forward_auth @not_websocket "http://forward-auth" {
12# uri /snow.oauth2/auth
13# header_up X-Real-IP {remote_host}
14# copy_headers X-Forwarded-User X-Forwarded-Groups X-Forwarded-Email
15# @error status 401
16# handle_response @error {
17# redir * /snow.oauth2/sign_in?rd={scheme}://{host}{uri}
18# }
19# }
20#
21# reverse_proxy {args[:]}
22# }
23# }
24(auth-optional) {
25 handle /snow.oauth2/* {
26 reverse_proxy "http://forward-auth" {
27 header_up X-Real-IP {remote_host}
28 header_up X-Forwarded-Uri {uri}
29 }
30 }
31
32 @not_websocket not header Connection Upgrade
33 reverse_proxy @not_websocket "http://forward-auth" {
34 method GET
35 rewrite /snow.oauth2/auth
436
5(auth) {
6 @not_websocket not header Connection Upgrade
7 forward_auth @not_websocket authelia:9091 {
8 uri /api/authz/forward-auth
9 copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
37 header_up X-Forwarded-Method {method}
38 header_up X-Forwarded-Uri {uri}
39
40 @good status 2xx
41 handle_response @good {
42 request_header User-Id {rp.header.X-Auth-Request-User}
43 request_header User-Groups {rp.header.X-Auth-Request-Groups}
44 request_header User-Email {rp.header.X-Auth-Request-Email}
45 request_header User-Name {rp.header.X-Auth-Request-Preferred-Username}
46 }
47 @bad status 4xx
48 handle_response @bad {
49 request_header User-Id ""
50 request_header User-Groups ""
51 request_header User-Email ""
52 request_header User-Name ""
53 }
1054 }
55
56 reverse_proxy {args[:]}
1157}
1258
13# jelly.{$HOME_DOMAIN}
14import ./jellyfin/Caddyfile
1559
60# services are sorted alphabetically
1661auth.{$HOME_DOMAIN} {
17 reverse_proxy "authelia:9091"
62 reverse_proxy "http://keycloak"
1863}
1964dawarich.{$HOME_DOMAIN} {
20 reverse_proxy "dawarich:30161"
65 reverse_proxy "http://dawarich:30161"
2166}
2267db.{$HOME_DOMAIN} {
23 reverse_proxy "clover-source-of-truth:80"
24}
25nas.{$HOME_DOMAIN} {
26 forward_auth authelia:9091 {
27 uri /api/authz/forward-auth
28 copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
29 }
30 route {
31 @login path /login
32 redir @login https://auth.{$HOME_DOMAIN}?rd=https%3A%2F%2Fnas.{$HOME_DOMAIN}{?rd}
33
34 @logout path /logout
35 redir @logout https://auth.{$HOME_DOMAIN}/logout?rd=https://nas.{$HOME_DOMAIN}
36
37 reverse_proxy copyparty:3923
38 }
68 reverse_proxy "http://clover-source-of-truth"
3969}
4070git.{$HOME_DOMAIN} {
41 reverse_proxy "forgejo-anubis:80" {
71 reverse_proxy "http://forgejo-anubis" {
4272 header_up X-Real-Ip {remote_host}
4373 header_up X-Http-Version {http.request.proto}
4474 }
4575}
76import ./jellyfin/Caddyfile
77# jkt.{$HOME_DOMAIN} {
78# import auth
79# reverse_proxy "http://jackett:9117" {
80# header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
81# }
82# }
4683music.{$HOME_DOMAIN} {
47 reverse_proxy "navidrome:4533"
48}
49qbt.{$HOME_DOMAIN} {
50 import auth
51 reverse_proxy "qbittorrent:23938"
52}
53snr.{$HOME_DOMAIN} {
54 import auth
55 reverse_proxy "sonarr:8989" {
56 header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
57 }
58}
59rdr.{$HOME_DOMAIN} {
60 import auth
61 reverse_proxy "radarr:7878" {
62 header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
63 }
64}
65jkt.{$HOME_DOMAIN} {
66 import auth
67 reverse_proxy "jackett:9117" {
68 header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
69 }
70}
84 reverse_proxy "http://navidrome:4533"
85}
86nas.{$HOME_DOMAIN} {
87 import auth-optional "http://copyparty"
88}
89# opencode.{$HOME_DOMAIN} {
90# import auth
91# reverse_proxy "http://opencode"
92# }
93# pg.{$HOME_DOMAIN} {
94# import auth
95# reverse_proxy "http://pgadmin"
96# }
97# qbt.{$HOME_DOMAIN} {
98# import auth
99# reverse_proxy "qbittorrent:23938"
100# }
101# redis.{$HOME_DOMAIN} {
102# import auth
103# reverse_proxy "redis-insight:5540"
104# }
105# rdr.{$HOME_DOMAIN} {
106# import auth
107# reverse_proxy "radarr:7878" {
108# header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
109# }
110# }
111# snr.{$HOME_DOMAIN} {
112# import auth
113# reverse_proxy "sonarr:8989" {
114# header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
115# }
116# }
71117speedtest.{$HOME_DOMAIN} {
72118 reverse_proxy "openspeedtest:3000"
73119}
74opencode.{$HOME_DOMAIN} {
75 import auth
76 reverse_proxy "opencode:3923"
77}
78pg.{$HOME_DOMAIN} {
79 import auth
80 reverse_proxy "pgadmin:80"
81}
82redis.{$HOME_DOMAIN} {
83 import auth
84 reverse_proxy "redis-insight:5540"
85}
86sync.{$HOME_DOMAIN} {
87 import auth
88 reverse_proxy "syncthing:8384"
89}
120# sync.{$HOME_DOMAIN} {
121# import auth
122# reverse_proxy "syncthing:8384"
123# }
90124
91125# redirections
92126paperclover.dev {
93 redir "https://git.paperclover.net{uri}"
94 tls {
95 on_demand
96 }
127 redir "https://git.paperclover.net{uri}"
128 tls {
129 on_demand
130 }
97131}
98132
99133:80, :443 {
......@@ -105,11 +139,13 @@ paperclover.dev {
105139 }
106140 }
107141
108 handle_path /generate_204 {
109 respond "" 204
110 }
111142 handle {
112143 respond "the 'Not Found' page was not found: {http.request.host}" 404
113144 }
114145}
115146
147local.test, *.local.test {
148 tls internal
149}
150
151
config/authelia/configuration.yml deleted-46
......@@ -1,46 +0,0 @@
1storage:
2 postgres:
3 address: "tcp://postgres:5432"
4 database: "authelia"
5 schema: "public"
6 username: "authelia"
7 timeout: "5s"
8authentication_backend:
9 refresh_interval: "5m"
10 file:
11 watch: true
12 path: "/config/users.yaml"
13 search:
14 email: true
15 case_insensitive: true
16totp:
17 issuer: "local.test"
18session:
19 name: "authelia_session"
20 same_site: "lax"
21 inactivity: "5m"
22 expiration: "1h"
23 remember_me: "3M"
24 cookies:
25 - name: "clover-authelia"
26 domain: "local.test"
27 authelia_url: "https://auth.local.test"
28notifier:
29 disable_startup_check: true
30 smtp:
31 address: "smtp://127.0.0.1:25"
32 timeout: "5s"
33 username: "test"
34 password: "password"
35 sender: "clover bot <noreply.auth@paperclover.net>"
36 identifier: "localhost"
37 subject: "[Authelia] {title}"
38access_control:
39 default_policy: one_factor
40 rules:
41 - domain: "nas.local.test"
42 policy: bypass # optional
43regulation:
44 max_retries: 10
45 find_time: "2 minutes"
46 ban_time: "5 minutes"
config/authelia/users.yaml deleted-11
......@@ -1,11 +0,0 @@
1users:
2 snow:
3 disabled: false
4 displayname: "snow"
5 password: "$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/"
6 email: "account@paperclover.net"
7 fish:
8 disabled: false
9 displayname: "fish"
10 password: "$6$rounds=50000$BpLnfgDsc2WD8F2q$Zis.ixdg9s/UOJYrs56b5QEZFiZECu0qZVNsIYxBaNJ7ucIL.nlxVCT5tqh8KHG8X4tlwCFm5r6NTOZZ5qRFN/"
11 email: "julia@paperclover.net"
config/copyparty.conf+9-7
......@@ -2,19 +2,21 @@
22 # e2dsa # enable file indexing and filesystem scanning
33 # e2ts # enable multimedia indexing
44 ansi # enable colors in log messages
5 p: 80 # port
56 shr: /shr # set the virtual path for shares
67 df: 16 # stop accepting uploads if less than 16 GB free disk space
78 ver # show copyparty version in the controlpanel
89 theme: 2 # monokai
9 name: clover nas # change the server-name that's displayed in the browser
10 name: clover's nas
1011 stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
1112
12 # authelia
13 xff-src: lan # accept X-Forwarded-For from `lan`
14 idp-h-usr: remote-user
15 idp-h-grp: remote-groups
16 idp-login: /login?rd={dst} # caddy redirect to avoid hardcoding a domain here
17 idp-logout: /logout
13 # keycloak
14 xff-src: lan # accept X-Forwarded-For from `lan`
15 rproxy: 1
16 idp-h-usr: user-name
17 idp-h-grp: user-groups
18 idp-login: /snow.oauth2/sign_in?rd={dst} # caddy redirect to avoid hardcoding a domain here
19 idp-logout: /snow.oauth2/sign_out
1820 idp-login-t: with sso (snow sign on)
1921
2022[/] # webroot
config/jellyfin/Caddyfile+40-45
......@@ -3,69 +3,64 @@ jelly.{$HOME_DOMAIN} {
33 header Content-Type "text/css"
44 respond 200 {
55 body `
6 @import url("/original{http.request.uri.path}") layer(base);
6 @import url("/original{http.request.uri.path}") layer(base);
77 @import url("/original/web/themes/dark/theme.css") layer(basetheme);
88 @import url("https://jellyfin.catppuccin.com/theme.css") layer(cat);
99 @import url("https://jellyfin.catppuccin.com/catppuccin-macchiato.css");
1010 @import url("https://jellyfin.catppuccin.com/catppuccin-latte.css") (prefers-color-scheme: light);
1111 @font-face {
12 font-family: 'Name Sans';
13 src: url('/web/extra/font/ATNameSansVariable-Regular.woff2') format('woff2') tech(variations);
14 font-weight: 100 900;
15 font-style: normal;
16 font-display: swap;
17 font-stretch: 75% 125%;
18 }
12 font-family: 'Name Sans';
13 src: url('/web/extra/font/ATNameSansVariable-Regular.woff2') format('woff2') tech(variations);
14 font-weight: 100 900;
15 font-style: normal;
16 font-display: swap;
17 font-stretch: 75% 125%;}
1918
2019 @font-face {
21 font-family: 'Name Sans';
22 src: url('/web/extra/font/ATNameSansVariable-Italic.woff2') format('woff2') tech(variations);
23 font-weight: 100 900;
24 font-style: italic;
25 font-display: swap;
26 font-stretch: 75% 125%;
27 }
20 font-family: 'Name Sans';
21 src: url('/web/extra/font/ATNameSansVariable-Italic.woff2') format('woff2') tech(variations);
22 font-weight: 100 900;
23 font-style: italic;
24 font-display: swap;
25 font-stretch: 75% 125%;}
2826
2927 body { font-family: "Name Sans", sans-serif;}
3028 .preload, .touch-menu-la, .mainDrawer-scrollContainer {
31 background-color: var(--main-background);
32 color: var(--main-text);
33 }
29 background-color: var(--main-background);
30 color: var(--main-text);}
31
3432 :root {
35 --main-color: var(--sapphire);
36 --jf-font-button: 500 0.875rem / 1.75 "Name Sans", sans-serif;
37 --jf-font-h1: 300 1.8rem / 1.167 "Name Sans", sans-serif;
38 --jf-font-h2: 300 1.5rem / 1.2 "Name Sans", sans-serif;
39 --jf-font-h3: 400 1.17rem / 1.167 "Name Sans", sans-serif;
40 --jf-font-h4: 400 2.125rem / 1.235 "Name Sans", sans-serif;
41 --jf-font-h5: 400 1.5rem / 1.334 "Name Sans", sans-serif;
42 --jf-font-h6: 500 1.25rem / 1.6 "Name Sans", sans-serif;
43 --jf-font-subtitle1: 400 1rem / 1.75 "Name Sans", sans-serif;
44 --jf-font-subtitle2: 500 0.875rem / 1.57 "Name Sans", sans-serif;
45 --jf-font-body1: 400 1rem / 1.5 "Name Sans", sans-serif;
46 --jf-font-body2: 400 0.875rem / 1.43 "Name Sans", sans-serif;
47 --jf-font-caption: 400 0.75rem / 1.66 "Name Sans", sans-serif;
48 --jf-font-overline: 400 0.75rem / 2.66 "Name Sans", sans-serif;
49 --jf-font-inherit: inherit inherit / inherit inherit;
50 }
33 --main-color: var(--sapphire);
34 --jf-font-button: 500 0.875rem / 1.75 "Name Sans", sans-serif;
35 --jf-font-h1: 300 1.8rem / 1.167 "Name Sans", sans-serif;
36 --jf-font-h2: 300 1.5rem / 1.2 "Name Sans", sans-serif;
37 --jf-font-h3: 400 1.17rem / 1.167 "Name Sans", sans-serif;
38 --jf-font-h4: 400 2.125rem / 1.235 "Name Sans", sans-serif;
39 --jf-font-h5: 400 1.5rem / 1.334 "Name Sans", sans-serif;
40 --jf-font-h6: 500 1.25rem / 1.6 "Name Sans", sans-serif;
41 --jf-font-subtitle1: 400 1rem / 1.75 "Name Sans", sans-serif;
42 --jf-font-subtitle2: 500 0.875rem / 1.57 "Name Sans", sans-serif;
43 --jf-font-body1: 400 1rem / 1.5 "Name Sans", sans-serif;
44 --jf-font-body2: 400 0.875rem / 1.43 "Name Sans", sans-serif;
45 --jf-font-caption: 400 0.75rem / 1.66 "Name Sans", sans-serif;
46 --jf-font-overline: 400 0.75rem / 2.66 "Name Sans", sans-serif;
47 --jf-font-inherit: inherit inherit / inherit inherit;}
5148
52 .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner.png)!important; }
53 .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon.png)!important; }
54 .splashLogo { background-image: url(/web/extra/cloverfin-icon.png)!important; }
49 .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner.png)!important;}
50 .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon.png)!important;}
51 .splashLogo { background-image: url(/web/extra/cloverfin-icon.png)!important;}
5552
5653 @media (min-device-width: 992px) {
57 .splashLogo { background-image: url(/web/extra/cloverfin-banner.png)!important; }
58 }
54 .splashLogo { background-image: url(/web/extra/cloverfin-banner.png)!important;}}
5955
6056 @media (prefers-color-scheme: light) {
61 .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important;}
62 .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important;}
63 .splashLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important; }
64 }
57 .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important;}
58 .layout-tv .pageTitleWithDefaultLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important;}
59 .splashLogo { background-image: url(/web/extra/cloverfin-icon-light.png)!important;}}
6560
6661 @media (min-device-width: 992px) and (prefers-color-scheme: light) {
67 .splashLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important; }
68 }
62 .splashLogo { background-image: url(/web/extra/cloverfin-banner-light.png)!important;}}
63
6964 `
7065 }
7166 }
config/keycloak/.gitignore created+2
......@@ -0,0 +1,2 @@
1.venv
2.python-version
config/keycloak/Dockerfile created+20
......@@ -0,0 +1,20 @@
1FROM registry.access.redhat.com/ubi9/python-311 AS python
2
3USER root
4RUN pip3 install --no-cache-dir --target=/install python-keycloak requests
5
6FROM quay.io/keycloak/keycloak:latest
7
8USER root
9COPY --from=python /usr/bin/python3.11 /usr/bin/python3
10COPY --from=python /usr/lib64/libpython3.11.so.1.0 /usr/lib64/
11COPY --from=python /usr/lib64/libcrypto.so.3 /usr/lib64/
12COPY --from=python /usr/lib64/libssl.so.3 /usr/lib64/
13COPY --from=python /usr/lib64/python3.11 /usr/lib64/python3.11
14COPY --from=python /install /usr/lib64/python3.11/site-packages
15RUN ln -s /usr/lib64/libpython3.11.so.1.0 /usr/lib64/libpython3.11.so
16
17COPY init.py /opt/keycloak/init.py
18COPY entry.sh /opt/keycloak/entry.sh
19
20ENTRYPOINT ["/opt/keycloak/entry.sh"]
config/keycloak/entry.sh created+16
......@@ -0,0 +1,16 @@
1#!/bin/bash
2set -e
3
4THEME_DEV_ARGS=""
5# THEME_DEV_ARGS='--spi-theme--static-max-age=-1 --spi-theme--cache-themes=false --spi-theme--cache-templates=false'
6
7/opt/keycloak/bin/kc.sh start --verbose \
8 --https-certificate-file=/run/secrets/keycloak-crt \
9 --https-certificate-key-file=/run/secrets/keycloak-key \
10 $THEME_DEV_ARGS &
11
12PID="$!"
13
14/opt/keycloak/init.py
15
16wait "$PID"
config/keycloak/forward-auth/Dockerfile created+5
......@@ -0,0 +1,5 @@
1FROM busybox:musl AS busybox
2FROM quay.io/oauth2-proxy/oauth2-proxy:v7.13.0
3COPY ./entry.sh /init/entry.sh
4COPY --from=busybox /bin /bin
5ENTRYPOINT "/init/entry.sh"
config/keycloak/forward-auth/entry.sh created+11
......@@ -0,0 +1,11 @@
1#!/bin/sh
2set -e
3
4CADDY_IP="$(nslookup caddy | awk '/^Address: / { print $2 }')"
5echo "${CADDY_IP} auth.${HOME_DOMAIN}" >>/etc/hosts
6rm -f /etc/ssl/certs/caddy.crt
7ln -s /caddy/pki/authorities/root.crt /etc/ssl/certs/caddy.crt
8
9export OAUTH2_PROXY_CLIENT_SECRET="$(cat /shared/forward-auth)"
10
11exec /bin/oauth2-proxy
config/keycloak/init.py created+173
......@@ -0,0 +1,173 @@
1#!/usr/bin/env python3
2# declarative configuration for keycloak
3# pyright: reportUnknownMemberType=false
4# pyright: reportUnknownVariableType=false
5# pyright: reportUnknownArgumentType=false
6import logging
7import os
8import requests
9import sys
10import time
11from keycloak import KeycloakAdmin, KeycloakOpenIDConnection
12from pathlib import Path
13
14
15def env(key: str):
16 val = os.environ.get(key)
17 if not val:
18 raise Exception(f"Missing ${key}")
19 return val
20
21
22groups_to_create = ["snow", "media", "media-manage", "git", "file"]
23realm_config = {
24 "realm": "master",
25 "displayName": "snow sign on",
26 "displayNameHtml": '<div class="kc-logo-text"><span>snow sign on</span></div>',
27 "editUsernameAllowed": True,
28 "resetPasswordAllowed": True,
29 "rememberMe": True,
30 "loginWithEmailAllowed": True,
31 "registrationAllowed": False,
32 "webAuthnPolicyRpEntityName": "snow sign on",
33 "webAuthnPolicyPasswordlessPasskeysEnabled": True,
34 "loginTheme": "snow",
35 "smtpServer": {
36 "from": f"noreply.auth@{env('HOME_DOMAIN')}",
37 "fromDisplayName": "clover bot",
38 "replyTo": f"me@{env('HOME_DOMAIN')}",
39 "replyToDisplayName": "clover caruso",
40 "envelopeFrom": "",
41 "host": env("MAILER_ADDRESS"),
42 "port": "587",
43 "ssl": "true",
44 "starttls": "true",
45 "auth": "true",
46 "user": env("MAILER_USERNAME"),
47 "authType": "basic",
48 "password": env("MAILER_PASSWORD"),
49 },
50}
51
52
53# Configure logging
54logging.basicConfig(
55 level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
56)
57logger = logging.getLogger(__name__)
58
59
60def wait_for_keycloak(timeout: int = 120) -> bool:
61 start_time = time.time()
62
63 while time.time() - start_time < timeout:
64 try:
65 response = requests.get(f"http://localhost:9000/health/ready", timeout=5)
66 if response.status_code == 200:
67 return True
68 except requests.exceptions.RequestException:
69 pass
70
71 time.sleep(2)
72
73 logger.error(f"Timeout waiting for Keycloak to be ready after {timeout} seconds")
74 return False
75
76
77def configure():
78 admin_user = env("KC_BOOTSTRAP_ADMIN_USERNAME")
79 admin_pass = env("KC_BOOTSTRAP_ADMIN_PASSWORD")
80 connection = KeycloakOpenIDConnection(
81 server_url="http://localhost:80",
82 username=admin_user,
83 password=admin_pass,
84 realm_name="master",
85 client_id="admin-cli",
86 )
87 kc_admin = KeycloakAdmin(connection=connection)
88
89 _ = kc_admin.update_realm("master", realm_config)
90
91 # admin
92 admin_email = env("ADMIN_EMAIL")
93 users = kc_admin.get_users({"username": "snow"})
94 if not users:
95 snow_user_id = kc_admin.create_user(
96 {
97 "username": "snow",
98 "firstName": "Snow",
99 "email": admin_email,
100 "enabled": True,
101 "emailVerified": True,
102 "requiredActions": [
103 "UPDATE_PASSWORD",
104 "webauthn-register-passwordless",
105 ],
106 }
107 )
108 _ = kc_admin.set_user_password(snow_user_id, admin_pass, temporary=True)
109
110 roles = kc_admin.get_realm_roles()
111 admin_role = next((role for role in roles if role["name"] == "admin"), None)
112 if not admin_role:
113 raise Exception("Missing admin role")
114
115 _ = kc_admin.assign_realm_roles(snow_user_id, [admin_role])
116 logger.info("Admin role assigned to snow :3")
117
118 # groups
119 existing_groups = kc_admin.get_groups()
120 existing_group_names = {group["name"] for group in existing_groups}
121
122 for group_name in groups_to_create:
123 if group_name not in existing_group_names:
124 logger.info(f"Creating group: {group_name}")
125 _ = kc_admin.create_group({"name": group_name})
126 else:
127 logger.info(f"Group {group_name} already exists, skipping")
128
129 client_id = kc_admin.create_client(
130 {
131 "protocol": "openid-connect",
132 "clientId": "forward-auth",
133 "name": "Forward-Auth",
134 "description": "",
135 "publicClient": False,
136 "authorizationServicesEnabled": False,
137 "serviceAccountsEnabled": False,
138 "implicitFlowEnabled": False,
139 "directAccessGrantsEnabled": False,
140 "standardFlowEnabled": True,
141 "frontchannelLogout": True,
142 "attributes": {
143 "saml_idp_initiated_sso_url_name": "",
144 "standard.token.exchange.enabled": False,
145 "oauth2.device.authorization.grant.enabled": False,
146 "oidc.ciba.grant.enabled": False,
147 "pkce.code.challenge.method": "",
148 "dpop.bound.access.tokens": "false",
149 "post.logout.redirect.uris": "*",
150 },
151 "alwaysDisplayInConsole": False,
152 "rootUrl": "",
153 "baseUrl": "",
154 "redirectUris": ["*"],
155 },
156 skip_exists=True,
157 )
158 client_data = kc_admin.get_client(client_id)
159 fw_secret_path = Path("/shared/forward-auth")
160 fw_secret_path.parent.mkdir(parents=True, exist_ok=True)
161 _ = fw_secret_path.write_text(client_data["secret"])
162
163
164def main():
165 if not wait_for_keycloak():
166 sys.exit(1)
167
168 configure()
169 logger.info("initial data upserted <3")
170
171
172if __name__ == "__main__":
173 main()
config/keycloak/pyproject.toml created+10
......@@ -0,0 +1,10 @@
1[project]
2name = "keycloak"
3version = "0.1.0"
4description = "Add your description here"
5readme = "README.md"
6requires-python = ">=3.12"
7dependencies = [
8 "python-keycloak>=6.0.0",
9 "requests>=2.32.5",
10]
config/keycloak/theme/login/resources/css/styles.css created+360
......@@ -0,0 +1,360 @@
1@font-face {
2 font-family: "Name Mono";
3 src: url("../font/ATNameMono-Italic.woff2") format("woff2");
4 font-weight: 400;
5 font-style: italic;
6 font-display: swap;
7}
8
9@font-face {
10 font-family: "Name Sans";
11 src: url("../font/nsv.woff2") format("woff2") tech(variations);
12 font-weight: 100 900;
13 font-style: normal;
14 font-display: swap;
15 font-stretch: 75% 125%;
16}
17
18@font-face {
19 font-family: "Name Sans";
20 src: url("../font/nsvi.woff2") format("woff2") tech(variations);
21 font-weight: 100 900;
22 font-style: italic;
23 font-display: swap;
24 font-stretch: 75% 125%;
25}
26
27/* reset */
28html,
29body {
30 height: 100%;
31}
32
33h1,
34h2,
35h3,
36h4,
37h5,
38h6 {
39 font-size: unset;
40 font-weight: unset;
41}
42
43:where(
44 html,
45 body,
46 p,
47 ol,
48 ul,
49 li,
50 dl,
51 dt,
52 dd,
53 blockquote,
54 figure,
55 fieldset,
56 legend,
57 textarea,
58 pre,
59 iframe,
60 hr,
61 h1,
62 h2,
63 h3,
64 h4,
65 h5,
66 h6
67) {
68 margin: 0;
69 padding: 0;
70}
71
72*, :after, :before {
73 box-sizing: border-box;
74 font: unset;
75}
76
77/* root */
78body {
79 color-scheme: light dark;
80 background-color: #f9feff;
81 background-image: url(../img/miku-light.png);
82 background-size: auto 100%;
83 background-position: right top;
84 background-repeat: no-repeat;
85 color: black;
86 --text: black;
87 font-family: "Name Sans", sans-serif;
88
89 --header: light-dark(#1a46cd, #938cff);
90 --primary: light-dark(#00238f, #938cff);
91}
92@media (prefers-color-scheme: dark) {
93 body {
94 background-image: url(../img/miku-dark.png);
95 background-color: #2f4b67;
96 color: white;
97 --text: white;
98 }
99}
100@media (max-width: 1313px) {
101 body {
102 background-position: right calc(-100px + 50%) top;
103 }
104}
105
106/* sidebar */
107.pf-v5-c-login__main {
108 max-width: 30rem;
109 padding: 2rem;
110 height: 100%;
111 display: flex;
112 flex-direction: column;
113 justify-content: center;
114 --bg: light-dark(rgba(30, 30, 30, 0.1), rgba(0, 0, 0, 0.3));
115 background-color: var(--bg);
116 border-style: solid;
117 border-right-width: 4px;
118 border-color: var(--bg);
119 backdrop-filter: blur(4px);
120 overflow-y: auto;
121}
122.kc-logo-text {
123 font-size: 3rem;
124 text-align: center;
125 color: var(--header);
126}
127#kc-page-title, #kc-info-wrapper {
128 text-align: center;
129 color: rgb(from var(--text) r g b / 0.8);
130}
131#kc-page-title {
132 margin-bottom: 1rem;
133}
134#kc-info-wrapper {
135 margin-top: 1rem;
136}
137#kc-form-options {
138 margin-bottom: 1rem;
139}
140a {
141 color: var(--header);
142 text-decoration: dotted underline;
143}
144a:hover {
145 text-decoration: underline;
146 background-color: rgb(from var(--header) r g b / 0.2);
147}
148
149/* branding */
150.kc-logo-text::before {
151 display: block;
152 content: " ";
153 width: 30%;
154 aspect-ratio: 1;
155 margin: auto;
156 background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDIwTDguNzUgMTcuNUw2IDE4TTEwIDRMOC43NSA2LjVMNiA2TTE0IDIwTDE1LjI1IDE3LjVMMTggMThNMTQgNEwxNS4yNSA2LjVMMTggNk0xNyAyMUwxNCAxNU0xNCAxNUgxME0xNCAxNUwxNS41IDEyTTEwIDE1TDcgMjFNMTAgMTVMOC41IDEyTTE3IDNMMTQgOU0xNCA5TDE1LjUgMTJNMTQgOUgxME0xNS41IDEySDIyTTIgMTJIOC41TTguNSAxMkwxMCA5TTEwIDlMNyAzTTIwIDEwTDE4LjUgMTJMMjAgMTRNNCAxMEw1LjUgMTJMNCAxNCIgc3Ryb2tlPSJ1cmwoI3BhaW50MF9saW5lYXJfNTQ4XzE5KSIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTEwIDIwTDguNzUgMTcuNUw2IDE4TTEwIDRMOC43NSA2LjVMNiA2TTE0IDIwTDE1LjI1IDE3LjVMMTggMThNMTQgNEwxNS4yNSA2LjVMMTggNk0xNyAyMUwxNCAxNU0xNCAxNUgxME0xNCAxNUwxNS41IDEyTTEwIDE1TDcgMjFNMTAgMTVMOC41IDEyTTE3IDNMMTQgOU0xNCA5TDE1LjUgMTJNMTQgOUgxME0xNS41IDEySDIyTTIgMTJIOC41TTguNSAxMkwxMCA5TTEwIDlMNyAzTTIwIDEwTDE4LjUgMTJMMjAgMTRNNCAxMEw1LjUgMTJMNCAxNCIgc3Ryb2tlPSJ1cmwoI3BhaW50MV9saW5lYXJfNTQ4XzE5KSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl81NDhfMTkiIHgxPSI0IiB5MT0iLTIiIHgyPSIxOC41IiB5Mj0iMjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzIyM0Q5OSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxNTQzOTIiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDFfbGluZWFyXzU0OF8xOSIgeDE9IjEwIiB5MT0iLTMiIHgyPSIxOCIgeTI9IjI3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNGMkUzRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRjJGOEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==);
157 background-size: cover;
158}
159#kc-page-title,
160#kc-info-wrapper,
161.checkbox,
162a,
163#kc-form-buttons,
164.pf-v5-c-helper-text__item-text,
165.pf-v5-c-alert__title,
166input[type="submit"],
167input[type="button"],
168button {
169 text-transform: lowercase;
170}
171
172/* text input */
173.pf-v5-c-form__group {
174 margin-bottom: 1rem;
175}
176.pf-v5-c-form__group input:not([type="checkbox"]) {
177 width: 100%;
178}
179.pf-v5-c-form__label {
180 margin-bottom: 0.25rem;
181 text-transform: lowercase;
182 user-select: none;
183 display: block;
184}
185.pf-v5-c-input-group {
186 display: flex;
187 border-radius: 8px;
188}
189.pf-v5-c-form-control {
190 background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
191 padding: 2px;
192 appearance: none;
193 border: 2px solid var(--text);
194 font-size: inherit;
195 color: var(--text);
196 text-indent: 8px;
197 height: 38px;
198 border-radius: 8px;
199 flex: 1;
200}
201.pf-v5-c-form-control:has(+ button) {
202 border-top-right-radius: 0;
203 border-bottom-right-radius: 0;
204}
205.pf-v5-c-form-control + button {
206 appearance: none;
207 border-top-right-radius: 8px;
208 border-bottom-right-radius: 8px;
209 width: 38px;
210 border: 2px solid var(--text);
211 border-left: none;
212 background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
213 transition: background-color 0.1s linear;
214}
215.pf-v5-c-form-control + button:hover {
216 background-color: light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2));
217}
218.pf-v5-c-form__group:has(input:focus-visible)
219 > :is(input, .pf-v5-c-input-group) {
220 outline: 2px solid rgb(from var(--primary) r g b / 0.5);
221}
222.pf-v5-c-form__group:has(input:focus-visible) * {
223 border-color: var(--primary);
224 outline: none;
225}
226.pf-v5-c-form__group:has(input:focus-visible) .pf-v5-c-form__label {
227 color: var(--header);
228}
229.pf-v5-c-helper-text__item-text {
230 display: block;
231 margin-top: 0.5rem;
232}
233.pf-m-error {
234 color: light-dark(#c80000, #f56666);
235}
236
237/* checkbox */
238.checkbox input {
239 display: none;
240}
241.checkbox label {
242 display: flex;
243 align-items: center;
244 user-select: none;
245 cursor: pointer;
246}
247.checkbox label:before {
248 content: " ";
249 display: block;
250 width: 24px;
251 height: 24px;
252 margin-right: 0.5rem;
253 margin-left: -2px;
254 background-color: var(--text);
255
256 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1pY29uIGx1Y2lkZS1zcXVhcmUiPjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgeD0iMyIgeT0iMyIgcng9IjIiLz48L3N2Zz4=);
257 mask-size: cover;
258}
259.checkbox label:has(:checked):before {
260 background-color: var(--primary);
261 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1jaGVjay1iaWctaWNvbiBsdWNpZGUtc3F1YXJlLWNoZWNrLWJpZyI+PHBhdGggZD0iTTIxIDEwLjY1NlYxOWEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMlY1YTIgMiAwIDAgMSAyLTJoMTIuMzQ0Ii8+PHBhdGggZD0ibTkgMTEgMyAzTDIyIDQiLz48L3N2Zz4=);
262}
263.checkbox label:has(:checked) {
264 color: var(--primary);
265}
266
267/* buttons */
268.pf-v5-c-button.pf-m-primary {
269 background-color: var(--primary);
270 display: block;
271 border: none;
272 height: 30px;
273 border-radius: 8px;
274 color: white;
275}
276.pf-v5-c-button.pf-m-block {
277 display: block;
278 width: 100%;
279}
280
281/* alert */
282.pf-v5-c-alert {
283 border-radius: 8px;
284 padding: 8px;
285 align-items: center;
286 margin-bottom: 1rem;
287 text-align: center;
288 text-wrap: balance;
289}
290.alert-error {
291 background-color: light-dark(#ff010182, #f56666a1);
292}
293.alert-warning, .alert-info {
294 background-color: rgb(from var(--primary) r g b / 0.5);
295}
296
297/* icons */
298[data-password-toggle] {
299 display: grid;
300 align-items: center;
301 justify-content: center;
302}
303[data-password-toggle] i {
304 display: block;
305 width: 24px;
306 height: 24px;
307 background-color: var(--text);
308 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV5ZS1pY29uIGx1Y2lkZS1leWUiPjxwYXRoIGQ9Ik0yLjA2MiAxMi4zNDhhMSAxIDAgMCAxIDAtLjY5NiAxMC43NSAxMC43NSAwIDAgMSAxOS44NzYgMCAxIDEgMCAwIDEgMCAuNjk2IDEwLjc1IDEwLjc1IDAgMCAxLTE5Ljg3NiAwIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIvPjwvc3ZnPg==);
309}
310input[type="text"] + [data-password-toggle] i {
311 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV5ZS1vZmYtaWNvbiBsdWNpZGUtZXllLW9mZiI+PHBhdGggZD0iTTEwLjczMyA1LjA3NmExMC43NDQgMTAuNzQ0IDAgMCAxIDExLjIwNSA2LjU3NSAxIDEgMCAwIDEgMCAuNjk2IDEwLjc0NyAxMC43NDcgMCAwIDEtMS40NDQgMi40OSIvPjxwYXRoIGQ9Ik0xNC4wODQgMTQuMTU4YTMgMyAwIDAgMS00LjI0Mi00LjI0MiIvPjxwYXRoIGQ9Ik0xNy40NzkgMTcuNDk5YTEwLjc1IDEwLjc1IDAgMCAxLTE1LjQxNy01LjE1MSAxIDEgMCAwIDEgMC0uNjk2IDEwLjc1IDEwLjc1IDAgMCAxIDQuNDQ2LTUuMTQzIi8+PHBhdGggZD0ibTIgMiAyMCAyMCIvPjwvc3ZnPg==);
312}
313
314@media (max-width: 799px) {
315 body {
316 background-position: left 65% bottom 60%;
317 background-size: auto 150%;
318 display: flex;
319 flex-direction: column;
320 align-items: center;
321 justify-content: flex-end;
322 }
323 body:before {
324 display: block;
325 content: "";
326 flex: 1.5;
327 }
328 .pf-v5-c-login__main {
329 flex: 1 1 40%;
330 max-width: 25rem;
331 min-width: 80%;
332 border-width: 4px;
333 border-bottom-width: 0;
334 border-top-left-radius: 32px;
335 border-top-right-radius: 32px;
336 corner-shape: superellipse(0);
337 justify-content: space-between;
338 --bg: light-dark(rgba(255, 255, 255, 0.7), rgba(0.2, 0, 0.2, 0.6));
339 }
340 .pf-v5-c-login__main:after {
341 display: block;
342 content: "";
343 }
344 .kc-logo-text::before {
345 position: absolute;
346 left: 50%;
347 width: 100px;
348 transform: translate(-50%, calc(-50% - 35px));
349 }
350}
351
352#credit {
353 position: fixed;
354 bottom: 2px;
355 right: 2px;
356 font-size: 14px;
357}
358#credit a:not(:hover) {
359 opacity: 0.5;
360}
config/keycloak/theme/login/resources/font/.gitignore
config/keycloak/theme/login/resources/img/license.txt created+2
......@@ -0,0 +1,2 @@
1https://safebooru.org/index.php?page=post&s=view&id=4405346
2dark mode by paper clover
config/keycloak/theme/login/resources/img/miku-dark.png
Binary files /dev/null and b/config/keycloak/theme/login/resources/img/miku-dark.png differ
config/keycloak/theme/login/resources/img/miku-light.png
Binary files /dev/null and b/config/keycloak/theme/login/resources/img/miku-light.png differ
config/keycloak/theme/login/resources/js/stars.js created+105
......@@ -0,0 +1,105 @@
1const shader = `
2 // Heavily modified off of https://www.shadertoy.com/view/3sKGWw
3 // big thanks to these resources as well:
4 // https://youtu.be/3CycKKJiwis
5 // https://www.shadertoy.com/view/4djSRW
6 // https://www.shadertoy.com/view/3s3GDn
7
8 precision mediump float;
9 uniform vec2 u_resolution;
10 uniform float u_time;
11 uniform bool u_mask;
12
13 float random(vec2 p){
14 vec3 p3 = fract(vec3(p.xyx) * .1031);
15 p3 += dot(p3, p3.yzx + 33.33);
16 return fract((p3.x + p3.y) * p3.z);
17 }
18
19 vec2 random2(vec2 p){
20 vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
21 p3 += dot(p3, p3.yzx+33.33);
22 return fract((p3.xx+p3.yz)*p3.zy);
23 }
24
25 void main() {
26 float glow;
27 vec2 uv;
28 vec2 fl;
29 vec2 local_uv;
30 vec2 seed;
31 vec2 centre;
32 vec2 cell;
33 float t = u_time * 0.00015;
34 float blinkTime = u_time * 0.0068;
35 float rotationAngle = -t * 21.0;
36 mat2 rotation = mat2(cos(rotationAngle), -sin(rotationAngle),
37 sin(rotationAngle), cos(rotationAngle));
38 vec3 col = vec3(0);
39
40 centre = vec2(cos(t), sin(t)) * 5.0 + 0.5;
41
42 // Get uv from the fragment coordinates, rotation and depth
43 uv = (centre - (gl_FragCoord.xy - u_resolution / 2.0) / (1600.0 / 25.0)) * rotation;
44 fl = floor(uv);
45 // The local cell coordinates. uv-fl == frac(uv)
46 local_uv = uv - fl - 0.5;
47
48 // For a 3x3 group of cells around the fragment, find the distance from the points
49 // of each to the current fragment and draw an accumulative glow accordingly
50 // The local cell is (0, 0)
51 for(float j = -1.0; j <= 1.0; j++){
52 for(float k = -1.0; k <= 1.0; k++){
53 cell = vec2(j, k);
54 seed = 128.0 + (fl + cell);
55 glow =
56 ((0.6 + 0.5 * sin((128.0 * random(seed)) * blinkTime))) * 25.0
57 * pow(0.005 / length(local_uv - (cell + 0.9 * (random2(seed) - 0.5))), 2.0);
58 col += vec3(0.2 * glow) + 0.1 * glow;
59 }
60 }
61
62 col = 1.0 - exp(-col);
63 col = pow(col, vec3(0.45));
64
65 if (u_mask) {
66 float mask = smoothstep(100.0, 300.0, length(vec2(gl_FragCoord.x - u_resolution.x/2.0, gl_FragCoord.y - 200.0)));
67 col *= mask;
68 }
69
70 gl_FragColor = vec4(col * (u_mask ? 0.6 : 0.25), 0);
71 }
72`;
73
74function createShader(gl, type, source) {
75 const shader = gl.createShader(type);
76 gl.shaderSource(shader, source);
77 gl.compileShader(shader);
78 const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
79 if (success) {
80 return shader;
81 }
82
83 // eslint-disable-next-line no-console
84 console.error(gl.getShaderInfoLog(shader));
85 gl.deleteShader(shader);
86 throw new Error("Failed to compile shader");
87}
88
89function createProgram(gl, vertexShader, fragmentShader) {
90 const program = gl.createProgram();
91
92 gl.attachShader(program, vertexShader);
93 gl.attachShader(program, fragmentShader);
94 gl.linkProgram(program);
95
96 const success = gl.getProgramParameter(program, gl.LINK_STATUS);
97 if (success) {
98 return program;
99 }
100
101 // eslint-disable-next-line no-console
102 console.error(gl.getProgramInfoLog(program));
103 gl.deleteProgram(program);
104 throw new Error("Failed to link program");
105}
config/keycloak/theme/login/resources/js/webauthnRegister.js created+159
......@@ -0,0 +1,159 @@
1import { base64url } from "rfc4648";
2
3export async function registerByWebAuthn(input) {
4 // Check if WebAuthn is supported by this browser
5 if (!window.PublicKeyCredential) {
6 returnFailure(input.errmsg);
7 return;
8 }
9
10 const publicKey = {
11 challenge: base64url.parse(input.challenge, { loose: true }),
12 rp: { id: input.rpId, name: input.rpEntityName },
13 user: {
14 id: base64url.parse(input.userid, { loose: true }),
15 name: input.username,
16 displayName: input.username,
17 },
18 pubKeyCredParams: getPubKeyCredParams(input.signatureAlgorithms),
19 };
20
21 if (input.attestationConveyancePreference !== "not specified") {
22 publicKey.attestation = input.attestationConveyancePreference;
23 }
24
25 const authenticatorSelection = {};
26 let isAuthenticatorSelectionSpecified = false;
27
28 if (input.authenticatorAttachment !== "not specified") {
29 authenticatorSelection.authenticatorAttachment =
30 input.authenticatorAttachment;
31 isAuthenticatorSelectionSpecified = true;
32 }
33
34 if (input.requireResidentKey !== "not specified") {
35 if (input.requireResidentKey === "Yes") {
36 authenticatorSelection.requireResidentKey = true;
37 } else {
38 authenticatorSelection.requireResidentKey = false;
39 }
40 isAuthenticatorSelectionSpecified = true;
41 }
42
43 if (input.userVerificationRequirement !== "not specified") {
44 authenticatorSelection.userVerification = input.userVerificationRequirement;
45 isAuthenticatorSelectionSpecified = true;
46 }
47
48 if (isAuthenticatorSelectionSpecified) {
49 publicKey.authenticatorSelection = authenticatorSelection;
50 }
51
52 if (input.createTimeout !== 0) {
53 publicKey.timeout = input.createTimeout * 1000;
54 }
55
56 const excludeCredentials = getExcludeCredentials(input.excludeCredentialIds);
57 if (excludeCredentials.length > 0) {
58 publicKey.excludeCredentials = excludeCredentials;
59 }
60
61 try {
62 const result = await doRegister(publicKey);
63 returnSuccess(result, input.initLabel, input.initLabelPrompt);
64 } catch (error) {
65 returnFailure(error);
66 }
67}
68
69function doRegister(publicKey) {
70 return navigator.credentials.create({ publicKey });
71}
72
73function getPubKeyCredParams(signatureAlgorithmsList) {
74 const pubKeyCredParams = [];
75 if (signatureAlgorithmsList.length === 0) {
76 pubKeyCredParams.push({ type: "public-key", alg: -7 });
77 return pubKeyCredParams;
78 }
79
80 for (const entry of signatureAlgorithmsList) {
81 pubKeyCredParams.push({
82 type: "public-key",
83 alg: entry,
84 });
85 }
86
87 return pubKeyCredParams;
88}
89
90function getExcludeCredentials(excludeCredentialIds) {
91 const excludeCredentials = [];
92 if (excludeCredentialIds === "") {
93 return excludeCredentials;
94 }
95
96 for (const entry of excludeCredentialIds.split(",")) {
97 excludeCredentials.push({
98 type: "public-key",
99 id: base64url.parse(entry, { loose: true }),
100 });
101 }
102
103 return excludeCredentials;
104}
105
106function getTransportsAsString(transportsList) {
107 if (!Array.isArray(transportsList)) {
108 return "";
109 }
110
111 return transportsList.join();
112}
113
114function returnSuccess(result, initLabel, initLabelPrompt) {
115 document.getElementById("clientDataJSON").value = base64url.stringify(
116 new Uint8Array(result.response.clientDataJSON),
117 { pad: false },
118 );
119 document.getElementById("attestationObject").value = base64url.stringify(
120 new Uint8Array(result.response.attestationObject),
121 { pad: false },
122 );
123 document.getElementById("publicKeyCredentialId").value = base64url.stringify(
124 new Uint8Array(result.rawId),
125 { pad: false },
126 );
127
128 if (typeof result.response.getTransports === "function") {
129 const transports = result.response.getTransports();
130 if (transports) {
131 document.getElementById("transports").value = getTransportsAsString(
132 transports,
133 );
134 }
135 } else {
136 console.log(
137 "Your browser is not able to recognize supported transport media for the authenticator.",
138 );
139 }
140
141 // let labelResult = window.prompt(initLabelPrompt, initLabel);
142 // if (labelResult === null) {
143 // labelResult = initLabel;
144 // }
145 document.getElementById("authenticatorLabel").value = "Passkey " +
146 new Date().toString();
147
148 document.getElementById("register").requestSubmit();
149}
150
151function returnFailure(err) {
152 if (err.name === "NotAllowedError") {
153 document.getElementById("error").value =
154 "the operation was cancelled / browser does not support passkey";
155 } else {
156 document.getElementById("error").value = err;
157 }
158 document.getElementById("register").requestSubmit();
159}
config/keycloak/theme/login/template.ftl created+209
......@@ -0,0 +1,209 @@
1<#import "footer.ftl" as loginFooter>
2<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
3<!DOCTYPE html>
4<html class="${properties.kcHtmlClass!}" lang="${lang}"<#if realm.internationalizationEnabled> dir="${(locale.rtl)?then('rtl','ltr')}"</#if>>
5
6<head>
7 <meta charset="utf-8">
8 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
9 <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
11 <#if properties.meta?has_content>
12 <#list properties.meta?split(' ') as meta>
13 <meta name="${meta?split('==')[0]}" content="${meta?split('==')[1]}"/>
14 </#list>
15 </#if>
16 <title>sso (snow sign on)</title>
17 <link rel="icon" href="${url.resourcesPath}/img/favicon.ico" />
18 <#if properties.stylesCommon?has_content>
19 <#list properties.stylesCommon?split(' ') as style>
20 <link href="${url.resourcesCommonPath}/${style}" rel="stylesheet" />
21 </#list>
22 </#if>
23 <#if properties.styles?has_content>
24 <#list properties.styles?split(' ') as style>
25 <link href="${url.resourcesPath}/${style}" rel="stylesheet" />
26 </#list>
27 </#if>
28 <#if properties.scripts?has_content>
29 <#list properties.scripts?split(' ') as script>
30 <script src="${url.resourcesPath}/${script}" type="text/javascript"></script>
31 </#list>
32 </#if>
33 <script type="importmap">
34 {
35 "imports": {
36 "rfc4648": "${url.resourcesCommonPath}/vendor/rfc4648/rfc4648.js"
37 }
38 }
39 </script>
40 <script src="${url.resourcesPath}/js/menu-button-links.js" type="module"></script>
41 <#if scripts??>
42 <#list scripts as script>
43 <script src="${script}" type="text/javascript"></script>
44 </#list>
45 </#if>
46 <script type="module">
47 import { startSessionPolling } from "${url.resourcesPath}/js/authChecker.js";
48
49 startSessionPolling(
50 "${url.ssoLoginInOtherTabsUrl?no_esc}"
51 );
52 </script>
53 <script type="module">
54 document.addEventListener("click", (event) => {
55 const link = event.target.closest("a[data-once-link]");
56
57 if (!link) {
58 return;
59 }
60
61 if (link.getAttribute("aria-disabled") === "true") {
62 event.preventDefault();
63 return;
64 }
65
66 const { disabledClass } = link.dataset;
67
68 if (disabledClass) {
69 link.classList.add(...disabledClass.trim().split(/\s+/));
70 }
71
72 link.setAttribute("role", "link");
73 link.setAttribute("aria-disabled", "true");
74 });
75 </script>
76 <#if authenticationSession??>
77 <script type="module">
78 import { checkAuthSession } from "${url.resourcesPath}/js/authChecker.js";
79
80 checkAuthSession(
81 "${authenticationSession.authSessionIdHash}"
82 );
83 </script>
84 </#if>
85</head>
86
87<body class="${properties.kcBodyClass!}" data-page-id="login-${pageId}">
88<div class="${properties.kcLoginClass!}">
89 <div id="kc-header" class="${properties.kcHeaderClass!}">
90 <div id="kc-header-wrapper"
91 class="${properties.kcHeaderWrapperClass!}">${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}</div>
92 </div>
93 <div class="${properties.kcFormCardClass!}">
94 <header class="${properties.kcFormHeaderClass!}">
95 <#if realm.internationalizationEnabled && locale.supported?size gt 1>
96 <div class="${properties.kcLocaleMainClass!}" id="kc-locale">
97 <div id="kc-locale-wrapper" class="${properties.kcLocaleWrapperClass!}">
98 <div id="kc-locale-dropdown" class="menu-button-links ${properties.kcLocaleDropDownClass!}">
99 <button tabindex="1" id="kc-current-locale-link" aria-label="${msg("languages")}" aria-haspopup="true" aria-expanded="false" aria-controls="language-switch1">${locale.current}</button>
100 <ul role="menu" tabindex="-1" aria-labelledby="kc-current-locale-link" aria-activedescendant="" id="language-switch1" class="${properties.kcLocaleListClass!}">
101 <#assign i = 1>
102 <#list locale.supported as l>
103 <li class="${properties.kcLocaleListItemClass!}" role="none">
104 <a role="menuitem" id="language-${i}" class="${properties.kcLocaleItemClass!}" href="${l.url}">${l.label}</a>
105 </li>
106 <#assign i++>
107 </#list>
108 </ul>
109 </div>
110 </div>
111 </div>
112 </#if>
113 <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
114 <#if displayRequiredFields>
115 <div class="${properties.kcContentWrapperClass!}">
116 <div class="${properties.kcLabelWrapperClass!} subtitle">
117 <span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
118 </div>
119 <div class="col-md-10">
120 <h1 id="kc-page-title"><#nested "header"></h1>
121 </div>
122 </div>
123 <#else>
124 <h1 id="kc-page-title"><#nested "header"></h1>
125 </#if>
126 <#else>
127 <#if displayRequiredFields>
128 <div class="${properties.kcContentWrapperClass!}">
129 <div class="${properties.kcLabelWrapperClass!} subtitle">
130 <span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
131 </div>
132 <div class="col-md-10">
133 <#nested "show-username">
134 <div id="kc-username" class="${properties.kcFormGroupClass!}">
135 <label id="kc-attempted-username">${auth.attemptedUsername}</label>
136 <a id="reset-login" href="${url.loginRestartFlowUrl}" aria-label="${msg("restartLoginTooltip")}">
137 <div class="kc-login-tooltip">
138 <i class="${properties.kcResetFlowIcon!}"></i>
139 <span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
140 </div>
141 </a>
142 </div>
143 </div>
144 </div>
145 <#else>
146 <#nested "show-username">
147 <div id="kc-username" class="${properties.kcFormGroupClass!}">
148 <label id="kc-attempted-username">${auth.attemptedUsername}</label>
149 <a id="reset-login" href="${url.loginRestartFlowUrl}" aria-label="${msg("restartLoginTooltip")}">
150 <div class="kc-login-tooltip">
151 <i class="${properties.kcResetFlowIcon!}"></i>
152 <span class="kc-tooltip-text">${msg("restartLoginTooltip")}</span>
153 </div>
154 </a>
155 </div>
156 </#if>
157 </#if>
158 </header>
159 <div id="kc-content">
160 <div id="kc-content-wrapper">
161
162 <#-- App-initiated actions should not see warning messages about the need to complete the action -->
163 <#-- during login. -->
164 <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
165 <div class="alert-${message.type} ${properties.kcAlertClass!} pf-m-<#if message.type = 'error'>danger<#else>${message.type}</#if>">
166 <div class="pf-c-alert__icon">
167 <#if message.type = 'success'><span class="${properties.kcFeedbackSuccessIcon!}"></span></#if>
168 <#if message.type = 'warning'><span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
169 <#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
170 <#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
171 </div>
172 <span class="${properties.kcAlertTitleClass!}">${kcSanitize(message.summary)?no_esc}</span>
173 </div>
174 </#if>
175
176 <#nested "form">
177
178 <#if auth?has_content && auth.showTryAnotherWayLink()>
179 <form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post">
180 <div class="${properties.kcFormGroupClass!}">
181 <input type="hidden" name="tryAnotherWay" value="on"/>
182 <a href="#" id="try-another-way"
183 onclick="document.forms['kc-select-try-another-way-form'].requestSubmit();return false;">${msg("doTryAnotherWay")}</a>
184 </div>
185 </form>
186 </#if>
187
188 <#nested "socialProviders">
189
190 <#if displayInfo>
191 <div id="kc-info" class="${properties.kcSignUpClass!}">
192 <div id="kc-info-wrapper" class="${properties.kcInfoAreaWrapperClass!}">
193 <#nested "info">
194 </div>
195 </div>
196 </#if>
197 </div>
198 </div>
199
200
201 <@loginFooter.content/>
202 </div>
203 </div>
204 <div id='credit'>
205 <a rel="noopener noreferrer" target="_blank" href="https://www.pixiv.net/en/artworks/109102745">art by 紺屋</a>
206 </div>
207</body>
208</html>
209</#macro>
config/keycloak/theme/login/theme.properties created+128
......@@ -0,0 +1,128 @@
1parent=base
2import=common/keycloak
3
4styles=css/styles.css
5stylesCommon=
6
7darkMode=false
8
9kcFormGroupClass=pf-v5-c-form__group
10kcFormGroupLabelClass=pf-v5-c-form__group-label pf-v5-u-pb-xs
11kcFormLabelClass=pf-v5-c-form__label
12kcFormLabelTextClass=pf-v5-c-form__label-text
13
14kcLabelClass=pf-v5-c-form__label
15kcInputClass=pf-v5-c-form-control
16kcInputGroup=pf-v5-c-input-group
17kcFormHelperTextClass=pf-v5-c-form__helper-text
18kcInputHelperTextClass=pf-v5-c-helper-text pf-v5-u-display-flex pf-v5-u-justify-content-space-between
19kcInputHelperTextItemClass=pf-v5-c-helper-text__item
20kcInputHelperTextItemTextClass=pf-v5-c-helper-text__item-text
21kcInputGroupItemClass=pf-v5-c-input-group__item
22kcFill=pf-m-fill
23kcError=pf-m-error
24kcLoginFooterBand=pf-v5-c-login__main-footer-band
25kcLoginFooterBandItem=pf-v5-c-login__main-footer-band-item
26
27kcCheckboxClass=pf-v5-c-check
28kcCheckboxInputClass=pf-v5-c-check__input
29kcCheckboxLabelClass=pf-v5-c-check__label
30kcCheckboxLabelRequiredClass=pf-v5-c-check__label-required
31
32kcInputRequiredClass=pf-v5-c-form__label-required
33kcInputErrorMessageClass=pf-v5-c-helper-text__item-text pf-m-error kc-feedback-text
34kcFormControlUtilClass=pf-v5-c-form-control__utilities
35kcInputErrorIconStatusClass=pf-v5-c-form-control__icon pf-m-status
36kcInputErrorIconClass=fas fa-exclamation-circle
37kcAlertClass=pf-v5-c-alert pf-m-inline pf-v5-u-mb-md
38kcAlertIconClass=pf-v5-c-alert__icon
39kcAlertTitleClass=pf-v5-c-alert__title
40kcAlertDescriptionClass=pf-v5-c-alert__description
41kcFormPasswordVisibilityButtonClass=pf-v5-c-button pf-m-control
42kcFormControlToggleIcon=pf-v5-c-form-control__toggle-icon
43kcFormActionGroupClass=pf-v5-c-form__actions pf-v5-u-pt-xs
44kcFormReadOnlyClass=pf-m-readonly
45
46kcPanelClass=pf-v5-c-panel pf-m-raised
47kcPanelMainClass=pf-v5-c-panel__main
48kcPanelMainBodyClass=pf-v5-c-panel__main-body
49kcListClass=pf-v5-c-list
50
51kcButtonClass=pf-v5-c-button
52kcButtonPrimaryClass=pf-v5-c-button pf-m-primary
53kcButtonSecondaryClass=pf-v5-c-button pf-m-secondary
54kcButtonBlockClass=pf-m-block
55kcButtonLinkClass=pf-v5-c-button pf-m-link
56kcCommonLogoIdP=pf-v5-c-login__main-footer-links-item
57kcFormSocialAccountListClass=pf-v5-c-login__main-body pf-v5-u-pl-0 pf-v5-u-pr-0
58kcFormSocialAccountListItemClass=pf-v5-u-pb-sm
59kcFormSocialAccountNameClass=pf-v5-u-m-auto
60kcFormSocialAccountListButtonClass=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
61kcFormSocialAccountListButtonDisabledClass=pf-m-aria-disabled
62kcFormSocialAccountListGridClass=pf-v5-l-grid pf-m-gutter pf-m-all-6-col-on-xl pf-m-all-6-col-on-sm
63kcFormSocialAccountGridItem=pf-v5-l-grid__item
64
65kcLoginClass=pf-v5-c-login__main
66kcFormClass=pf-v5-c-form pf-v5-u-w-100
67kcFormCardClass=card-pf
68
69kcResetFlowIcon=pf-icon fas fa-share-square
70
71kcSelectAuthListClass=pf-v5-c-data-list select-auth-container
72kcSelectAuthListItemWrapperClass=pf-v5-c-data-list__item pf-m-clickable
73kcSelectAuthListItemClass=pf-v5-c-data-list__item-row select-auth-box-parent
74kcSelectAuthListItemHeadingClass=pf-v5-u-font-family-heading select-auth-box-headline
75kcSelectAuthListItemBodyClass=pf-v5-c-data-list__cell pf-m-no-fill pf-v5-u-pt-md pf-v5-u-pb-md
76kcSelectAuthListItemIconClass=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
77kcSelectAuthListItemFillClass=pf-v5-c-data-list__item-action
78kcSelectAuthListItemDescriptionClass=pf-v5-c-data-list__cell pf-m-no-fill select-auth-box-desc
79
80kcRecoveryCodesWarning=pf-v5-c-alert pf-m-warning pf-m-inline pf-v5-u-mb-md kc-recovery-codes-warning
81kcLogin=pf-v5-c-login
82kcLoginContainer=pf-v5-c-login__container
83kcLoginMain=pf-v5-c-login__main
84kcLoginMainHeader=pf-v5-c-login__main-header
85kcLoginMainFooter=pf-v5-c-login__main-footer
86kcLoginMainFooterBand=pf-v5-c-login__main-footer-band
87kcLoginMainFooterBandItem=pf-v5-c-login__main-footer-band-item
88kcLoginMainFooterHelperText=pf-v5-u-font-size-sm pf-v5-u-color-200
89kcLoginMainTitle=pf-v5-c-title pf-m-3xl
90kcLoginMainHeaderUtilities=pf-v5-c-login__main-header-utilities
91kcLoginMainBody=pf-v5-c-login__main-body
92
93kcContentWrapperClass=pf-v5-u-mb-md-on-md
94kcWebAuthnDefaultIcon=pf-v5-c-icon pf-m-lg
95kcMarginTopClass=pf-v5-u-mt-md-on-md
96
97kcLoginOTPListClass=pf-v5-c-tile
98kcLoginOTPListItemHeaderClass=pf-v5-c-tile__header pf-m-stacked
99kcLoginOTPListItemIconBodyClass=pf-v5-c-tile__icon
100kcLoginOTPListItemIconClass=fa fa-mobile
101kcLoginOTPListItemTitleClass=pf-v5-c-tile__title
102kcLoginOTPListSelectedClass=pf-m-selected
103
104kcCopyIconClass=fas fa-copy
105kcCheckIconClass=fas fa-check
106kcAngleRightIconClass=fas fa-angle-right
107kcAngleDownIconClass=fas fa-angle-down
108kcExpandedClass=pf-m-expanded
109
110kcCodeClipboardCopyClass=pf-v5-c-clipboard-copy
111kcCodeClipboardCopyGroupClass=pf-v5-c-clipboard-copy__group
112kcCodeClipboardCopyContentClass=pf-v5-c-clipboard-copy__expandable-content
113
114kcDarkModeClass=pf-v5-theme-dark
115
116kcHtmlClass=login-pf
117kcLogoIdP-facebook=
118kcLogoIdP-google=
119kcLogoIdP-github=
120kcLogoIdP-linkedin=
121kcLogoIdP-instagram=
122kcLogoIdP-microsoft=
123kcLogoIdP-bitbucket=
124kcLogoIdP-gitlab=
125kcLogoIdP-paypal=
126kcLogoIdP-stackoverflow=
127kcLogoIdP-twitter=
128kcLogoIdP-openshift-v4=
config/keycloak/theme/login/webauthn-error.ftl created+36
......@@ -0,0 +1,36 @@
1<#import "template.ftl" as layout>
2<@layout.registrationLayout displayMessage=true; section>
3 <#if section = "header">
4 ${kcSanitize(msg("webauthn-error-title"))?no_esc}
5 <#elseif section = "form">
6
7 <script type="text/javascript">
8 refreshPage = () => {
9 document.getElementById('isSetRetry').value = 'retry';
10 document.getElementById('executionValue').value = '${execution}';
11 document.getElementById('kc-error-credential-form').requestSubmit();
12 }
13 </script>
14
15 <form id="kc-error-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}"
16 method="post">
17 <input type="hidden" id="executionValue" name="authenticationExecution"/>
18 <input type="hidden" id="isSetRetry" name="isSetRetry"/>
19 </form>
20
21 <input tabindex="4" onclick="refreshPage()" type="button"
22 class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
23 name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"
24 />
25
26 <#if isAppInitiatedAction??>
27 <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form" method="post">
28 <button type="submit"
29 class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
30 id="cancelWebAuthnAIA" name="cancel-aia" value="true">${msg("doCancel")}
31 </button>
32 </form>
33 </#if>
34
35 </#if>
36</@layout.registrationLayout>
\ No newline at end of file
config/keycloak/theme/login/webauthn-register.ftl created+72
......@@ -0,0 +1,72 @@
1<#import "template.ftl" as layout>
2<#import "password-commons.ftl" as passwordCommons>
3
4<@layout.registrationLayout; section>
5 <#if section = "title">
6 title
7 <#elseif section = "header">
8 <div class="alert-info pf-v5-c-alert pf-m-inline pf-v5-u-mb-md pf-m-danger">
9 <div class="pf-c-alert__icon">
10 <span class=""></span>
11 </div>
12 <span class="pf-v5-c-alert__title">
13 you will be prompted to create a passkey. please contact clover if
14 this doesn't work in your browser.
15 </span>
16 </div>
17 <#elseif section = "form">
18
19 <form id="register" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
20 <div class="${properties.kcFormGroupClass!}">
21 <input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
22 <input type="hidden" id="attestationObject" name="attestationObject"/>
23 <input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
24 <input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
25 <input type="hidden" id="transports" name="transports"/>
26 <input type="hidden" id="error" name="error"/>
27 </div>
28 </form>
29
30 <script type="module">
31 import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
32 function init() {
33 const input = {
34 challenge : '${challenge}',
35 userid : '${userid}',
36 username : '${username}',
37 signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg?c},</#list>],
38 rpEntityName : '${rpEntityName}',
39 rpId : '${rpId}',
40 attestationConveyancePreference : '${attestationConveyancePreference}',
41 authenticatorAttachment : '${authenticatorAttachment}',
42 requireResidentKey : '${requireResidentKey}',
43 userVerificationRequirement : '${userVerificationRequirement}',
44 createTimeout : ${createTimeout?c},
45 excludeCredentialIds : '${excludeCredentialIds}',
46 initLabel : "${msg("webauthn-registration-init-label")?no_esc}",
47 initLabelPrompt : "${msg("webauthn-registration-init-label-prompt")?no_esc}",
48 errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
49 };
50 registerByWebAuthn(input);
51 }
52 init();
53 </script>
54
55<!--
56 <input type="submit"
57 class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
58 id="registerWebAuthn" value="${msg("doRegisterSecurityKey")}"/>
59-->
60
61 <#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
62 <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
63 method="post">
64 <button type="submit"
65 class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
66 id="cancelWebAuthnAIA" name="cancel-aia" value="true">${msg("doCancel")}
67 </button>
68 </form>
69 </#if>
70
71 </#if>
72</@layout.registrationLayout>
config/keycloak/uv.lock created+380
......@@ -0,0 +1,380 @@
1version = 1
2revision = 3
3requires-python = ">=3.12"
4
5[[package]]
6name = "aiofiles"
7version = "25.1.0"
8source = { registry = "https://pypi.org/simple" }
9sdist = { 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" }
10wheels = [
11 { 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" },
12]
13
14[[package]]
15name = "anyio"
16version = "4.12.0"
17source = { registry = "https://pypi.org/simple" }
18dependencies = [
19 { name = "idna" },
20 { name = "typing-extensions", marker = "python_full_version < '3.13'" },
21]
22sdist = { 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" }
23wheels = [
24 { 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" },
25]
26
27[[package]]
28name = "async-property"
29version = "0.2.2"
30source = { registry = "https://pypi.org/simple" }
31sdist = { 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" }
32wheels = [
33 { 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" },
34]
35
36[[package]]
37name = "certifi"
38version = "2026.1.4"
39source = { registry = "https://pypi.org/simple" }
40sdist = { 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" }
41wheels = [
42 { 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" },
43]
44
45[[package]]
46name = "cffi"
47version = "2.0.0"
48source = { registry = "https://pypi.org/simple" }
49dependencies = [
50 { name = "pycparser", marker = "implementation_name != 'PyPy'" },
51]
52sdist = { 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" }
53wheels = [
54 { 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" },
55 { 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" },
56 { 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" },
57 { 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" },
58 { 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" },
59 { 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" },
60 { 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" },
61 { 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" },
62 { 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" },
63 { 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" },
64 { 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" },
65 { 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" },
66 { 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" },
67 { 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" },
68 { 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" },
69 { 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" },
70 { 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" },
71 { 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" },
72 { 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" },
73 { 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" },
74 { 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" },
75 { 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" },
76 { 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" },
77 { 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" },
78 { 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" },
79 { 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" },
80 { 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" },
81 { 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" },
82 { 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" },
83 { 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" },
84 { 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" },
85 { 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" },
86 { 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" },
87 { 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" },
88 { 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" },
89 { 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" },
90 { 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" },
91 { 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" },
92 { 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" },
93 { 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" },
94 { 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" },
95 { 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" },
96 { 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" },
97 { 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" },
98 { 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" },
99 { 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" },
100]
101
102[[package]]
103name = "charset-normalizer"
104version = "3.4.4"
105source = { registry = "https://pypi.org/simple" }
106sdist = { 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" }
107wheels = [
108 { 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" },
109 { 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" },
110 { 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" },
111 { 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" },
112 { 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" },
113 { 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" },
114 { 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" },
115 { 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" },
116 { 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" },
117 { 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" },
118 { 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" },
119 { 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" },
120 { 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" },
121 { 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" },
122 { 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" },
123 { 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" },
124 { 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" },
125 { 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" },
126 { 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" },
127 { 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" },
128 { 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" },
129 { 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" },
130 { 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" },
131 { 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" },
132 { 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" },
133 { 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" },
134 { 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" },
135 { 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" },
136 { 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" },
137 { 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" },
138 { 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" },
139 { 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" },
140 { 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" },
141 { 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" },
142 { 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" },
143 { 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" },
144 { 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" },
145 { 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" },
146 { 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" },
147 { 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" },
148 { 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" },
149 { 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" },
150 { 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" },
151 { 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" },
152 { 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" },
153 { 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" },
154 { 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" },
155 { 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" },
156 { 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" },
157]
158
159[[package]]
160name = "cryptography"
161version = "46.0.3"
162source = { registry = "https://pypi.org/simple" }
163dependencies = [
164 { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
165]
166sdist = { 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" }
167wheels = [
168 { 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" },
169 { 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" },
170 { 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" },
171 { 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" },
172 { 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" },
173 { 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" },
174 { 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" },
175 { 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" },
176 { 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" },
177 { 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" },
178 { 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" },
179 { 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" },
180 { 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" },
181 { 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" },
182 { 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" },
183 { 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" },
184 { 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" },
185 { 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" },
186 { 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" },
187 { 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" },
188 { 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" },
189 { 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" },
190 { 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" },
191 { 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" },
192 { 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" },
193 { 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" },
194 { 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" },
195 { 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" },
196 { 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" },
197 { 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" },
198 { 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" },
199 { 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" },
200 { 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" },
201 { 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" },
202 { 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" },
203 { 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" },
204 { 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" },
205 { 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" },
206 { 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" },
207 { 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" },
208 { 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" },
209 { 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" },
210 { 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" },
211 { 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" },
212 { 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" },
213]
214
215[[package]]
216name = "deprecation"
217version = "2.1.0"
218source = { registry = "https://pypi.org/simple" }
219dependencies = [
220 { name = "packaging" },
221]
222sdist = { 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" }
223wheels = [
224 { 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" },
225]
226
227[[package]]
228name = "h11"
229version = "0.16.0"
230source = { registry = "https://pypi.org/simple" }
231sdist = { 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" }
232wheels = [
233 { 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" },
234]
235
236[[package]]
237name = "httpcore"
238version = "1.0.9"
239source = { registry = "https://pypi.org/simple" }
240dependencies = [
241 { name = "certifi" },
242 { name = "h11" },
243]
244sdist = { 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" }
245wheels = [
246 { 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" },
247]
248
249[[package]]
250name = "httpx"
251version = "0.28.1"
252source = { registry = "https://pypi.org/simple" }
253dependencies = [
254 { name = "anyio" },
255 { name = "certifi" },
256 { name = "httpcore" },
257 { name = "idna" },
258]
259sdist = { 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" }
260wheels = [
261 { 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" },
262]
263
264[[package]]
265name = "idna"
266version = "3.11"
267source = { registry = "https://pypi.org/simple" }
268sdist = { 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" }
269wheels = [
270 { 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" },
271]
272
273[[package]]
274name = "jwcrypto"
275version = "1.5.6"
276source = { registry = "https://pypi.org/simple" }
277dependencies = [
278 { name = "cryptography" },
279 { name = "typing-extensions" },
280]
281sdist = { 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" }
282wheels = [
283 { 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" },
284]
285
286[[package]]
287name = "keycloak"
288version = "0.1.0"
289source = { virtual = "." }
290dependencies = [
291 { name = "python-keycloak" },
292 { name = "requests" },
293]
294
295[package.metadata]
296requires-dist = [
297 { name = "python-keycloak", specifier = ">=6.0.0" },
298 { name = "requests", specifier = ">=2.32.5" },
299]
300
301[[package]]
302name = "packaging"
303version = "25.0"
304source = { registry = "https://pypi.org/simple" }
305sdist = { 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" }
306wheels = [
307 { 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" },
308]
309
310[[package]]
311name = "pycparser"
312version = "2.23"
313source = { registry = "https://pypi.org/simple" }
314sdist = { 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" }
315wheels = [
316 { 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" },
317]
318
319[[package]]
320name = "python-keycloak"
321version = "6.0.0"
322source = { registry = "https://pypi.org/simple" }
323dependencies = [
324 { name = "aiofiles" },
325 { name = "async-property" },
326 { name = "deprecation" },
327 { name = "httpx" },
328 { name = "jwcrypto" },
329 { name = "requests" },
330 { name = "requests-toolbelt" },
331]
332sdist = { 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" }
333wheels = [
334 { 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" },
335]
336
337[[package]]
338name = "requests"
339version = "2.32.5"
340source = { registry = "https://pypi.org/simple" }
341dependencies = [
342 { name = "certifi" },
343 { name = "charset-normalizer" },
344 { name = "idna" },
345 { name = "urllib3" },
346]
347sdist = { 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" }
348wheels = [
349 { 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" },
350]
351
352[[package]]
353name = "requests-toolbelt"
354version = "1.0.0"
355source = { registry = "https://pypi.org/simple" }
356dependencies = [
357 { name = "requests" },
358]
359sdist = { 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" }
360wheels = [
361 { 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" },
362]
363
364[[package]]
365name = "typing-extensions"
366version = "4.15.0"
367source = { registry = "https://pypi.org/simple" }
368sdist = { 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" }
369wheels = [
370 { 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" },
371]
372
373[[package]]
374name = "urllib3"
375version = "2.6.2"
376source = { registry = "https://pypi.org/simple" }
377sdist = { 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" }
378wheels = [
379 { 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" },
380]
config/postgres/init.sh+13-1
......@@ -7,6 +7,18 @@ until pg_isready -U postgres; do
77 sleep 1
88done
99
10psql -v ON_ERROR_STOP=1 -U postgres postgres <<-SQL
11 DO \$\$
12 BEGIN
13 IF EXISTS (SELECT FROM pg_user WHERE usename = 'postgres') THEN
14 ALTER USER "postgres" WITH PASSWORD '$POSTGRES_PASSWORD';
15 ELSE
16 CREATE USER "postgres" WITH PASSWORD '$POSTGRES_PASSWORD';
17 END IF;
18 END
19 \$\$;
20SQL
21
1022ensure_db_user() {
1123 printf "Configuring Database %s\n" "$1"
1224 user="$1"
......@@ -30,7 +42,7 @@ SQL
3042}
3143
3244ensure_db_user "clover" "$POSTGRES_PASSWORD"
33ensure_db_user "authelia" "$POSTGRES_PASSWORD_AUTHELIA"
45ensure_db_user "keycloak" "$POSTGRES_PASSWORD_KEYCLOAK"
3446ensure_db_user "dawarich" "$POSTGRES_PASSWORD_DAWARICH"
3547ensure_db_user "forgejo" "$POSTGRES_PASSWORD_FORGEJO"
3648ensure_db_user "jellyfin" "$POSTGRES_PASSWORD_JELLYFIN"
config/postgres/pgadmin-init.sh created+12
......@@ -0,0 +1,12 @@
1#!/bin/sh
2set -e
3
4export PGADMIN_SERVER_JSON_FILE=/init/pgadmin.json
5
6mkdir -p /var/lib/pgadmin/storage/pgadmin4_pgadmin.org
7echo "postgres:5432:*:postgres:$POSTGRES_PASSWORD" >/var/lib/pgadmin/storage/pgadmin4_pgadmin.org/pgpass
8
9chmod 600 /var/lib/pgadmin/storage/*/pgpass
10chown -R 5050:5050 /var/lib/pgadmin/storage/*
11
12/entrypoint.sh python3
config/postgres/pgadmin.json created+13
......@@ -0,0 +1,13 @@
1{
2 "Servers": {
3 "1": {
4 "Name": "Clover Home",
5 "Group": "Servers",
6 "Host": "postgres",
7 "Port": 5432,
8 "MaintenanceDB": "postgres",
9 "Username": "postgres",
10 "PassFile": "/var/lib/pgadmin/storage/pgadmin4_pgadmin.org/pgpass"
11 }
12 }
13}
local-dns.sh+3
......@@ -14,3 +14,6 @@ while ! [ -f .apps/caddy/pki/authorities/local/root.crt ]; do
1414done
1515
1616sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain .apps/caddy/pki/authorities/local/root.crt
17
18echo
19echo "If using OrbStack, you may need to restart it now"
postgres-rescue.sh created+12
......@@ -0,0 +1,12 @@
1#!/bin/bash
2set -e
3. ./.env
4cd "$(dirname "${BASH_SOURCE[0]}")"
5
6docker build -t postgres-rescue "config/postgres" --build-arg USER_ID="$USER_ID" --build-arg GROUP_ID="$GROUP_ID"
7
8docker run -it --rm \
9 --user "$(id -u):$(id -g)" \
10 -v "${APP_ROOT}/pg_data:/var/lib/postgresql/data" \
11 --entrypoint /bin/bash \
12 postgres-rescue
readme.md-3
......@@ -16,9 +16,6 @@ cp .env.example .env
1616
1717# fill secrets
1818vi .env
19vi secrets/authelia-encryption-key.txt
20vi secrets/authelia-session-secret.txt
21vi secrets/authelia-jwt-secret.txt
2219vi secrets/openvpn-credentials.txt
2320
2421docker compose up -d
secrets/.gitignore+2-1
......@@ -1 +1,2 @@
1*.txt
1!.gitignore
2*