package main import ( "encoding/json" "log" "net/http" "os" "github.com/go-chi/chi/v5" chiMiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/joho/godotenv" "rideaware/internal/activity" "rideaware/internal/ai" "rideaware/internal/auth" "rideaware/internal/config" "rideaware/internal/equipment" "rideaware/internal/event" "rideaware/internal/export" "rideaware/internal/goal" "rideaware/internal/integration" "rideaware/internal/middleware" "rideaware/internal/nutrition" "rideaware/internal/stats" "rideaware/internal/templates" "rideaware/internal/user" "rideaware/internal/workout" "rideaware/pkg/database" ) func main() { godotenv.Load() // Initialize database database.Init() defer database.Close() // Run migrations if err := database.Migrate( &user.User{}, &user.Profile{}, &user.PasswordReset{}, &user.Session{}, &equipment.Equipment{}, &workout.Workout{}, &integration.OAuthConnection{}, &integration.OAuthState{}, &ai.AIRecommendation{}, &event.Event{}, ); err != nil { log.Fatalf("Failed to migrate database: %v", err) } // Initialize JWT config config.InitJWT() // Initialize OAuth config config.InitOAuth() r := chi.NewRouter() // Logging middleware r.Use(chiMiddleware.RequestID) r.Use(chiMiddleware.RealIP) r.Use(loggingMiddleware) r.Use(chiMiddleware.Recoverer) // CORS middleware r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{ "GET", "POST", "PUT", "DELETE", "OPTIONS", }, AllowedHeaders: []string{ "Accept", "Authorization", "Content-Type", }, ExposedHeaders: []string{"Link"}, MaxAge: 300, })) // Routes setupRoutes(r) port := os.Getenv("PORT") if port == "" { port = "5000" } log.Printf("🚀 Server running on port %s", port) log.Fatal(http.ListenAndServe(":"+port, r)) } // Logging middleware func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf( "[%s] %s %s", r.Method, r.RequestURI, r.RemoteAddr, ) next.ServeHTTP(w, r) }) } func setupRoutes(r *chi.Mux) { r.Options("/*", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") w.WriteHeader(http.StatusOK) }) // Public routes r.Get("/health", healthCheck) // Auth routes (public - no /api prefix needed for these) authHandler := auth.NewHandler() r.Post("/api/signup", authHandler.Signup) r.Post("/api/login", authHandler.Login) r.Post("/api/logout", authHandler.Logout) r.Post("/api/password-reset/request", authHandler.RequestPasswordReset) r.Post("/api/password-reset/confirm", authHandler.ConfirmPasswordReset) r.Post("/api/refresh-token", authHandler.RefreshToken) // OAuth callbacks (public - called by provider redirects) garminHandler := integration.NewGarminHandler() wahooHandler := integration.NewWahooHandler() r.Get("/api/garmin/callback", garminHandler.Callback) r.Get("/api/wahoo/callback", wahooHandler.Callback) // Protected routes authMiddleware := middleware.NewAuthMiddleware() r.Route("/api/protected", func(r chi.Router) { r.Use(authMiddleware.ProtectedRoute) // User routes userHandler := user.NewHandler() r.Get("/profile", userHandler.GetProfile) r.Put("/profile", userHandler.UpdateProfile) // Equipment routes equipmentHandler := equipment.NewHandler() r.Post("/equipment", equipmentHandler.CreateEquipment) r.Get("/equipment", equipmentHandler.GetEquipment) r.Put("/equipment", equipmentHandler.UpdateEquipment) r.Delete("/equipment", equipmentHandler.DeleteEquipment) // Equipment service tracking r.Post("/equipment/service", equipmentHandler.RecordService) r.Get("/equipment/service-status", equipmentHandler.GetServiceStatus) // Training zones r.Get("/zones", equipmentHandler.GetTrainingZones) // Workout routes workoutHandler := workout.NewHandler() r.Post("/workouts", workoutHandler.CreateWorkout) r.Get("/workouts", workoutHandler.GetWorkouts) r.Get("/workouts/month", workoutHandler.GetWorkoutsByMonth) r.Get("/workouts/equipment-stats", workoutHandler.GetEquipmentStats) r.Put("/workouts", workoutHandler.UpdateWorkout) r.Delete("/workouts", workoutHandler.DeleteWorkout) r.Post("/workouts/remove-duplicates", workoutHandler.RemoveDuplicates) r.Put("/workouts/reschedule", workoutHandler.RescheduleWorkout) r.Get("/workout-types", workoutHandler.GetWorkoutTypes) r.Post("/workouts/upload", workoutHandler.UploadWorkoutFile) // Activity import (FIT/TCX/GPX) activityHandler := activity.NewHandler() r.Post("/workouts/import", activityHandler.ImportActivity) // Workout export routes exportHandler := export.NewHandler() r.Get("/workouts/export/fit", exportHandler.ExportFIT) r.Get("/workouts/export/zwo", exportHandler.ExportZWO) // Garmin integration routes r.Get("/garmin/auth", garminHandler.StartAuth) r.Post("/workouts/push/garmin", garminHandler.PushWorkout) r.Get("/garmin/status", garminHandler.ConnectionStatus) r.Delete("/garmin/disconnect", garminHandler.Disconnect) // Wahoo integration routes r.Get("/wahoo/auth", wahooHandler.StartAuth) r.Post("/workouts/push/wahoo", wahooHandler.PushWorkout) r.Get("/wahoo/status", wahooHandler.ConnectionStatus) r.Delete("/wahoo/disconnect", wahooHandler.Disconnect) // Intervals.icu integration routes intervalsHandler := integration.NewIntervalsHandler() r.Post("/intervals/connect", intervalsHandler.SaveApiKey) r.Get("/intervals/status", intervalsHandler.ConnectionStatus) r.Delete("/intervals/disconnect", intervalsHandler.Disconnect) r.Post("/workouts/push/intervals", intervalsHandler.PushWorkout) r.Post("/intervals/sync", intervalsHandler.SyncActivities) // Stats routes statsHandler := stats.NewHandler() r.Get("/stats/summary", statsHandler.GetSummary) r.Get("/stats/weekly", statsHandler.GetWeeklyStats) r.Get("/stats/monthly", statsHandler.GetMonthlyStats) r.Get("/stats/personal-bests", statsHandler.GetPersonalBests) r.Get("/stats/training-load", statsHandler.GetTrainingLoad) r.Get("/stats/power-history", statsHandler.GetPowerHistory) // AI Training Plan routes aiHandler := ai.NewHandler() r.Post("/ai/generate", aiHandler.GenerateRecommendations) r.Post("/ai/schedule", aiHandler.ScheduleRecommendations) r.Get("/ai/history", aiHandler.GetRecommendationHistory) // Event routes eventHandler := event.NewHandler() r.Post("/events", eventHandler.CreateEvent) r.Get("/events", eventHandler.GetEvents) r.Get("/events/upcoming", eventHandler.GetUpcomingEvents) r.Put("/events", eventHandler.UpdateEvent) r.Delete("/events", eventHandler.DeleteEvent) r.Get("/event-types", eventHandler.GetEventTypes) // Nutrition routes nutritionHandler := nutrition.NewHandler() r.Get("/nutrition/targets", nutritionHandler.GetTargets) r.Get("/nutrition/weekly", nutritionHandler.GetWeekly) // Workout template routes templateHandler := templates.NewHandler() r.Get("/workout-templates", templateHandler.ListTemplates) r.Get("/workout-templates/detail", templateHandler.GetTemplate) r.Post("/workouts/from-template", templateHandler.CreateFromTemplate) // Goal routes goalHandler := goal.NewHandler() r.Post("/goals", goalHandler.CreateGoal) r.Get("/goals", goalHandler.GetGoals) r.Put("/goals", goalHandler.UpdateGoal) r.Delete("/goals", goalHandler.DeleteGoal) // Admin-only routes (require 'admin' role in addition to valid JWT) r.Route("/admin", func(r chi.Router) { r.Use(authMiddleware.RequireRole("admin")) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{ "status": "admin API operational", }) }) }) }) log.Println("✅ Routes registered successfully") } func healthCheck(w http.ResponseWriter, r *http.Request) { log.Println("📊 Health check called") w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }