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

244 lines
9.0 KiB
Go

package api
import (
"embed"
"html/template"
"io/fs"
"log"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/johanj/vault1984/lib"
)
// NewRouter creates the main router with all routes registered.
func NewRouter(cfg *lib.Config, webFS embed.FS, templateFS embed.FS) *chi.Mux {
// Parse all website templates (nil if no templates embedded, e.g. tests)
tmpl, _ := template.ParseFS(templateFS, "templates/*.html")
// Helper: render a marketing page
renderPage := func(w http.ResponseWriter, page, title, desc, activeNav string) {
if tmpl == nil {
http.Error(w, "templates not available", 500)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.ExecuteTemplate(w, "base.html", map[string]string{
"Page": page, "Title": title, "Desc": desc, "ActiveNav": activeNav,
}); err != nil {
log.Printf("template error: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}
r := chi.NewRouter()
h := NewHandlers(cfg)
// Global middleware
r.Use(LoggingMiddleware)
r.Use(CORSMiddleware)
r.Use(SecurityHeadersMiddleware)
r.Use(RateLimitMiddleware(120)) // 120 req/min per IP
r.Use(L1Middleware(cfg.DataDir)) // stateless: extract L1 from Bearer, open DB, forget
// Health check (unauthenticated — no Bearer needed)
r.Get("/health", h.Health)
// Ping — minimal latency probe for looking glass (no DB, no auth)
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", "11")
w.Write([]byte(`{"ok":true}`))
})
// Auth endpoints (unauthenticated — no Bearer, 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)
// Legacy setup (only works when no credentials exist — for tests)
r.Post("/api/auth/setup", h.Setup)
// API routes (authenticated — L1 in Bearer, already validated by L1Middleware)
r.Route("/api", func(r chi.Router) {
mountAPIRoutes(r, h)
})
r.Get("/geo", h.GeoLookup)
// --- Vault App UI at /app/* ---
// Vault UI files (index.html, security.html, tokens.html) live in web/
appRoot, err := fs.Sub(webFS, "web")
if err == nil {
appServer := http.FileServer(http.FS(appRoot))
// /app → /app/
r.Get("/app", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/app/", http.StatusMovedPermanently)
})
// /app/* → strip /app prefix and serve from web/
r.Handle("/app/*", http.StripPrefix("/app", appServer))
}
// --- Website static assets at root ---
serveEmbedded := func(path, contentType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, err := fs.ReadFile(webFS, "web/"+path)
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", contentType)
w.Write(data)
}
}
r.Get("/vault1984.css", serveEmbedded("vault1984.css", "text/css; charset=utf-8"))
r.Get("/worldmap.svg", serveEmbedded("worldmap.svg", "image/svg+xml"))
r.Get("/favicon.svg", serveEmbedded("favicon.svg", "image/svg+xml"))
// --- Marketing Site ---
// Server-rendered Go templates — agent-friendly, no JS required.
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "landing",
"Clavitor — Password manager for humans with AI assistants",
"Password manager built for humans with AI assistants. Two-tier encryption keeps agents useful and secrets safe.", "")
})
r.Get("/hosted", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "hosted", "Hosted — Clavitor",
"Your vault needs to work everywhere. 22 regions. $12/yr.", "hosted")
})
r.Get("/pricing", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "pricing", "Pricing — Clavitor",
"No tiers. No per-seat. No surprises. Self-host free or hosted $12/yr.", "pricing")
})
r.Get("/install", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "install", "Install — Clavitor",
"Self-host clavitor. One binary. No Docker. No Postgres.", "install")
})
r.Get("/privacy", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "privacy", "Privacy Policy — Clavitor",
"No analytics. No tracking. No data sales.", "")
})
r.Get("/terms", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "terms", "Terms of Service — Clavitor", "", "")
})
r.Get("/sources", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "sources", "Sources — Clavitor",
"Real users. Real quotes. All verified.", "")
})
// Integration guides (SEO pages)
r.Get("/integrations/claude-code", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "claude-code",
"Clavitor + Claude Code — Secure MCP credential access",
"Give Claude Code secure access to credentials and TOTP via MCP. Two-tier encryption keeps personal data sealed.", "integrations")
})
r.Get("/integrations/codex", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "codex",
"Clavitor + OpenAI Codex — REST API and MCP integration",
"Connect Codex to your vault via REST API or MCP. Scoped tokens, TOTP generation, field-level encryption.", "integrations")
})
r.Get("/integrations/openclaw", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "openclaw",
"Clavitor + OpenClaw — Multi-agent credential management",
"Give your OpenClaw agents scoped access to credentials. Two-tier encryption for autonomous agents.", "integrations")
})
r.Get("/integrations/openclaw/cn", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "openclaw-cn",
"Clavitor + OpenClaw — AI 智能体凭据管理",
"为 OpenClaw 智能体提供安全凭据访问。两层加密,共享字段 AI 可读,个人字段仅生物识别解锁。", "integrations")
})
r.Get("/homepage2", func(w http.ResponseWriter, r *http.Request) {
renderPage(w, "landing",
"Clavitor — Password manager for humans with AI assistants",
"Password manager built for humans with AI assistants. Two-tier encryption keeps agents useful and secrets safe.", "")
})
// SEO
r.Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("User-agent: *\nAllow: /\nSitemap: https://clavitor.com/sitemap.xml\n"))
})
r.Get("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
pages := []string{
"/", "/hosted", "/pricing", "/install", "/privacy", "/terms",
"/integrations/claude-code", "/integrations/codex",
"/integrations/openclaw", "/integrations/openclaw/cn",
}
xml := `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`
for _, p := range pages {
xml += `<url><loc>https://clavitor.com` + p + `</loc></url>`
}
xml += `</urlset>`
w.Write([]byte(xml))
})
return r
}
// mountAPIRoutes registers the authenticated API handlers on the given router.
// Used by both self-hosted (/api/*) and hosted (/{vault_id}/api/*) routes.
func mountAPIRoutes(r chi.Router, h *Handlers) {
// Vault info (for Tokens page config snippets)
r.Get("/vault-info", h.VaultInfo)
// Entries CRUD
r.Get("/entries", h.ListEntries)
r.Post("/entries", h.CreateEntry)
r.Get("/entries/{id}", h.GetEntry)
r.Put("/entries/{id}", h.UpdateEntry)
r.Delete("/entries/{id}", h.DeleteEntry)
// Search
r.Get("/search", h.SearchEntries)
// Password generator
r.Get("/generate", h.GeneratePassword)
// Import
r.Post("/import", h.ImportEntries)
r.Post("/import/confirm", h.ImportConfirm)
// Audit log
r.Get("/audit", h.GetAuditLog)
// Extension API
r.Get("/ext/totp/{id}", h.GetTOTP)
r.Get("/ext/match", h.MatchURL)
r.Post("/ext/map", h.MapFields)
// MCP Token management
r.Post("/mcp-tokens", h.HandleCreateMCPToken)
r.Get("/mcp-tokens", h.HandleListMCPTokens)
r.Delete("/mcp-tokens/{id}", h.HandleDeleteMCPToken)
// Backups
r.Get("/backups", h.ListBackups)
r.Post("/backups", h.CreateBackup)
r.Post("/backups/restore", h.RestoreBackup)
// Agent management
r.Post("/agents", h.HandleCreateAgent)
r.Get("/agents", h.HandleListAgents)
r.Get("/agents/{id}", h.HandleGetAgent)
r.Put("/agents/{id}/whitelist", h.HandleUpdateAgentWhitelist)
r.Put("/agents/{id}/rate-limits", h.HandleUpdateAgentRateLimits)
r.Post("/agents/{id}/lock", h.HandleLockAgent)
r.Post("/agents/{id}/unlock", h.HandleUnlockAgent)
r.Delete("/agents/{id}", h.HandleRevokeAgent)
// Vault lock
r.Get("/vault-lock", h.HandleVaultLockStatus)
r.Post("/vault-unlock", h.HandleVaultUnlock)
// WebAuthn
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)
}