233 lines
7.6 KiB
Go
233 lines
7.6 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"
|
|
"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)
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|