af9f331781
Minimal Go service: /healthz + /v1/tenants/by-slug/:slug + /v1/tenants/:id with an in-memory store seeded with the acme tenant. Stdlib-only; pgx + JWT validation land in M4.1 follow-up.
107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"gitea.meghsakha.com/platform/tenant-registry/internal/config"
|
|
"gitea.meghsakha.com/platform/tenant-registry/internal/store"
|
|
)
|
|
|
|
type deps struct {
|
|
cfg *config.Config
|
|
log *slog.Logger
|
|
tenant *store.Memory
|
|
}
|
|
|
|
func NewRouter(cfg *config.Config, log *slog.Logger) http.Handler {
|
|
d := &deps{cfg: cfg, log: log, tenant: store.NewMemory()}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /healthz", d.healthz)
|
|
mux.HandleFunc("GET /v1/tenants/by-slug/{slug}", d.tenantBySlug)
|
|
mux.HandleFunc("GET /v1/tenants/{id}", d.tenantByID)
|
|
|
|
return logRequest(log)(mux)
|
|
}
|
|
|
|
func (d *deps) healthz(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (d *deps) tenantBySlug(w http.ResponseWriter, r *http.Request) {
|
|
slug := r.PathValue("slug")
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
t, err := d.tenant.BySlug(ctx, slug)
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "tenant_not_found", "no tenant with that slug")
|
|
return
|
|
}
|
|
if err != nil {
|
|
d.log.Error("tenant lookup failed", "err", err)
|
|
writeError(w, http.StatusInternalServerError, "internal", "lookup failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, t)
|
|
}
|
|
|
|
func (d *deps) tenantByID(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
t, err := d.tenant.ByID(ctx, id)
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "tenant_not_found", "no tenant with that id")
|
|
return
|
|
}
|
|
if err != nil {
|
|
d.log.Error("tenant lookup failed", "err", err)
|
|
writeError(w, http.StatusInternalServerError, "internal", "lookup failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, t)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, code int, body any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, code int, kind, msg string) {
|
|
writeJSON(w, code, map[string]string{"error": kind, "message": msg})
|
|
}
|
|
|
|
func logRequest(log *slog.Logger) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
ww := &statusRecorder{ResponseWriter: w, code: 200}
|
|
next.ServeHTTP(ww, r)
|
|
log.Info("http",
|
|
"method", r.Method,
|
|
"path", r.URL.Path,
|
|
"status", ww.code,
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
type statusRecorder struct {
|
|
http.ResponseWriter
|
|
code int
|
|
}
|
|
|
|
func (s *statusRecorder) WriteHeader(c int) {
|
|
s.code = c
|
|
s.ResponseWriter.WriteHeader(c)
|
|
}
|