Files
llmgw/internal/web/templates/account.html
T
Ilker ManapandClaude Opus 5.5 f27d722111 People sign in with their Active Directory account
auth.ldap checks a username and password against the domain controllers
over LDAPS or StartTLS: found with a service account, checked by binding
as the person. Groups, nested ones included, give staff roles and places
in the tree. Accounts are tied to the objectGUID; the leader reads the
directory every 15 minutes, suspends whom it disabled or removed and
reinstates them after, and refuses a sync that would suspend most.

Entra ID sends no groups for someone in more than 200, only where to
fetch them; that was read as none and took an administrator's role away.
It is now left as it was, and logged. An account made by SCIM or OIDC
can be reinstated again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 10:59:50 +02:00

283 lines
15 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}}">
{{if .Directory}}<p class="sub">{{t "login.with_directory" .Directory}}</p>
<label>{{t "login.username"}}<input type="text" name="email" value="{{.Email}}" required autocomplete="username" autocapitalize="none" spellcheck="false"></label>
{{else}}<label>{{t "login.email"}}<input type="email" name="email" value="{{.Email}}" required autocomplete="email"></label>{{end}}
<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>
{{with .Licences}}
<h2 id="licences">{{t "profile.licences"}}</h2>
{{if $.LicencesSaved}}<p>{{t "profile.licences.saved"}}</p>{{end}}
{{if $.LicenceError}}<p class="error">{{$.LicenceError}}</p>{{end}}
<p class="note">{{t "profile.licences.intro"}}</p>
<form method="post" action="/profile/licences" class="narrow">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<fieldset><legend>{{t "profile.licences.rules"}}</legend>
<label class="check"><input type="checkbox" name="non_commercial" value="1"{{if .NonCommercial}} checked{{end}}>
{{t "profile.licences.non_commercial"}}</label>
{{range .Regions}}<label class="check"><input type="checkbox" name="region" value="{{.}}"{{if has $.Licences.ExcludedRegions .}} checked{{end}}>
{{t "profile.licences.region" .}}</label>{{end}}
<label>{{t "profile.licences.regions_other"}}<input type="text" name="regions_other" placeholder="{{t "profile.licences.regions_example"}}" autocomplete="off"></label>
</fieldset>
<fieldset><legend>{{t "profile.licences.each"}}</legend>
{{range .Licences}}<label class="check"><input type="checkbox" name="licence" value="{{.Key}}"{{if .Excluded}} checked{{end}}>
{{if .Name}}{{if .URL}}<a href="{{.URL}}" rel="noopener noreferrer">{{.Name}}</a>{{else}}{{.Name}}{{end}}{{else}}{{t "profile.licences.unspecified"}}{{end}}
<span class="opt">{{if eq .Commercial "yes"}}{{t "profile.licences.commercial_yes"}} · {{else if eq .Commercial "no"}}{{t "profile.licences.commercial_no"}} · {{end}}{{with .ExcludedRegions}}{{t "profile.licences.not_in" (join .)}} · {{end}}{{join .Models}}</span></label>{{end}}
</fieldset>
<button type="submit">{{t "profile.licences.save"}}</button>
</form>
<p class="note">{{t "profile.licences.note"}}</p>
{{end}}
<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}}