authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-05 02:15:16-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-05 15:36:02-07:00
log1b39c599adc0da4b903d160236d31172e251645b
tree846db4bd382c0e5c5b88e607d7094cf934f1e5ab
parentc14c62225850a095a6e327e5525cea6966a92948
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

Retire Keycloak and redirect legacy sign-in domains

Keep the legacy passkey origin association and link the original theme assets to their canonical dashboard home. Assisted-by: gpt-6

7 files changed, 17 insertions(+), 358 deletions(-)

config/excluded-services.json+2-1
......@@ -1,4 +1,5 @@
11[
22 "evil-forgejo",
3 "evil-hedgedoc"
3 "evil-hedgedoc",
4 "keycloak"
45]
service/keycloak/theme/login/resources/css/styles.css deleted-345
......@@ -1,345 +0,0 @@
1@import "https://file.paperclover.net/.static/font.css";
2
3/* reset */
4html,
5body {
6 height: 100%;
7}
8
9h1,
10h2,
11h3,
12h4,
13h5,
14h6 {
15 font-size: unset;
16 font-weight: unset;
17}
18
19:where(
20 html,
21 body,
22 p,
23 ol,
24 ul,
25 li,
26 dl,
27 dt,
28 dd,
29 blockquote,
30 figure,
31 fieldset,
32 legend,
33 textarea,
34 pre,
35 iframe,
36 hr,
37 h1,
38 h2,
39 h3,
40 h4,
41 h5,
42 h6
43) {
44 margin: 0;
45 padding: 0;
46}
47
48*, :after, :before {
49 box-sizing: border-box;
50 font: unset;
51}
52
53/* root */
54body {
55 color-scheme: light dark;
56 background-color: #f9feff;
57 background-image: url(../img/miku-light.png);
58 background-size: auto 100%;
59 background-position: right top;
60 background-repeat: no-repeat;
61 color: black;
62 --text: black;
63
64 --header: light-dark(#1a46cd, #938cff);
65 --primary: light-dark(#00238f, #938cff);
66}
67@media (prefers-color-scheme: dark) {
68 body {
69 background-image: url(../img/miku-dark.png);
70 background-color: #2f4b67;
71 color: white;
72 --text: white;
73 }
74}
75@media (max-width: 1313px) {
76 body {
77 background-position: right calc(-100px + 50%) top;
78 }
79}
80
81/* sidebar */
82.pf-v5-c-login__main {
83 max-width: 30rem;
84 padding: 2rem;
85 height: 100%;
86 display: flex;
87 flex-direction: column;
88 justify-content: center;
89 --bg: light-dark(rgba(30, 30, 30, 0.1), rgba(0, 0, 0, 0.3));
90 background-color: var(--bg);
91 border-style: solid;
92 border-right-width: 4px;
93 border-color: var(--bg);
94 backdrop-filter: blur(4px);
95 overflow-y: auto;
96}
97.kc-logo-text {
98 font-size: 3rem;
99 text-align: center;
100 color: var(--header);
101}
102#kc-page-title, #kc-info-wrapper {
103 text-align: center;
104 color: rgb(from var(--text) r g b / 0.8);
105}
106#kc-page-title {
107 margin-bottom: 1rem;
108}
109#kc-info-wrapper {
110 margin-top: 1rem;
111}
112#kc-form-options {
113 margin-bottom: 1rem;
114}
115a {
116 color: var(--header);
117 text-decoration: dotted underline;
118}
119a:hover {
120 text-decoration: underline;
121 background-color: rgb(from var(--header) r g b / 0.2);
122}
123
124/* branding */
125.kc-logo-text::before {
126 display: block;
127 content: " ";
128 width: 30%;
129 aspect-ratio: 1;
130 margin: auto;
131 background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9Ii0yIC0yIDI4IDI4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMTAgMjBMOC43NSAxNy41TDYgMThNMTAgNEw4Ljc1IDYuNUw2IDZNMTQgMjBMMTUuMjUgMTcuNUwxOCAxOE0xNCA0TDE1LjI1IDYuNUwxOCA2TTE3IDIxTDE0IDE1TTE0IDE1SDEwTTE0IDE1TDE1LjUgMTJNMTAgMTVMNyAyMU0xMCAxNUw4LjUgMTJNMTcgM0wxNCA5TTE0IDlMMTUuNSAxMk0xNCA5SDEwTTE1LjUgMTJIMjJNMiAxMkg4LjVNOC41IDEyTDEwIDlNMTAgOUw3IDNNMjAgMTBMMTguNSAxMkwyMCAxNE00IDEwTDUuNSAxMkw0IDE0IiBzdHJva2U9InVybCgjcGFpbnQwX2xpbmVhcl81NDhfMTkpIiBzdHJva2Utd2lkdGg9IjMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTAgMjBMOC43NSAxNy41TDYgMThNMTAgNEw4Ljc1IDYuNUw2IDZNMTQgMjBMMTUuMjUgMTcuNUwxOCAxOE0xNCA0TDE1LjI1IDYuNUwxOCA2TTE3IDIxTDE0IDE1TTE0IDE1SDEwTTE0IDE1TDE1LjUgMTJNMTAgMTVMNyAyMU0xMCAxNUw4LjUgMTJNMTcgM0wxNCA5TTE0IDlMMTUuNSAxMk0xNCA5SDEwTTE1LjUgMTJIMjJNMiAxMkg4LjVNOC41IDEyTDEwIDlNMTAgOUw3IDNNMjAgMTBMMTguNSAxMkwyMCAxNE00IDEwTDUuNSAxMkw0IDE0IiBzdHJva2U9InVybCgjcGFpbnQxX2xpbmVhcl81NDhfMTkpIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzU0OF8xOSIgeDE9IjQiIHkxPSItMiIgeDI9IjE4LjUiIHkyPSIyNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjMjIzRDk5Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzE1NDM5MiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfNTQ4XzE5IiB4MT0iMTAiIHkxPSItMyIgeDI9IjE4IiB5Mj0iMjciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0YyRTNGRiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGMkY4RkYiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K);
132 background-size: cover;
133}
134#kc-page-title,
135#kc-info-wrapper,
136.checkbox,
137a,
138#kc-form-buttons,
139.pf-v5-c-helper-text__item-text,
140.pf-v5-c-alert__title,
141input[type="submit"],
142input[type="button"],
143button {
144 text-transform: lowercase;
145}
146
147/* text input */
148.pf-v5-c-form__group {
149 margin-bottom: 1rem;
150}
151.pf-v5-c-form__group input:not([type="checkbox"]) {
152 width: 100%;
153}
154.pf-v5-c-form__group > div:first-child {
155 display: flex;
156}
157.pf-v5-c-form__label {
158 margin-bottom: 0.25rem;
159 text-transform: lowercase;
160 user-select: none;
161 display: block;
162}
163.pf-v5-c-input-group {
164 display: flex;
165 border-radius: 8px;
166}
167.pf-v5-c-form-control {
168 background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
169 padding: 2px;
170 appearance: none;
171 border: 2px solid var(--text);
172 font-size: inherit;
173 color: var(--text);
174 text-indent: 8px;
175 height: 38px;
176 border-radius: 8px;
177 flex: 1;
178}
179.pf-v5-c-form-control:has(+ button) {
180 border-top-right-radius: 0;
181 border-bottom-right-radius: 0;
182}
183.pf-v5-c-form-control + button {
184 appearance: none;
185 border-top-right-radius: 8px;
186 border-bottom-right-radius: 8px;
187 width: 38px;
188 border: 2px solid var(--text);
189 border-left: none;
190 background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
191 transition: background-color 0.1s linear;
192}
193.pf-v5-c-form-control + button:hover {
194 background-color: light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2));
195}
196.pf-v5-c-form__group:has(input:focus-visible)
197 > :is(input, .pf-v5-c-input-group) {
198 outline: 2px solid rgb(from var(--primary) r g b / 0.5);
199}
200.pf-v5-c-form__group:has(input:focus-visible) * {
201 border-color: var(--primary);
202 outline: none;
203}
204.pf-v5-c-form__group:has(input:focus-visible) .pf-v5-c-form__label {
205 color: var(--header);
206}
207.pf-v5-c-helper-text__item-text {
208 display: block;
209 margin-top: 0.5rem;
210}
211.pf-m-error {
212 color: light-dark(#c80000, #f56666);
213}
214
215/* checkbox */
216.checkbox input {
217 display: none;
218}
219.checkbox label {
220 display: flex;
221 align-items: center;
222 user-select: none;
223 cursor: pointer;
224}
225.checkbox label:before {
226 content: " ";
227 display: block;
228 width: 24px;
229 height: 24px;
230 margin-right: 0.5rem;
231 margin-left: -2px;
232 background-color: var(--text);
233
234 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1pY29uIGx1Y2lkZS1zcXVhcmUiPjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgeD0iMyIgeT0iMyIgcng9IjIiLz48L3N2Zz4=);
235 mask-size: cover;
236}
237.checkbox label:has(:checked):before {
238 background-color: var(--primary);
239 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1jaGVjay1iaWctaWNvbiBsdWNpZGUtc3F1YXJlLWNoZWNrLWJpZyI+PHBhdGggZD0iTTIxIDEwLjY1NlYxOWEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMlY1YTIgMiAwIDAgMSAyLTJoMTIuMzQ0Ii8+PHBhdGggZD0ibTkgMTEgMyAzTDIyIDQiLz48L3N2Zz4=);
240}
241.checkbox label:has(:checked) {
242 color: var(--primary);
243}
244
245/* buttons */
246.pf-v5-c-button.pf-m-primary {
247 background-color: var(--primary);
248 display: block;
249 border: none;
250 height: 30px;
251 border-radius: 8px;
252 color: white;
253}
254.pf-v5-c-button.pf-m-block {
255 display: block;
256 width: 100%;
257}
258
259/* alert */
260.pf-v5-c-alert {
261 border-radius: 8px;
262 padding: 8px;
263 align-items: center;
264 margin-bottom: 1rem;
265 text-align: center;
266 text-wrap: balance;
267}
268.alert-error {
269 background-color: light-dark(#ff010182, #f56666a1);
270}
271.alert-warning, .alert-info {
272 background-color: rgb(from var(--primary) r g b / 0.5);
273}
274
275/* icons */
276[data-password-toggle] {
277 display: grid;
278 align-items: center;
279 justify-content: center;
280}
281[data-password-toggle] i {
282 display: block;
283 width: 24px;
284 height: 24px;
285 background-color: var(--text);
286 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV5ZS1pY29uIGx1Y2lkZS1leWUiPjxwYXRoIGQ9Ik0yLjA2MiAxMi4zNDhhMSAxIDAgMCAxIDAtLjY5NiAxMC43NSAxMC43NSAwIDAgMSAxOS44NzYgMCAxIDEgMCAwIDEgMCAuNjk2IDEwLjc1IDEwLjc1IDAgMCAxLTE5Ljg3NiAwIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIvPjwvc3ZnPg==);
287}
288input[type="text"] + [data-password-toggle] i {
289 mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWV5ZS1vZmYtaWNvbiBsdWNpZGUtZXllLW9mZiI+PHBhdGggZD0iTTEwLjczMyA1LjA3NmExMC43NDQgMTAuNzQ0IDAgMCAxIDExLjIwNSA2LjU3NSAxIDEgMCAwIDEgMCAuNjk2IDEwLjc0NyAxMC43NDcgMCAwIDEtMS40NDQgMi40OSIvPjxwYXRoIGQ9Ik0xNC4wODQgMTQuMTU4YTMgMyAwIDAgMS00LjI0Mi00LjI0MiIvPjxwYXRoIGQ9Ik0xNy40NzkgMTcuNDk5YTEwLjc1IDEwLjc1IDAgMCAxLTE1LjQxNy01LjE1MSAxIDEgMCAwIDEgMC0uNjk2IDEwLjc1IDEwLjc1IDAgMCAxIDQuNDQ2LTUuMTQzIi8+PHBhdGggZD0ibTIgMiAyMCAyMCIvPjwvc3ZnPg==);
290}
291
292@media (max-width: 799px) {
293 body {
294 background-position: left 65% bottom 60%;
295 background-size: auto 150%;
296 display: flex;
297 flex-direction: column;
298 align-items: center;
299 justify-content: flex-end;
300 }
301 body:before {
302 display: block;
303 content: "";
304 flex: 1.5;
305 }
306 .pf-v5-c-login__main {
307 flex: 1 1 40%;
308 max-width: 25rem;
309 min-width: 80%;
310 border-width: 4px;
311 border-bottom-width: 0;
312 border-top-left-radius: 32px;
313 border-top-right-radius: 32px;
314 corner-shape: superellipse(0);
315 justify-content: space-between;
316 --bg: light-dark(rgba(255, 255, 255, 0.7), rgba(0.2, 0, 0.2, 0.6));
317 /* the logo straddles the card's top edge; overflow-y: auto would clip
318 the half that sticks out above (#35) */
319 overflow-y: visible;
320 }
321 .pf-v5-c-login__main:after {
322 display: block;
323 content: "";
324 }
325 .kc-logo-text::before {
326 position: absolute;
327 left: 50%;
328 width: 100px;
329 transform: translate(-50%, calc(-50% - 35px));
330 }
331}
332
333#credit {
334 position: fixed;
335 bottom: 2px;
336 right: 2px;
337 font-size: 14px;
338}
339#credit a:not(:hover) {
340 opacity: 0.5;
341}
342
343.subtitle:has(.subtitle .required) {
344 display: none;
345}
service/keycloak/theme/login/resources/css/styles.css created+1
......@@ -0,0 +1 @@
1../../../../../../dashboard/web/sso/css/styles.css
\ No newline at end of file
service/keycloak/theme/login/resources/img/license.txt deleted-2
......@@ -1,2 +0,0 @@
1https://safebooru.org/index.php?page=post&s=view&id=4405346
2dark mode by paper clover
service/keycloak/theme/login/resources/img/license.txt created+1
......@@ -0,0 +1 @@
1../../../../../../dashboard/web/sso/img/license.txt
\ No newline at end of file
service/keycloak/theme/login/resources/img/miku-dark.png deleted
Binary files a/service/keycloak/theme/login/resources/img/miku-dark.png and /dev/null differ
service/keycloak/theme/login/resources/img/miku-dark.png created+1
......@@ -0,0 +1 @@
1../../../../../../dashboard/web/sso/img/miku-dark.png
\ No newline at end of file
service/keycloak/theme/login/resources/img/miku-light.png deleted
Binary files a/service/keycloak/theme/login/resources/img/miku-light.png and /dev/null differ
service/keycloak/theme/login/resources/img/miku-light.png created+1
......@@ -0,0 +1 @@
1../../../../../../dashboard/web/sso/img/miku-light.png
\ No newline at end of file
service/postgres/service.pkl-1
......@@ -26,7 +26,6 @@ container {
2626 tcp {
2727 name = "db"
2828 containerPort = 5432
29 // The podman0 firewall permits this port for Keycloak's connection.
3029 hostPort = 15432
3130 loopback = false
3231 }
tools/router.py+11-9
......@@ -89,6 +89,7 @@ def render(token):
8989 if not re.fullmatch(r"[0-9a-fA-F]{64}", dashboard_proof):
9090 raise ValueError("invalid dashboard proxy token")
9191 auth_host = "auth." + os.environ["STUDIO_DOMAIN"]
92 legacy_host = "keycloak." + os.environ["STUDIO_DOMAIN"]
9293 origins = json.dumps({"origins": ["https://snowglobe." + os.environ["STUDIO_DOMAIN"]]}, separators=(",", ":"))
9394 webauthn = [" handle /.well-known/webauthn {", ' header Content-Type application/json',
9495 f" respond {json.dumps(origins)} 200", " }"]
......@@ -159,6 +160,8 @@ def render(token):
159160 lines = []
160161 for (listener, host), route in sorted(routes.items()):
161162 if host:
163 if host in {auth_host, legacy_host}:
164 continue
162165 role = route["authRole"]
163166 if role and not auth_upstreams:
164167 continue
......@@ -172,8 +175,6 @@ def render(token):
172175 service = next(iter(route["services"]))
173176 if not re.fullmatch(r"[a-z][a-z0-9-]*", service):
174177 raise ValueError(f"invalid service name: {service}")
175 if service == "keycloak" and host == auth_host:
176 lines += webauthn
177178 if service == "shale" or re.fullmatch(r"shale-preview-[0-9a-f]{8}", service):
178179 # Keep the Origin guard before every static/proxy handle;
179180 # otherwise Caddy sorts those handles ahead of respond.
......@@ -291,18 +292,19 @@ def render(token):
291292 lines.insert(len(lines) - 1, " }")
292293 else:
293294 lines += [f":{listener} {{", *proxy(" ".join(sorted(route["upstreams"])), " "), "}"]
294 if (80, auth_host) not in routes:
295 if not HOST.fullmatch(auth_host):
296 raise ValueError("invalid passkey domain")
297 lines += [f"{auth_host} {{", *([" tls internal"] if auth_host.endswith(".test") else []),
298 *webauthn, " handle {", " respond 503", " }", "}"]
295 dashboard_host = "snowglobe." + os.environ["STUDIO_DOMAIN"]
296 if any(not HOST.fullmatch(host) for host in [auth_host, legacy_host, dashboard_host]):
297 raise ValueError("invalid login domain")
298 for host in [auth_host, legacy_host]:
299 lines += [f"{host} {{", *([" tls internal"] if host.endswith(".test") else []),
300 *(webauthn if host == auth_host else []), " handle {",
301 f" redir https://{dashboard_host}/ 308", " }", "}"]
299302 traces = next((route for route in routes.values() if "victoria-traces" in route["services"]), None)
300303 if traces:
301304 lines += ["http://127.0.0.1:10428 {", " bind 127.0.0.1",
302305 *proxy(" ".join(sorted(traces["upstreams"])), " "), "}"]
303 dashboard_host = "snowglobe." + os.environ["STUDIO_DOMAIN"]
304306 dashboard_port = int(os.environ["STUDIO_DASHBOARD_PORT"])
305 if not HOST.fullmatch(dashboard_host) or not 1 <= dashboard_port <= 65535:
307 if not 1 <= dashboard_port <= 65535:
306308 raise ValueError("invalid dashboard route")
307309 if (80, dashboard_host) in routes:
308310 raise ValueError(f"dashboard route conflicts with a Nomad service: {dashboard_host}")