feat: add authentication, audit log, HTTP API and base web UI
Phase 1 foundation. Local auth per ADR-0006: Argon2id passwords, opaque hashed session tokens with idle/absolute expiry, CSRF token plus Origin check on cookie sessions, per-IP token bucket and per-account exponential lockout, three roles enforced by middleware, personal API tokens for noemactl. Audit log writer used by every admin mutation. API: request ids, panic recovery, strict CSP and security headers, body limits, single JSON error envelope, health/ready/status, users, sessions, tokens, audit, token-protected /metrics. UI: embedded html/template pages (login, first-run setup, dashboard, system, users, account, audit, interpretation boundary), light/dark theme, dependency-free app.js with CSRF-aware fetch and SSE helper. Binaries: noemad with graceful shutdown, migrations at start, bootstrap admin from environment, supervised background tasks; noemactl with status, migrate, user create and meaningful exit codes.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/andydixon/noema/internal/app"
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
)
|
||||
|
||||
// Server composes the API routes, the web UI and the middleware chain.
|
||||
type Server struct {
|
||||
deps app.Deps
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// New builds the server. ui registers the HTML routes on the same mux so that
|
||||
// a single middleware chain (auth, CSRF, headers, limits) covers both.
|
||||
func New(deps app.Deps, ui interface{ Register(*http.ServeMux) }) *Server {
|
||||
s := &Server{deps: deps, mux: http.NewServeMux()}
|
||||
s.routes()
|
||||
if ui != nil {
|
||||
ui.Register(s.mux)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Handler returns the fully wrapped root handler.
|
||||
func (s *Server) Handler() http.Handler {
|
||||
return chain(s.mux, s.requestID, s.recoverer, s.securityHeaders, s.bodyLimit, s.logging, s.deps.Auth.Middleware)
|
||||
}
|
||||
|
||||
func (s *Server) routes() {
|
||||
m := s.mux
|
||||
m.HandleFunc("GET /healthz", s.healthz)
|
||||
m.HandleFunc("GET /readyz", s.readyz)
|
||||
// Metrics need a token with at least observer role; Prometheus supports bearer_token.
|
||||
m.Handle("GET /metrics", require(auth.RoleObserver, promhttp.HandlerFor(s.deps.Metrics.Registry, promhttp.HandlerOpts{}).ServeHTTP))
|
||||
|
||||
m.Handle("GET /api/v1/system/status", require(auth.RoleObserver, s.systemStatus))
|
||||
m.Handle("GET /api/v1/audit", require(auth.RoleOperator, s.auditList))
|
||||
|
||||
m.Handle("GET /api/v1/users", require(auth.RoleAdministrator, s.usersList))
|
||||
m.Handle("POST /api/v1/users", require(auth.RoleAdministrator, s.usersCreate))
|
||||
m.Handle("PATCH /api/v1/users/{id}", require(auth.RoleAdministrator, s.usersPatch))
|
||||
m.Handle("GET /api/v1/me", require(auth.RoleObserver, s.me))
|
||||
m.Handle("GET /api/v1/me/sessions", require(auth.RoleObserver, s.mySessions))
|
||||
m.Handle("DELETE /api/v1/me/sessions/{id}", require(auth.RoleObserver, s.mySessionRevoke))
|
||||
m.Handle("POST /api/v1/me/tokens", require(auth.RoleObserver, s.myTokenCreate))
|
||||
m.Handle("GET /api/v1/me/tokens", require(auth.RoleObserver, s.myTokens))
|
||||
m.Handle("DELETE /api/v1/me/tokens/{id}", require(auth.RoleObserver, s.myTokenRevoke))
|
||||
|
||||
m.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, r, http.StatusNotFound, "not_found", "no such endpoint")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user