| 1 |
{% extends "base.html" %} |
| 2 |
{% import "assistants_panel.html" as ai %} |
| 3 |
{% import "variables_panel.html" as va %} |
| 4 |
{% import "secrets_panel.html" as se %} |
| 5 |
{% block title %}Administration{% endblock %} |
| 6 |
{% block content %} |
| 7 |
<div class="head"><h1>Administration <small>{{ accounts.len() }} accounts · {{ orgs.len() }} organisations · {{ repos }} repositories</small></h1></div> |
| 8 |
<div class="sectioned"> |
| 9 |
<nav class="section-nav" aria-label="Sections"> |
| 10 |
<a href="#accounts">Accounts</a> |
| 11 |
<a href="#registration">Registration</a> |
| 12 |
<a href="#organisations">Organisations</a> |
| 13 |
<a href="#signin">Sign in with</a> |
| 14 |
<a href="#mail">Mail</a> |
| 15 |
<a href="#variables">Variables</a> |
| 16 |
<a href="#secrets">Secrets</a> |
| 17 |
<a href="#assistants">AI assistants</a> |
| 18 |
<a href="#audit">Audit log</a> |
| 19 |
<a href="/admin/runners">Runners ↗</a> |
| 20 |
<a href="/admin/runners/images">Images ↗</a> |
| 21 |
<a href="/admin/diffusion">Diffusion ↗</a> |
| 22 |
<a href="/admin/backups">Backups ↗</a> |
| 23 |
<a href="/admin/storage">Storage and maintenance ↗</a> |
| 24 |
</nav> |
| 25 |
<div class="sections"> |
| 26 |
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %} |
| 27 |
<section class="panel" id="accounts"> |
| 28 |
<h2>Accounts</h2> |
| 29 |
<div class="table"><table class="small"><tr><th>Username</th><th>Email</th><th>Flags</th><th>Last seen</th><th></th></tr> |
| 30 |
{% for a in accounts %} |
| 31 |
<tr><td><a href="/{{ a.username }}">{{ a.username }}</a></td><td>{{ a.email }}{% if !a.email.is_empty() %} <span class="small mute" title="{% if a.email_verified %}verified{% else %}not verified{% endif %}">{% if a.email_verified %}✔{% else %}○{% endif %}</span>{% endif %}</td><td>{% if a.is_admin %}<span class="tag">admin</span>{% endif %}{% if a.banned %} <span class="tag bad">banned</span>{% endif %}{% if a.disabled %} <span class="tag warn">disabled</span>{% endif %}{% if a.totp_enabled() %} <span class="tag">2fa</span>{% endif %}</td> |
| 32 |
<td>{% if a.last_seen_ms > 0 %}<time title="{{ a.last_seen_ms|when_i }}">{{ a.last_seen_ms|ago_i }}</time>{% else %}<span class="mute">never</span>{% endif %}</td> |
| 33 |
<td><form class="tiny" method="post" action="/admin/account"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="username" value="{{ a.username }}"> |
| 34 |
{% if a.banned %}<button name="action" value="unban">unban</button>{% else %}<button name="action" value="ban">ban</button>{% endif %} |
| 35 |
{% if a.is_admin %}<button name="action" value="demote">demote</button>{% else %}<button name="action" value="promote">make admin</button>{% endif %} |
| 36 |
{% if a.disabled %}<button name="action" value="enable">enable</button>{% else %}<button name="action" value="disable" title="Refuse sign-in, sessions, tokens and pushes until enabled again">disable</button>{% endif %} |
| 37 |
{% if !a.blocked() %}<button name="action" value="reset_link" title="A one-time password reset link to hand over">reset link</button>{% endif %}</form> |
| 38 |
<details class="tiny delete-account"><summary>delete</summary><form class="tiny" method="post" action="/admin/account"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="username" value="{{ a.username }}"> |
| 39 |
<label class="small">Deleting removes the account, its tokens, keys and memberships for good; what it wrote stays under its name, which stays retired. Type <strong>{{ a.username }}</strong> to confirm: |
| 40 |
<input name="confirm" autocomplete="off" required></label> <button class="danger" name="action" value="delete">delete {{ a.username }}</button></form></details></td></tr> |
| 41 |
{% endfor %} |
| 42 |
</table></div> |
| 43 |
</section> |
| 44 |
<section class="panel" id="registration"> |
| 45 |
<h2>Registration and invitations</h2> |
| 46 |
<form class="card" method="post" action="/admin/registration"> |
| 47 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 48 |
<label for="regmode">Who may register</label> |
| 49 |
<select id="regmode" name="mode">{% for m in ["open", "invite", "closed"] %}<option value="{{ m }}"{% if mode.as_str() == *m %} selected{% endif %}>{{ m }}</option>{% endfor %}</select> |
| 50 |
<button type="submit">SET</button> |
| 51 |
</form> |
| 52 |
<form class="card" method="post" action="/admin/invite"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit">NEW INVITATION</button></form> |
| 53 |
{% if !invitations.is_empty() %}<ul class="plain small">{% for (code, used, at) in invitations %}<li><code>{{ code }}</code> {% if used.is_some() %}<span class="mute">used</span>{% else %}<a href="/register?invite={{ code }}">link</a>{% endif %} · {{ at|ago_i }}</li>{% endfor %}</ul>{% endif %} |
| 54 |
</section> |
| 55 |
<section class="panel" id="organisations"> |
| 56 |
<h2>Organisations</h2> |
| 57 |
{% if orgs.is_empty() %}<p class="mute small">None yet.</p>{% else %}<ul class="plain">{% for o in orgs %}<li><img class="avatar xs" src="/avatar/{{ o.name }}" alt=""> <a href="/{{ o.name }}">{{ o.name }}</a> · <a class="small" href="/orgs/{{ o.name }}/settings">settings</a></li>{% endfor %}</ul>{% endif %} |
| 58 |
</section> |
| 59 |
<section class="panel" id="signin"> |
| 60 |
<h2>Sign in with</h2> |
| 61 |
<p class="small mute">A first sign-in links the account with the same verified email, or makes an account when registration is open. Secrets are kept sealed.</p> |
| 62 |
{% for p in providers %} |
| 63 |
<form class="card" method="post" action="/admin/signin"> |
| 64 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="provider" value="{{ p.id }}"> |
| 65 |
<h3>{{ p.name }} {% if p.enabled && p.has_secret && !p.client_id.is_empty() %}<span class="tag head">on</span>{% else %}<span class="tag dim">off</span>{% endif %}</h3> |
| 66 |
<label for="cid-{{ p.id }}">Client id</label><input type="text" id="cid-{{ p.id }}" name="client_id" value="{{ p.client_id }}" autocomplete="off"> |
| 67 |
<label for="sec-{{ p.id }}">Client secret</label><input type="password" id="sec-{{ p.id }}" name="secret" placeholder="{% if p.has_secret %}unchanged{% else %}none{% endif %}" autocomplete="new-password"> |
| 68 |
{% if let Some(d) = p.default_base %}<label for="base-{{ p.id }}">{{ p.name }} address</label><input type="url" id="base-{{ p.id }}" name="base" value="{{ p.base }}" placeholder="{{ d }}" autocomplete="off"> |
| 69 |
<p class="small mute">Empty for {{ d }}; or a self-managed {{ p.name }}, like https://gitlab.example.org. Register the hub there as an application with the scopes <code>openid profile email</code>.</p>{% endif %} |
| 70 |
<div class="choice"><label><input type="checkbox" name="enabled" value="1"{% if p.enabled %} checked{% endif %}> on</label></div> |
| 71 |
<p class="small mute">Callback URL to register with {{ p.name }}: <code>{{ p.callback }}</code></p> |
| 72 |
<button type="submit">SAVE</button> |
| 73 |
</form> |
| 74 |
{% endfor %} |
| 75 |
</section> |
| 76 |
<section class="panel" id="mail"> |
| 77 |
<h2>Mail</h2> |
| 78 |
<form class="card" method="post" action="/admin/mail"> |
| 79 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 80 |
<label for="smtp">SMTP URL</label> |
| 81 |
<input type="text" id="smtp" name="smtp" value="{{ mail_smtp }}" placeholder="smtps://user:[email protected]:465"> |
| 82 |
<label for="mailfrom">From</label> |
| 83 |
<input type="text" id="mailfrom" name="from" value="{{ mail_from }}" placeholder="OxygenHub <[email protected]>"> |
| 84 |
<button type="submit">SAVE</button> |
| 85 |
</form> |
| 86 |
<form class="card" method="post" action="/admin/mail/test"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit"{% if !mail_enabled || my_email.is_empty() %} disabled{% endif %}>SEND A TEST{% if !my_email.is_empty() %} TO {{ my_email }}{% endif %}</button></form> |
| 87 |
<p class="small mute">{% if mail_enabled %}Mail is on: verification and reset links go by email.{% else %}Mail is off: verification and reset links are issued by hand.{% endif %} An empty URL turns it off. The password stays on the server and is shown masked.</p> |
| 88 |
</section> |
| 89 |
{% call va::panel("/admin", variables, shell.csrf, "Every repository's jobs get these, unless an organisation or repository sets the same name.") %} |
| 90 |
{% call se::panel("/admin", secrets, shell.csrf, "Every repository's jobs may use these, unless an organisation or repository sets the same name.") %} |
| 91 |
{% call ai::panel("/admin", assistants, shell.csrf, "Every repository on the site may ask these, unless an organisation or repository turns one off.") %} |
| 92 |
<section class="panel" id="audit"> |
| 93 |
<h2>Audit log</h2> |
| 94 |
<div class="table"><table class="small"><tr><th>When</th><th>Who</th><th>Action</th><th>Target</th><th>Detail</th></tr> |
| 95 |
{% for r in audit %}<tr><td><time title="{{ r.at_ms|when_i }}">{{ r.at_ms|ago_i }}</time></td><td>{{ r.actor }}</td><td><code>{{ r.action }}</code></td><td>{{ r.target }}</td><td class="mute">{{ r.detail }}</td></tr>{% endfor %} |
| 96 |
</table></div> |
| 97 |
<p class="pager small"><a href="/admin#audit">newest</a>{% if let Some(o) = audit_older %} <a class="go small" href="/admin?audit_before={{ o }}#audit">older</a>{% endif %}</p> |
| 98 |
</section> |
| 99 |
</div> |
| 100 |
</div> |
| 101 |
{% endblock %} |