clavitor/clavis/clavis-vault/api/routes.go

147 lines
4.3 KiB
Go

package api
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
"github.com/johanj/clavitor/lib"
)
func NewRouter(cfg *lib.Config, webFS embed.FS) *chi.Mux {
r := chi.NewRouter()
h := NewHandlers(cfg)
r.Use(LoggingMiddleware)
r.Use(CORSMiddleware)
r.Use(SecurityHeadersMiddleware)
// Security: Limit request body to 64KB. Rejects binary uploads (images, executables).
// Markdown notes and text data only. Returns 413 if exceeded, 415 for binary.
r.Use(MaxBodySizeMiddleware(65536))
r.Use(RateLimitMiddleware(120))
r.Use(L1Middleware(cfg.DataDir))
// Health (unauthenticated)
r.Get("/health", h.Health)
// Ping (no DB, no auth)
node, _ := os.Hostname()
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
fmt.Fprintf(w, `{"ok":true,"node":"%s","ts":%d}`, node, time.Now().Unix())
})
// Auth (unauthenticated — DB found by glob)
r.Get("/api/auth/status", h.AuthStatus)
r.Post("/api/auth/register/begin", h.AuthRegisterBegin)
r.Post("/api/auth/register/complete", h.AuthRegisterComplete)
r.Post("/api/auth/login/begin", h.AuthLoginBegin)
r.Post("/api/auth/login/complete", h.AuthLoginComplete)
r.Post("/api/auth/setup", h.Setup)
// API (authenticated — L1 or CVT in Bearer)
r.Route("/api", func(r chi.Router) {
mountAPIRoutes(r, h)
})
// Static app
appRoot, err := fs.Sub(webFS, "web")
if err == nil {
appServer := http.FileServer(http.FS(appRoot))
r.Get("/app", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/app/", http.StatusMovedPermanently)
})
r.Handle("/app/*", http.StripPrefix("/app", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
appServer.ServeHTTP(w, r)
})))
}
// Favicon & crawlers
favicon, _ := fs.ReadFile(webFS, "web/favicon.svg")
serveFavicon := func(w http.ResponseWriter, r *http.Request) {
if favicon != nil {
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Write(favicon)
} else {
w.WriteHeader(204)
}
}
nothing := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }
disallow := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("User-agent: *\nDisallow: /\n"))
}
r.Get("/", nothing)
r.Get("/favicon.ico", serveFavicon)
r.Get("/favicon.svg", serveFavicon)
r.Get("/robots.txt", disallow)
r.Get("/sitemap.xml", nothing)
r.Get("/.well-known/security.txt", nothing)
r.Get("/.well-known/acme-challenge/*", nothing)
// Tarpit
r.NotFound(tarpitHandler)
r.MethodNotAllowed(tarpitHandler)
return r
}
func mountAPIRoutes(r chi.Router, h *Handlers) {
r.Get("/vault-info", h.VaultInfo)
// Entry CRUD
r.Get("/entries", h.ListEntries)
r.Post("/entries", h.CreateEntry)
r.Post("/entries/batch", h.CreateEntryBatch)
r.Put("/entries", h.UpsertEntry)
r.Get("/entries/{id}", h.GetEntry)
r.Put("/entries/{id}", h.UpdateEntry)
r.Delete("/entries/{id}", h.DeleteEntry)
// Entry scope management (owner-only)
r.Put("/entries/{id}/scopes", h.HandleUpdateEntryScopes)
// Search
r.Get("/search", h.SearchEntries)
// Password generator
r.Get("/generate", h.GeneratePassword)
// Audit log
r.Get("/audit", h.GetAuditLog)
// Extension API
r.Get("/ext/totp/{id}", h.GetTOTP)
r.Get("/ext/match", h.MatchURL)
// Backups
r.Get("/backups", h.ListBackups)
r.Post("/backups", h.CreateBackup)
r.Post("/backups/restore", h.RestoreBackup)
// Admin auth (PRF tap required for admin operations)
r.Post("/auth/admin/begin", h.AdminAuthBegin)
r.Post("/auth/admin/complete", h.AdminAuthComplete)
// Agent management (admin-only — requires PRF tap + admin token)
r.Post("/agents", h.HandleCreateAgent)
r.Get("/agents", h.HandleListAgents)
r.Delete("/agents/{id}", h.HandleDeleteAgent)
// WebAuthn credential management
r.Post("/webauthn/register/begin", h.HandleWebAuthnRegisterBegin)
r.Post("/webauthn/register/complete", h.HandleWebAuthnRegisterComplete)
r.Post("/webauthn/auth/begin", h.HandleWebAuthnAuthBegin)
r.Post("/webauthn/auth/complete", h.HandleWebAuthnAuthComplete)
r.Get("/webauthn/credentials", h.HandleListWebAuthnCredentials)
r.Delete("/webauthn/credentials/{id}", h.HandleDeleteWebAuthnCredential)
}