Files
Ilker ManapandClaude Opus 5.5 66f702d5c1 A forgotten password is set through a one-time link
/forgot mails a link to set a new password when the cluster's mail settings
say how - SMTP relay, sender, and the public base_url the link points at,
never the request's Host - and says the same whether or not the address has
an account. /reset sets it once, closing every session. Links are kept by
their hash in password_resets, good for an hour, a newer one replacing an
older. Without mail, /forgot says to ask an operator, and llmgw users
reset-link makes the link to hand over. Single sign-on accounts get none.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:20:11 +02:00

257 lines
13 KiB
HTML

{{define "signup"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "signup.title"}}</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap narrow">
<header><h1>{{t "signup.heading"}}</h1>
<p class="sub">{{t "signup.sub"}}</p></header>
{{if .Done}}
<p>{{t "signup.thanks"}}</p>
<p class="note">{{t "signup.no_email"}}</p>
<p>{{th "signup.sign_in_once_approved"}}</p>
{{/* The same page whether or not the address already had an account, so
it says what to do in either case without saying which this was. */}}
<p class="note">{{th "signup.already_had"}}</p>
{{else}}
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<form method="post" autocomplete="on">
<label>{{t "signup.email"}}<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
<label>{{t "signup.name"}}<input type="text" name="name" value="{{.Name}}" autocomplete="name"></label>
<label>{{t "signup.password"}}<input type="password" name="password" required minlength="{{.MinPassword}}"
autocomplete="new-password"></label>
<p class="note">{{t "signup.password_rule" .MinPassword}}</p>
<button type="submit">{{t "signup.submit"}}</button>
</form>
<p class="note">{{th "signup.have_account"}}</p>
{{end}}
<footer><span>llmgw {{.Version}}</span><span><a href="/">{{t "common.about_gateway"}}</a></span>{{template "langs"}}</footer>
</div></body></html>{{end}}
{{define "login"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "login.title"}}</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap narrow">
<header><h1>{{t "login.heading"}}</h1></header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<form method="post" autocomplete="on">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>{{t "login.email"}}<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
<label>{{t "login.password"}}<input type="password" name="password" required autocomplete="current-password"></label>
<button type="submit">{{t "login.submit"}}</button>
</form>
{{if .SSO}}<p><a class="btn" href="/login/oidc">{{t "login.with_sso" .SSO}}</a></p>{{end}}
{{if .Forgot}}<p class="note"><a href="/forgot">{{t "login.forgot"}}</a></p>{{end}}
{{if .SignupOpen}}<p class="note">{{th "login.no_account"}}</p>{{end}}
<footer><span>llmgw {{.Version}}</span><span><a href="/">{{t "common.about_gateway"}}</a></span>{{template "langs"}}</footer>
</div></body></html>{{end}}
{{define "profile"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "profile.title"}}</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap">
<header>
<h1>{{t "profile.heading"}}</h1>
<p class="sub">{{.User.Email}}{{if ne .User.Name .User.Email}} ({{.User.Name}}){{end}}</p>
{{template "nav" .Nav}}
</header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .NewKey}}
<h2>{{t "profile.new_key"}}</h2>
<p>{{t "profile.shown_once"}}</p>
<pre>{{.NewKey}}</pre>
<p>{{t "profile.store_it"}}</p>
<pre>curl {{.BaseURL}}/v1/chat/completions \
-H "Authorization: Bearer {{.NewKey}}" \
-H "Content-Type: application/json" \
-d '{"model": "{{.ExampleModel}}", "messages": [{"role":"user","content":"hello"}]}'</pre>
{{end}}
{{with .Connect}}
<section class="connect-result" id="connected">
<h2>{{t "connect.ready" .ToolName}}</h2>
<p>{{th "connect.shown_once"}}</p>
{{if .Command}}
<h3>{{t "connect.one_command"}}</h3>
<p class="note">{{if eq .Tool "codex"}}{{t "connect.command_note_codex"}}{{else}}{{t "connect.command_note"}}{{end}}</p>
<div class="copyable"><pre>{{.Command}}</pre><button type="button" class="link copy">{{t "common.copy"}}</button></div>
{{end}}
{{range .Files}}
<h3>{{th "connect.by_hand" .Path}}</h3>
<div class="copyable"><pre>{{.Body}}</pre><button type="button" class="link copy">{{t "common.copy"}}</button>
<button type="button" class="link download-file" data-name="{{.Name}}">{{t "common.download"}}</button></div>
{{end}}
{{if .Fields}}
<h3>{{t "connect.fields"}}</h3>
<table class="rows compact narrow-table connect-fields">{{range .Fields}}
<tr><th>{{.Label}}</th><td><code>{{.Value}}</code></td><td><button type="button" class="link copy" data-value="{{.Value}}">{{t "common.copy"}}</button></td></tr>{{end}}
</table>
{{end}}
<h3>{{t "connect.then"}}</h3>
<p>{{if eq .Tool "codex"}}<code>{{.Run}}</code>{{else}}{{.Run}}{{end}}</p>
{{range .Notes}}<p class="note">{{.}}</p>{{end}}
</section>
{{end}}
<h2>{{t "connect.heading"}}</h2>
<p class="note">{{t "connect.intro"}}</p>
<p class="note">{{th "connect.own_code"}}</p>
<form method="post" action="/profile/connect#connected" class="connect-form">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<div class="field-row">
<label>{{t "connect.tool"}}
<select name="tool">{{range .Tools}}<option value="{{.Key}}">{{.Name}}</option>{{end}}</select></label>
<label>{{t "connect.model"}}
{{if .Models}}<select name="model">{{range .Models}}<option value="{{.}}">{{.}}</option>{{end}}</select>
{{else}}<input type="text" name="model" placeholder="{{t "connect.model_placeholder"}}" required>{{end}}</label>
{{if .Contexts}}<label>{{t "connect.spends_against"}}
<select name="context">{{range .Contexts}}<option value="{{.Value}}">{{.Label}}</option>{{end}}</select></label>{{end}}
</div>
<button type="submit">{{t "connect.submit"}}</button>
</form>
<script>
// Copy and download for what the connect panel shows, done in the page: the
// key in it must not go back to the server to become a file.
const copiedWord = {{t "common.copied"}}, copyPrompt = {{t "profile.copy_prompt"}};
document.addEventListener("click", (e) => {
const b = e.target.closest("button.copy, button.download-file");
if (!b) return;
const text = b.dataset.value || b.parentElement.querySelector("pre").textContent;
if (b.classList.contains("copy")) {
const done = () => { const was = b.textContent; b.textContent = copiedWord; setTimeout(() => (b.textContent = was), 1500); };
if (navigator.clipboard) navigator.clipboard.writeText(text).then(done, () => window.prompt(copyPrompt, text));
else window.prompt(copyPrompt, text);
return;
}
const url = URL.createObjectURL(new Blob([text], { type: "text/plain" }));
const a = document.createElement("a");
a.href = url; a.download = b.dataset.name || "config";
a.click();
URL.revokeObjectURL(url);
});
</script>
<h2>{{t "profile.create_key"}}</h2>
<form method="post" action="/profile/keys" class="narrow">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>{{t "profile.key_for"}}<input type="text" name="name" placeholder="{{t "profile.key_for_placeholder"}}" required></label>
{{if .Contexts}}
<label>{{t "profile.spends_against"}}
<select name="context">
{{range .Contexts}}<option value="{{.Value}}">{{.Label}}</option>{{end}}
</select></label>
<button type="submit">{{t "profile.create"}}</button>
{{else}}
<p class="note">{{t "profile.no_project"}}</p>
{{end}}
</form>
<h2>{{t "profile.keys"}}</h2>
{{if .Keys}}
<table class="rows"><tr><th>{{t "profile.col.name"}}</th><th>{{t "profile.col.key"}}</th><th>{{t "profile.col.spends_against"}}</th><th>{{t "profile.col.created"}}</th><th>{{t "profile.col.last_used"}}</th><th>{{t "profile.col.state"}}</th><th></th></tr>
{{range .Keys}}<tr>
<td>{{.Name}}</td>
<td><code>{{.Prefix}}</code></td>
<td>{{if .ProjectID}}{{or (index $.Where .ProjectID) (t "profile.left_project")}}{{else}}{{t "profile.personal"}}{{end}}</td>
<td>{{date .CreatedAt}}</td>
<td>{{ago .LastUsedAt}}</td>
<td class="state state-{{state .}}">{{t (print "profile.state." (state .))}}</td>
<td>{{if eq (state .) "active"}}
<form method="post" action="/profile/keys/{{.ID}}/revoke">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<button type="submit" class="link danger">{{t "profile.revoke"}}</button>
</form>
{{end}}</td>
</tr>{{end}}</table>
<p class="note">{{t "profile.revoke_note"}}</p>
{{else}}<p class="empty">{{t "profile.no_keys"}}</p>{{end}}
<h2>{{t "profile.where"}}</h2>
{{if .Memberships}}
<p>{{range $i, $m := .Memberships}}{{if $i}} · {{end}}{{$m.Path}} <span class="opt">{{t (print "profile.scope." $m.Scope)}}</span>{{end}}</p>
{{else}}<p class="empty">{{t "profile.no_memberships"}}</p>{{end}}
<h2>{{t "selfuse.heading"}}</h2>
{{with .Own}}
<div class="tiles own-use">
<div class="tile"><div class="v">{{compact .Requests}}</div><div class="k">{{t "selfuse.requests_30d"}}</div></div>
<div class="tile"><div class="v">{{compact .Tokens}}</div><div class="k">{{t "selfuse.tokens_30d"}}</div></div>
<div class="tile"><div class="v">{{.Spend}}</div><div class="k">{{t "selfuse.spend_30d"}}</div></div>
</div>
{{with .Chart}}<div class="chart-wrap"><h3 class="chart-title">{{t "selfuse.tokens_per_day"}}</h3>{{.}}</div>{{end}}
{{end}}
{{range .LimitsBy}}
<h3 class="limits-for">{{.Label}}{{with .Use}} <span class="opt">{{t "selfuse.place_use" (compact .Requests) (compact .Tokens)}}</span>{{end}}</h3>
{{range .Budgets}}
<div class="budget{{if .Over}} over{{else if .Near}} near{{end}}">
<div class="budget-head"><span>{{.Label}}</span><span>{{th "selfuse.budget_of" .Used .Budget .Period}}</span></div>
<div class="budget-bar"><span style="width: {{.Percent}}%"></span></div>
</div>
{{end}}
{{if .Rows}}
<table class="rows compact narrow-table"><tr><th>{{t "selfuse.col.limit"}}</th><th class="num">{{t "selfuse.col.value"}}</th></tr>
{{range .Rows}}<tr><td>{{.Name}}</td><td class="num">{{.Value}}</td></tr>{{end}}</table>
{{else}}<p class="empty">{{t "selfuse.no_limits"}}</p>{{end}}
{{else}}<p class="empty">{{t "selfuse.nowhere"}}</p>{{end}}
<p class="note">{{t "selfuse.strictest"}}</p>
<h2>{{t "profile.password"}}</h2>
{{if .PasswordChanged}}<p>{{t "profile.password_changed"}}</p>{{end}}
{{if .PasswordError}}<p class="error">{{.PasswordError}}</p>{{end}}
<form method="post" action="/profile/password" autocomplete="on" class="narrow">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>{{t "profile.current_password"}}<input type="password" name="current" required
autocomplete="current-password"></label>
<label>{{t "profile.new_password"}}<input type="password" name="new" required
minlength="{{.MinPassword}}" autocomplete="new-password"></label>
<button type="submit">{{t "profile.change_password"}}</button>
</form>
<p class="note">{{t "profile.password_note"}}</p>
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
</div></body></html>{{end}}
{{define "forgot"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "forgot.title"}}</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap narrow">
<header><h1>{{t "forgot.heading"}}</h1></header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .Sent}}<p>{{t "forgot.sent"}}</p><p class="note"><a href="/login">{{t "login.submit"}}</a></p>
{{else if .NoMail}}<p>{{th "forgot.no_mail"}}</p>
{{else}}<p class="sub">{{t "forgot.sub"}}</p>
<form method="post" autocomplete="on">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>{{t "login.email"}}<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>
<button type="submit">{{t "forgot.submit"}}</button>
</form>{{end}}
<footer><span>llmgw {{.Version}}</span><span><a href="/login">{{t "login.heading"}}</a></span>{{template "langs"}}</footer>
</div></body></html>{{end}}
{{define "reset"}}<!doctype html>
<html lang="{{lang}}"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{t "reset.title"}}</title><link rel="stylesheet" href="{{stylesheet}}">
</head><body><div class="wrap narrow">
<header><h1>{{t "reset.heading"}}</h1></header>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
{{if .Done}}<p>{{t "reset.done"}}</p><p class="note"><a href="/login">{{t "login.submit"}}</a></p>
{{else if .Bad}}<p class="error">{{t "reset.bad"}}</p><p class="note"><a href="/forgot">{{t "forgot.heading"}}</a></p>
{{else}}
<form method="post" autocomplete="off">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="hidden" name="token" value="{{.Token}}">
<label>{{t "reset.password"}}<input type="password" name="password" required minlength="{{.MinPassword}}" autocomplete="new-password"></label>
<label>{{t "reset.confirm"}}<input type="password" name="confirm" required minlength="{{.MinPassword}}" autocomplete="new-password"></label>
<p class="note">{{t "signup.password_rule" .MinPassword}}</p>
<button type="submit">{{t "reset.submit"}}</button>
</form>{{end}}
<footer><span>llmgw {{.Version}}</span>{{template "langs"}}</footer>
</div></body></html>{{end}}