package api import ( "log/slog" "net/http" "strings" "time" "messenger/internal/auth" "messenger/internal/media" "messenger/internal/platform/config" "messenger/internal/platform/ratelimit" "messenger/internal/realtime" "messenger/internal/store" ) type apiHandler struct { store *store.Store tokens *auth.TokenManager hub *realtime.Hub log *slog.Logger secure bool media media.Storage notifier *pushNotifier } // NewHandler builds the /api/v1 route tree with all middleware applied. // static, when non-nil, serves the built frontend for non-API paths. func NewHandler(st *store.Store, tm *auth.TokenManager, hub *realtime.Hub, log *slog.Logger, cfg *config.Config, ms media.Storage, static http.Handler) http.Handler { h := &apiHandler{ store: st, tokens: tm, hub: hub, log: log, secure: cfg.Environment == "production", media: ms, notifier: newPushNotifier(st, hub, log, cfg.VAPIDPublicKey, cfg.VAPIDPrivateKey, cfg.VAPIDSubject), } mux := http.NewServeMux() authHandler := requireAuth(tm, http.HandlerFunc(h.websocket)) authLimiter := ratelimit.New(20, time.Minute) limited := func(inner http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !authLimiter.Allow(r) { w.Header().Set("Retry-After", "60") writeError(w, http.StatusTooManyRequests, "too many requests") return } inner(w, r) } } mux.HandleFunc("GET /api/v1/ping", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"message": "pong"}) }) mux.HandleFunc("POST /api/v1/auth/register", limited(h.register)) mux.HandleFunc("POST /api/v1/auth/login", limited(h.login)) mux.HandleFunc("POST /api/v1/auth/refresh", limited(h.refresh)) mux.HandleFunc("POST /api/v1/auth/logout", h.logout) mux.Handle("GET /api/v1/users/me", requireAuth(tm, h.me)) mux.Handle("GET /api/v1/users", requireAuth(tm, h.searchUsers)) mux.Handle("GET /api/v1/search/messages", requireAuth(tm, h.searchMessages)) mux.Handle("GET /api/v1/contacts", requireAuth(tm, h.listContacts)) mux.Handle("POST /api/v1/contacts", requireAuth(tm, h.addContact)) mux.Handle("DELETE /api/v1/contacts/{user_id}", requireAuth(tm, h.removeContact)) mux.Handle("GET /api/v1/conversations", requireAuth(tm, h.listConversations)) mux.Handle("POST /api/v1/conversations", requireAuth(tm, h.createConversation)) mux.Handle("GET /api/v1/conversations/{id}/members", requireAuth(tm, h.listMembers)) mux.Handle("POST /api/v1/conversations/{id}/members", requireAuth(tm, h.addMember)) mux.Handle("DELETE /api/v1/conversations/{id}/members/{user_id}", requireAuth(tm, h.removeMember)) mux.Handle("GET /api/v1/conversations/{id}/messages", requireAuth(tm, h.listMessages)) mux.Handle("POST /api/v1/conversations/{id}/messages", requireAuth(tm, h.sendMessage)) mux.Handle("PUT /api/v1/conversations/{id}/messages/{msg_id}/reactions", requireAuth(tm, h.addReaction)) mux.Handle("DELETE /api/v1/conversations/{id}/messages/{msg_id}/reactions/{emoji}", requireAuth(tm, h.removeReaction)) mux.Handle("POST /api/v1/media", requireAuth(tm, h.uploadMedia)) mux.Handle("GET /api/v1/media/{id}", requireAuth(tm, h.downloadMedia)) mux.Handle("GET /api/v1/media/{id}/thumb", requireAuth(tm, h.downloadThumb)) mux.Handle("POST /api/v1/push/subscribe", requireAuth(tm, h.subscribePush)) mux.Handle("DELETE /api/v1/push/subscribe", requireAuth(tm, h.unsubscribePush)) mux.Handle("GET /ws", authHandler) if static != nil { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/") { writeError(w, http.StatusNotFound, "not found") return } static.ServeHTTP(w, r) }) } return mux } func (h *apiHandler) websocket(w http.ResponseWriter, r *http.Request) { userID, ok := userIDFrom(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthenticated") return } h.hub.HandleWS(w, r, userID) }