Files
rideaware-api/cmd/server/main.go
2026-05-17 20:39:47 -05:00

260 lines
8.0 KiB
Go

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"))
}