From 724dada3f41e616c713f366a2e98e96c90e8ebe8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 16:28:10 +0000 Subject: [PATCH] Brand Gitea's auth screens, and stop Salir overstating itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two halves of the same complaint: the journey into the members area does not feel like one platform. Members sign in to /comunidad/ through Gitea, so Gitea's sign-in form and authorize dialog are part of everyone's journey, not just of anyone who opens a repository. That is what the earlier "is Gitea only for admins?" question got wrong. Unthemed they are two dark screens in the middle of a cream site. Unlike Decap, Gitea supports being themed, so this needs no forking and nothing a release can silently undo. The repository holds only the colour overrides; scripts/gitea-theme.sh concatenates them onto the light theme it reads out of the running container. Keeping somebody else's stylesheet in here would go stale and turn every Gitea upgrade into a merge — the script is re-run instead, which is one line in the upgrade notes. Getting a variable name wrong leaves a corner grey rather than breaking a page, which is the failure mode worth having when the names belong to someone else's project. The logo is the site's wordmark, as live text in the same font stack rather than traced to paths: the site loads no webfont at all, deliberately, so visitors already see it in whatever sans-serif their machine substitutes. Paths would render a Montserrat most people never see on the site itself. Salir was the sharper problem. It cleared this session and the stored token, and then said "Sesión cerrada" — while the Gitea session in the same browser stayed open and Gitea still remembered the authorisation, so one click signed the next person straight back in with no password. On a laptop shared around an association, that button was lying. It cannot be fixed from here. Gitea's logout has been POST-only since 1.11.2, so a link cannot trigger it and a cross-site POST needs a CSRF token this app does not have; prompt=login is undocumented in every released version of Gitea's OAuth2 provider, and a security control should not rest on that. So the logout page now says exactly what is closed and what is not, and links to the one place that finishes it. Being honest about a limit beats a reassuring message that is false. Verified: 101 checks, two of them new — logout warns rather than redirecting, and leaves no token the server could still act with. The theme itself is visual and has to be looked at; docs/server-setup.md §12 says where. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn --- apps/board/auth.py | 19 +++- apps/board/templates/logged_out.html | 32 +++++++ apps/board/tests/test_auth.py | 38 ++++++++ docs/server-setup.md | 73 +++++++++++++++ infra/gitea/assets/logo.svg | 24 +++++ infra/gitea/docker-compose.yml | 7 ++ infra/gitea/theme-vienalatina.overrides.css | 98 +++++++++++++++++++++ scripts/gitea-theme.sh | 71 +++++++++++++++ 8 files changed, 360 insertions(+), 2 deletions(-) create mode 100644 apps/board/templates/logged_out.html create mode 100644 infra/gitea/assets/logo.svg create mode 100644 infra/gitea/theme-vienalatina.overrides.css create mode 100755 scripts/gitea-theme.sh diff --git a/apps/board/auth.py b/apps/board/auth.py index f2b76b2..17d6660 100644 --- a/apps/board/auth.py +++ b/apps/board/auth.py @@ -136,5 +136,20 @@ def logout(): if g.member is not None: tokens.forget(g.member["id"]) session.clear() - flash("Sesión cerrada.", "ok") - return redirect(url_for("auth.login")) + + # Deliberately NOT a redirect back to the login page. + # + # Clearing this session does not touch the Gitea session in the same + # browser, and Gitea remembers that this app was authorised. So the next + # click on "Entrar con Gitea" gets a code back immediately and signs the + # person straight back in without a password — which on a laptop shared + # around an association means "Salir" was telling them something untrue. + # + # Gitea cannot be signed out from here: its logout has been POST-only since + # 1.11.2, and a cross-site POST would need Gitea's CSRF token. `prompt=login` + # would be the other way round it, and is undocumented in every released + # version of Gitea's OAuth2 provider — not something to rest this on. + # + # So the honest thing is to say so and point at the one place that can + # finish the job. + return render_template("logged_out.html", gitea_url=current_app.config["GITEA_URL"].rstrip("/")) diff --git a/apps/board/templates/logged_out.html b/apps/board/templates/logged_out.html new file mode 100644 index 0000000..95b8318 --- /dev/null +++ b/apps/board/templates/logged_out.html @@ -0,0 +1,32 @@ +{# Shown instead of bouncing back to the login page, because bouncing back would + hide the fact that one click gets you straight in again. #} +{% extends "base.html" %} +{% block title %}Sesión cerrada{% endblock %} + +{% block main %} +
+

Saliste del área de la comunidad

+ +

+ Tu sesión aquí está cerrada. Pero este navegador sigue conectado a + Gitea, que es donde se guardan las cuentas — así que quien use + este ordenador después podría volver a entrar sin contraseña. +

+ +

+ Cerrar sesión también en Gitea +

+ +

+ Si esa página no te desconecta, abre el menú de tu perfil en + Gitea y elige Sign Out. + Cerrar el navegador también sirve. +

+ +

+ En tu propio ordenador no hace falta: puedes volver a entrar cuando quieras. +

+ +

Volver a entrar

+
+{% endblock %} diff --git a/apps/board/tests/test_auth.py b/apps/board/tests/test_auth.py index ae5a652..a5985d4 100644 --- a/apps/board/tests/test_auth.py +++ b/apps/board/tests/test_auth.py @@ -106,3 +106,41 @@ def test_login_redirect_cannot_be_pointed_offsite(client, monkeypatch, make_memb def test_responses_say_do_not_index(client): response = client.get("/comunidad/login") assert response.headers["X-Robots-Tag"] == "noindex, nofollow" + + +def test_logout_says_the_gitea_session_is_still_open(client, db, make_member, sign_in, post): + """Redirecting to the login page would hide the problem: one click on + "Entrar con Gitea" signs you straight back in, because Gitea's session and + its record of the authorisation both survive.""" + member_id = make_member("maria") + db.execute( + "INSERT INTO gitea_tokens (member_id, access_token) VALUES (?, 'tok')", + (member_id,), + ) + sign_in(member_id) + + response = post("/comunidad/logout") + page = response.get_data(as_text=True) + + assert response.status_code == 200 + assert "sigue conectado" in page # the warning, not a redirect + assert "/user/logout" in page + + with client.session_transaction() as session: + assert "member_id" not in session + assert db.execute("SELECT 1 FROM gitea_tokens WHERE member_id = ?", + (member_id,)).fetchone() is None + + +def test_logout_leaves_nothing_the_server_can_act_with(client, db, make_member, sign_in, post): + """The token is what lets this server commit as the member. Clearing the + cookie without dropping it would end the browser's access but not ours.""" + member_id = make_member("maria") + db.execute( + "INSERT INTO gitea_tokens (member_id, access_token) VALUES (?, 'tok')", + (member_id,), + ) + sign_in(member_id) + post("/comunidad/logout") + + assert db.execute("SELECT COUNT(*) AS n FROM gitea_tokens").fetchone()["n"] == 0 diff --git a/docs/server-setup.md b/docs/server-setup.md index bf57b19..fae2d64 100644 --- a/docs/server-setup.md +++ b/docs/server-setup.md @@ -501,3 +501,76 @@ Not urgent — leaving it costs a folder and one `wget` in the pipeline: 1. `rm -rf static/admin/` 2. Delete the `wget … decap-cms.js` line from `.woodpecker.yml` 3. Delete the `decap-cms` OAuth application in Gitea + +## 12. Make Gitea look like the site + +Members sign in to `/comunidad/` through Gitea, so Gitea's sign-in form and its +authorize dialog are part of the journey for everyone — not just for you, and +not just for people who open a repository. Unthemed they are two dark screens in +the middle of a cream-coloured site. + +How often anyone sees them is worth knowing before judging the result: the +**authorize dialog appears once per person, ever** — Gitea remembers the grant — +and the **sign-in form only when their Gitea session has lapsed**, which +"Remember This Device" pushes out to weeks. This is a first-impression fix. + +Unlike Decap, Gitea supports this properly: a theme is a CSS file in a directory +it already reads. + +```sh +cd ~/vienalatina +git pull --no-rebase --no-edit gitea main +bash scripts/gitea-theme.sh +``` + +Then add the line the script prints to `/srv/gitea/docker-compose.yml` (it is +already in `infra/gitea/docker-compose.yml`): + +``` +- GITEA__ui__DEFAULT_THEME=vienalatina +``` + +```sh +cd /srv/gitea && sudo docker compose up -d +``` + +Check it in a private window at **https://git.vienalatina.com/user/login** — +cream background, the Viena Latina wordmark, `#c0391c` buttons. Then browse a +repository and open a commit: a theme that only looks right on the login page is +half done. + +### Re-run it after every Gitea upgrade + +The theme is Gitea's own light theme with our colours appended, and the base is +read out of the running container so it matches the installed version. A new +Gitea release can introduce variables our overrides do not mention, and a base +frozen in the repository would drift out of date in ways nobody notices until a +page looks wrong. + +```sh +bash scripts/gitea-theme.sh +cd /srv/gitea && sudo docker compose restart gitea +``` + +Nothing breaks if you forget — an unknown variable is a declaration nobody +reads, so the worst case is a corner that stays grey. + +### Signing out is two steps, and the app says so + +Clicking **Salir** in the members area closes that session and deletes the +stored Gitea token. It cannot close the **Gitea** session in the same browser, +and Gitea remembers that the app was authorised — so without saying anything, +the next click on *Entrar con Gitea* would sign the person straight back in with +no password. On a laptop shared around the association, that is a button that +lies. + +Gitea cannot be signed out from another site: its logout has been POST-only +since 1.11.2, so a link cannot trigger it and a cross-site POST would need +Gitea's CSRF token. The `prompt=login` parameter that would force +re-authentication is undocumented in every released version of Gitea's OAuth2 +provider, and a security control should not rest on that. + +So the logout page says plainly what is and is not closed, and offers the link +that finishes the job. On a shared computer, use it — or close the browser, +which also works. The members-area cookie is already a browser-session cookie, +so it does not survive that either way. diff --git a/infra/gitea/assets/logo.svg b/infra/gitea/assets/logo.svg new file mode 100644 index 0000000..ec506d3 --- /dev/null +++ b/infra/gitea/assets/logo.svg @@ -0,0 +1,24 @@ + + + Viena Latina + + Viena + Latina + + diff --git a/infra/gitea/docker-compose.yml b/infra/gitea/docker-compose.yml index 1caa404..1398553 100644 --- a/infra/gitea/docker-compose.yml +++ b/infra/gitea/docker-compose.yml @@ -23,6 +23,13 @@ services: - GITEA__cors__ALLOW_DOMAIN=https://vienalatina.com - GITEA__cors__METHODS=GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS - GITEA__cors__HEADERS=Content-Type,User-Agent,Authorization + + # Members reach /comunidad/ through Gitea's sign-in and authorize + # screens, so those are part of the site's journey even for people + # who never open a repository. DEFAULT_THEME is what anonymous + # visitors get, which is exactly those two pages. + # Install the theme first: bash scripts/gitea-theme.sh + - GITEA__ui__DEFAULT_THEME=vienalatina volumes: - ./data:/data - /etc/timezone:/etc/timezone:ro diff --git a/infra/gitea/theme-vienalatina.overrides.css b/infra/gitea/theme-vienalatina.overrides.css new file mode 100644 index 0000000..eb8b4e7 --- /dev/null +++ b/infra/gitea/theme-vienalatina.overrides.css @@ -0,0 +1,98 @@ +/* Viena Latina colours for Gitea. + * + * Deltas only. scripts/gitea-theme.sh concatenates Gitea's own light theme — + * read out of the running container, so it matches the installed version — and + * then this file. Shipping the full theme instead would mean a copy of + * somebody else's stylesheet going stale in our repository, and an upgrade + * turning into a merge. + * + * Why this exists: members sign in to /comunidad/ through Gitea, so Gitea's + * sign-in form and authorize dialog are part of the journey whether or not + * anyone ever browses a repository. Unthemed, they are two dark screens in the + * middle of a cream-coloured site, and the platform stops feeling like one + * thing. + * + * The values come from themes/vienalatina/assets/css/main.css. If the brand + * colours change there, change them here too — Hugo fingerprints its CSS and + * Gitea serves this as a static file, so there is no way to share one source. + * + * Safe failure mode: anything Gitea renames is simply a declaration nobody + * reads, and that part keeps the light theme's value. The result of getting a + * variable name wrong is a corner that stays grey, not a broken page. + */ + +:root { + /* Brand — #c0391c, with the darker and lighter steps Gitea expects for + hover, active and focus states. */ + --color-primary: #c0391c; + --color-primary-contrast: #ffffff; + --color-primary-dark-1: #ad331a; + --color-primary-dark-2: #9a2a0f; + --color-primary-dark-3: #86240d; + --color-primary-dark-4: #731f0b; + --color-primary-dark-5: #5f1a09; + --color-primary-dark-6: #4c1507; + --color-primary-dark-7: #380f05; + --color-primary-light-1: #cd5137; + --color-primary-light-2: #d66a53; + --color-primary-light-3: #de836f; + --color-primary-light-4: #e69c8b; + --color-primary-light-5: #eeb5a7; + --color-primary-light-6: #f6cec3; + --color-primary-light-7: #fbe2db; + --color-primary-alpha-10: #c0391c1a; + --color-primary-alpha-20: #c0391c33; + --color-primary-alpha-30: #c0391c4d; + --color-primary-alpha-40: #c0391c66; + --color-primary-alpha-50: #c0391c80; + --color-primary-alpha-60: #c0391c99; + --color-primary-alpha-70: #c0391cb3; + --color-primary-alpha-80: #c0391ccc; + --color-primary-alpha-90: #c0391ce6; + --color-primary-hover: #ad331a; + --color-primary-active: #9a2a0f; + + /* Page and panels — the site's cream background and white cards. */ + --color-body: #f8f3ef; + --color-box-body: #ffffff; + --color-box-body-highlight: #fdf1ed; + --color-box-header: #f2ebe6; + --color-secondary-bg: #f2ebe6; + --color-menu: #ffffff; + --color-card: #ffffff; + --color-markup-code-block: #f2ebe6; + + /* Type — the same near-black and muted brown the site uses. */ + --color-text: #1a0d09; + --color-text-dark: #1a0d09; + --color-text-light: #5c3d37; + --color-text-light-1: #6b4a43; + --color-text-light-2: #7d5a52; + --color-text-light-3: #9a7870; + + /* Borders — warm rather than the default neutral grey, which is most of + what makes an unthemed Gitea read as "a different website". */ + --color-secondary: #e0cec8; + --color-secondary-dark-1: #d3bdb6; + --color-secondary-light-1: #ebdcd7; + --color-light-border: #e0cec8; + --color-input-border: #c2a89f; + --color-input-background: #ffffff; + --color-input-text: #1a0d09; + + /* The top bar, which is the first thing anyone sees. */ + --color-nav-bg: #ffffff; + --color-nav-hover-bg: #fdf1ed; + --color-nav-text: #1a0d09; + --color-header-wrapper: #ffffff; + --color-footer: #f8f3ef; + + --color-accent: #c0391c; + --color-small-accent: #fbe2db; +} + +/* The wordmark is wide, unlike Gitea's square cup, so it needs room. */ +.logo { + height: 26px !important; + width: auto !important; +} diff --git a/scripts/gitea-theme.sh b/scripts/gitea-theme.sh new file mode 100755 index 0000000..6e7924a --- /dev/null +++ b/scripts/gitea-theme.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Install (or reinstall) the Viena Latina theme for Gitea. +# +# Members sign in to /comunidad/ through Gitea, so Gitea's sign-in form and +# authorize dialog are part of the journey whether or not anyone ever browses a +# repository. This makes those two screens look like the rest of the platform. +# +# bash scripts/gitea-theme.sh +# +# Run it again after every Gitea upgrade. The theme is built by concatenating +# Gitea's own light theme with our overrides, so the base has to come from the +# version that is actually installed — a new release can add variables, and a +# copy frozen in this repository would slowly drift out of date in ways nobody +# would notice until a page looked wrong. + +set -euo pipefail + +GITEA_DIR="${GITEA_DIR:-/srv/gitea}" +REPO_DIR="${REPO_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +SERVICE="${SERVICE:-gitea}" + +OVERRIDES="$REPO_DIR/infra/gitea/theme-vienalatina.overrides.css" +LOGO="$REPO_DIR/infra/gitea/assets/logo.svg" + +CUSTOM="$GITEA_DIR/data/gitea" # GITEA_CUSTOM inside the container is /data/gitea +CSS_DIR="$CUSTOM/public/assets/css" +IMG_DIR="$CUSTOM/public/assets/img" + +for file in "$OVERRIDES" "$LOGO"; do + [ -f "$file" ] || { echo "Missing $file" >&2; exit 1; } +done + +echo "== reading the installed Gitea's light theme" +cd "$GITEA_DIR" +BASE="$(docker compose exec -T "$SERVICE" \ + cat /app/gitea/public/assets/css/theme-gitea-light.css)" + +if [ -z "$BASE" ]; then + echo "Could not read Gitea's own theme — is the container running?" >&2 + echo "Check with: cd $GITEA_DIR && docker compose ps" >&2 + exit 1 +fi + +sudo mkdir -p "$CSS_DIR" "$IMG_DIR" + +echo "== writing theme-vienalatina.css" +{ + printf '/* Built by scripts/gitea-theme.sh on %s.\n' "$(date -u +%FT%TZ)" + printf ' Gitea theme-gitea-light.css + infra/gitea/theme-vienalatina.overrides.css\n' + printf ' Do not edit here: rerun the script. */\n' + printf '%s\n' "$BASE" + cat "$OVERRIDES" +} | sudo tee "$CSS_DIR/theme-vienalatina.css" > /dev/null + +echo "== writing the logo and favicon" +# logo.svg is the header mark, favicon.svg the tab icon. Gitea also looks for +# PNG fallbacks; without them it falls back to its own, which is why the tab +# can still show a cup in older browsers. Converting needs a tool this box does +# not have, and an SVG favicon covers everything current. +sudo cp "$LOGO" "$IMG_DIR/logo.svg" +sudo cp "$LOGO" "$IMG_DIR/favicon.svg" + +sudo chown -R 1000:1000 "$CUSTOM/public" + +echo +echo "Installed. Now make it the default (once):" +echo " add GITEA__ui__DEFAULT_THEME=vienalatina to $GITEA_DIR/docker-compose.yml" +echo " then cd $GITEA_DIR && sudo docker compose up -d" +echo +echo "Already set? A restart is enough to pick up the new file:" +echo " cd $GITEA_DIR && sudo docker compose restart $SERVICE"