// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package adminui import ( "bytes" "html/template" "log/slog" "net/http" ) // Templates are parsed once at start. html/template escapes by context, which is what makes it // safe to render device names and finding text that ultimately arrived over a network. var tpl = template.Must(template.New("base").Funcs(template.FuncMap{ "kb": func(n int64) int64 { return n / 1024 }, }).Parse(baseHTML)) func (s *Server) render(w http.ResponseWriter, r *http.Request, page string, data map[string]any) { data["Page"] = page var buf bytes.Buffer if err := tpl.Execute(&buf, data); err != nil { slog.Error("admin template", "page", page, "err", err) http.Error(w, "template error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") // There is no script here and nothing loaded from anywhere else, so a strict policy costs // nothing and closes injected-script attacks even if an escaping bug ever slips through. w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Content-Type-Options", "nosniff") _, _ = buf.WriteTo(w) } const baseHTML = ` Echolot — {{.Page}} {{if ne .Page "login"}}

Echolot

{{.Session.Display}}{{if not .Session.Admin}} (your account){{end}}
{{end}}
{{if eq .Page "login"}}

Sign in

{{with .Error}}

{{.}}

{{end}} {{if .OIDC}}

or use the break-glass account:

{{end}} {{if .LocalSet}}

{{else}}

No break-glass admin is set. Run echolot-server --set-admin-password on the host.

{{end}} {{else if eq .Page "dashboard"}}
{{.Devices}}{{if .Admin}}devices{{else}}your devices{{end}}
{{if .Admin}}
{{.Linked}}signed in
{{end}}
{{.Runs}}{{if .Admin}}stored runs{{else}}your runs{{end}}
{{if not .Admin}}

This is your account. You can see the devices you have signed in on, review everything they have uploaded, and delete any of it.

Administering the server — enrolling devices, revoking them, and seeing other people's uploads — needs an administrator account.

{{end}} {{if .Admin}}

Server

version {{.Version}}

{{with .SelfTest}}
{{printf "%+v" .}}
{{end}}
{{end}} {{else if eq .Page "devices"}}

{{if .Admin}}Devices{{else}}Your devices{{end}}

{{with .Link}}

Enrolment link — single use, valid 24 hours. Treat it like a password until spent.

{{.}}

On a device with adb:
adb shell am start -a android.intent.action.VIEW -d "{{.}}"

{{end}} {{if .Admin}}
{{end}}
{{range .Rows}} {{else}} {{end}}
DeviceNameAccountEnrolledRuns
{{.ID}} {{if .Name}}{{.Name}}{{else}}{{end}} {{if .LinkedToAccount}}{{.AccountName}}{{else}}not signed in{{end}} {{.Enrolled.Format "2006-01-02 15:04"}} {{.Runs}} {{if $.Admin}}
{{end}}
{{if $.Admin}}No devices enrolled.{{else}}You have not signed in on any device yet. Sign in from the Echolot app to link one.{{end}}
{{else if eq .Page "runs"}}

{{if .Admin}}Uploaded runs{{else}}Your uploaded runs{{end}}

Shown exactly as uploaded, at the privacy level the uploader chose. Nothing here can un-redact a run.

{{range .Rows}} {{else}} {{end}}
UploadedDeviceVerdictFindingsSizeLevel
{{.UploadedAt.Format "2006-01-02 15:04"}} {{.DeviceName}} {{if .Verdict}}{{.Verdict}}{{else}}{{end}} {{.FindingCount}} {{kb .SizeBytes}} kB {{.Anonymization}} open
Nothing uploaded yet.
{{else if eq .Page "run"}}

Run {{.ID}}

{{.JSON}}
{{end}}
`