package server import ( "context" "log/slog" "net/http" "strings" "time" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" "glchat/internal/auth" "glchat/internal/config" "glchat/internal/database" "glchat/internal/gateway" "glchat/internal/httpx" "glchat/internal/meta" "glchat/internal/store" ) // Deps — зависимости HTTP-слоя: хранилище и сервис аутентификации. // В Фазе 0 они могут отсутствовать (инстанс без секретов ещё поднимается). type Deps struct { Store *store.Store Auth *auth.Service Gateway *gateway.Service } type Server struct { cfg config.Config db *database.DB store *store.Store auth *auth.Service gateway *gateway.Service logger *slog.Logger http *http.Server static *staticHandler api huma.API } func New(cfg config.Config, db *database.DB, logger *slog.Logger, deps Deps) *Server { s := &Server{ cfg: cfg, db: db, store: deps.Store, auth: deps.Auth, gateway: deps.Gateway, logger: logger, static: newStaticHandler(cfg.WebRoot), } router := chi.NewRouter() router.Route("/api/v1", func(apiRouter chi.Router) { s.api = s.registerAPI(apiRouter) s.registerMetaRoutes(s.api) s.registerAuthRoutes(apiRouter) apiRouter.Get("/openapi.json", s.handleOpenAPI) }) s.registerRoutes(router) handler := httpx.Chain(router, httpx.SecurityHeaders(cfg.FilesDomain), httpx.RequestID, httpx.RequestInfoMiddleware, httpx.Logger(logger), httpx.Recoverer(logger), httpx.JSONBodyLimit(1<<20), ) s.http = &http.Server{ Addr: cfg.ListenAddr, Handler: handler, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, MaxHeaderBytes: 1 << 16, } return s } // registerAPI создаёт huma-API: OpenAPI 3.1 и TS-типы выводятся из Go-типов // (AGENT.md 8.1, решение D-006). func (s *Server) registerAPI(router chi.Router) huma.API { cfg := huma.DefaultConfig("glchat API", s.cfg.Version) cfg.Info.Description = "Self-hosted платформа общения: REST /api/v1 и WebSocket Gateway." cfg.Info.License = &huma.License{Name: "AGPL-3.0-or-later", Identifier: "AGPL-3.0-or-later"} cfg.Servers = []*huma.Server{{URL: "/api/v1"}} cfg.OpenAPIPath = "/openapi" cfg.DocsPath = "/docs" cfg.Components.SecuritySchemes = map[string]*huma.SecurityScheme{ "sessionCookie": {Type: "apiKey", In: "cookie", Name: sessionCookieName}, "bearerAuth": {Type: "http", Scheme: "bearer"}, } return humachi.New(router, cfg) } func (s *Server) registerRoutes(router chi.Router) { router.Get("/healthz", s.handleHealthz) router.Get("/readyz", s.handleReadyz) if s.gateway != nil { router.Handle("/gateway", s.gateway.Handler()) } else { router.HandleFunc("/gateway", func(w http.ResponseWriter, _ *http.Request) { writeAPIError(w, auth.ErrSessionExpired) }) } router.NotFound(s.handleFallback) router.MethodNotAllowed(func(w http.ResponseWriter, _ *http.Request) { httpx.WriteErrorStatus(w, http.StatusMethodNotAllowed, httpx.CodeBadRequest, "method not allowed") }) } func (s *Server) Handler() http.Handler { return s.http.Handler } func (s *Server) ListenAndServe() error { return s.http.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { return s.http.Shutdown(ctx) } func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) { httpx.WriteJSON(w, http.StatusOK, map[string]any{ "status": "ok", "version": s.cfg.Version, "uptime": time.Since(startedAt).Round(time.Second).String(), }) } func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() checks := map[string]string{"database": "ok"} ready := true if err := s.db.Ready(ctx); err != nil { checks["database"] = "failed" ready = false s.logger.WarnContext(ctx, "readiness check failed", slog.Any("error", err)) } status := http.StatusOK state := "ready" if !ready { status = http.StatusServiceUnavailable state = "unavailable" } httpx.WriteJSON(w, status, map[string]any{ "status": state, "version": s.cfg.Version, "checks": checks, }) } type metaOutput struct { Body meta.Response } func (s *Server) registerMetaRoutes(api huma.API) { huma.Register(api, huma.Operation{ OperationID: "getMeta", Method: http.MethodGet, Path: "/meta", Summary: "Метаданные инстанса, версия API и флаги функций", Tags: []string{"Meta"}, }, func(_ context.Context, _ *struct{}) (*metaOutput, error) { return &metaOutput{Body: meta.New(s.cfg)}, nil }) } func normalizeBearer(value string) string { if strings.HasPrefix(strings.ToLower(value), "bearer ") { return strings.TrimSpace(value[7:]) } return strings.TrimSpace(value) } var startedAt = time.Now()