// Package api provides the HTTP API for mosis-portal package api import ( "log" "net/http" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "github.com/omixlab/mosis-portal/internal/api/handlers" "github.com/omixlab/mosis-portal/internal/api/middleware" "github.com/omixlab/mosis-portal/internal/auth" "github.com/omixlab/mosis-portal/internal/config" "github.com/omixlab/mosis-portal/internal/database" "github.com/omixlab/mosis-portal/internal/storage" "github.com/omixlab/mosis-portal/internal/web" ) // NewRouter creates and configures the HTTP router func NewRouter(cfg *config.Config, db *database.DB) http.Handler { r := chi.NewRouter() // Middleware r.Use(chimw.Logger) r.Use(chimw.Recoverer) r.Use(chimw.RealIP) r.Use(chimw.RequestID) // Initialize storage store, err := storage.New(cfg.StoragePath) if err != nil { log.Fatalf("Failed to initialize storage: %v", err) } // Initialize auth components jwtManager := auth.NewJWTManager(cfg.JWTSecret) oauthManager := auth.NewOAuthManager( cfg.BaseURL, cfg.GitHubClientID, cfg.GitHubClientSecret, cfg.GoogleClientID, cfg.GoogleClientSecret, ) authMiddleware := middleware.NewAuthMiddleware(jwtManager, db) authHandler := handlers.NewAuthHandler(oauthManager, jwtManager, db) appHandler := handlers.NewAppHandler(db, store) storeHandler := handlers.NewStoreHandler(db, store) // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) // API v1 r.Route("/v1", func(r chi.Router) { // Auth routes (public) r.Route("/auth", func(r chi.Router) { // OAuth - use GET for initiating (redirect based) r.Get("/oauth/github", authHandler.OAuthStart(auth.ProviderGitHub)) r.Get("/oauth/github/callback", authHandler.OAuthCallback(auth.ProviderGitHub)) r.Get("/oauth/google", authHandler.OAuthStart(auth.ProviderGoogle)) r.Get("/oauth/google/callback", authHandler.OAuthCallback(auth.ProviderGoogle)) // Token management r.Post("/refresh", authHandler.Refresh) r.Post("/logout", authHandler.Logout) // Current user (requires auth) r.With(authMiddleware.RequireAuth).Get("/me", authHandler.Me) }) // Protected developer routes r.Group(func(r chi.Router) { r.Use(authMiddleware.RequireAuth) // Developer apps r.Route("/apps", func(r chi.Router) { r.Get("/", appHandler.List) r.Post("/", appHandler.Create) r.Get("/{appID}", appHandler.Get) r.Patch("/{appID}", appHandler.Update) r.Delete("/{appID}", appHandler.Delete) // Icon upload r.Post("/{appID}/icon", appHandler.UploadIcon) // Versions r.Route("/{appID}/versions", func(r chi.Router) { r.Get("/", appHandler.ListVersions) r.Post("/", appHandler.CreateVersion) r.Get("/{versionID}", appHandler.GetVersion) r.Post("/{versionID}/submit", appHandler.SubmitVersion) r.Post("/{versionID}/publish", appHandler.PublishVersion) // Package upload r.Post("/{versionID}/upload", appHandler.UploadPackage) }) }) // API Keys r.Route("/api-keys", func(r chi.Router) { r.Get("/", handlers.NotImplemented) r.Post("/", handlers.NotImplemented) r.Delete("/{keyID}", handlers.NotImplemented) }) // Signing Keys r.Route("/signing-keys", func(r chi.Router) { r.Get("/", handlers.NotImplemented) r.Post("/", handlers.NotImplemented) r.Delete("/{keyID}", handlers.NotImplemented) }) }) // Public store endpoints r.Route("/store", func(r chi.Router) { r.Get("/apps", storeHandler.ListApps) r.Get("/apps/updates", storeHandler.CheckUpdates) r.Get("/apps/{packageID}", storeHandler.GetApp) r.Get("/apps/{packageID}/download", storeHandler.Download) r.Get("/apps/{packageID}/versions/{versionCode}/download", storeHandler.DownloadVersion) }) // Telemetry (API key auth preferred, but can work without for initial setup) r.Route("/telemetry", func(r chi.Router) { r.Post("/events", handlers.NotImplemented) r.Post("/crash", handlers.NotImplemented) }) }) // Admin routes (htmx UI) - requires auth r.Route("/admin", func(r chi.Router) { r.Use(authMiddleware.RequireAuth) r.Get("/", handlers.NotImplemented) r.Get("/review-queue", handlers.NotImplemented) r.Get("/review/{versionID}", handlers.NotImplemented) r.Post("/review/{versionID}/approve", handlers.NotImplemented) r.Post("/review/{versionID}/reject", handlers.NotImplemented) }) // Web UI routes (htmx + Go templates) webHandler, err := web.NewHandler(db) if err != nil { log.Printf("Warning: Failed to initialize web handler: %v", err) } else { sessionMW := web.NewSessionMiddleware(db, cfg.JWTSecret) // Public web pages r.Group(func(r chi.Router) { r.Use(sessionMW.LoadSession) r.Get("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/dashboard", http.StatusSeeOther) }) r.Get("/login", webHandler.Login) }) // Protected web pages r.Group(func(r chi.Router) { r.Use(sessionMW.LoadSession) r.Use(sessionMW.RequireSession) r.Get("/dashboard", webHandler.Dashboard) r.Get("/apps/new", webHandler.AppNew) r.Get("/apps/{appID}", webHandler.AppDetail) // htmx partials r.Get("/partials/apps", webHandler.AppListPartial) }) // Auth callback that sets session (after OAuth) r.Get("/auth/callback", func(w http.ResponseWriter, r *http.Request) { developerID := r.URL.Query().Get("developer_id") if developerID == "" { http.Redirect(w, r, "/login?error=Authentication failed", http.StatusSeeOther) return } web.SetSession(w, developerID) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) }) // Logout (clears session) r.Get("/auth/logout", func(w http.ResponseWriter, r *http.Request) { web.ClearSession(w) http.Redirect(w, r, "/login", http.StatusSeeOther) }) } // Static file servers for packages and assets // Downloads - serve package files with proper headers r.Handle("/downloads/*", http.StripPrefix("/downloads/", http.FileServer(http.Dir(store.PackagesPath())))) // Assets - serve icons and screenshots r.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServer(http.Dir(store.AssetsPath())))) return r }