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