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 | |
| download | git-forge-master.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')
| -rw-r--r-- | templates/admin.html | 236 | ||||
| -rw-r--r-- | templates/base.html | 23 | ||||
| -rw-r--r-- | templates/error.html | 5 | ||||
| -rw-r--r-- | templates/home.html | 16 | ||||
| -rw-r--r-- | templates/issue-new.html | 27 | ||||
| -rw-r--r-- | templates/issue.html | 62 | ||||
| -rw-r--r-- | templates/issues.html | 34 | ||||
| -rw-r--r-- | templates/login.html | 33 | ||||
| -rw-r--r-- | templates/nav.html | 112 | ||||
| -rw-r--r-- | templates/repo-settings.html | 122 | ||||
| -rw-r--r-- | templates/reset.html | 23 | ||||
| -rw-r--r-- | templates/set-password.html | 20 | ||||
| -rw-r--r-- | templates/settings.html | 126 | ||||
| -rw-r--r-- | templates/setup.html | 33 | ||||
| -rw-r--r-- | templates/totp-enroll.html | 96 | ||||
| -rw-r--r-- | templates/totp.html | 16 | ||||
| -rw-r--r-- | templates/wiki-edit.html | 27 | ||||
| -rw-r--r-- | templates/wiki.html | 37 |
18 files changed, 1048 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}} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..0f1751f --- /dev/null +++ b/templates/base.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{block "title" .}}{{.Site}}{{end}}</title> +<link rel="stylesheet" href="/cgit.css"> +<link rel="shortcut icon" href="/favicon.ico"> +</head> +<body> +<div id="cgit"> +{{template "header" .}} +{{template "tabrow" .}} +<div class="content"> +{{if .Error}}<div class="error">{{.Error}}</div>{{end}} +{{if .Notice}}<div class="notice">{{.Notice}}</div>{{end}} +{{block "main" .}}{{end}} +</div> +{{/* cgit's footer names cgit, so this one names the forge. */}} +<div class="footer">generated by {{.Site}} at {{.Now}}</div> +</div> +</body> +</html> diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..9c39077 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,5 @@ +{{define "title"}}{{.Data.Status}} {{.Data.Text}} - {{.Site}}{{end}} +{{define "main"}} +<h2>{{.Data.Status}} {{.Data.Text}}</h2> +<p><a href="/">Back to the index</a></p> +{{end}} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..a7460f8 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,16 @@ +{{define "title"}}{{.Site}}{{end}} +{{define "main"}} +<table summary="repository list" class="list nowrap"> +<tr class="nohover"><th class="left">Name</th><th class="left">Description</th></tr> +{{range .Data.Repos}} +<tr> +<td><a href="/{{.Path}}/">{{.Path}}</a>{{if eq .Repo.Visibility "private"}} <span class="dim">private</span>{{end}}</td> +<td>{{.Repo.Desc}}</td> +</tr> +{{else}} +<tr><td colspan="2" class="dim"> +{{if $.User}}Nothing you can read yet.{{else}}Nothing public here. <a href="/login">Sign in</a> to see the rest.{{end}} +</td></tr> +{{end}} +</table> +{{end}} diff --git a/templates/issue-new.html b/templates/issue-new.html new file mode 100644 index 0000000..ed102aa --- /dev/null +++ b/templates/issue-new.html @@ -0,0 +1,27 @@ +{{define "title"}}New issue, {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +<h2>Open an issue</h2> +<form method="post" action="/{{.Data.Path}}/issues"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="title">Title</label> +<input id="title" name="title" required maxlength="200"> +</p> +<p> +<label for="body">Description. Markdown, and raw HTML is not rendered.</label> +<textarea id="body" name="body" rows="12"></textarea> +</p> +{{with .Data.Labels}} +<fieldset> +<legend>Labels</legend> +{{range .}} +<label class="inline" for="label-{{.}}"> +<input id="label-{{.}}" type="checkbox" name="label" value="{{.}}"> +{{.}} +</label> +{{end}} +</fieldset> +{{end}} +<p><button type="submit">Open</button></p> +</form> +{{end}} diff --git a/templates/issue.html b/templates/issue.html new file mode 100644 index 0000000..e5135a0 --- /dev/null +++ b/templates/issue.html @@ -0,0 +1,62 @@ +{{define "title"}}{{.Data.Issue.Title}} - {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +{{$issue := .Data.Issue}} +<h2>{{$issue.Title}} <span class="dim">#{{$issue.ID}}</span></h2> +<p> +<span class="state-{{$issue.State}}">{{$issue.State}}</span> +<span class="dim">opened by</span> {{template "who" $issue.Author}} +<span class="dim">on {{$issue.Created.Format "2006-01-02 15:04"}} UTC</span> +{{range $issue.Labels}}<span class="label">{{.}}</span>{{end}} +</p> + +{{if .Data.Triage}} +<div class="bar"> +<form method="post" action="/{{.Data.Path}}/issues/{{$issue.ID}}/state"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<button type="submit">{{if eq $issue.State "open"}}close this issue{{else}}reopen this issue{{end}}</button> +</form> +{{with .Data.Labels}} +<form method="post" action="/{{$.Data.Path}}/issues/{{$issue.ID}}/labels"> +<input type="hidden" name="csrf" value="{{$.CSRF}}"> +{{range .}} +<label class="inline" for="label-{{.Name}}"> +<input id="label-{{.Name}}" type="checkbox" name="label" value="{{.Name}}" {{if .On}}checked{{end}}> +{{.Name}} +</label> +{{end}} +<button type="submit">set labels</button> +</form> +{{end}} +</div> +{{end}} + +<div class="comment"> +<div class="byline">{{template "who" $issue.Author}} <span class="dim">opened this</span></div> +<article>{{.Data.Rendered}}</article> +</div> + +{{range .Data.Comments}} +<div class="comment"> +<div class="byline"> +{{template "who" .Comment.Author}} +<span class="dim">commented on {{.Comment.Created.Format "2006-01-02 15:04"}} UTC</span> +</div> +<article>{{.Rendered}}</article> +</div> +{{end}} + +{{if .Data.Can}} +<h3>Comment</h3> +<form method="post" action="/{{.Data.Path}}/issues/{{$issue.ID}}/comment"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="body"> +Markdown: **bold**, `code`, a list, or four spaces for a code block. Raw HTML +is not rendered. +</label> +<textarea id="body" name="body" rows="8" required></textarea> +</p> +<p><button type="submit">Comment</button></p> +</form> +{{end}} +{{end}} diff --git a/templates/issues.html b/templates/issues.html new file mode 100644 index 0000000..e0907ce --- /dev/null +++ b/templates/issues.html @@ -0,0 +1,34 @@ +{{define "title"}}Issues, {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +<div class="bar"> +<form method="get" action="/{{.Data.Path}}/issues"> +<input id="q" name="q" value="{{.Data.Query}}" placeholder="search the tracker" size="30"> +<input type="hidden" name="state" value="{{.Data.State}}"> +<button type="submit">search</button> +</form> +<span> +{{if eq .Data.State "open"}}<strong>open</strong>{{else}}<a href="/{{.Data.Path}}/issues?state=open&q={{.Data.Query}}">open</a>{{end}} +{{if eq .Data.State "closed"}}<strong>closed</strong>{{else}}<a href="/{{.Data.Path}}/issues?state=closed&q={{.Data.Query}}">closed</a>{{end}} +<a href="/{{.Data.Path}}.issues/">history</a> +{{if .Data.Can}}<a href="/{{.Data.Path}}/issues/new">open an issue</a>{{end}} +</span> +</div> + +<table summary="issues" class="list nowrap"> +<tr class="nohover"> +<th class="left">#</th><th class="left">Title</th> +<th class="left">Labels</th><th class="left">Author</th><th class="left">Updated</th> +</tr> +{{range .Data.Issues}} +<tr> +<td>{{.ID}}</td> +<td><a href="/{{$.Data.Path}}/issues/{{.ID}}">{{.Title}}</a></td> +<td class="dim">{{range .Labels}}<span class="label">{{.}}</span> {{end}}</td> +<td>{{template "who" .Author}}</td> +<td class="dim">{{.Updated.Format "2006-01-02"}}</td> +</tr> +{{else}} +<tr><td colspan="5" class="dim">Nothing here.</td></tr> +{{end}} +</table> +{{end}} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..eb9dcd9 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,33 @@ +{{define "title"}}Sign in - {{.Site}}{{end}} +{{define "main"}} +<h2>Sign in</h2> +<form method="post" action="/login"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="name">Account</label> +<input id="name" name="name" autocomplete="username" autocapitalize="none" required> +</p> +<p> +<label for="password">Password</label> +<input id="password" name="password" type="password" autocomplete="current-password" required> +</p> +<p><button type="submit">Sign in</button></p> +</form> +{{if .MailOff}} +<p class="dim"> +Mail is turned off on this instance, so there is no password reset. An admin +can issue you a one time link instead. +</p> +{{else}} +<p><a href="/reset">Forgotten your password?</a></p> +{{end}} + +{{with .Data.Providers}} +<h3>Or sign in through</h3> +<ul> +{{range .}} +<li><a href="/oidc/{{.Key}}">{{.Label}}</a></li> +{{end}} +</ul> +{{end}} +{{end}} diff --git a/templates/nav.html b/templates/nav.html new file mode 100644 index 0000000..82f0714 --- /dev/null +++ b/templates/nav.html @@ -0,0 +1,112 @@ +{{/* + cgit's own header markup, rendered in place of the one noheader=1 turns off, + so that both kinds of page carry the same header and it can name the account. +*/}} +{{define "header"}} +<table id="header"> +<tr> +<td class="logo" rowspan="2"><a href="/"><img src="{{.Logo}}" alt="home"></a></td> +<td class="main"> +{{if .Repo}}<a href="/">{{.Site}}</a> : <a href="/{{.Repo.Path}}/">{{.Repo.Path}}</a> +{{else}}<a href="/">{{.Site}}</a>{{end}} +</td> +<td class="form"> +{{if .User}} +{{if or .Admin .MustEnrol}}<a href="/admin">admin</a>{{end}} +<a href="/settings">{{.User}}</a> +<form method="post" action="/logout"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<button type="submit" class="link">sign out</button> +</form> +{{else}} +<a href="/login">sign in</a> +{{end}} +</td> +</tr> +<tr><td class="sub">{{if .Repo}}{{.Repo.Desc}}{{else}}{{.Desc}}{{end}}</td></tr> +</table> +{{end}} + +{{/* The forge's sections, in cgit's tab markup. */}} +{{define "tabs"}}{{with .Repo}}{{if .Issues}}<a href="/{{.Path}}/issues"{{if eq $.Repo.Active "issues"}} class="active"{{end}}>issues</a>{{end}}{{if .Wiki}}<a href="/{{.Path}}/wiki"{{if eq $.Repo.Active "wiki"}} class="active"{{end}}>wiki</a>{{end}}{{if .Settings}}<a href="/{{.Path}}/settings"{{if eq $.Repo.Active "settings"}} class="active"{{end}}>settings</a>{{end}}{{end}}{{end}} + +{{/* + cgit's row copied, for the forge's own pages where cgit renders nothing: the + same tabs in the same order, and the same search cell. +*/}} +{{define "tabrow"}}{{with .Repo}} +<table class="tabs"><tr><td> +<a href="/{{.Path}}/about/">about</a><a href="/{{.Path}}/">summary</a><a href="/{{.Path}}/refs/">refs</a><a href="/{{.Path}}/log/">log</a><a href="/{{.Path}}/tree/">tree</a><a href="/{{.Path}}/commit/">commit</a><a href="/{{.Path}}/diff/">diff</a>{{template "tabs" $}}</td> +<td class="form"><form class="right" method="get" action="/{{.Path}}/log/"> +<select name="qt"> +<option value="grep">log msg</option> +<option value="author">author</option> +<option value="committer">committer</option> +<option value="range">range</option> +</select> +<input class="txt" type="search" size="10" name="q" value=""> +<input type="submit" value="search"> +</form> +</td></tr></table> +{{end}}{{end}} + +{{/* + In the layout because every page is parsed as its own set, and a partial + defined in one page is invisible to the next. +*/}} +{{define "who"}}{{if .}}<img class="avatar" src="/avatar/{{.}}" alt=""> {{.}}{{else}}<span class="dim">nobody</span>{{end}}{{end}} + +{{/* + One fragment for both the create form and the settings form, so that the two + cannot drift apart on what members and users mean. +*/}} +{{define "knobs"}} +<fieldset> +<legend>Who may read it</legend> +<label class="choice" for="v-private"> +<input id="v-private" type="radio" name="visibility" value="private" {{if eq .Visibility "private"}}checked{{end}}> +<strong>private</strong> <span class="dim">accounts holding a grant on it, and global admins</span> +</label> +<label class="choice" for="v-public"> +<input id="v-public" type="radio" name="visibility" value="public" {{if eq .Visibility "public"}}checked{{end}}> +<strong>public</strong> <span class="dim">anybody, including visitors who are not signed in</span> +</label> +</fieldset> + +<fieldset> +<legend>Who may file issues</legend> +<label class="choice" for="i-disabled"> +<input id="i-disabled" type="radio" name="issues" value="disabled" {{if eq .Issues "disabled"}}checked{{end}}> +<strong>disabled</strong> <span class="dim">nobody, and the tab is hidden</span> +</label> +<label class="choice" for="i-members"> +<input id="i-members" type="radio" name="issues" value="members" {{if eq .Issues "members"}}checked{{end}}> +<strong>members</strong> <span class="dim">anybody holding a grant here, even a read grant</span> +</label> +<label class="choice" for="i-users"> +<input id="i-users" type="radio" name="issues" value="users" {{if eq .Issues "users"}}checked{{end}}> +<strong>users</strong> <span class="dim">any signed in account. Public repositories only</span> +</label> +</fieldset> + +<fieldset> +<legend>Who may edit the wiki</legend> +<label class="choice" for="w-disabled"> +<input id="w-disabled" type="radio" name="wiki" value="disabled" {{if eq .Wiki "disabled"}}checked{{end}}> +<strong>disabled</strong> <span class="dim">nobody, and the tab is hidden</span> +</label> +<label class="choice" for="w-members"> +<input id="w-members" type="radio" name="wiki" value="members" {{if eq .Wiki "members"}}checked{{end}}> +<strong>members</strong> <span class="dim">anybody holding a grant here</span> +</label> +<label class="choice" for="w-users"> +<input id="w-users" type="radio" name="wiki" value="users" {{if eq .Wiki "users"}}checked{{end}}> +<strong>users</strong> <span class="dim">any signed in account. Public repositories only</span> +</label> +</fieldset> +<p class="dim"> +Reading is visibility's job alone. The other two say who may write, so a +repository can take bug reports and documentation fixes from people who will +never push code. users on a private repository is refused rather than coerced. +</p> +{{end}} diff --git a/templates/repo-settings.html b/templates/repo-settings.html new file mode 100644 index 0000000..d90f744 --- /dev/null +++ b/templates/repo-settings.html @@ -0,0 +1,122 @@ +{{define "title"}}{{.Data.Path}} settings - {{.Site}}{{end}} +{{define "main"}} +{{$repo := .Data.Repo}} +<h2>Settings</h2> + +{{if not .Data.Enrolled}} +<div class="error"> +This account has no authenticator, so it cannot change who may read or write +this repository, and cannot change grants. +<a href="/settings/totp">Enrol one</a> and come back. +</div> +{{end}} + +<form method="post" action="/{{.Data.Path}}/settings"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="desc">Description, shown in the header and in the index</label> +<input id="desc" name="desc" value="{{$repo.Desc}}"> +</p> +{{template "knobs" $repo}} +<p> +<label for="labels">Labels an issue may carry, one per line</label> +<textarea id="labels" name="labels" rows="5">{{.Data.Labels}}</textarea> +</p> +<p> +<label for="settings-code">Your authenticator code</label> +<input id="settings-code" name="code" inputmode="numeric" autocomplete="one-time-code"> +<span class="dim"> +Needed only to move one of the three above: who may read or write outlives this +session, the way a grant does. A description or a label does not. +</span> +</p> +<p><button type="submit">Save</button></p> +</form> + +<h3>Grants</h3> +<p class="dim"> +read clones and browses a private repository. write adds pushing to it. admin +adds this page. A global admin holds admin everywhere without appearing here. +A push token can be minted for any repository an account can reach, and carries +the access that account already has there and nothing more. +</p> +<p class="dim"> +Access handed out here outlives the session that hands it out, so changing a +grant asks for your own authenticator code as well. +</p> +<table summary="grants" class="list nowrap"> +<tr class="nohover"><th class="left">Account</th><th class="left">Level</th><th class="left"></th></tr> +{{range $account, $level := $repo.Grants}} +<tr> +<td>{{template "who" $account}}</td> +<td>{{$level}}</td> +<td> +{{if $.Data.Enrolled}} +<form method="post" action="/{{$.Data.Path}}/settings/grant"> +<input type="hidden" name="csrf" value="{{$.CSRF}}"> +<input type="hidden" name="account" value="{{$account}}"> +<input name="code" size="6" inputmode="numeric" autocomplete="one-time-code" aria-label="Your authenticator code" placeholder="code" required> +<button type="submit">revoke</button> +</form> +{{end}} +</td> +</tr> +{{else}} +<tr><td colspan="3" class="dim">Nobody, so only global admins can reach it.</td></tr> +{{end}} +</table> + +{{if .Data.Enrolled}} +<form method="post" action="/{{.Data.Path}}/settings/grant"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="account">Account</label> +<input id="account" name="account" autocapitalize="none" required> +</p> +<fieldset> +<legend>Level</legend> +<label class="choice" for="g-read"> +<input id="g-read" type="radio" name="level" value="read" checked> +<strong>read</strong> <span class="dim">clone and browse it</span> +</label> +<label class="choice" for="g-write"> +<input id="g-write" type="radio" name="level" value="write"> +<strong>write</strong> <span class="dim">and push to it</span> +</label> +<label class="choice" for="g-admin"> +<input id="g-admin" type="radio" name="level" value="admin"> +<strong>admin</strong> <span class="dim">and change this page</span> +</label> +</fieldset> +<p> +<label for="grant-code">Your authenticator code</label> +<input id="grant-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +<p><button type="submit">Grant</button></p> +</form> +{{end}} + +{{if .Data.Delete}} +<div class="danger"> +<h3>Delete this repository</h3> +<p> +This removes the repository, its issues, and its wiki together. There is no +undo and the history goes with it. Type <code>{{.Data.Path}}</code> to confirm, +and your own authenticator code, so that a session somebody else is sitting in +front of cannot do it. +</p> +<form method="post" action="/{{.Data.Path}}/settings/delete"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="confirm">Repository path</label> +<input id="confirm" name="confirm" autocapitalize="none" required> +</p> +<p> +<label for="code">Your authenticator code</label> +<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +<p><button type="submit">Delete it</button></p> +</form> +</div> +{{end}} +{{end}} diff --git a/templates/reset.html b/templates/reset.html new file mode 100644 index 0000000..9025f2e --- /dev/null +++ b/templates/reset.html @@ -0,0 +1,23 @@ +{{define "title"}}Forgotten password - {{.Site}}{{end}} +{{define "main"}} +<h2>Forgotten password</h2> +{{if .MailOff}} +<div class="error"> +Mail is turned off on this instance, so no link can be sent. Ask an admin for +a one time link. +</div> +{{end}} +<p> +This mails a one time link to the address on file for the account. The answer +below is the same whether or not the account exists, so that this page cannot +be used to find out which names are taken. +</p> +<form method="post" action="/reset"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="name">Account name</label> +<input id="name" name="name" autocomplete="username" autocapitalize="none" required> +</p> +<p><button type="submit">Send the link</button></p> +</form> +{{end}} diff --git a/templates/set-password.html b/templates/set-password.html new file mode 100644 index 0000000..6a38413 --- /dev/null +++ b/templates/set-password.html @@ -0,0 +1,20 @@ +{{define "title"}}Set a password - {{.Site}}{{end}} +{{define "main"}} +<h2>Set a password for {{.Data.Name}}</h2> +<p>This link works once, and setting the password is what spends it.</p> +<form method="post" action="/set-password"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<input type="hidden" name="name" value="{{.Data.Name}}"> +<input type="hidden" name="expires" value="{{.Data.Expires}}"> +<input type="hidden" name="t" value="{{.Data.Token}}"> +<p> +<label for="new">Password</label> +<input id="new" name="new" type="password" autocomplete="new-password" required> +</p> +<p> +<label for="again">Password again</label> +<input id="again" name="again" type="password" autocomplete="new-password" required> +</p> +<p><button type="submit">Set the password</button></p> +</form> +{{end}} diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..8b2f0f1 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,126 @@ +{{define "title"}}Settings - {{.Site}}{{end}} +{{define "main"}} +<h2>Account</h2> +<p> +Signed in as {{template "who" .User}}{{if .Admin}}, a global admin{{end}}. +</p> + +<h3>Email and notifications</h3> +{{if .MailOff}} +<p class="dim"> +Mail is turned off on this instance, so nothing is sent. The address below is +still what commits made through the web are attributed to. +</p> +{{else}} +<p class="dim"> +An address receives nothing until a link sent to it has been followed. Notices +about your own account ignore the toggle below, because they are security facts +rather than activity. +</p> +{{end}} +<form method="post" action="/settings/email"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="email">Email address</label> +<input id="email" name="email" type="email" value="{{.Data.Email}}"> +{{if .Data.Email}}{{if not .Data.EmailConfirmed}}<span class="dim">unconfirmed</span>{{end}}{{end}} +</p> +<p> +<label for="notify"> +<input id="notify" name="notify" type="checkbox" value="yes" {{if .Data.Notify}}checked{{end}}> +Send me issue and repository notifications +</label> +</p> +<p><button type="submit">Save</button></p> +</form> + +{{if .Data.Local}} +<h3>Change password</h3> +<form method="post" action="/settings/password"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="current">Current password</label> +<input id="current" name="current" type="password" autocomplete="current-password" required> +</p> +<p> +<label for="new">New password</label> +<input id="new" name="new" type="password" autocomplete="new-password" required> +</p> +<p> +<label for="again">New password again</label> +<input id="again" name="again" type="password" autocomplete="new-password" required> +</p> +<p><button type="submit">Change password</button></p> +</form> +{{else}} +<h3>Password</h3> +<p>This account signs in through an external provider, so there is no password here.</p> +{{end}} + +<h3>Push tokens</h3> +<p class="dim"> +Git over HTTP takes a token, never your password. A token is scoped to one +repository and carries the access you already have there: clone if you can read +it, push if you can write it, the wiki if you may edit it. It carries no second +factor, and it can be revoked on its own without touching anything else. +</p> +<pre>git clone {{.Base}}/section/repo.git +Username: {{.User}} +Password: the token</pre> +{{with .Data.Secret}} +<div class="notice">Token for {{$.User}}, shown once:</div> +<pre id="token">{{.}}</pre> +{{end}} +<table summary="push tokens" class="list nowrap"> +<tr class="nohover"> +<th class="left">Id</th><th class="left">Repository</th> +<th class="left">Made</th><th class="left">Last used</th><th class="left"></th> +</tr> +{{range .Data.Tokens}} +<tr> +<td>{{.ID}}</td> +<td><a href="/{{.Repo}}/">{{.Repo}}</a></td> +<td class="dim">{{if .Created}}{{.Created}}{{else}}unknown{{end}}</td> +<td class="dim">{{if .Used}}{{.Used}}{{else}}never{{end}}</td> +<td> +<form method="post" action="/settings/tokens/revoke"> +<input type="hidden" name="csrf" value="{{$.CSRF}}"> +<input type="hidden" name="id" value="{{.ID}}"> +<button type="submit">revoke</button> +</form> +</td> +</tr> +{{else}} +<tr><td colspan="5" class="dim">None. Mint one below to push over HTTP.</td></tr> +{{end}} +</table> +<p class="dim"> +Dates are days, not times: a write on every request git makes would be a write +on every request git makes. A token that has never been used, or was last used +a long time ago, is one worth revoking. +</p> +<form method="post" action="/settings/tokens"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="repo">Repository you can reach</label> +<input id="repo" name="repo" placeholder="section/repo" autocapitalize="none" required> +</p> +<p><button type="submit">Mint a token</button></p> +</form> + +<h3>Authenticator</h3> +<p> +<a href="/settings/totp">Second factor</a>. An account with the global admin +flag has to have one before it can administer anything. +</p> + +<h3>Sessions</h3> +<p> +Signing out everywhere invalidates every session cookie ever issued for this +account, including this browser's. +</p> +<form method="post" action="/settings/sessions"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p><button type="submit">Sign out everywhere</button></p> +</form> +{{end}} diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 0000000..9089bd3 --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,33 @@ +{{define "title"}}Set up - {{.Site}}{{end}} +{{define "main"}} +<h2>Set up {{.Site}}</h2> +<p> +This creates the first account and gives it the global admin flag. The setup +token was printed to the log when git-forge started, and this page is gone once +an account exists. +</p> +<form method="post" action="/setup"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<p> +<label for="token">Setup token</label> +<input id="token" name="token" required> +</p> +<p> +<label for="name">Account name</label> +<input id="name" name="name" autocomplete="username" autocapitalize="none" required> +</p> +<p> +<label for="email">Email address, optional</label> +<input id="email" name="email" type="email" autocomplete="email"> +</p> +<p> +<label for="password">Password</label> +<input id="password" name="password" type="password" autocomplete="new-password" required> +</p> +<p> +<label for="again">Password again</label> +<input id="again" name="again" type="password" autocomplete="new-password" required> +</p> +<p><button type="submit">Create the first admin</button></p> +</form> +{{end}} diff --git a/templates/totp-enroll.html b/templates/totp-enroll.html new file mode 100644 index 0000000..da3d301 --- /dev/null +++ b/templates/totp-enroll.html @@ -0,0 +1,96 @@ +{{define "title"}}Authenticator - {{.Site}}{{end}} +{{define "main"}} +<h2>Authenticator</h2> + +{{with .Data.Codes}} +<h3>Recovery codes</h3> +<p>Each of these works once, in place of a code. They are not shown again.</p> +<pre>{{range .}}{{.}} +{{end}}</pre> +{{end}} + +{{if .Data.Enrolled}} +<p> +This account has an authenticator, and {{.Data.Left}} recovery codes left. +</p> + +<h3>New recovery codes</h3> +<p>Making new ones stops the old ones working.</p> +<form method="post" action="/settings/totp/codes"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +{{if .Data.Local}} +<p> +<label for="codes-current">Current password</label> +<input id="codes-current" name="current" type="password" autocomplete="current-password" required> +</p> +{{else}} +<p> +<label for="codes-code">Code from the authenticator</label> +<input id="codes-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +{{end}} +<p><button type="submit">Make new recovery codes</button></p> +</form> + +<h3>Remove</h3> +{{if .Data.Required}} +<p> +This account holds the global admin flag, so it must keep an authenticator. +Another admin has to take the flag away first. +</p> +{{else}} +<p>Removing it signs out every session for this account.</p> +<form method="post" action="/settings/totp/remove"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +{{if .Data.Local}} +<p> +<label for="current">Current password</label> +<input id="current" name="current" type="password" autocomplete="current-password" required> +</p> +{{else}} +<p> +<label for="remove-code">Code from the authenticator</label> +<input id="remove-code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +{{end}} +<p><button type="submit">Remove the authenticator</button></p> +</form> +{{end}} + +{{else}} +<p> +Type this secret into an authenticator, or give it the URI. There is no QR +code: it would be useless in a terminal browser. +</p> +<pre>{{.Data.Secret}} + +{{.Data.URI}}</pre> +<p> +Then prove it took, which is what enrols it. Enrolling signs out every other +session. +{{if .Data.Local}} +It takes the password as well: a stolen cookie must not be able to put a second +factor on this account and lock its owner out. +{{else}} +This account signs in through an external provider and has no password here, so +enrolling rests on this session alone. The address on file is told, and an admin +can clear an authenticator you did not enrol. +{{end}} +</p> +<form method="post" action="/settings/totp"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<input type="hidden" name="secret" value="{{.Data.Secret}}"> +<p> +<label for="code">Code from the authenticator</label> +<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" required> +</p> +{{if .Data.Local}} +<p> +<label for="enrol-current">Current password</label> +<input id="enrol-current" name="current" type="password" autocomplete="current-password" required> +</p> +{{end}} +<p><button type="submit">Enroll</button></p> +</form> +{{end}} +{{end}} diff --git a/templates/totp.html b/templates/totp.html new file mode 100644 index 0000000..329d6cc --- /dev/null +++ b/templates/totp.html @@ -0,0 +1,16 @@ +{{define "title"}}Authenticator code - {{.Site}}{{end}} +{{define "main"}} +<h2>Authenticator code</h2> +<p>The password was right. Now the six digit code, or one recovery code.</p> +<form method="post" action="/login/totp"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<input type="hidden" name="name" value="{{.Data.Name}}"> +<input type="hidden" name="expires" value="{{.Data.Expires}}"> +<input type="hidden" name="t" value="{{.Data.Token}}"> +<p> +<label for="code">Code</label> +<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" autofocus required> +</p> +<p><button type="submit">Sign in</button></p> +</form> +{{end}} diff --git a/templates/wiki-edit.html b/templates/wiki-edit.html new file mode 100644 index 0000000..ed896d2 --- /dev/null +++ b/templates/wiki-edit.html @@ -0,0 +1,27 @@ +{{define "title"}}Edit {{.Data.Page}} - {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +<h2>Edit wiki{{with .Data.Page}} <span class="dim">{{.}}</span>{{end}}</h2> + +{{with .Data.Preview}} +<h3>Preview</h3> +<article>{{.}}</article> +<hr> +{{end}} + +<form method="post" action="/{{.Data.Path}}/wiki/{{.Data.Page}}"> +<input type="hidden" name="csrf" value="{{.CSRF}}"> +<input type="hidden" name="base" value="{{.Data.Base}}"> +<p> +<label for="body">{{.Data.File}}, markdown</label> +<textarea id="body" name="body" rows="24">{{.Data.Body}}</textarea> +</p> +<p> +<button type="submit" name="action" value="save">Save</button> +<button type="submit" name="action" value="preview">Preview</button> +</p> +</form> +<p class="dim"> +Links between pages are plain relative markdown links, and a subdirectory's +index.md is served at the subdirectory itself. +</p> +{{end}} diff --git a/templates/wiki.html b/templates/wiki.html new file mode 100644 index 0000000..36305c6 --- /dev/null +++ b/templates/wiki.html @@ -0,0 +1,37 @@ +{{define "title"}}Wiki {{.Data.Page}} - {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +<h2>Wiki{{with .Data.Page}} <span class="dim">{{.}}</span>{{end}}</h2> + +{{with .Data.Pages}} +<p> +Pages: +{{range .}}<a href="/{{$.Data.Path}}/wiki/{{.}}">{{if .}}{{.}}{{else}}index{{end}}</a>  +{{end}} +</p> +{{end}} + +{{if .Data.Can}} +<form method="get" action="/{{.Data.Path}}/wiki/"> +<p> +<label for="page">New page, or an existing one to edit</label> +<input id="page" name="page" placeholder="setup, or runbooks/dns" autocapitalize="none" required> +<button type="submit">Write it</button> +</p> +</form> +{{end}} + +{{if .Data.Empty}} +<p>This wiki has no pages yet.</p> +{{if .Data.Can}}<p><a href="/{{.Data.Path}}/wiki?edit">Write the first one</a>.</p>{{end}} +{{else if .Data.Missing}} +<p>There is no page here yet.</p> +{{if .Data.Can}}<p><a href="/{{.Data.Path}}/wiki/{{.Data.Page}}?edit">Write it</a>.</p>{{end}} +{{else}} +<article>{{.Data.Rendered}}</article> +<p> +{{if .Data.Can}}<a href="/{{.Data.Path}}/wiki/{{.Data.Page}}?edit">Edit</a>{{end}} +<a href="/{{.Data.Path}}.wiki/log/{{.Data.File}}">History</a> +</p> +{{end}} + +{{end}} |