| 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 | 43 | # services are sorted alphabetically |
| 44 | 44 | auth.{$HOME_DOMAIN} { |
| 45 | 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 | 63 | handle /apps { |
| 49 | 64 | rewrite * /static/apps.html |
| 65 | templates | |
| 50 | 66 | file_server { |
| 51 | 67 | root /etc/caddy |
| 52 | 68 | } |
config/static/apps.html+87-56| ... | ... | @@ -1,5 +1,9 @@ |
| 1 | 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 | 7 | <html lang="en"> |
| 4 | 8 | <head> |
| 5 | 9 | <meta charset="utf-8"> |
| ... | ... | @@ -10,74 +14,101 @@ |
| 10 | 14 | :root { color-scheme: light dark; } |
| 11 | 15 | body { |
| 12 | 16 | font-family: "national-medium", system-ui, sans-serif; |
| 13 | background-color: light-dark(#e8eefa, #2f4b67); | |
| 17 | background-color: light-dark(#e8eefa, #152c42); | |
| 14 | 18 | color: light-dark(black, white); |
| 15 | 19 | max-width: 34rem; |
| 16 | 20 | margin: 0 auto; |
| 17 | 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 | 24 | h1 { text-align: center; font-weight: normal; } |
| 21 | 25 | a { text-decoration: dotted underline; } |
| 22 | 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 | 37 | .domain { opacity: 0.5; font-size: 0.85rem; } |
| 25 | ul { list-style: "\2744\FE0E "; padding-left: 1.5rem; } | |
| 26 | li { margin: 0.4rem 0; } | |
| 27 | footer { margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; } | |
| 38 | ul { list-style: none; padding-left: 0.25rem; } | |
| 39 | li { margin: 0.55rem 0; } | |
| 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 | 51 | </style> |
| 29 | 52 | </head> |
| 30 | 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 | 70 | <main> |
| 33 | <section> | |
| 34 | <h2>for everyone</h2> | |
| 35 | <p class="who">any snow sign on account</p> | |
| 36 | <ul> | |
| 37 | <li><a href="https://xmpp.paperclover.net">paper clover xmpp</a> <span class="domain">xmpp.paperclover.net</span></li> | |
| 38 | <li><a href="https://spindle.paperclover.net">tangled spindle</a> <span class="domain">spindle.paperclover.net</span></li> | |
| 39 | <li><a href="https://knot.paperclover.net">tangled knot</a> <span class="domain">knot.paperclover.net</span></li> | |
| 40 | <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> | |
| 42 | <li><a href="https://speedtest.paperclover.net">open speed test</a> <span class="domain">speedtest.paperclover.net</span></li> | |
| 43 | </ul> | |
| 44 | </section> | |
| 45 | <section> | |
| 46 | <h2>media</h2> | |
| 47 | <p class="who">members of the media group</p> | |
| 48 | <ul> | |
| 49 | <li><a href="https://jelly.paperclover.net">cloverfin</a> <span class="domain">jelly.paperclover.net</span></li> | |
| 50 | <li><a href="https://music.paperclover.net">navidrome music</a> <span class="domain">music.paperclover.net</span></li> | |
| 51 | </ul> | |
| 52 | </section> | |
| 53 | <section> | |
| 54 | <h2>media management</h2> | |
| 55 | <p class="who">members of the media-manage group</p> | |
| 56 | <ul> | |
| 57 | <li><a href="https://qbt.paperclover.net">qbittorrent</a> <span class="domain">qbt.paperclover.net</span></li> | |
| 58 | <li><a href="https://snr.paperclover.net">sonarr tv pvr</a> <span class="domain">snr.paperclover.net</span></li> | |
| 59 | <li><a href="https://rdr.paperclover.net">radarr movie organizer</a> <span class="domain">rdr.paperclover.net</span></li> | |
| 60 | <li><a href="https://jkt.paperclover.net">jackett api adapter</a> <span class="domain">jkt.paperclover.net</span></li> | |
| 61 | </ul> | |
| 62 | </section> | |
| 63 | <section> | |
| 64 | <h2>personal</h2> | |
| 65 | <p class="who">just clover</p> | |
| 66 | <ul> | |
| 67 | <li><a href="https://opencode.paperclover.net">opencode</a> <span class="domain">opencode.paperclover.net</span></li> | |
| 68 | <li><a href="https://sync.paperclover.net">syncthing</a> <span class="domain">sync.paperclover.net</span></li> | |
| 69 | <li><a href="https://dawarich.paperclover.net">dawarich location history</a> <span class="domain">dawarich.paperclover.net</span></li> | |
| 70 | <li><a href="https://pg.paperclover.net">pgadmin</a> <span class="domain">pg.paperclover.net</span></li> | |
| 71 | <li><a href="https://redis.paperclover.net">redis insight</a> <span class="domain">redis.paperclover.net</span></li> | |
| 72 | <li><a href="https://dns.paperclover.net">dns</a> <span class="domain">dns.paperclover.net</span></li> | |
| 73 | <li>ddns <span class="domain">(no web ui)</span></li> | |
| 74 | </ul> | |
| 75 | </section> | |
| 71 | <ul> | |
| 72 | {{ if contains "role:media" $groups }} | |
| 73 | <li><a href="https://jelly.paperclover.net">cloverfin</a> <span class="domain">jelly.paperclover.net</span></li> | |
| 74 | {{ end }} | |
| 75 | {{ if contains "role:media" $groups }} | |
| 76 | <li><a href="https://music.paperclover.net">navidrome music</a> <span class="domain">music.paperclover.net</span></li> | |
| 77 | {{ end }} | |
| 78 | <li><a href="https://git.paperclover.net">clover git forge</a> <span class="domain">git.paperclover.net</span></li> | |
| 79 | <li><a href="https://file.paperclover.net">copyparty file storage</a> <span class="domain">file.paperclover.net</span></li> | |
| 80 | {{ if contains "role:admin" $groups }} | |
| 81 | <li><a href="https://opencode.paperclover.net">opencode</a> <span class="domain">opencode.paperclover.net</span></li> | |
| 82 | {{ end }} | |
| 83 | {{ if contains "role:media-manage" $groups }} | |
| 84 | <li><a href="https://qbt.paperclover.net">qbittorrent</a> <span class="domain">qbt.paperclover.net</span></li> | |
| 85 | {{ end }} | |
| 86 | {{ if contains "role:media-manage" $groups }} | |
| 87 | <li><a href="https://snr.paperclover.net">sonarr tv pvr</a> <span class="domain">snr.paperclover.net</span></li> | |
| 88 | {{ end }} | |
| 89 | {{ if contains "role:media-manage" $groups }} | |
| 90 | <li><a href="https://rdr.paperclover.net">radarr movie organizer</a> <span class="domain">rdr.paperclover.net</span></li> | |
| 91 | {{ end }} | |
| 92 | {{ if contains "role:media-manage" $groups }} | |
| 93 | <li><a href="https://jkt.paperclover.net">jackett api adapter</a> <span class="domain">jkt.paperclover.net</span></li> | |
| 94 | {{ end }} | |
| 95 | {{ if contains "role:admin" $groups }} | |
| 96 | <li><a href="https://sync.paperclover.net">syncthing</a> <span class="domain">sync.paperclover.net</span></li> | |
| 97 | {{ end }} | |
| 98 | {{ if contains "role:admin" $groups }} | |
| 99 | <li><a href="https://dawarich.paperclover.net">dawarich location history</a> <span class="domain">dawarich.paperclover.net</span></li> | |
| 100 | {{ end }} | |
| 101 | {{ if contains "role:admin" $groups }} | |
| 102 | <li><a href="https://pg.paperclover.net">pgadmin</a> <span class="domain">pg.paperclover.net</span></li> | |
| 103 | {{ end }} | |
| 104 | {{ if contains "role:admin" $groups }} | |
| 105 | <li><a href="https://redis.paperclover.net">redis insight</a> <span class="domain">redis.paperclover.net</span></li> | |
| 106 | {{ end }} | |
| 107 | <li><a href="https://speedtest.paperclover.net">open speed test</a> <span class="domain">speedtest.paperclover.net</span></li> | |
| 108 | {{ if contains "role:admin" $groups }} | |
| 109 | <li><a href="https://dns.paperclover.net">dns</a> <span class="domain">dns.paperclover.net</span></li> | |
| 110 | {{ end }} | |
| 111 | </ul> | |
| 76 | 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 | 113 | </body> |
| 83 | 114 | </html> |
generate-app-list.py+85-47| ... | ... | @@ -1,21 +1,48 @@ |
| 1 | 1 | #!/usr/bin/env python3 |
| 2 | 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 | 6 | # sh docker.sh compose config --format json | python3 generate-app-list.py > config/static/apps.html |
| 5 | 7 | # (or local: docker compose config --format json | ...) |
| 6 | 8 | import json |
| 7 | 9 | import os |
| 8 | 10 | import sys |
| 11 | import urllib.parse | |
| 9 | 12 | |
| 10 | 13 | PFX = "net.paperclover.list." |
| 11 | 14 | DOMAIN = os.environ.get("HOME_DOMAIN", "paperclover.net") |
| 12 | 15 | |
| 13 | ACCESS_GROUPS = [ | |
| 14 | (None, "for everyone", "any snow sign on account"), | |
| 15 | ("media", "media", "members of the media group"), | |
| 16 | ("media-manage", "media management", "members of the media-manage group"), | |
| 17 | ("personal", "personal", "just clover"), | |
| 18 | ] | |
| 16 | # access label -> keycloak role required to see the entry (None = everyone) | |
| 17 | ACCESS_ROLE = { | |
| 18 | None: None, | |
| 19 | "media": "role:media", | |
| 20 | "media-manage": "role:media-manage", | |
| 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 | 47 | cfg = json.load(sys.stdin) |
| 21 | 48 | apps = [] |
| ... | ... | @@ -25,46 +52,37 @@ for name, svc in cfg["services"].items(): |
| 25 | 52 | labels = dict(l.split("=", 1) for l in labels) |
| 26 | 53 | if PFX + "name" not in labels: |
| 27 | 54 | continue |
| 55 | if labels.get(PFX + "web") == "false" or not labels.get(PFX + "domain"): | |
| 56 | continue | |
| 28 | 57 | apps.append({ |
| 29 | 58 | "name": labels[PFX + "name"], |
| 30 | "domain": labels.get(PFX + "domain"), | |
| 59 | "domain": labels[PFX + "domain"], | |
| 31 | 60 | "priority": int(labels.get(PFX + "priority", 0)), |
| 32 | 61 | "access": labels.get(PFX + "access"), |
| 33 | 62 | }) |
| 34 | 63 | |
| 35 | 64 | apps.sort(key=lambda a: -a["priority"]) |
| 36 | 65 | |
| 37 | def entries(access): | |
| 38 | out = [] | |
| 39 | for a in apps: | |
| 40 | if a["access"] != access: | |
| 41 | continue | |
| 42 | if a["domain"]: | |
| 43 | out.append( | |
| 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>" | |
| 66 | items = [] | |
| 67 | for a in apps: | |
| 68 | role = ACCESS_ROLE.get(a["access"], "role:admin") | |
| 69 | li = ( | |
| 70 | f' <li><a href="https://{a["domain"]}.{DOMAIN}">' | |
| 71 | f'{a["name"].lower()}</a>' | |
| 72 | f' <span class="domain">{a["domain"]}.{DOMAIN}</span></li>' | |
| 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 | 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 | 86 | <html lang="en"> |
| 69 | 87 | <head> |
| 70 | 88 | <meta charset="utf-8"> |
| ... | ... | @@ -75,32 +93,52 @@ print(f"""<!doctype html> |
| 75 | 93 | :root {{ color-scheme: light dark; }} |
| 76 | 94 | body {{ |
| 77 | 95 | font-family: "national-medium", system-ui, sans-serif; |
| 78 | background-color: light-dark(#e8eefa, #2f4b67); | |
| 96 | background-color: light-dark(#e8eefa, #152c42); | |
| 79 | 97 | color: light-dark(black, white); |
| 80 | 98 | max-width: 34rem; |
| 81 | 99 | margin: 0 auto; |
| 82 | 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 | 103 | h1 {{ text-align: center; font-weight: normal; }} |
| 86 | 104 | a {{ text-decoration: dotted underline; }} |
| 87 | 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 | 116 | .domain {{ opacity: 0.5; font-size: 0.85rem; }} |
| 90 | ul {{ list-style: "\\2744\\FE0E "; padding-left: 1.5rem; }} | |
| 91 | li {{ margin: 0.4rem 0; }} | |
| 92 | footer {{ margin-top: 3rem; opacity: 0.7; font-size: 0.9rem; text-align: center; }} | |
| 117 | ul {{ list-style: none; padding-left: 0.25rem; }} | |
| 118 | li {{ margin: 0.55rem 0; }} | |
| 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 | 130 | </style> |
| 94 | 131 | </head> |
| 95 | 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 | 138 | <main> |
| 139 | <ul> | |
| 98 | 140 | {body} |
| 141 | </ul> | |
| 99 | 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 | 143 | </body> |
| 106 | 144 | </html>""") |