package main import ( "encoding/json" "fmt" "log" "net/http" "os" "time" _ "github.com/go-sql-driver/mysql" ) // RecoveryMiddleware catches panics and returns 500 func RecoveryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf("[PANIC] %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } // getStepManager creates a new StepManager for the given user, loading state from DB func getStepManager(userID string) *StepManager { return NewStepManager(userID) } func initTimezone() { tz := os.Getenv("TZ") if tz != "" { loc, err := time.LoadLocation(tz) if err != nil { log.Printf("Error loading timezone %s: %v. Using UTC.", tz, err) return } time.Local = loc log.Printf("Timezone set to %s", tz) } else { log.Printf("TZ environment variable not set, using system default (usually UTC)") } log.Printf("Current system time: %s", time.Now().Format(time.RFC1123Z)) } func main() { // Initialize components initTimezone() InitDB() InitFitbit() // 1. Serve Static Files (Frontend) fs := http.FileServer(http.Dir("frontend")) http.Handle("/", fs) // 2. API Endpoints (all require authentication) http.HandleFunc("/api/status", RequireAuth(func(w http.ResponseWriter, r *http.Request) { userID, _ := getUserID(r.Context()) sm := getStepManager(userID) status := sm.GetStatus() // Add user info to status user, err := GetUser(userID) if err == nil && user != nil { status["user"] = map[string]string{ "displayName": user.DisplayName, "avatarUrl": user.AvatarURL, } } else { fmt.Printf("[API Status] WARNING: User info not found for ID: %s (err=%v)\n", userID, err) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(status) })) http.HandleFunc("/api/refresh", RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID, _ := getUserID(r.Context()) sm := getStepManager(userID) sm.Sync() w.WriteHeader(http.StatusOK) })) http.HandleFunc("/api/trip", RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID, _ := getUserID(r.Context()) sm := getStepManager(userID) sm.StartNewTrip() w.WriteHeader(http.StatusOK) })) http.HandleFunc("/api/drain", RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID, _ := getUserID(r.Context()) sm := getStepManager(userID) go sm.Drain() // Async so we don't block w.WriteHeader(http.StatusOK) })) // 3. KML Management Endpoints http.HandleFunc("/api/kml/upload", RequireAuth(HandleKMLUpload)) http.HandleFunc("/api/kml/list", RequireAuth(HandleKMLList)) http.HandleFunc("/api/kml/privacy", RequireAuth(HandleKMLPrivacyToggle)) http.HandleFunc("/api/kml/vote", RequireAuth(HandleKMLVote)) http.HandleFunc("/api/kml/delete", RequireAuth(HandleKMLDelete)) http.HandleFunc("/api/kml/download", RequireAuth(HandleKMLDownload)) // 4. Fitbit OAuth Endpoints http.HandleFunc("/auth/fitbit", HandleFitbitAuth) http.HandleFunc("/auth/callback", HandleFitbitCallback) // 5. Logout Endpoint http.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) { session, err := GetSessionFromRequest(r) if err == nil { DeleteSession(session.Token) } ClearSessionCookie(w) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) }) // 6. Start Server binding := "0.0.0.0:8080" fmt.Printf("Server starting on http://%s\n", binding) log.Fatal(http.ListenAndServe(binding, RecoveryMiddleware(http.DefaultServeMux))) }