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) // 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 := `` for _, p := range pages { xml += `https://clavitor.com` + p + `` } xml += `` 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) }