package server import ( "html/template" "net/http" "cc.tiredsysadmin.keeporg/internal/store" ) // The administration pages. Each control is its own small form carrying the // session's CSRF token and one action, which is why there is no JavaScript // here and no way to act on a page you were not served. var groupsTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}

Groups

Nested groups decide who reaches which vault.

{{if .Error}}

{{.Error}}

{{end}} {{if .Notice}}

{{.Notice}}

{{end}} {{if .Groups}}
{{range .Groups}}{{end}}
GroupPeopleVaults
{{.Name}} {{.Members}} {{.Owns}} {{if .Admin}}you administer{{end}}
{{else}}

There are no groups you can see.

{{end}} {{if or .Parents .ServerAdmin}}
New group

Access flows downwards: a vault granted to a group is reachable by every group inside it, and whoever administers a group administers everything inside it too.

{{end}} {{end}}`)) var groupTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}} {{with .G}}

{{.Name}}

{{if .Parent}}Inside {{.Parent}}. {{end}}All groups

{{end}} {{if .Error}}

{{.Error}}

{{end}} {{if .Notice}}

{{.Notice}}

{{end}} {{with .G}}

People

{{if .Members}}
{{range .Members}} {{end}}
WhoRole
{{.Username}}{{if .Disabled}} disabled{{end}} {{.Role}} {{if $.G.Admin}}
{{end}}
{{else}}

Nobody is in this group yet.

{{end}} {{if .Admin}}
Add somebody
{{range .People}}

Only accounts that already exist can be added. To bring in somebody new, send them an invite link for this group.

{{end}}

Vaults this group reaches

{{if .Access}}
{{range .Access}}{{end}}
VaultAccessWhere from
{{.Name}}.kdbx {{.Perm}} {{if .Owned}}owns it{{else if .Via}}granted to {{.Via}}{{else}}granted here{{end}} {{if and $.G.Admin (not .Owned) (not .Via)}}
{{end}}
{{else}}

This group reaches no vaults.

{{end}} {{if and .Admin .Vaults}}
Grant access
{{end}} {{if .Children}}

Groups inside this one

{{range .Children}}{{end}}
{{.Name}} {{.Members}} people{{.Owns}} vaults
{{end}} {{if .Admin}}
Delete this group

A group can only go once it has no members, no child groups and owns no vaults. Deleting it also drops the grants that hang on it.

{{end}} {{end}} {{end}}`)) var usersTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}

People

{{if .ServerAdmin}}Every account on this server.{{else}}The people in the groups you administer.{{end}}

{{if .Error}}

{{.Error}}

{{end}} {{if .Notice}}

{{.Notice}}

{{end}}
{{if .ServerAdmin}}{{end}} {{range .Users}} {{if $.ServerAdmin}}{{end}} {{end}}
WhoSigns in withGroups
{{.Username}} {{if .ServerAdmin}}server admin{{end}} {{if .Disabled}}disabled{{end}} {{if .Email}}
{{.Email}}{{end}}
{{.SignIn}}{{if .TOTP}}
one-time code{{end}}
{{if .Groups}}{{.Groups}}{{else}}none{{end}} {{if .Disabled}}
{{else if not .Self}}
{{end}} {{if .TOTP}}
{{end}} {{if not .Self}}
{{end}}
{{if .ServerAdmin}}

Disabling an account signs it out everywhere and keeps its history; deleting it removes the account and its group memberships. Clearing the second factor is what you do when somebody loses their phone — they can sign in with their password alone until they enrol a new one.

{{else}}

Only a server administrator can change an account. You decide who is in your groups, from each group's page.

{{end}} {{end}}`)) var invitesTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}

Invites

An invite link lets somebody create their own account and lands them in one group.

{{if .Error}}

{{.Error}}

{{end}} {{if .Notice}}

{{.Notice}}

{{end}} {{if .Invites}}
{{range .Invites}} {{end}}
LinkJoinsUsedExpires
{{.Group}}
as {{.Role}}
{{.Uses}} of {{.Max}} {{.Expires}}{{if .Expired}}
spent or expired{{end}} {{if .By}}
by {{.By}}{{end}}
{{else}}

No invites outstanding.

{{end}} {{if or .Groups .ServerAdmin}}
New invite

Anyone holding the link can create an account, so send it the way you would send a password and keep it short-lived. Revoking a link stops it at once.

{{end}} {{end}}`)) func (s *Server) renderGroups(w http.ResponseWriter, sess *webSession, errMsg, notice string) { d := s.shell(sess, "Groups", "Groups") s.st.Read(func(st *store.State) { d["Groups"] = groupTree(st, sess.user) d["Parents"] = administeredGroups(st, sess.user) }) d["Error"], d["Notice"] = errMsg, notice render(w, groupsTmpl, d) } func (s *Server) renderGroup(w http.ResponseWriter, sess *webSession, id, errMsg, notice string) { d := s.shell(sess, "Group", "Groups") var detail *webGroupDetail s.st.Read(func(st *store.State) { detail = groupDetail(st, sess.user, id) }) if detail == nil { s.renderGroups(w, sess, "There is no such group, or it is not yours to see.", "") return } d["G"], d["Error"], d["Notice"] = detail, errMsg, notice render(w, groupTmpl, d) } func (s *Server) renderUsers(w http.ResponseWriter, sess *webSession, errMsg, notice string) { d := s.shell(sess, "People", "People") s.st.Read(func(st *store.State) { d["Users"] = userRows(st, sess.user) }) d["Error"], d["Notice"] = errMsg, notice render(w, usersTmpl, d) } func (s *Server) renderInvites(w http.ResponseWriter, sess *webSession, errMsg, notice string) { d := s.shell(sess, "Invites", "Invites") s.st.Read(func(st *store.State) { d["Invites"] = s.inviteRows(st, sess.user) d["Groups"] = administeredGroups(st, sess.user) }) d["Error"], d["Notice"] = errMsg, notice render(w, invitesTmpl, d) }