Files
MosisService/portal/internal/api/router.go

197 lines
6.1 KiB
Go

// 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
}