diff options
| author | Lena <lena@omega> | 2026-08-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-08-01 00:00:00 +0000 |
| commit | bb6bc2387d58c737dbf92e73f8544bf39319dbd8 (patch) | |
| tree | 6d5eaf5dac213074cd832da50f64260fce728fed /templates/admin.html | |
| download | git-forge-bb6bc2387d58c737dbf92e73f8544bf39319dbd8.tar.gz | |
A small self-hosted git forge: HTTP push and pull, cgit browsing,
local and OIDC accounts, TOTP, scoped push tokens, per-repo
permissions, issues, a wiki, and outbound mail.
It does not reimplement git. git-http-backend and cgit run as CGI
behind its own listener, which authorises every request before
exec'ing either. One binary, one data directory, one listener: no
database, no JavaScript, and state is JSON in a git repo with a
working tree.
Diffstat (limited to 'templates/admin.html')
| -rw-r--r-- | templates/admin.html | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..871b061 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,236 @@ +{{define "title"}}Admin - {{.Site}}{{end}} +{{define "main"}} +<h2>This instance</h2> +<form method="post" action="/admin/site" enctype="multipart/form-data"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="site_name">Name, shown in the header and in mail subjects</label> +<input id="site_name" name="site_name" value="{{.Data.Site.Name}}" required> +</p> +<p> +<label for="site_desc">Subtitle, under the name</label> +<input id="site_desc" name="site_desc" value="{{.Data.Site.Desc}}"> +</p> +<fieldset> +<legend>Logo</legend> +<p> +{{if .Data.Site.Logo.Name}} +Currently <code>{{.Data.Site.Logo.Name}}</code>. +{{else}} +Currently cgit's own. +{{end}} +</p> +<p> +<label for="logo_file">Upload one</label> +<input id="logo_file" name="logo_file" type="file" accept="image/png,image/jpeg,image/gif,image/webp,image/x-icon"> +<span class="dim"> +Drawn in the header at 96 pixels at most, and left at its own size below that. +</span> +</p> +<p> +<label class="inline" for="logo-cgit"> +<input id="logo-cgit" name="logo" type="checkbox" value="cgit"> +Use cgit's own logo instead +</label> +</p> +</fieldset> + +<fieldset> +<legend>Favicon</legend> +<p> +{{if .Data.Site.Icon.Name}} +Currently <code>{{.Data.Site.Icon.Name}}</code>. +{{else}} +Currently cgit's own. +{{end}} +</p> +<p> +<label for="icon_file">Upload one</label> +<input id="icon_file" name="icon_file" type="file" accept="image/png,image/jpeg,image/gif,image/webp,image/x-icon"> +<span class="dim"> +Served at <code>/favicon.ico</code>, whatever type it is. +</span> +</p> +<p> +<label class="inline" for="icon-cgit"> +<input id="icon-cgit" name="icon" type="checkbox" value="cgit"> +Use cgit's own favicon instead +</label> +</p> +</fieldset> +<p class="dim"> +Both take a png, jpeg, gif, webp, or an icon, up to 512 KiB, and are kept in +the config repository, so they are in the backup and in the history with +everything else. SVG is refused: it is XML that can carry script, and this is +served back to browsers. +</p> +<p><button type="submit">Save</button></p> +</form> +<p class="dim"> +Everything else in forge.json decides how the process starts, and is left to +the file: a listener or a base url typed into a browser is a way to lock +everybody out. +</p> + +<h2>Repositories</h2> +<table summary="repositories" class="list nowrap"> +<tr class="nohover"> +<th class="left">Name</th><th class="left">Visibility</th> +<th class="left">Issues</th><th class="left">Wiki</th><th class="left"></th> +</tr> +{{range .Data.Repos}} +<tr> +<td><a href="/{{.Path}}/settings">{{.Path}}</a></td> +<td>{{.Repo.Visibility}}</td> +<td>{{.Repo.Issues}}</td> +<td>{{.Repo.Wiki}}</td> +<td> +{{if not .Registered}} +<form method="post" action="/admin/repo/adopt"> +<input type="hidden" name="csrf" value="{{$.CSRF}}"> +<input type="hidden" name="path" value="{{.Path}}"> +<button type="submit">adopt</button> +</form> +{{end}} +</td> +</tr> +{{else}} +<tr><td colspan="5" class="dim">None yet.</td></tr> +{{end}} +</table> +<p class="dim"> +A repository with an adopt button has a directory but no entry in repos.json. +It is served private with issues and wiki off until it is adopted or deleted. +</p> + +<h3>Create a repository</h3> +<form method="post" action="/admin/repo/create"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="section">Section, optional</label> +<input id="section" name="section" autocapitalize="none"> +</p> +<p> +<label for="name">Name</label> +<input id="name" name="name" autocapitalize="none" required> +</p> +<p> +<label for="desc">Description</label> +<input id="desc" name="desc"> +</p> +{{template "knobs" .Data.New}} +<p><button type="submit">Create</button></p> +</form> + +<h2>Accounts</h2> +{{with .Data.Link}} +<div class="notice"> +One time link for {{$.Data.For}}, valid for two days. It both sets the password +and confirms the address, and it dies the moment it is used. +</div> +<pre>{{.}}</pre> +{{end}} + +<table summary="accounts" class="list nowrap"> +<tr class="nohover"> +<th class="left">Account</th><th class="left">Origin</th><th class="left">Email</th> +<th class="left">Admin</th><th class="left">Second factor</th> +</tr> +{{range $name, $u := .Data.Users}} +<tr> +<td>{{$name}}</td> +<td>{{$u.Origin}}</td> +<td>{{with $u.Email}}{{.}}{{if not $u.EmailConfirmed}} <span class="dim">unconfirmed</span>{{end}}{{end}}</td> +<td>{{if $u.Admin}}yes{{end}}</td> +<td>{{if $u.TOTP}}yes{{else}}<span class="dim">none</span>{{end}}</td> +</tr> +{{end}} +</table> + +<h3>Create an account</h3> +<p> +Creating an account issues a one time set-password link rather than a password, +so an admin never handles one. That link is a way in that outlives this session, +so this asks for your own authenticator code. +</p> +<form method="post" action="/admin/user/create"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="account">Account name</label> +<input id="account" name="name" autocapitalize="none" required> +</p> +<p> +<label for="email">Email address, optional</label> +<input id="email" name="email" type="email"> +</p> +<p> +<label for="create-code">Your authenticator code</label> +<input id="create-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +<p class="dim"> +The admin flag is not set here. Handing it out is its own act, below. +</p> +<p><button type="submit">Create</button></p> +</form> + +<div class="danger"> +<h3>Changes that hand out power, or cannot be undone</h3> +<p> +Each of these asks for your own authenticator code as well, so that a session +somebody else is sitting in front of cannot use them. +</p> + +<h4>Admin flag</h4> +<form method="post" action="/admin/user/admin"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="flag-name">Account</label> +<input id="flag-name" name="name" autocapitalize="none" required> +</p> +<p> +<label for="flag-admin"> +<input id="flag-admin" name="admin" type="checkbox" value="yes"> +Hold the global admin flag. Leave this unticked to take it away. +</label> +</p> +<p> +<label for="flag-code">Your authenticator code</label> +<input id="flag-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +<p><button type="submit">Set the flag</button></p> +</form> + +<h4>Clear an authenticator</h4> +<p> +The way back in for somebody who has lost both their authenticator and their +recovery codes. It signs every session for that account out. +</p> +<form method="post" action="/admin/user/totp"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="totp-name">Account</label> +<input id="totp-name" name="name" autocapitalize="none" required> +</p> +<p> +<label for="totp-code">Your authenticator code</label> +<input id="totp-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +<p><button type="submit">Clear it</button></p> +</form> + +<h4>Delete an account</h4> +<p>This also drops every grant that pointed at it.</p> +<form method="post" action="/admin/user/delete"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="delete-name">Account name</label> +<input id="delete-name" name="name" autocapitalize="none" required> +</p> +<p> +<label for="delete-code">Your authenticator code</label> +<input id="delete-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +<p><button type="submit">Delete</button></p> +</form> +</div> +{{end}} |