135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"log"
|
||
|
|
"net/http"
|
||
|
|
"sync"
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
stepManagers = make(map[string]*StepManager) // userID -> StepManager
|
||
|
|
smMutex sync.RWMutex
|
||
|
|
)
|
||
|
|
|
||
|
|
// getOrCreateStepManager retrieves or creates a StepManager for the given user
|
||
|
|
func getOrCreateStepManager(userID string) *StepManager {
|
||
|
|
smMutex.RLock()
|
||
|
|
sm, exists := stepManagers[userID]
|
||
|
|
smMutex.RUnlock()
|
||
|
|
|
||
|
|
if exists {
|
||
|
|
return sm
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create new StepManager for this user
|
||
|
|
smMutex.Lock()
|
||
|
|
defer smMutex.Unlock()
|
||
|
|
|
||
|
|
// Double-check it wasn't created while we were waiting for the lock
|
||
|
|
if sm, exists := stepManagers[userID]; exists {
|
||
|
|
return sm
|
||
|
|
}
|
||
|
|
|
||
|
|
sm = NewStepManager(userID)
|
||
|
|
stepManagers[userID] = sm
|
||
|
|
return sm
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
// Initialize components
|
||
|
|
InitFitbit()
|
||
|
|
InitUserRegistry()
|
||
|
|
InitVoteRegistry()
|
||
|
|
|
||
|
|
// 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 := getOrCreateStepManager(userID)
|
||
|
|
|
||
|
|
status := sm.GetStatus()
|
||
|
|
|
||
|
|
// Add user info to status
|
||
|
|
user, exists := userRegistry.GetUser(userID)
|
||
|
|
if exists && 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 (exists=%v)\n", userID, exists)
|
||
|
|
}
|
||
|
|
|
||
|
|
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 := getOrCreateStepManager(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 := getOrCreateStepManager(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 := getOrCreateStepManager(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, nil))
|
||
|
|
}
|