diff --git a/config/Caddyfile b/config/Caddyfile
index 1604bdc69ea3bd61f7566de83fd0ea66cb2e6d9e..895a617f87c02c8daf910a71c94f78fc720e996d 100644
--- a/config/Caddyfile
+++ b/config/Caddyfile
@@ -43,10 +43,26 @@
# services are sorted alphabetically
auth.{$HOME_DOMAIN} {
handle / {
- redir / /realms/master/account
+ redir / /apps
+ }
+ handle /snow.oauth2/* {
+ reverse_proxy "http://forward-auth" {
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-Uri {uri}
+ }
+ }
+ forward_auth /apps "http://forward-auth" {
+ uri /snow.oauth2/auth
+ header_up X-Real-IP {remote_host}
+ copy_headers X-Auth-Request-Groups>User-Groups X-Auth-Request-Preferred-Username>User-Name
+ @error status 401
+ handle_response @error {
+ redir * /snow.oauth2/sign_in?rd={scheme}://{host}{uri}
+ }
}
handle /apps {
rewrite * /static/apps.html
+ templates
file_server {
root /etc/caddy
}
diff --git a/config/static/apps.html b/config/static/apps.html
index 8990e80d3b86b22c3065e1bab900b1420871976c..98a0c8c0950f45a051de12e13b2eca787b5dd438 100644
--- a/config/static/apps.html
+++ b/config/static/apps.html
@@ -1,5 +1,9 @@
-
+
+{{ $groups := .Req.Header.Get "User-Groups" }}
+{{ $name := .Req.Header.Get "User-Name" }}
@@ -10,74 +14,101 @@
:root { color-scheme: light dark; }
body {
font-family: "national-medium", system-ui, sans-serif;
- background-color: light-dark(#e8eefa, #2f4b67);
+ background-color: light-dark(#e8eefa, #152c42);
color: light-dark(black, white);
max-width: 34rem;
margin: 0 auto;
padding: 2rem 1rem 4rem;
}
-h1, h2, a { color: light-dark(#1a46cd, #938cff); }
+h1, a { color: light-dark(#1a46cd, #938cff); }
h1 { text-align: center; font-weight: normal; }
a { text-decoration: dotted underline; }
a:hover { text-decoration: underline; }
-.who { opacity: 0.6; font-size: 0.9rem; margin-top: -0.5rem; }
+header { text-align: center; }
+header svg { display: block; margin: 0 auto; }
+#logout {
+ position: absolute;
+ top: 1rem;
+ right: 1.25rem;
+ font-size: 0.9rem;
+ opacity: 0.75;
+}
+#logout:hover { opacity: 1; }
.domain { opacity: 0.5; font-size: 0.85rem; }
-ul { list-style: "\2744\FE0E "; padding-left: 1.5rem; }
-li { margin: 0.4rem 0; }
-footer { margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; }
+ul { list-style: none; padding-left: 0.25rem; }
+li { margin: 0.55rem 0; }
+li::before {
+ content: "";
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin-right: 0.55em;
+ vertical-align: -0.12em;
+ background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%22-2%20-2%2028%2028%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M10%2020L8.75%2017.5L6%2018M10%204L8.75%206.5L6%206M14%2020L15.25%2017.5L18%2018M14%204L15.25%206.5L18%206M17%2021L14%2015M14%2015H10M14%2015L15.5%2012M10%2015L7%2021M10%2015L8.5%2012M17%203L14%209M14%209L15.5%2012M14%209H10M15.5%2012H22M2%2012H8.5M8.5%2012L10%209M10%209L7%203M20%2010L18.5%2012L20%2014M4%2010L5.5%2012L4%2014%22%20stroke%3D%22url%28%23sfo%29%22%20stroke-width%3D%223%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3Cpath%20d%3D%22M10%2020L8.75%2017.5L6%2018M10%204L8.75%206.5L6%206M14%2020L15.25%2017.5L18%2018M14%204L15.25%206.5L18%206M17%2021L14%2015M14%2015H10M14%2015L15.5%2012M10%2015L7%2021M10%2015L8.5%2012M17%203L14%209M14%209L15.5%2012M14%209H10M15.5%2012H22M2%2012H8.5M8.5%2012L10%209M10%209L7%203M20%2010L18.5%2012L20%2014M4%2010L5.5%2012L4%2014%22%20stroke%3D%22url%28%23sfi%29%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%0A%3Cdefs%3E%0A%3ClinearGradient%20id%3D%22sfo%22%20x1%3D%224%22%20y1%3D%22-2%22%20x2%3D%2218.5%22%20y2%3D%2225%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%0A%3Cstop%20stop-color%3D%22%23223D99%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23154392%22%2F%3E%0A%3C%2FlinearGradient%3E%0A%3ClinearGradient%20id%3D%22sfi%22%20x1%3D%2210%22%20y1%3D%22-3%22%20x2%3D%2218%22%20y2%3D%2227%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%0A%3Cstop%20stop-color%3D%22%23F2E3FF%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23F2F8FF%22%2F%3E%0A%3C%2FlinearGradient%3E%0A%3C%2Fdefs%3E%0A%3C%2Fsvg%3E");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
- ❄︎ apps on paperclover.net
+ log out
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hi {{ $name }}
+
-
- for everyone
- any snow sign on account
-
-
-
- media
- members of the media group
-
-
-
- media management
- members of the media-manage group
-
-
-
- personal
- just clover
-
-
+
+ {{ if contains "role:media" $groups }}
+ cloverfin jelly.paperclover.net
+ {{ end }}
+ {{ if contains "role:media" $groups }}
+ navidrome music music.paperclover.net
+ {{ end }}
+ clover git forge git.paperclover.net
+ copyparty file storage file.paperclover.net
+ {{ if contains "role:admin" $groups }}
+ opencode opencode.paperclover.net
+ {{ end }}
+ {{ if contains "role:media-manage" $groups }}
+ qbittorrent qbt.paperclover.net
+ {{ end }}
+ {{ if contains "role:media-manage" $groups }}
+ sonarr tv pvr snr.paperclover.net
+ {{ end }}
+ {{ if contains "role:media-manage" $groups }}
+ radarr movie organizer rdr.paperclover.net
+ {{ end }}
+ {{ if contains "role:media-manage" $groups }}
+ jackett api adapter jkt.paperclover.net
+ {{ end }}
+ {{ if contains "role:admin" $groups }}
+ syncthing sync.paperclover.net
+ {{ end }}
+ {{ if contains "role:admin" $groups }}
+ dawarich location history dawarich.paperclover.net
+ {{ end }}
+ {{ if contains "role:admin" $groups }}
+ pgadmin pg.paperclover.net
+ {{ end }}
+ {{ if contains "role:admin" $groups }}
+ redis insight redis.paperclover.net
+ {{ end }}
+ open speed test speedtest.paperclover.net
+ {{ if contains "role:admin" $groups }}
+ dns dns.paperclover.net
+ {{ end }}
+
-
diff --git a/generate-app-list.py b/generate-app-list.py
index 2275c51c5e9694d5833e6db98435f61ae96a634a..76eb0302b9cde62966d1bf1ba8e8067d6a6dfa3b 100644
--- a/generate-app-list.py
+++ b/generate-app-list.py
@@ -1,21 +1,48 @@
#!/usr/bin/env python3
# generates config/static/apps.html from the net.paperclover.list.* labels
-# in compose.yaml. usage:
+# in compose.yaml. the output is a caddy template: caddy's `templates`
+# directive renders it per-request, filtering entries by the User-Groups
+# header and greeting the User-Name that forward-auth populates. usage:
# sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html
# (or local: docker compose config --format json | ...)
import json
import os
import sys
+import urllib.parse
PFX = "net.paperclover.list."
DOMAIN = os.environ.get("HOME_DOMAIN", "paperclover.net")
-ACCESS_GROUPS = [
- (None, "for everyone", "any snow sign on account"),
- ("media", "media", "members of the media group"),
- ("media-manage", "media management", "members of the media-manage group"),
- ("personal", "personal", "just clover"),
-]
+# access label -> keycloak role required to see the entry (None = everyone)
+ACCESS_ROLE = {
+ None: None,
+ "media": "role:media",
+ "media-manage": "role:media-manage",
+ "personal": "role:admin",
+}
+
+# the snow sign on snowflake (same artwork as the login theme, viewBox
+# pre-padded so the round caps don't clip)
+SNOWFLAKE_PATH = ("M10 20L8.75 17.5L6 18M10 4L8.75 6.5L6 6M14 20L15.25 17.5L18 18"
+ "M14 4L15.25 6.5L18 6M17 21L14 15M14 15H10M14 15L15.5 12M10 15L7 21M10 15L8.5 12"
+ "M17 3L14 9M14 9L15.5 12M14 9H10M15.5 12H22M2 12H8.5M8.5 12L10 9M10 9L7 3"
+ "M20 10L18.5 12L20 14M4 10L5.5 12L4 14")
+
+def snowflake_svg(size):
+ return f'''
+
+
+
+
+
+
+
+
+
+
+ '''
+
+BULLET_URI = "data:image/svg+xml," + urllib.parse.quote(snowflake_svg(16), safe="")
cfg = json.load(sys.stdin)
apps = []
@@ -25,46 +52,37 @@ for name, svc in cfg["services"].items():
labels = dict(l.split("=", 1) for l in labels)
if PFX + "name" not in labels:
continue
+ if labels.get(PFX + "web") == "false" or not labels.get(PFX + "domain"):
+ continue
apps.append({
"name": labels[PFX + "name"],
- "domain": labels.get(PFX + "domain"),
+ "domain": labels[PFX + "domain"],
"priority": int(labels.get(PFX + "priority", 0)),
"access": labels.get(PFX + "access"),
})
apps.sort(key=lambda a: -a["priority"])
-def entries(access):
- out = []
- for a in apps:
- if a["access"] != access:
- continue
- if a["domain"]:
- out.append(
- f' '
- f'{a["name"].lower()} '
- f' {a["domain"]}.{DOMAIN} '
- )
- else:
- out.append(f' {a["name"].lower()} (no web ui) ')
- return out
-
-sections = []
-for access, title, who in ACCESS_GROUPS:
- items = entries(access)
- if not items:
- continue
- sections.append(
- f' \n {title} \n'
- f' {who}
\n \n'
- + "\n".join(" " + i for i in items)
- + "\n \n "
+items = []
+for a in apps:
+ role = ACCESS_ROLE.get(a["access"], "role:admin")
+ li = (
+ f' '
+ f'{a["name"].lower()} '
+ f' {a["domain"]}.{DOMAIN} '
)
+ if role:
+ li = f' {{{{ if contains "{role}" $groups }}}}\n{li}\n {{{{ end }}}}'
+ items.append(li)
-body = "\n".join(sections)
+body = "\n".join(items)
print(f"""
-
+
+{{{{ $groups := .Req.Header.Get "User-Groups" }}}}
+{{{{ $name := .Req.Header.Get "User-Name" }}}}
@@ -75,32 +93,52 @@ print(f"""
:root {{ color-scheme: light dark; }}
body {{
font-family: "national-medium", system-ui, sans-serif;
- background-color: light-dark(#e8eefa, #2f4b67);
+ background-color: light-dark(#e8eefa, #152c42);
color: light-dark(black, white);
max-width: 34rem;
margin: 0 auto;
padding: 2rem 1rem 4rem;
}}
-h1, h2, a {{ color: light-dark(#1a46cd, #938cff); }}
+h1, a {{ color: light-dark(#1a46cd, #938cff); }}
h1 {{ text-align: center; font-weight: normal; }}
a {{ text-decoration: dotted underline; }}
a:hover {{ text-decoration: underline; }}
-.who {{ opacity: 0.6; font-size: 0.9rem; margin-top: -0.5rem; }}
+header {{ text-align: center; }}
+header svg {{ display: block; margin: 0 auto; }}
+#logout {{
+ position: absolute;
+ top: 1rem;
+ right: 1.25rem;
+ font-size: 0.9rem;
+ opacity: 0.75;
+}}
+#logout:hover {{ opacity: 1; }}
.domain {{ opacity: 0.5; font-size: 0.85rem; }}
-ul {{ list-style: "\\2744\\FE0E "; padding-left: 1.5rem; }}
-li {{ margin: 0.4rem 0; }}
-footer {{ margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; }}
+ul {{ list-style: none; padding-left: 0.25rem; }}
+li {{ margin: 0.55rem 0; }}
+li::before {{
+ content: "";
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin-right: 0.55em;
+ vertical-align: -0.12em;
+ background-image: url("{BULLET_URI}");
+ background-size: contain;
+ background-repeat: no-repeat;
+}}
- ❄︎ apps on {DOMAIN}
+ log out
+
+{snowflake_svg(88)}
+ hi {{{{ $name }}}}
+
+
-
""")