// 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" "omixlab.com/mosis-portal/internal/api/handlers" "omixlab.com/mosis-portal/internal/api/middleware" "omixlab.com/mosis-portal/internal/auth" "omixlab.com/mosis-portal/internal/config" "omixlab.com/mosis-portal/internal/database" "omixlab.com/mosis-portal/internal/storage" "omixlab.com/mosis-portal/internal/telemetry" "omixlab.com/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 telemetry service telemetryDB := cfg.StoragePath + "/telemetry.db" telemetrySvc, err := telemetry.New(telemetryDB) if err != nil { log.Fatalf("Failed to initialize telemetry service: %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) adminHandler := handlers.NewAdminHandler(db, store) telemetryHandler := handlers.NewTelemetryHandler(db, telemetrySvc) // 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) }) // Analytics r.Route("/{appID}/analytics", func(r chi.Router) { r.Get("/overview", telemetryHandler.GetAnalyticsOverview) r.Get("/events", telemetryHandler.GetAnalyticsEvents) }) // Crashes r.Route("/{appID}/crashes", func(r chi.Router) { r.Get("/", telemetryHandler.GetCrashes) r.Get("/{crashID}", telemetryHandler.GetCrash) r.Patch("/{crashID}", telemetryHandler.UpdateCrashStatus) }) }) // 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 endpoints (public - devices send events here) r.Route("/telemetry", func(r chi.Router) { r.Post("/events", telemetryHandler.RecordEvents) r.Post("/crash", telemetryHandler.RecordCrash) }) }) // Admin API routes (JSON responses) r.Route("/api/admin", func(r chi.Router) { r.Use(authMiddleware.RequireAuth) r.Get("/stats", adminHandler.Dashboard) r.Get("/review-queue", adminHandler.ReviewQueue) r.Get("/review/{versionID}", adminHandler.ReviewDetail) r.Post("/review/{versionID}/approve", adminHandler.ApproveVersion) r.Post("/review/{versionID}/reject", adminHandler.RejectVersion) r.Get("/review/{versionID}/validate", adminHandler.ValidatePackage) }) // 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 { webHandler.SetStorage(store) webHandler.SetTelemetry(telemetrySvc) 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) r.Get("/apps/{appID}/analytics", webHandler.AppAnalytics) r.Get("/apps/{appID}/crashes", webHandler.AppCrashes) // htmx partials r.Get("/partials/apps", webHandler.AppListPartial) // Admin pages (htmx UI) r.Get("/admin/review-queue", webHandler.AdminReviewQueue) r.Get("/admin/review/{versionID}", webHandler.AdminReviewDetail) r.Get("/admin/partials/review-queue", webHandler.AdminReviewQueuePartial) r.Post("/admin/review/{versionID}/approve", webHandler.AdminApprove) r.Post("/admin/review/{versionID}/reject", webHandler.AdminReject) r.Get("/admin/review/{versionID}/validate", webHandler.AdminValidate) }) // 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) }) } // Documentation site docsHandler, err := web.NewDocsHandler() if err != nil { log.Printf("Warning: Failed to initialize docs handler: %v", err) } else { r.Handle("/docs", docsHandler) r.Handle("/docs/*", docsHandler) } // 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 }