| author | |
| committer | |
| log | 7724489e4f16f750a945aef25084e7b30bbde95b |
| tree | 00e35a228374f5b30e25dbb49600628d1113fb73 |
| parent | 345d7576fee9ee12c5032dec7e5d0ac999e29880 |
| signature |
#16
/apps now sits behind forward-auth; the page is a caddy template that
shows only entries whose role (media / media-manage / admin) appears in
the User-Groups header forward-auth sets. flat list, no section
headers.3 files changed, 189 insertions(+), 104 deletions(-)
config/Caddyfile+17-1| ... | @@ -43,10 +43,26 @@ | ... | @@ -43,10 +43,26 @@ |
| 43 | # services are sorted alphabetically | 43 | # services are sorted alphabetically |
| 44 | auth.{$HOME_DOMAIN} { | 44 | auth.{$HOME_DOMAIN} { |
| 45 | handle / { | 45 | handle / { |
| 46 | redir / /realms/master/account | 46 | redir / /apps |
| 47 | } | ||
| 48 | handle /snow.oauth2/* { | ||
| 49 | reverse_proxy "http://forward-auth" { | ||
| 50 | header_up X-Real-IP {remote_host} | ||
| 51 | header_up X-Forwarded-Uri {uri} | ||
| 52 | } | ||
| 53 | } | ||
| 54 | forward_auth /apps "http://forward-auth" { | ||
| 55 | uri /snow.oauth2/auth | ||
| 56 | header_up X-Real-IP {remote_host} | ||
| 57 | copy_headers X-Auth-Request-Groups>User-Groups X-Auth-Request-Preferred-Username>User-Name | ||
| 58 | @error status 401 | ||
| 59 | handle_response @error { | ||
| 60 | redir * /snow.oauth2/sign_in?rd={scheme}://{host}{uri} | ||
| 61 | } | ||
| 47 | } | 62 | } |
| 48 | handle /apps { | 63 | handle /apps { |
| 49 | rewrite * /static/apps.html | 64 | rewrite * /static/apps.html |
| 65 | templates | ||
| 50 | file_server { | 66 | file_server { |
| 51 | root /etc/caddy | 67 | root /etc/caddy |
| 52 | } | 68 | } |
config/static/apps.html+87-56| ... | @@ -1,5 +1,9 @@ | ... | @@ -1,5 +1,9 @@ |
| 1 | <!doctype html> | 1 | <!doctype html> |
| 2 | <!-- generated by generate-app-list.py — do not edit by hand --> | 2 | <!-- generated by generate-app-list.py — do not edit by hand. |
| 3 | rendered by caddy's templates directive; entries are filtered by the | ||
| 4 | User-Groups header that forward-auth sets for the signed-in user. --> | ||
| 5 | {{ $groups := .Req.Header.Get "User-Groups" }} | ||
| 6 | {{ $name := .Req.Header.Get "User-Name" }} | ||
| 3 | <html lang="en"> | 7 | <html lang="en"> |
| 4 | <head> | 8 | <head> |
| 5 | <meta charset="utf-8"> | 9 | <meta charset="utf-8"> |
| ... | @@ -10,74 +14,101 @@ | ... | @@ -10,74 +14,101 @@ |
| 10 | :root { color-scheme: light dark; } | 14 | :root { color-scheme: light dark; } |
| 11 | body { | 15 | body { |
| 12 | font-family: "national-medium", system-ui, sans-serif; | 16 | font-family: "national-medium", system-ui, sans-serif; |
| 13 | background-color: light-dark(#e8eefa, #2f4b67); | 17 | background-color: light-dark(#e8eefa, #152c42); |
| 14 | color: light-dark(black, white); | 18 | color: light-dark(black, white); |
| 15 | max-width: 34rem; | 19 | max-width: 34rem; |
| 16 | margin: 0 auto; | 20 | margin: 0 auto; |
| 17 | padding: 2rem 1rem 4rem; | 21 | padding: 2rem 1rem 4rem; |
| 18 | } | 22 | } |
| 19 | h1, h2, a { color: light-dark(#1a46cd, #938cff); } | 23 | h1, a { color: light-dark(#1a46cd, #938cff); } |
| 20 | h1 { text-align: center; font-weight: normal; } | 24 | h1 { text-align: center; font-weight: normal; } |
| 21 | a { text-decoration: dotted underline; } | 25 | a { text-decoration: dotted underline; } |
| 22 | a:hover { text-decoration: underline; } | 26 | a:hover { text-decoration: underline; } |
| 23 | .who { opacity: 0.6; font-size: 0.9rem; margin-top: -0.5rem; } | 27 | header { text-align: center; } |
| 28 | header svg { display: block; margin: 0 auto; } | ||
| 29 | #logout { | ||
| 30 | position: absolute; | ||
| 31 | top: 1rem; | ||
| 32 | right: 1.25rem; | ||
| 33 | font-size: 0.9rem; | ||
| 34 | opacity: 0.75; | ||
| 35 | } | ||
| 36 | #logout:hover { opacity: 1; } | ||
| 24 | .domain { opacity: 0.5; font-size: 0.85rem; } | 37 | .domain { opacity: 0.5; font-size: 0.85rem; } |
| 25 | ul { list-style: "\2744\FE0E "; padding-left: 1.5rem; } | 38 | ul { list-style: none; padding-left: 0.25rem; } |
| 26 | li { margin: 0.4rem 0; } | 39 | li { margin: 0.55rem 0; } |
| 27 | footer { margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; } | 40 | li::before { |
| 41 | content: ""; | ||
| 42 | display: inline-block; | ||
| 43 | width: 1em; | ||
| 44 | height: 1em; | ||
| 45 | margin-right: 0.55em; | ||
| 46 | vertical-align: -0.12em; | ||
| 47 | 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"); | ||
| 48 | background-size: contain; | ||
| 49 | background-repeat: no-repeat; | ||
| 50 | } | ||
| 28 | </style> | 51 | </style> |
| 29 | </head> | 52 | </head> |
| 30 | <body> | 53 | <body> |
| 31 | <h1>&#x2744;&#xFE0E; apps on paperclover.net</h1> | 54 | <a id="logout" href="/snow.oauth2/sign_out?rd=https://auth.paperclover.net/apps">log out</a> |
| 55 | <header> | ||
| 56 | <svg width="88" height="88" viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 57 | <path d="M10 20L8.75 17.5L6 18M10 4L8.75 6.5L6 6M14 20L15.25 17.5L18 18M14 4L15.25 6.5L18 6M17 21L14 15M14 15H10M14 15L15.5 12M10 15L7 21M10 15L8.5 12M17 3L14 9M14 9L15.5 12M14 9H10M15.5 12H22M2 12H8.5M8.5 12L10 9M10 9L7 3M20 10L18.5 12L20 14M4 10L5.5 12L4 14" stroke="url(#sfo)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/> | ||
| 58 | <path d="M10 20L8.75 17.5L6 18M10 4L8.75 6.5L6 6M14 20L15.25 17.5L18 18M14 4L15.25 6.5L18 6M17 21L14 15M14 15H10M14 15L15.5 12M10 15L7 21M10 15L8.5 12M17 3L14 9M14 9L15.5 12M14 9H10M15.5 12H22M2 12H8.5M8.5 12L10 9M10 9L7 3M20 10L18.5 12L20 14M4 10L5.5 12L4 14" stroke="url(#sfi)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||
| 59 | <defs> | ||
| 60 | <linearGradient id="sfo" x1="4" y1="-2" x2="18.5" y2="25" gradientUnits="userSpaceOnUse"> | ||
| 61 | <stop stop-color="#223D99"/><stop offset="1" stop-color="#154392"/> | ||
| 62 | </linearGradient> | ||
| 63 | <linearGradient id="sfi" x1="10" y1="-3" x2="18" y2="27" gradientUnits="userSpaceOnUse"> | ||
| 64 | <stop stop-color="#F2E3FF"/><stop offset="1" stop-color="#F2F8FF"/> | ||
| 65 | </linearGradient> | ||
| 66 | </defs> | ||
| 67 | </svg> | ||
| 68 | <h1>hi {{ $name }}</h1> | ||
| 69 | </header> | ||
| 32 | <main> | 70 | <main> |
| 33 | <section> | 71 | <ul> |
| 34 | <h2>for everyone</h2> | 72 | {{ if contains "role:media" $groups }} |
| 35 | <p class="who">any snow sign on account</p> | 73 | <li><a href="https://jelly.paperclover.net">cloverfin</a> <span class="domain">jelly.paperclover.net</span></li> |
| 36 | <ul> | 74 | {{ end }} |
| 37 | <li><a href="https://xmpp.paperclover.net">paper clover xmpp</a> <span class="domain">xmpp.paperclover.net</span></li> | 75 | {{ if contains "role:media" $groups }} |
| 38 | <li><a href="https://spindle.paperclover.net">tangled spindle</a> <span class="domain">spindle.paperclover.net</span></li> | 76 | <li><a href="https://music.paperclover.net">navidrome music</a> <span class="domain">music.paperclover.net</span></li> |
| 39 | <li><a href="https://knot.paperclover.net">tangled knot</a> <span class="domain">knot.paperclover.net</span></li> | 77 | {{ end }} |
| 40 | <li><a href="https://git.paperclover.net">clover git forge</a> <span class="domain">git.paperclover.net</span></li> | 78 | <li><a href="https://git.paperclover.net">clover git forge</a> <span class="domain">git.paperclover.net</span></li> |
| 41 | <li><a href="https://file.paperclover.net">copyparty file storage</a> <span class="domain">file.paperclover.net</span></li> | 79 | <li><a href="https://file.paperclover.net">copyparty file storage</a> <span class="domain">file.paperclover.net</span></li> |
| 42 | <li><a href="https://speedtest.paperclover.net">open speed test</a> <span class="domain">speedtest.paperclover.net</span></li> | 80 | {{ if contains "role:admin" $groups }} |
| 43 | </ul> | 81 | <li><a href="https://opencode.paperclover.net">opencode</a> <span class="domain">opencode.paperclover.net</span></li> |
| 44 | </section> | 82 | {{ end }} |
| 45 | <section> | 83 | {{ if contains "role:media-manage" $groups }} |
| 46 | <h2>media</h2> | 84 | <li><a href="https://qbt.paperclover.net">qbittorrent</a> <span class="domain">qbt.paperclover.net</span></li> |
| 47 | <p class="who">members of the media group</p> | 85 | {{ end }} |
| 48 | <ul> | 86 | {{ if contains "role:media-manage" $groups }} |
| 49 | <li><a href="https://jelly.paperclover.net">cloverfin</a> <span class="domain">jelly.paperclover.net</span></li> | 87 | <li><a href="https://snr.paperclover.net">sonarr tv pvr</a> <span class="domain">snr.paperclover.net</span></li> |
| 50 | <li><a href="https://music.paperclover.net">navidrome music</a> <span class="domain">music.paperclover.net</span></li> | 88 | {{ end }} |
| 51 | </ul> | 89 | {{ if contains "role:media-manage" $groups }} |
| 52 | </section> | 90 | <li><a href="https://rdr.paperclover.net">radarr movie organizer</a> <span class="domain">rdr.paperclover.net</span></li> |
| 53 | <section> | 91 | {{ end }} |
| 54 | <h2>media management</h2> | 92 | {{ if contains "role:media-manage" $groups }} |
| 55 | <p class="who">members of the media-manage group</p> | 93 | <li><a href="https://jkt.paperclover.net">jackett api adapter</a> <span class="domain">jkt.paperclover.net</span></li> |
| 56 | <ul> | 94 | {{ end }} |
| 57 | <li><a href="https://qbt.paperclover.net">qbittorrent</a> <span class="domain">qbt.paperclover.net</span></li> | 95 | {{ if contains "role:admin" $groups }} |
| 58 | <li><a href="https://snr.paperclover.net">sonarr tv pvr</a> <span class="domain">snr.paperclover.net</span></li> | 96 | <li><a href="https://sync.paperclover.net">syncthing</a> <span class="domain">sync.paperclover.net</span></li> |
| 59 | <li><a href="https://rdr.paperclover.net">radarr movie organizer</a> <span class="domain">rdr.paperclover.net</span></li> | 97 | {{ end }} |
| 60 | <li><a href="https://jkt.paperclover.net">jackett api adapter</a> <span class="domain">jkt.paperclover.net</span></li> | 98 | {{ if contains "role:admin" $groups }} |
| 61 | </ul> | 99 | <li><a href="https://dawarich.paperclover.net">dawarich location history</a> <span class="domain">dawarich.paperclover.net</span></li> |
| 62 | </section> | 100 | {{ end }} |
| 63 | <section> | 101 | {{ if contains "role:admin" $groups }} |
| 64 | <h2>personal</h2> | 102 | <li><a href="https://pg.paperclover.net">pgadmin</a> <span class="domain">pg.paperclover.net</span></li> |
| 65 | <p class="who">just clover</p> | 103 | {{ end }} |
| 66 | <ul> | 104 | {{ if contains "role:admin" $groups }} |
| 67 | <li><a href="https://opencode.paperclover.net">opencode</a> <span class="domain">opencode.paperclover.net</span></li> | 105 | <li><a href="https://redis.paperclover.net">redis insight</a> <span class="domain">redis.paperclover.net</span></li> |
| 68 | <li><a href="https://sync.paperclover.net">syncthing</a> <span class="domain">sync.paperclover.net</span></li> | 106 | {{ end }} |
| 69 | <li><a href="https://dawarich.paperclover.net">dawarich location history</a> <span class="domain">dawarich.paperclover.net</span></li> | 107 | <li><a href="https://speedtest.paperclover.net">open speed test</a> <span class="domain">speedtest.paperclover.net</span></li> |
| 70 | <li><a href="https://pg.paperclover.net">pgadmin</a> <span class="domain">pg.paperclover.net</span></li> | 108 | {{ if contains "role:admin" $groups }} |
| 71 | <li><a href="https://redis.paperclover.net">redis insight</a> <span class="domain">redis.paperclover.net</span></li> | 109 | <li><a href="https://dns.paperclover.net">dns</a> <span class="domain">dns.paperclover.net</span></li> |
| 72 | <li><a href="https://dns.paperclover.net">dns</a> <span class="domain">dns.paperclover.net</span></li> | 110 | {{ end }} |
| 73 | <li>ddns <span class="domain">(no web ui)</span></li> | 111 | </ul> |
| 74 | </ul> | ||
| 75 | </section> | ||
| 76 | </main> | 112 | </main> |
| 77 | <footer> | ||
| 78 | <p>signed in with <a href="https://auth.paperclover.net/realms/master/account">snow sign on</a> — | ||
| 79 | one account for everything here.</p> | ||
| 80 | <p>missing access? contact clover.</p> | ||
| 81 | </footer> | ||
| 82 | </body> | 113 | </body> |
| 83 | </html> | 114 | </html> |
generate-app-list.py+85-47| ... | @@ -1,21 +1,48 @@ | ... | @@ -1,21 +1,48 @@ |
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | # generates config/static/apps.html from the net.paperclover.list.* labels | 2 | # generates config/static/apps.html from the net.paperclover.list.* labels |
| 3 | # in compose.yaml. usage: | 3 | # in compose.yaml. the output is a caddy template: caddy's `templates` |
| 4 | # directive renders it per-request, filtering entries by the User-Groups | ||
| 5 | # header and greeting the User-Name that forward-auth populates. usage: | ||
| 4 | # sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html | 6 | # sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html |
| 5 | # (or local: docker compose config --format json | ...) | 7 | # (or local: docker compose config --format json | ...) |
| 6 | import json | 8 | import json |
| 7 | import os | 9 | import os |
| 8 | import sys | 10 | import sys |
| 11 | import urllib.parse | ||
| 9 | 12 | ||
| 10 | PFX = "net.paperclover.list." | 13 | PFX = "net.paperclover.list." |
| 11 | DOMAIN = os.environ.get("HOME_DOMAIN", "paperclover.net") | 14 | DOMAIN = os.environ.get("HOME_DOMAIN", "paperclover.net") |
| 12 | 15 | ||
| 13 | ACCESS_GROUPS = [ | 16 | # access label -> keycloak role required to see the entry (None = everyone) |
| 14 | (None, "for everyone", "any snow sign on account"), | 17 | ACCESS_ROLE = { |
| 15 | ("media", "media", "members of the media group"), | 18 | None: None, |
| 16 | ("media-manage", "media management", "members of the media-manage group"), | 19 | "media": "role:media", |
| 17 | ("personal", "personal", "just clover"), | 20 | "media-manage": "role:media-manage", |
| 18 | ] | 21 | "personal": "role:admin", |
| 22 | } | ||
| 23 | |||
| 24 | # the snow sign on snowflake (same artwork as the login theme, viewBox | ||
| 25 | # pre-padded so the round caps don't clip) | ||
| 26 | SNOWFLAKE_PATH = ("M10 20L8.75 17.5L6 18M10 4L8.75 6.5L6 6M14 20L15.25 17.5L18 18" | ||
| 27 | "M14 4L15.25 6.5L18 6M17 21L14 15M14 15H10M14 15L15.5 12M10 15L7 21M10 15L8.5 12" | ||
| 28 | "M17 3L14 9M14 9L15.5 12M14 9H10M15.5 12H22M2 12H8.5M8.5 12L10 9M10 9L7 3" | ||
| 29 | "M20 10L18.5 12L20 14M4 10L5.5 12L4 14") | ||
| 30 | |||
| 31 | def snowflake_svg(size): | ||
| 32 | return f'''<svg width="{size}" height="{size}" viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 33 | <path d="{SNOWFLAKE_PATH}" stroke="url(#sfo)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/> | ||
| 34 | <path d="{SNOWFLAKE_PATH}" stroke="url(#sfi)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||
| 35 | <defs> | ||
| 36 | <linearGradient id="sfo" x1="4" y1="-2" x2="18.5" y2="25" gradientUnits="userSpaceOnUse"> | ||
| 37 | <stop stop-color="#223D99"/><stop offset="1" stop-color="#154392"/> | ||
| 38 | </linearGradient> | ||
| 39 | <linearGradient id="sfi" x1="10" y1="-3" x2="18" y2="27" gradientUnits="userSpaceOnUse"> | ||
| 40 | <stop stop-color="#F2E3FF"/><stop offset="1" stop-color="#F2F8FF"/> | ||
| 41 | </linearGradient> | ||
| 42 | </defs> | ||
| 43 | </svg>''' | ||
| 44 | |||
| 45 | BULLET_URI = "data:image/svg+xml," + urllib.parse.quote(snowflake_svg(16), safe="") | ||
| 19 | 46 | ||
| 20 | cfg = json.load(sys.stdin) | 47 | cfg = json.load(sys.stdin) |
| 21 | apps = [] | 48 | apps = [] |
| ... | @@ -25,46 +52,37 @@ for name, svc in cfg["services"].items(): | ... | @@ -25,46 +52,37 @@ for name, svc in cfg["services"].items(): |
| 25 | labels = dict(l.split("=", 1) for l in labels) | 52 | labels = dict(l.split("=", 1) for l in labels) |
| 26 | if PFX + "name" not in labels: | 53 | if PFX + "name" not in labels: |
| 27 | continue | 54 | continue |
| 55 | if labels.get(PFX + "web") == "false" or not labels.get(PFX + "domain"): | ||
| 56 | continue | ||
| 28 | apps.append({ | 57 | apps.append({ |
| 29 | "name": labels[PFX + "name"], | 58 | "name": labels[PFX + "name"], |
| 30 | "domain": labels.get(PFX + "domain"), | 59 | "domain": labels[PFX + "domain"], |
| 31 | "priority": int(labels.get(PFX + "priority", 0)), | 60 | "priority": int(labels.get(PFX + "priority", 0)), |
| 32 | "access": labels.get(PFX + "access"), | 61 | "access": labels.get(PFX + "access"), |
| 33 | }) | 62 | }) |
| 34 | 63 | ||
| 35 | apps.sort(key=lambda a: -a["priority"]) | 64 | apps.sort(key=lambda a: -a["priority"]) |
| 36 | 65 | ||
| 37 | def entries(access): | 66 | items = [] |
| 38 | out = [] | 67 | for a in apps: |
| 39 | for a in apps: | 68 | role = ACCESS_ROLE.get(a["access"], "role:admin") |
| 40 | if a["access"] != access: | 69 | li = ( |
| 41 | continue | 70 | f' <li><a href="https://{a["domain"]}.{DOMAIN}">' |
| 42 | if a["domain"]: | 71 | f'{a["name"].lower()}</a>' |
| 43 | out.append( | 72 | f' <span class="domain">{a["domain"]}.{DOMAIN}</span></li>' |
| 44 | f' <li><a href="https://{a["domain"]}.{DOMAIN}">' | ||
| 45 | f'{a["name"].lower()}</a>' | ||
| 46 | f' <span class="domain">{a["domain"]}.{DOMAIN}</span></li>' | ||
| 47 | ) | ||
| 48 | else: | ||
| 49 | out.append(f' <li>{a["name"].lower()} <span class="domain">(no web ui)</span></li>') | ||
| 50 | return out | ||
| 51 | |||
| 52 | sections = [] | ||
| 53 | for access, title, who in ACCESS_GROUPS: | ||
| 54 | items = entries(access) | ||
| 55 | if not items: | ||
| 56 | continue | ||
| 57 | sections.append( | ||
| 58 | f' <section>\n <h2>{title}</h2>\n' | ||
| 59 | f' <p class="who">{who}</p>\n <ul>\n' | ||
| 60 | + "\n".join(" " + i for i in items) | ||
| 61 | + "\n </ul>\n </section>" | ||
| 62 | ) | 73 | ) |
| 74 | if role: | ||
| 75 | li = f' {{{{ if contains "{role}" $groups }}}}\n{li}\n {{{{ end }}}}' | ||
| 76 | items.append(li) | ||
| 63 | 77 | ||
| 64 | body = "\n".join(sections) | 78 | body = "\n".join(items) |
| 65 | 79 | ||
| 66 | print(f"""<!doctype html> | 80 | print(f"""<!doctype html> |
| 67 | <!-- generated by generate-app-list.py — do not edit by hand --> | 81 | <!-- generated by generate-app-list.py — do not edit by hand. |
| 82 | rendered by caddy's templates directive; entries are filtered by the | ||
| 83 | User-Groups header that forward-auth sets for the signed-in user. --> | ||
| 84 | {{{{ $groups := .Req.Header.Get "User-Groups" }}}} | ||
| 85 | {{{{ $name := .Req.Header.Get "User-Name" }}}} | ||
| 68 | <html lang="en"> | 86 | <html lang="en"> |
| 69 | <head> | 87 | <head> |
| 70 | <meta charset="utf-8"> | 88 | <meta charset="utf-8"> |
| ... | @@ -75,32 +93,52 @@ print(f"""<!doctype html> | ... | @@ -75,32 +93,52 @@ print(f"""<!doctype html> |
| 75 | :root {{ color-scheme: light dark; }} | 93 | :root {{ color-scheme: light dark; }} |
| 76 | body {{ | 94 | body {{ |
| 77 | font-family: "national-medium", system-ui, sans-serif; | 95 | font-family: "national-medium", system-ui, sans-serif; |
| 78 | background-color: light-dark(#e8eefa, #2f4b67); | 96 | background-color: light-dark(#e8eefa, #152c42); |
| 79 | color: light-dark(black, white); | 97 | color: light-dark(black, white); |
| 80 | max-width: 34rem; | 98 | max-width: 34rem; |
| 81 | margin: 0 auto; | 99 | margin: 0 auto; |
| 82 | padding: 2rem 1rem 4rem; | 100 | padding: 2rem 1rem 4rem; |
| 83 | }} | 101 | }} |
| 84 | h1, h2, a {{ color: light-dark(#1a46cd, #938cff); }} | 102 | h1, a {{ color: light-dark(#1a46cd, #938cff); }} |
| 85 | h1 {{ text-align: center; font-weight: normal; }} | 103 | h1 {{ text-align: center; font-weight: normal; }} |
| 86 | a {{ text-decoration: dotted underline; }} | 104 | a {{ text-decoration: dotted underline; }} |
| 87 | a:hover {{ text-decoration: underline; }} | 105 | a:hover {{ text-decoration: underline; }} |
| 88 | .who {{ opacity: 0.6; font-size: 0.9rem; margin-top: -0.5rem; }} | 106 | header {{ text-align: center; }} |
| 107 | header svg {{ display: block; margin: 0 auto; }} | ||
| 108 | #logout {{ | ||
| 109 | position: absolute; | ||
| 110 | top: 1rem; | ||
| 111 | right: 1.25rem; | ||
| 112 | font-size: 0.9rem; | ||
| 113 | opacity: 0.75; | ||
| 114 | }} | ||
| 115 | #logout:hover {{ opacity: 1; }} | ||
| 89 | .domain {{ opacity: 0.5; font-size: 0.85rem; }} | 116 | .domain {{ opacity: 0.5; font-size: 0.85rem; }} |
| 90 | ul {{ list-style: "\\2744\\FE0E "; padding-left: 1.5rem; }} | 117 | ul {{ list-style: none; padding-left: 0.25rem; }} |
| 91 | li {{ margin: 0.4rem 0; }} | 118 | li {{ margin: 0.55rem 0; }} |
| 92 | footer {{ margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; }} | 119 | li::before {{ |
| 120 | content: ""; | ||
| 121 | display: inline-block; | ||
| 122 | width: 1em; | ||
| 123 | height: 1em; | ||
| 124 | margin-right: 0.55em; | ||
| 125 | vertical-align: -0.12em; | ||
| 126 | background-image: url("{BULLET_URI}"); | ||
| 127 | background-size: contain; | ||
| 128 | background-repeat: no-repeat; | ||
| 129 | }} | ||
| 93 | </style> | 130 | </style> |
| 94 | </head> | 131 | </head> |
| 95 | <body> | 132 | <body> |
| 96 | <h1>&#x2744;&#xFE0E; apps on {DOMAIN}</h1> | 133 | <a id="logout" href="/snow.oauth2/sign_out?rd=https://auth.{DOMAIN}/apps">log out</a> |
| 134 | <header> | ||
| 135 | {snowflake_svg(88)} | ||
| 136 | <h1>hi {{{{ $name }}}}</h1> | ||
| 137 | </header> | ||
| 97 | <main> | 138 | <main> |
| 139 | <ul> | ||
| 98 | {body} | 140 | {body} |
| 141 | </ul> | ||
| 99 | </main> | 142 | </main> |
| 100 | <footer> | ||
| 101 | <p>signed in with <a href="https://auth.{DOMAIN}/realms/master/account">snow sign on</a> — | ||
| 102 | one account for everything here.</p> | ||
| 103 | <p>missing access? contact clover.</p> | ||
| 104 | </footer> | ||
| 105 | </body> | 143 | </body> |
| 106 | </html>""") | 144 | </html>""") |