aboutsummaryrefslogtreecommitdiff
path: root/templates/admin.html
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-01 00:00:00 +0000
committerLena <lena@omega>2026-08-01 00:00:00 +0000
commitbb6bc2387d58c737dbf92e73f8544bf39319dbd8 (patch)
tree6d5eaf5dac213074cd832da50f64260fce728fed /templates/admin.html
downloadgit-forge-bb6bc2387d58c737dbf92e73f8544bf39319dbd8.tar.gz
Enter git-forgeHEADmaster
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.html236
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}}