From a79e04396b527ac72ee1489923d2c4aa7fed5946 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:30:06 +0000 Subject: [PATCH] Stop showing members the name of the software behind the login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A member signing in has no idea what Gitea is, and for anyone this platform is ever sold to it is a competitor's name on their login page. Fourteen strings named it — a button, a logout page, an account-creation notice and eleven error messages — plus Gitea's own sign-in and authorize screens, which every member passes through. Ours are reworded: "el servidor de cuentas" where the thing has to be referred to at all, and nothing where it did not. Gitea's own screens take APP_NAME plus the two footer switches, which are supported settings rather than a patched template. APP_NAME goes in app.ini's unnamed root section, spelled DEFAULT in the environment mapping, so the docs carry a command to confirm it landed — a key written to a section that does not exist is accepted in silence. Comments, docstrings, column names and env vars keep the real name. The code has to stay honest about what it talks to, none of it reaches a browser, and renaming gitea_login would mean a migration for nothing. Two guards added, since this is the kind of thing that creeps back one error message at a time: no template renders the word outside a Jinja comment, and no string literal outside a docstring contains it. Checked against the previous commit, where they catch the one message that had already been missed by hand. Licence: MIT, no attribution-in-UI clause, and we redistribute nothing — the official image runs unmodified with its own LICENSE intact. Gitea ships the "powered by" switch itself. Reasoning recorded in §12. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn --- apps/board/auth.py | 5 ++-- apps/board/gitea.py | 24 +++++++-------- apps/board/members.py | 2 +- apps/board/templates/logged_out.html | 12 ++++---- apps/board/templates/login.html | 2 +- apps/board/templates/member_new.html | 8 ++--- apps/board/tests/test_invites.py | 2 +- apps/board/tests/test_templates.py | 44 ++++++++++++++++++++++++++++ docs/server-setup.md | 38 ++++++++++++++++++++++++ infra/gitea/docker-compose.yml | 18 ++++++++++++ 10 files changed, 128 insertions(+), 27 deletions(-) diff --git a/apps/board/auth.py b/apps/board/auth.py index 1749863..a073034 100644 --- a/apps/board/auth.py +++ b/apps/board/auth.py @@ -81,7 +81,7 @@ def callback(): code = request.args.get("code", "") if not code: - flash("Gitea no devolvió un código de autorización.", "error") + flash("No se recibió el código de autorización. Inténtalo de nuevo.", "error") return redirect(url_for("auth.login")) try: @@ -93,7 +93,8 @@ def callback(): return redirect(url_for("auth.login")) except Exception: # network trouble, malformed JSON, Gitea down current_app.logger.exception("OAuth failed unexpectedly") - flash("No se pudo contactar con Gitea. Inténtalo más tarde.", "error") + flash("No se pudo contactar con el servidor de cuentas. " + "Inténtalo más tarde.", "error") return redirect(url_for("auth.login")) login_name = (profile.get("login") or "").strip() diff --git a/apps/board/gitea.py b/apps/board/gitea.py index 9f174a1..9ca0198 100644 --- a/apps/board/gitea.py +++ b/apps/board/gitea.py @@ -86,7 +86,7 @@ def _token_request(payload: dict) -> dict: raise GiteaError("No se pudo completar el inicio de sesión.") data = response.json() if not data.get("access_token"): - raise GiteaError("Gitea no devolvió un token de acceso.") + raise GiteaError("El servidor de cuentas no devolvió un token de acceso.") return data @@ -115,7 +115,7 @@ def fetch_user(token: str) -> dict: timeout=TIMEOUT, ) if response.status_code != 200: - raise GiteaError("No se pudo leer el perfil desde Gitea.") + raise GiteaError("No se pudo leer tu perfil desde el servidor de cuentas.") return response.json() @@ -163,10 +163,10 @@ def admin_create_user(login: str, email: str, full_name: str, password: str) -> if response.status_code in (201, 200): return if response.status_code == 422: - raise GiteaError("Ese usuario o correo ya existe en Gitea.") + raise GiteaError("Ese usuario o ese correo ya están en uso.") if response.status_code in (401, 403): - raise GiteaError("El token de administración de Gitea no es válido.") - raise GiteaError(f"Gitea rechazó la creación del usuario ({response.status_code}).") + raise GiteaError("El token de administración no es válido.") + raise GiteaError(f"No se pudo crear la cuenta ({response.status_code}).") def admin_set_password(login: str, password: str) -> None: @@ -204,19 +204,19 @@ def admin_set_password(login: str, password: str) -> None: # Gitea enforces its own minimum length and complexity, and its message # is in the admin's language rather than the member's, so it is not # passed through. - raise GiteaError("Gitea rechazó esa contraseña. Prueba con una más larga.") + raise GiteaError("No se aceptó esa contraseña. Prueba con una más larga.") if response.status_code in (401, 403): - raise GiteaError("El token de administración de Gitea no es válido.") + raise GiteaError("El token de administración no es válido.") if response.status_code == 404: # Reachable: a member can be added here without ticking "crear también # su cuenta", and then invited. Everything works right up to this call, # which is asked to change the password of an account that was never # made. A bare "(404)" sends the admin looking at the wrong thing. raise GiteaError( - f"No existe la cuenta «{login}» en Gitea, así que no se le puede " + f"No existe la cuenta «{login}», así que no se le puede " "poner contraseña. Pide a un administrador que la cree." ) - raise GiteaError(f"Gitea rechazó el cambio de contraseña ({response.status_code}).") + raise GiteaError(f"No se pudo cambiar la contraseña ({response.status_code}).") # --- content (the member's own token) ------------------------------------ @@ -245,7 +245,7 @@ def list_directory(path: str, token: str) -> list[dict]: if response.status_code == 404: return [] # an empty content folder is normal, not an error if response.status_code != 200: - raise GiteaError(f"Gitea no devolvió la lista de archivos ({response.status_code}).") + raise GiteaError(f"No se pudo leer la lista de archivos ({response.status_code}).") payload = response.json() return [item for item in payload if item.get("type") == "file"] @@ -282,7 +282,7 @@ def write_file(path: str, data: bytes, message: str, token: str, "Alguien más guardó este archivo mientras lo editabas. " "Vuelve a abrirlo para no perder su trabajo." ) - raise GiteaError(f"Gitea rechazó el guardado ({response.status_code}).") + raise GiteaError(f"No se pudo guardar el archivo ({response.status_code}).") def delete_file(path: str, sha: str, message: str, token: str) -> None: @@ -293,4 +293,4 @@ def delete_file(path: str, sha: str, message: str, token: str) -> None: return if response.status_code in (409, 422): raise StaleFile("El archivo cambió desde que lo abriste. Recarga la lista.") - raise GiteaError(f"Gitea rechazó el borrado ({response.status_code}).") + raise GiteaError(f"No se pudo borrar el archivo ({response.status_code}).") diff --git a/apps/board/members.py b/apps/board/members.py index d6d2218..5d2d52a 100644 --- a/apps/board/members.py +++ b/apps/board/members.py @@ -148,7 +148,7 @@ def new(): flash("El usuario solo puede tener letras, números, punto, guion y guion bajo.", "error") return redirect(url_for("members.new")) if create_account and "@" not in email: - flash("Hace falta un correo válido para crear la cuenta en Gitea.", "error") + flash("Hace falta un correo válido para crear la cuenta.", "error") return redirect(url_for("members.new")) db = get_db() diff --git a/apps/board/templates/logged_out.html b/apps/board/templates/logged_out.html index 95b8318..d3bd326 100644 --- a/apps/board/templates/logged_out.html +++ b/apps/board/templates/logged_out.html @@ -8,18 +8,18 @@

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. + Tu sesión aquí está cerrada. Pero este navegador sigue conectado al + servidor de cuentas, que es donde se guardan las contraseñas — así + que quien use este ordenador después podría volver a entrar sin escribirla.

- Cerrar sesión también en Gitea + Cerrar sesión del todo

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

diff --git a/apps/board/templates/login.html b/apps/board/templates/login.html index 477646b..9676b22 100644 --- a/apps/board/templates/login.html +++ b/apps/board/templates/login.html @@ -8,7 +8,7 @@ Este espacio es sólo para miembros de Viena Latina. Se entra con la misma cuenta que se usa para publicar en el sitio.

- Entrar con Gitea + Entrar {# Offered only when the server can actually complete it: without a Gitea admin token the recovery page can do nothing but apologise. #} {% if can_recover %} diff --git a/apps/board/templates/member_new.html b/apps/board/templates/member_new.html index 3e522b4..4c7198b 100644 --- a/apps/board/templates/member_new.html +++ b/apps/board/templates/member_new.html @@ -30,10 +30,10 @@ Crear también su cuenta

- Este servidor no puede crear cuentas (no tiene un token de administración - de Gitea). Crea la cuenta primero en - Gitea y luego da de alta - aquí ese mismo usuario. + Este servidor no puede crear cuentas (le falta el token de administración). + Créala primero en el + servidor de cuentas y luego + da de alta aquí ese mismo usuario.

{% endif %} diff --git a/apps/board/tests/test_invites.py b/apps/board/tests/test_invites.py index 04b6024..59d087b 100644 --- a/apps/board/tests/test_invites.py +++ b/apps/board/tests/test_invites.py @@ -380,7 +380,7 @@ def test_a_member_without_a_gitea_account_is_named_as_such( {"password": "una-contrasena-larga", "confirm": "una-contrasena-larga"}).get_data(as_text=True) - assert "No existe la cuenta «fantasma» en Gitea" in page + assert "No existe la cuenta «fantasma»" in page assert "404" not in page # And the link survives, so it still works once the account exists. assert db.execute("SELECT used_at FROM invites").fetchone()["used_at"] is None diff --git a/apps/board/tests/test_templates.py b/apps/board/tests/test_templates.py index 2853f0c..61f76d4 100644 --- a/apps/board/tests/test_templates.py +++ b/apps/board/tests/test_templates.py @@ -36,3 +36,47 @@ def test_every_post_form_carries_a_csrf_token(template): re.IGNORECASE | re.DOTALL) for form in forms: assert "csrf_token" in form, f"{template.name} has a POST form without a CSRF token" + + +# --- the name of the software behind the login --------------------------- +# +# Members sign in through an OAuth provider that happens to be Gitea. They are +# never told so: as far as anyone using vienalatina.com is concerned there is +# one site, and a stray brand name in an error message is the seam showing. +# Comments and docstrings are exempt — the code has to stay honest about what +# it talks to, and neither reaches a browser. + +JINJA_COMMENT = re.compile(r"\{#.*?#\}", re.DOTALL) + + +@pytest.mark.parametrize("template", TEMPLATES, ids=lambda p: p.name) +def test_no_template_shows_the_name_of_the_account_server(template): + body = JINJA_COMMENT.sub("", template.read_text(encoding="utf-8")) + assert "Gitea" not in body, ( + f"{template.name} shows 'Gitea' to the member; call it el servidor de " + "cuentas, or put the remark in a {# Jinja comment #}" + ) + + +def test_no_message_in_the_code_shows_it_either(): + """String literals only, docstrings excluded, and case-sensitive on + purpose: `gitea_login` and `gitea_tokens` are column names nobody sees, so + only the capitalised prose form is worth failing on.""" + import ast + + offenders = [] + for path in sorted(Path(__file__).resolve().parents[1].glob("*.py")): + tree = ast.parse(path.read_text(encoding="utf-8")) + docstrings = { + doc for node in ast.walk(tree) + if isinstance(node, (ast.Module, ast.FunctionDef, + ast.AsyncFunctionDef, ast.ClassDef)) + and (doc := ast.get_docstring(node, clean=False)) + } + offenders += [ + f"{path.name}:{node.lineno}: {node.value!r}" + for node in ast.walk(tree) + if isinstance(node, ast.Constant) and isinstance(node.value, str) + and "Gitea" in node.value and node.value not in docstrings + ] + assert not offenders, "\n".join(offenders) diff --git a/docs/server-setup.md b/docs/server-setup.md index 86d6d72..8bb0f01 100644 --- a/docs/server-setup.md +++ b/docs/server-setup.md @@ -586,6 +586,44 @@ How often anyone sees them is worth knowing before judging the result: the 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. +### The name, not just the colours + +Themed or not, those two screens said **Gitea** — in the tab, the heading and +the footer. A member has no idea what that is, and for anyone the platform is +ever sold to it is a competitor's name on their login page. Three settings in +`/srv/gitea/docker-compose.yml` take care of it: + +``` +GITEA__DEFAULT__APP_NAME=Viena Latina +GITEA__other__SHOW_FOOTER_POWERED_BY=false +GITEA__other__SHOW_FOOTER_VERSION=false +``` + +`APP_NAME` lives in `app.ini`'s unnamed root section, which the environment +mapping spells `DEFAULT`. **Check it took**, because a key written to a section +that does not exist is accepted in silence: + +```sh +cd /srv/gitea && sudo docker compose up -d +sudo docker compose exec gitea head -5 /data/gitea/conf/app.ini +``` + +That should show `APP_NAME = Viena Latina`. Hiding the version is the one with +a security argument as well as a cosmetic one: it tells a passer-by exactly +which advisories to try. + +**On the licence**, since this is rebranding somebody else's software: Gitea is +MIT, whose only obligation is that the copyright and permission notice travel +with copies of the software. We are not redistributing it — the official image +runs unmodified, with its own `LICENSE` file untouched, and we talk to it over +HTTP. MIT requires no attribution in a user interface, and Gitea itself ships +`SHOW_FOOTER_POWERED_BY` as a supported setting, which settles what the project +intends. The name is a trademark of Gitea Limited; that restricts using it to +brand something else, not declining to display it. Redistributing a modified +Gitea under its own name would be a different question — this is not that. + +### The theme + Unlike Decap, Gitea supports this properly: a theme is a CSS file in a directory it already reads. diff --git a/infra/gitea/docker-compose.yml b/infra/gitea/docker-compose.yml index 04f42e4..fc5d507 100644 --- a/infra/gitea/docker-compose.yml +++ b/infra/gitea/docker-compose.yml @@ -31,6 +31,24 @@ services: # Install the theme first: bash scripts/gitea-theme.sh - GITEA__ui__DEFAULT_THEME=vienalatina + # The name on those same screens. A member signing in should not be + # handed off to a product they have never heard of — as far as they are + # concerned this is still Viena Latina, and the page title, the tab and + # the heading should say so. APP_NAME lives in app.ini's unnamed root + # section, which is spelled DEFAULT in the environment mapping. + # + # VERIFY IT TOOK: `docker compose exec gitea cat /data/gitea/conf/app.ini + # | head -5` should show APP_NAME = Viena Latina. A key written to the + # wrong section is accepted in silence and changes nothing. + - GITEA__DEFAULT__APP_NAME=Viena Latina + + # The footer, which otherwise advertises the software and its version on + # every page. The version is the one with a security argument: it tells a + # passer-by exactly which advisories to try. + - GITEA__other__SHOW_FOOTER_POWERED_BY=false + - GITEA__other__SHOW_FOOTER_VERSION=false + - GITEA__other__SHOW_FOOTER_TEMPLATE_LOAD_TIME=false + # Two tabs on the sign-in page that should not be offered here. # OpenID is sign-in with an external identity URL, which nobody in # this association will ever use; the register button contradicts