Files
OwnCord/Server/api/router.go
T
jevb 36640e3051 feat: implement Phase 4 real-time chat (WebSocket hub + message REST)
- db: channel_queries (ListChannels, GetChannel, CRUD, permissions),
  message_queries (CreateMessage, GetMessage, GetMessages paginated,
  EditMessage, DeleteMessage soft, AddReaction, RemoveReaction,
  GetReactions, SearchMessages FTS5, UpdateReadState)
- db: fix in-memory DB isolation — SetMaxOpenConns(1) for :memory: path
- ws/hub: replace stub with full Hub (register/unregister, broadcast to
  channel/all, send to user, thread-safe, buffered broadcast channel)
- ws/client: Client with send channel, NewTestClient helpers for tests
- ws/handlers: dispatch chat_send/edit/delete, reaction_add/remove,
  typing_start, presence_update — all with rate limiting and permission checks
- ws/messages: JSON builder helpers for all server→client message types
- ws/serve: ServeWS HTTP handler, WS auth handshake (10s timeout),
  ready payload, writePump/readPump goroutines, graceful disconnect
- api: channel_handler — GET /channels, GET /channels/{id}/messages,
  GET /search; fixed double-mount of /api/v1 route group
- api/router: mount channel routes, start hub, register /api/v1/ws

Test coverage: api 77.6%, auth 90.9%, db 84.2%, ws 26.7% (serve.go
requires live WS connection; hub/handlers/messages fully covered)
2026-03-14 21:17:09 +01:00

102 lines
2.7 KiB
Go

// Package api provides the HTTP router and handlers for the OwnCord server.
package api
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/owncord/server/auth"
"github.com/owncord/server/config"
"github.com/owncord/server/db"
"github.com/owncord/server/ws"
)
// version is the server version string, overridden at build time via ldflags.
var version = "dev"
// NewRouter builds and returns the fully configured HTTP handler.
func NewRouter(cfg *config.Config, database *db.DB) http.Handler {
r := chi.NewRouter()
// Middleware stack.
r.Use(middleware.RequestID)
r.Use(setRequestIDHeader) // echo request ID into response header
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
// Health check — unauthenticated, no versioning prefix.
r.Get("/health", handleHealth)
// Shared rate limiter for auth endpoints.
limiter := auth.NewRateLimiter()
// Versioned API routes.
r.Route("/api/v1", func(r chi.Router) {
r.Get("/info", handleInfo(cfg))
})
// Auth routes: register, login, logout, me.
MountAuthRoutes(r, database, limiter)
// Invite management routes (require MANAGE_INVITES permission).
MountInviteRoutes(r, database)
// Channel and message REST routes.
MountChannelRoutes(r, database)
// WebSocket hub — WS does its own in-band auth, so no AuthMiddleware here.
hub := ws.NewHub(database, limiter)
go hub.Run()
r.Get("/api/v1/ws", ws.ServeWS(hub, database))
return r
}
// healthResponse is the JSON shape returned by GET /health.
type healthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
}
// infoResponse is the JSON shape returned by GET /api/v1/info.
type infoResponse struct {
Name string `json:"name"`
Version string `json:"version"`
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, healthResponse{
Status: "ok",
Version: version,
})
}
func handleInfo(cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, infoResponse{
Name: cfg.Server.Name,
Version: version,
})
}
}
// setRequestIDHeader copies the request ID from context into the response header.
func setRequestIDHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := middleware.GetReqID(r.Context())
if requestID != "" {
w.Header().Set("X-Request-Id", requestID)
}
next.ServeHTTP(w, r)
})
}
// writeJSON encodes v as JSON and writes it to w with the given status code.
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}