package server
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
"time"
)
func (s *Server) handleAdminLogin(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`
Admin Login
`))
case "POST":
if err := r.ParseForm(); err != nil {
http.Error(w, "bad form", 400)
return
}
user := r.FormValue("username")
pass := r.FormValue("password")
if !s.cfg.CheckAdmin(user, pass) {
http.Error(w, "401 Unauthorized", 401)
return
}
tok := s.tokens.Create()
http.SetCookie(w, &http.Cookie{
Name: "admin_session", Value: tok, Path: "/admin",
HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 86400,
})
http.Redirect(w, r, "/admin/dashboard", http.StatusFound)
default:
http.Error(w, "method not allowed", 405)
}
}
func (s *Server) handleAdminDashboard(w http.ResponseWriter, r *http.Request) {
if !s.requireAdminCookie(w, r) {
return
}
var userCount, deviceCount, opsCount int
s.db.QueryRow("SELECT COUNT(*) FROM server_users").Scan(&userCount)
s.db.QueryRow("SELECT COUNT(*) FROM server_devices").Scan(&deviceCount)
s.db.QueryRow("SELECT COUNT(*) FROM server_ops").Scan(&opsCount)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`Admin Dashboard
Verstak Sync Server — Admin
` + intToStr(userCount) + `
Users
` + intToStr(deviceCount) + `
Devices
` + intToStr(opsCount) + `
Sync Ops
`))
}
func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
if !s.requireAdminCookie(w, r) {
return
}
rows, err := s.db.Query("SELECT id, username, email, confirmed, blocked, created_at FROM server_users ORDER BY created_at DESC")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var users []map[string]interface{}
for rows.Next() {
var id, username, email, createdAt string
var confirmed, blocked int
rows.Scan(&id, &username, &email, &confirmed, &blocked, &createdAt)
users = append(users, map[string]interface{}{
"id": id, "username": username, "email": email,
"confirmed": confirmed, "blocked": blocked, "created_at": createdAt,
})
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`Users
| Username | Email | Confirmed | Blocked | Created |
`))
for _, u := range users {
confirmed := "✅"
if u["confirmed"].(int) == 0 {
confirmed = "❌"
}
blocked := ""
if u["blocked"].(int) != 0 {
blocked = "🚫"
}
w.Write([]byte(`| ` + u["username"].(string) + ` | ` + u["email"].(string) +
` | ` + confirmed + ` | ` + blocked + ` | ` + u["created_at"].(string) + ` |
`))
}
w.Write([]byte(`
`))
}
func (s *Server) handleAdminDevices(w http.ResponseWriter, r *http.Request) {
if !s.requireAdminCookie(w, r) {
return
}
rows, err := s.db.Query(`SELECT d.id, d.name, d.client_version, COALESCE(d.last_seen,''), COALESCE(d.revoked_at,''), d.created_at
FROM server_devices d ORDER BY d.created_at DESC`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`Devices
| Name | ID | Version | Last Seen | Revoked | Created |
`))
for rows.Next() {
var id, name, clientVer, lastSeen, revokedAt, createdAt string
rows.Scan(&id, &name, &clientVer, &lastSeen, &revokedAt, &createdAt)
if lastSeen == "" {
lastSeen = "never"
}
if revokedAt == "" {
revokedAt = "-"
}
w.Write([]byte(`| ` + name + ` | ` + id +
` | ` + clientVer + ` | ` + lastSeen + ` | ` + revokedAt + ` | ` + createdAt + ` |
`))
}
w.Write([]byte(`
`))
}
func (s *Server) requireAdminCookie(w http.ResponseWriter, r *http.Request) bool {
cookie, err := r.Cookie("admin_session")
if err != nil || cookie.Value == "" {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return false
}
if !s.tokens.Check(cookie.Value) {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return false
}
return true
}
func intToStr(n int) string {
b, _ := json.Marshal(n)
return strings.Trim(string(b), "\"")
}
var _ = time.Now
var _ = rand.Read
var _ = hex.EncodeToString