Two findings from one screenshot of a dead forgot-password page. The page is Gitea's own, and it will always say recovery is disabled: the SMTP details are the members area's, and Gitea is a different container with no mailer and no need for one. But every member reaches Gitea's sign-in form on the way in, and that form links to it — so the broken route is the one they find first. The theme now hides the link. Ours, which works, is /comunidad/recuperar. The footer on that page still advertised the software and its version, which proves the settings added days ago never reached the server. /srv/gitea/docker-compose.yml is a copy and nothing ever synced it: deploy-board.sh syncs the board's compose file, and the Gitea directory has been hand-made since setup. CORS, the theme, OpenID, the register button, the footer — committed, documented, never applied. The file stays valid and the container stays healthy, which is why nobody noticed. scripts/deploy-gitea.sh syncs it, restarts, and then reads the settings back out of the running container and prints them, because this session has lost three separate afternoons to settings that were accepted somewhere and read by nobody. Separately: an invitation was only ever issued when the app created the account. Make the account by hand, add the member with the box unticked, and no invitation exists and none can be made — which is exactly how somebody ended up with an account nobody knew the password to. Miembros now has "Enviar invitación" on any active member with an address, for that case and for a failed send, an expired link or a corrected address. Issuing a new token voids the old one, so a forwarded link stops working. Refused before a token is issued when there is no address, since otherwise a working invitation would be spent on one that cannot be delivered. 183 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
86 lines
3.6 KiB
HTML
86 lines
3.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Miembros{% endblock %}
|
|
|
|
{% set me = g.member %}
|
|
{% set is_owner = me.role == 'owner' %}
|
|
{% set is_admin = me.role in ('owner', 'admin') %}
|
|
|
|
{% block main %}
|
|
<div class="feed-head">
|
|
<h1>Miembros</h1>
|
|
{% if is_admin %}
|
|
<a class="btn" href="{{ url_for('members.new') }}">Dar de alta</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<table class="table">
|
|
<thead>
|
|
<tr><th>Nombre</th><th>Usuario</th><th>Rol</th><th>Estado</th>{% if is_admin %}<th></th>{% endif %}</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for m in members %}
|
|
<tr class="{{ '' if m.active else 'row--off' }}">
|
|
<td>{{ m.display_name }}{% if m.id == me.id %} <span class="muted small">(tú)</span>{% endif %}</td>
|
|
<td class="mono">{{ m.gitea_login }}</td>
|
|
<td>{{ labels[m.role] }}</td>
|
|
<td>{{ 'Activo' if m.active else 'Suspendido' }}</td>
|
|
{% if is_admin %}
|
|
<td class="actions actions--row">
|
|
{# The owner is untouchable, so the row shows nothing rather than
|
|
buttons that would only produce a 403. #}
|
|
{% if m.role != 'owner' %}
|
|
<form method="post" action="{{ url_for('members.set_active', member_id=m.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="active" value="{{ 0 if m.active else 1 }}">
|
|
<button class="linkish" type="submit">{{ 'Suspender' if m.active else 'Reactivar' }}</button>
|
|
</form>
|
|
|
|
{# Any admin can re-send, not just the owner: the usual reason
|
|
somebody needs this is that the admin who added them made
|
|
the account by hand, and waiting for the owner to be
|
|
around defeats the point. Hidden without an address,
|
|
because the handler would only refuse. #}
|
|
{% if m.email and m.active %}
|
|
<form method="post" action="{{ url_for('members.invite', member_id=m.id) }}"
|
|
data-confirm="Se enviará un enlace nuevo a {{ m.email }} y el anterior dejará de servir. ¿Seguro?">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button class="linkish" type="submit">Enviar invitación</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if is_owner %}
|
|
<form method="post" action="{{ url_for('members.set_role', member_id=m.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="role" value="{{ 'user' if m.role == 'admin' else 'admin' }}">
|
|
<button class="linkish" type="submit">{{ 'Hacer usuario' if m.role == 'admin' else 'Hacer administrador' }}</button>
|
|
</form>
|
|
|
|
{% if m.role == 'admin' and m.active %}
|
|
<form method="post" action="{{ url_for('members.transfer', member_id=m.id) }}"
|
|
data-confirm="Vas a dejar de ser el responsable y {{ m.display_name }} pasará a serlo. ¿Seguro?">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button class="linkish" type="submit">Transferir titularidad</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
<form method="post" action="{{ url_for('members.erase', member_id=m.id) }}"
|
|
data-confirm="Se borrarán sus datos personales. Sus mensajes quedarán como «Miembro eliminado». ¿Seguro?">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button class="linkish linkish--danger" type="submit">Eliminar</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
<p class="muted small">
|
|
Hay un solo responsable. El responsable da de alta administradores; los
|
|
administradores dan de alta usuarios. Nadie puede suspender ni degradar al
|
|
responsable: para dejar el cargo hay que transferirlo a un administrador.
|
|
</p>
|
|
{% endblock %}
|