feat: add admin dashboard

This commit is contained in:
2026-04-29 09:44:57 -04:00
parent e319a5d092
commit baf7a497eb
11 changed files with 939 additions and 67 deletions

View File

@@ -0,0 +1,139 @@
package httpserver
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/tim/md-hub-secure/apps/server/internal/docs"
)
type adminUserInput struct {
Email string `json:"email"`
DisplayName string `json:"displayName"`
Role string `json:"role"`
}
func (s *Server) handleAdminLogin(w http.ResponseWriter, r *http.Request) {
var input adminUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
user, ok := s.normalizeAdminUserInput(input)
if !ok {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "email is required"})
return
}
if user.Role == "viewer" {
user.Role = "admin"
}
created, err := s.repository.UpsertUser(r.Context(), user)
if err != nil {
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"user": created,
"session": map[string]any{
"mode": "foundation",
"createdAt": time.Now().UTC(),
},
})
}
func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
users, err := s.repository.ListUsers(r.Context())
if err != nil {
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"users": users})
}
func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
var input adminUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
user, ok := s.normalizeAdminUserInput(input)
if !ok {
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "email is required"})
return
}
created, err := s.repository.UpsertUser(r.Context(), user)
if err != nil {
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSONWithStatus(w, http.StatusCreated, map[string]any{"user": created})
}
func (s *Server) handleAdminWorkspace(w http.ResponseWriter, r *http.Request) {
documents, err := s.documents.ListDocuments(r.Context())
if err != nil {
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
users, err := s.repository.ListUsers(r.Context())
if err != nil {
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"workspace": map[string]any{
"sourceDir": s.config.Content.SourceDir,
"storeDir": s.config.Content.StoreDir,
"databasePath": s.config.Database.Path,
"documentCount": len(documents),
"userCount": len(users),
"webDistDir": s.config.Web.DistDir,
},
"documents": documents,
})
}
func (s *Server) handleAdminWorkspaceSync(w http.ResponseWriter, r *http.Request) {
changes, err := s.documents.SyncSourceDir(r.Context())
if err != nil {
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"changes": changes,
"syncedAt": time.Now().UTC(),
})
}
func (s *Server) normalizeAdminUserInput(input adminUserInput) (docs.UserRecord, bool) {
email := strings.TrimSpace(strings.ToLower(input.Email))
if email == "" {
return docs.UserRecord{}, false
}
role := strings.TrimSpace(strings.ToLower(input.Role))
switch role {
case "admin", "editor", "viewer":
default:
role = "viewer"
}
displayName := strings.TrimSpace(input.DisplayName)
if displayName == "" {
displayName = email
}
return docs.UserRecord{
Email: email,
DisplayName: displayName,
Role: role,
}, true
}