package gateway import ( "net/http" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" ) // This file owns the HTTP surface: the route table (REST /api/v1, the realtime // WebSocket hub, /api/v1/admin/* and externally-mounted webhook callbacks) and // the outer middleware chain. Route registration is split by audience so the // RBAC gate guarding each group stays visible: public, authenticated user, // private history, admin reads and owner-only writes. // Handler returns the fully assembled http.Handler: the ServeMux routing // REST + WS + admin, wrapped in recovery, CORS and request logging. func (s *Server) Handler() http.Handler { mux := http.NewServeMux() // The RBAC middlewares are built once per role set and shared by every route // that requires it (owner>admin>viewer, DevDocs/02 §九). anyAuth := s.auth.Authorize() ownerAuth := s.auth.Authorize(auth.RoleOwner) writeAuth := s.auth.Authorize(auth.RoleOwner, auth.RoleAdmin) s.registerPublicRoutes(mux) s.registerUserRoutes(mux, anyAuth, ownerAuth, writeAuth) s.registerHistoryRoutes(mux, writeAuth) s.registerAdminRoutes(mux, ownerAuth, writeAuth) s.mountWebhooks(mux) return s.withMiddleware(mux) } // registerPublicRoutes mounts the unauthenticated surface: liveness plus the // credential endpoints. Refresh/logout are public by design — the opaque refresh // token is itself the credential, so a client with an expired access token can // still rotate or revoke it. Both fail closed with 401 invalid_grant on a bad // token. func (s *Server) registerPublicRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/v1/health", s.handleHealth) mux.HandleFunc("POST /api/v1/auth/login", s.handleLogin) mux.HandleFunc("POST /api/v1/auth/refresh", s.handleRefresh) mux.HandleFunc("POST /api/v1/auth/logout", s.handleLogout) } // registerUserRoutes mounts the authenticated user-facing surface. Chat is a // write (owner/admin); reads accept any authenticated role so a viewer can still // list personas/sessions. Audio STT/TTS and image upload are owner/admin-only (the // owner's private assistant, audited) and are the REST surface native clients use // to bypass the 4KB WebSocket frame limit. func (s *Server) registerUserRoutes( mux *http.ServeMux, anyAuth func(http.Handler) http.Handler, ownerAuth func(http.Handler) http.Handler, writeAuth func(http.Handler) http.Handler, ) { mux.Handle("POST /api/v1/chat", writeAuth(http.HandlerFunc(s.handleChat))) mux.Handle("GET /api/v1/sessions", anyAuth(http.HandlerFunc(s.handleListSessions))) mux.Handle("GET /api/v1/personas", anyAuth(http.HandlerFunc(s.handleListPersonas))) // Shared client enum tables (Android collaboration B4, design/07 §6): the // authority every client maps ids to labels from, so a source badge or a mood // label cannot drift per client. A read-only projection of // internal/contract/enums with nothing sensitive in it, so any authenticated // role may read it (same gate as /sessions above). mux.Handle("GET /api/v1/meta/enums", anyAuth(http.HandlerFunc(s.handleListEnums))) mux.Handle("POST /api/v1/audio/transcribe", ownerAuth(http.HandlerFunc(s.handleTranscribe))) mux.Handle("POST /api/v1/audio/speech", ownerAuth(http.HandlerFunc(s.handleSpeech))) mux.Handle("POST /api/v1/audio/voice-clone", ownerAuth(http.HandlerFunc(s.handleVoiceClone))) // Image upload (B1): a multipart POST that stores a raster image under the // resolved data dir and returns a same-origin URL usable directly as // chat.image. Upload is owner/admin + audited; the static route is a // capability URL (unguessable name) because chat.image / cannot carry a // bearer token. Both ride the one gateway port (single-binary red line). mux.Handle("POST /api/v1/upload", writeAuth(http.HandlerFunc(s.handleUpload))) mux.HandleFunc("GET /api/v1/files/{name}", s.handleUploadFile) } // registerHistoryRoutes mounts the persistent chat-history surface (design/16) // plus the realtime endpoint. Session/model reads and writes are the owner's // private history, so owner/admin only (chat uses the same roles); the message // content (and thinking) is owner/admin-only per design/16 §6, while the session // metadata in GET /sessions above stays readable by any authenticated role. // Creating and deleting a sub session mutate persistent state. func (s *Server) registerHistoryRoutes(mux *http.ServeMux, writeAuth func(http.Handler) http.Handler) { mux.Handle("GET /api/v1/sessions/{id}/messages", writeAuth(http.HandlerFunc(s.handleListMessages))) mux.Handle("POST /api/v1/sessions", writeAuth(http.HandlerFunc(s.handleCreateSubSession))) mux.Handle("DELETE /api/v1/sessions/{id}", writeAuth(http.HandlerFunc(s.handleDeleteSession))) mux.Handle("PATCH /api/v1/sessions/{id}", writeAuth(http.HandlerFunc(s.handlePatchSession))) // Read cursor (D4 / Android collaboration B3): advancing it clears the session's // unread badge on every end of the account, so it is an owner/admin history write // like the other session mutations. It broadcasts a `session_read` frame. mux.Handle("POST /api/v1/sessions/{id}/read", writeAuth(http.HandlerFunc(s.handleMarkSessionRead))) // Single-message edit/delete (Android collaboration batch ③a, D3). A message is // addressed by its own opaque id; both are owner/admin-gated history writes. mux.Handle("PATCH /api/v1/messages/{id}", writeAuth(http.HandlerFunc(s.handlePatchMessage))) mux.Handle("DELETE /api/v1/messages/{id}", writeAuth(http.HandlerFunc(s.handleDeleteMessage))) // Realtime. mux.HandleFunc("/ws/chat", s.handleWS) } // registerAdminRoutes mounts the admin API: reads require owner or admin, the // audited policy write requires owner, and every other write is reserved for the // owner (RBAC red line). Each write handler persists to the real backing store or // answers honestly with a clear "M10 / not-wired" error rather than fabricating a // success. func (s *Server) registerAdminRoutes( mux *http.ServeMux, ownerAuth func(http.Handler) http.Handler, writeAuth func(http.Handler) http.Handler, ) { // Admin reads: owner/admin. mux.Handle("GET /api/v1/admin/personas", writeAuth(http.HandlerFunc(s.adminListPersonas))) mux.Handle("GET /api/v1/admin/sessions", writeAuth(http.HandlerFunc(s.adminListSessions))) mux.Handle("GET /api/v1/admin/memories", writeAuth(http.HandlerFunc(s.adminListMemories))) mux.Handle("GET /api/v1/admin/plugins", writeAuth(http.HandlerFunc(s.adminListPlugins))) mux.Handle("GET /api/v1/admin/plugins/installed", writeAuth(http.HandlerFunc(s.adminListInstalledPlugins))) mux.Handle("GET /api/v1/admin/channels", writeAuth(http.HandlerFunc(s.adminListChannels))) mux.Handle("GET /api/v1/admin/channels/messages", writeAuth(http.HandlerFunc(s.handleChannelMessages))) mux.Handle("GET /api/v1/admin/channels/{platform}/messages", writeAuth(http.HandlerFunc(s.handleChannelPlatformLog))) mux.Handle("GET /api/v1/admin/iot", writeAuth(http.HandlerFunc(s.adminListIoT))) mux.Handle("GET /api/v1/admin/llm", writeAuth(http.HandlerFunc(s.adminGetLlm))) mux.Handle("GET /api/v1/admin/automations", writeAuth(http.HandlerFunc(s.adminListAutomations))) mux.Handle("GET /api/v1/admin/users", writeAuth(http.HandlerFunc(s.adminListUsers))) mux.Handle("GET /api/v1/admin/backthink", writeAuth(http.HandlerFunc(s.adminListBackthink))) mux.Handle("GET /api/v1/admin/rag", writeAuth(http.HandlerFunc(s.adminListRag))) mux.Handle("POST /api/v1/admin/rag/query", writeAuth(http.HandlerFunc(s.adminSearchRag))) mux.Handle("GET /api/v1/admin/observability/metrics", writeAuth(http.HandlerFunc(s.adminObservabilityMetrics))) mux.Handle("GET /api/v1/admin/observability/trace", writeAuth(http.HandlerFunc(s.adminObservabilityTrace))) mux.Handle("GET /api/v1/admin/observability/audit", writeAuth(http.HandlerFunc(s.adminObservabilityAudit))) // Backend log tail: the logger's own bounded ring buffer, injected by the // composition root (501 until then). Newest first, `?level=`/`?limit=` filtered. mux.Handle("GET /api/v1/admin/observability/logs", writeAuth(http.HandlerFunc(s.adminObservabilityLogs))) // Unified notification feed (design/10 §3 #16): a read-only projection of the // audit trail + backthink pushes, so the panel's notification page needs no // new persisted model. mux.Handle("GET /api/v1/admin/notifications", writeAuth(http.HandlerFunc(s.adminListNotifications))) mux.Handle("POST /api/v1/admin/plugins/policy", ownerAuth(http.HandlerFunc(s.adminSetPolicy))) mux.Handle("GET /api/v1/admin/sessions/{id}/export", writeAuth(http.HandlerFunc(s.adminExportSession))) // Admin writes: owner-only, audited. mux.Handle("POST /api/v1/admin/personas", ownerAuth(http.HandlerFunc(s.adminCreatePersona))) mux.Handle("PUT /api/v1/admin/personas/{id}", ownerAuth(http.HandlerFunc(s.adminUpdatePersona))) mux.Handle("POST /api/v1/admin/users", ownerAuth(http.HandlerFunc(s.adminCreateUser))) mux.Handle("POST /api/v1/admin/users/apikey", ownerAuth(http.HandlerFunc(s.adminIssueApiKey))) mux.Handle("DELETE /api/v1/admin/users/apikey/{id}", ownerAuth(http.HandlerFunc(s.adminRevokeApiKey))) mux.Handle("POST /api/v1/admin/iot/{id}/control", ownerAuth(http.HandlerFunc(s.adminIoTControl))) mux.Handle("POST /api/v1/admin/iot/scenarios", ownerAuth(http.HandlerFunc(s.adminIoTScenario))) mux.Handle("POST /api/v1/admin/iot/rules", ownerAuth(http.HandlerFunc(s.adminIoTRule))) mux.Handle("POST /api/v1/admin/automations", ownerAuth(http.HandlerFunc(s.adminCreateAutomation))) mux.Handle("POST /api/v1/admin/automations/{id}/run", ownerAuth(http.HandlerFunc(s.adminRunAutomation))) mux.Handle("DELETE /api/v1/admin/automations/{id}", ownerAuth(http.HandlerFunc(s.adminDeleteAutomation))) mux.Handle("POST /api/v1/admin/llm", ownerAuth(http.HandlerFunc(s.adminAddLlmProvider))) mux.Handle("POST /api/v1/admin/llm/test", ownerAuth(http.HandlerFunc(s.adminTestLlm))) mux.Handle("POST /api/v1/admin/channels/open", ownerAuth(http.HandlerFunc(s.adminChannelOpen))) mux.Handle("POST /api/v1/admin/channels/close", ownerAuth(http.HandlerFunc(s.adminChannelClose))) mux.Handle("POST /api/v1/admin/channels/test", ownerAuth(http.HandlerFunc(s.adminChannelTest))) mux.Handle("GET /api/v1/admin/channels/{platform}/login-qr", ownerAuth(http.HandlerFunc(s.adminWeixinLoginQR))) mux.Handle("GET /api/v1/admin/channels/{platform}/login-status", ownerAuth(http.HandlerFunc(s.adminWeixinLoginStatus))) mux.Handle("GET /api/v1/admin/channels/{platform}/view", ownerAuth(http.HandlerFunc(s.adminChannelPlatformView))) mux.Handle("GET /api/v1/admin/channels/{platform}/friends", ownerAuth(http.HandlerFunc(s.adminChannelPlatformFriends))) mux.Handle("POST /api/v1/admin/channels/{platform}/friend/handle", ownerAuth(http.HandlerFunc(s.adminChannelFriendHandle))) mux.Handle("POST /api/v1/admin/channels/{platform}/message/test", ownerAuth(http.HandlerFunc(s.adminChannelMessageTest))) mux.Handle("PUT /api/v1/admin/channels/{platform}/config", ownerAuth(http.HandlerFunc(s.adminSaveChannelConfig))) mux.Handle("DELETE /api/v1/admin/channels/{platform}/config", ownerAuth(http.HandlerFunc(s.adminDeleteChannelAccount))) mux.Handle("POST /api/v1/admin/plugins/install", ownerAuth(http.HandlerFunc(s.adminPluginInstall))) mux.Handle("POST /api/v1/admin/plugins/uninstall", ownerAuth(http.HandlerFunc(s.adminPluginUninstall))) mux.Handle("POST /api/v1/admin/memories", ownerAuth(http.HandlerFunc(s.adminAddMemory))) mux.Handle("POST /api/v1/admin/memories/search", ownerAuth(http.HandlerFunc(s.adminMemorySearch))) mux.Handle("POST /api/v1/admin/rag", ownerAuth(http.HandlerFunc(s.adminIndexRag))) mux.Handle("POST /api/v1/admin/rag/delete", ownerAuth(http.HandlerFunc(s.adminDeleteRag))) mux.Handle("POST /api/v1/admin/system/revoke-keys", ownerAuth(http.HandlerFunc(s.adminRevokeAllKeys))) mux.Handle("POST /api/v1/admin/system/rotate-jwt", ownerAuth(http.HandlerFunc(s.adminRotateJWT))) mux.Handle("POST /api/v1/admin/system/backup", ownerAuth(http.HandlerFunc(s.adminBackup))) mux.Handle("POST /api/v1/admin/system/restore", ownerAuth(http.HandlerFunc(s.adminRestore))) } // mountWebhooks mounts externally-supplied inbound callbacks (e.g. the webhook // channel) without gateway auth; each handler enforces its own // signature/authorization. func (s *Server) mountWebhooks(mux *http.ServeMux) { for path, hdl := range s.webhooks { if path == "" { continue } mux.Handle(path, hdl) } } // withMiddleware wraps the routed handler in the outer layers, outermost first: // the upload capability-URL guard, the SPA fallback (when a frontend is // configured), panic recovery, CORS and request logging. func (s *Server) withMiddleware(mux http.Handler) http.Handler { var h http.Handler = mux h = guardUploadPaths(h) if s.frontend != nil { h = s.spa(h) } h = s.recover(h) h = s.cors(h) h = s.logging(h) return h }