package server import ( "html/template" "net/http" ) // The pages: sign-in, the vault list, the administration pages in // pagesadmin.go, and the three one-shot pages a browser is the right place // for — approving a device, redeeming an invite and finishing an OIDC round // trip. There is no JavaScript anywhere in them: every control is a form, // which is what makes the whole interface work in a locked-down browser and // keeps the security surface down to the CSRF token. var pages = template.Must(template.New("layout").Parse(` {{define "layout"}}
Sign in to let the client on your device sync your databases.
{{if .Error}}{{.Error}}
{{end}} {{if .OIDC}}Sign in with single sign-on →{{end}} {{end}}`)) var inviteTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}You were invited to a KeepOrg server.
{{if .Error}}{{.Error}}
{{end}} {{end}}`)) var signInTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}Sign in to create and manage your vaults.
{{if .Error}}{{.Error}}
{{end}} {{if .OIDC}}Sign in with single sign-on →{{end}} {{end}}`)) var vaultsTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}The files your KeepOrg client keeps in sync.
{{if .Error}}{{.Error}}
{{end}} {{if .Notice}}{{.Notice}}
{{end}} {{if .Vaults}}| Vault | Access | On conflict | Version | Last saved | |
|---|---|---|---|---|---|
| {{.Name}}.kdbx | {{.Perm}} | {{.Strategy}} | {{.Version}} | {{.Updated}}{{if .By}} by {{.By}}{{end}} |
{{if eq .Perm "admin"}}Delete{{end}} |
No vaults yet. Create one below, then open it from the KeepOrg client on your computer or phone.
{{end}} {{if .Groups}} {{else}}You do not administer a group yet, so there is nowhere to put a vault. Ask an administrator to add you to one.
{{end}}Your password, two-factor authentication and signed-in devices are on the Account page.
{{end}}`)) var deleteVaultTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}} {{with .V}}This cannot be undone. Back to your vaults
{{end}} {{if .Error}}{{.Error}}
{{end}} {{with .V}}| Owned by | {{.Group}} |
| Versions stored | {{.Version}} |
| Last saved | {{.Updated}} |
| Groups granted access | {{.Grants}} |
Every stored version of this file is erased from the server, along with the grants that point at it. Copies already in people's mirror directories stay on their computers: the server stops tracking the vault, it does not reach into anybody's home directory. If the vault is merged here, the key the server holds goes with it.
{{end}} {{end}}`)) var deviceConfirmTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}{{with .D}}A KeepOrg client asked to sign in as {{.User}}.
| Device | {{if .Label}}{{.Label}}{{else}}unnamed{{end}} |
|---|---|
| Its code | {{.Code}} |
| Started | {{.Started}} |
Approve only if you started this sign-in yourself, just now, and your device shows the code above. Anyone who sent you a link to this page gets your access if you approve.
{{end}}{{end}}`)) var doneTmpl = template.Must(template.Must(pages.Clone()).Parse(` {{define "body"}}{{.Message}}
{{if .Return}}{{end}}{{end}}`)) func (s *Server) renderDevice(w http.ResponseWriter, code, errMsg string) { render(w, deviceTmpl, map[string]any{ "Title": "Approve device", "Code": code, "Error": errMsg, "OIDC": s.oidc != nil, }) } func (s *Server) renderInvite(w http.ResponseWriter, token, errMsg string) { render(w, inviteTmpl, map[string]any{"Title": "Create account", "Token": token, "Error": errMsg}) } func (s *Server) renderSignIn(w http.ResponseWriter, r *http.Request, errMsg string) { render(w, signInTmpl, map[string]any{ "Title": "Sign in", "Error": errMsg, "OIDC": s.oidc != nil, "Pre": s.preSessionToken(w, r), }) } func (s *Server) renderVaults(w http.ResponseWriter, sess *webSession, errMsg, notice string) { vaults, groups := s.vaultRows(sess.user) d := s.shell(sess, "Vaults", "Vaults") d["Vaults"], d["Groups"], d["Error"], d["Notice"] = vaults, groups, errMsg, notice render(w, vaultsTmpl, d) } func (s *Server) renderDeleteVault(w http.ResponseWriter, sess *webSession, id, errMsg string) { v := s.deleteVaultView(sess.user, id) if v == nil { s.renderVaults(w, sess, "There is no such vault, or you do not administer it.", "") return } d := s.shell(sess, "Delete vault", "Vaults") d["V"], d["Error"] = v, errMsg render(w, deleteVaultTmpl, d) } func (s *Server) renderDone(w http.ResponseWriter, title, msg string) { render(w, doneTmpl, map[string]any{"Title": title, "Message": msg}) } // renderApproved ends a device approval. A client that gave a return URL // gets a link back to itself; html/template would neutralise its custom // scheme, which is why it is marked safe here, after validReturnURL. func (s *Server) renderApproved(w http.ResponseWriter, returnURL string) { d := map[string]any{"Title": "Device approved", "Message": "You can close this page and return to the application."} if returnURL != "" && validReturnURL(returnURL) { d["Return"] = template.URL(returnURL) } render(w, doneTmpl, d) } func render(w http.ResponseWriter, t *template.Template, data map[string]any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Referrer-Policy", "no-referrer") _ = t.ExecuteTemplate(w, "layout", data) }