6a6cd76426
Minimal Go service so platform/portal has something to resolve in local
dev. Stdlib net/http with Go 1.22 enhanced ServeMux (method+path
patterns); no third-party deps yet.
Layout:
cmd/server/main.go entry point with graceful shutdown
internal/config/ env-driven config (APP_ENV, ADDR, KC issuer)
internal/server/ http handlers + request-logging middleware
internal/store/memory.go in-memory tenant store, seeded with acme
migrations/0001_init.up.sql schema for the M4.1 follow-up (unapplied)
Makefile dev/test/build/lint/docker targets
Dockerfile multi-stage distroless build
Endpoints (under :8080 in dev):
GET /healthz
GET /v1/tenants/by-slug/{slug} 200 acme | 404
GET /v1/tenants/{id} 200 by uuid | 404
JWT validation and the real Postgres-backed store land in the M4.1
follow-up PR — keeping this PR strictly to 'boots, replies, tests pass'.
Refs: M4.1 (skeleton)
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)
|
|
}
|