Stop showing members the name of the software behind the login

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
This commit is contained in:
Claude 2026-09-25 19:30:06 +00:00
parent e11016a8e9
commit a79e04396b
No known key found for this signature in database
10 changed files with 128 additions and 27 deletions

View File

@ -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()

View File

@ -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}).")

View File

@ -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()

View File

@ -8,18 +8,18 @@
<h1>Saliste del área de la comunidad</h1>
<p class="muted">
Tu sesión aquí está cerrada. Pero este navegador <strong>sigue conectado a
Gitea</strong>, 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 <strong>sigue conectado al
servidor de cuentas</strong>, que es donde se guardan las contraseñas — así
que quien use este ordenador después podría volver a entrar sin escribirla.
</p>
<p>
<a class="btn" href="{{ gitea_url }}/user/logout">Cerrar sesión también en Gitea</a>
<a class="btn" href="{{ gitea_url }}/user/logout">Cerrar sesión del todo</a>
</p>
<p class="muted small">
Si esa página no te desconecta, abre el menú de tu perfil en
<a href="{{ gitea_url }}">Gitea</a> y elige <em>Sign Out</em>.
Si esa página no te desconecta, abre el menú de tu perfil
<a href="{{ gitea_url }}">ahí</a> y elige <em>Sign Out</em>.
Cerrar el navegador también sirve.
</p>

View File

@ -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.
</p>
<a class="btn" href="{{ url_for('auth.start', next=next) }}">Entrar con Gitea</a>
<a class="btn" href="{{ url_for('auth.start', next=next) }}">Entrar</a>
{# 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 %}

View File

@ -30,10 +30,10 @@
Crear también su cuenta
</label>
<p class="muted small">
Este servidor no puede crear cuentas (no tiene un token de administración
de Gitea). Crea la cuenta primero en
<a href="{{ gitea_url }}/admin/users/new">Gitea</a> 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
<a href="{{ gitea_url }}/admin/users/new">servidor de cuentas</a> y luego
da de alta aquí ese mismo usuario.
</p>
{% endif %}

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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