anti cheat: don't trust the client, move trip completions to server
All checks were successful
pedestrian-simulator / build (push) Successful in 1m11s
All checks were successful
pedestrian-simulator / build (push) Successful in 1m11s
This commit is contained in:
@@ -134,5 +134,13 @@ func createTables() {
|
||||
// Migration for KML description
|
||||
db.Exec("ALTER TABLE kml_metadata ADD COLUMN IF NOT EXISTS description TEXT")
|
||||
|
||||
// Migration for trips metadata
|
||||
db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS trip_type ENUM('address', 'kml') DEFAULT 'address'")
|
||||
db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS route_name TEXT")
|
||||
db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS start_address TEXT")
|
||||
db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS end_address TEXT")
|
||||
db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS kml_id INT")
|
||||
db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS total_distance DOUBLE")
|
||||
|
||||
log.Println("Database tables initialized")
|
||||
}
|
||||
|
||||
@@ -687,7 +687,10 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Fetch completed trips
|
||||
rows, err := db.Query(`
|
||||
SELECT ct.id, ct.trip_type, ct.route_name, ct.start_address, ct.end_address, ct.kml_id, ct.distance, ct.completed_at,
|
||||
SELECT ct.id, ct.trip_type, ct.route_name,
|
||||
COALESCE(ct.start_address, '') as start_address,
|
||||
COALESCE(ct.end_address, '') as end_address,
|
||||
ct.kml_id, ct.distance, ct.completed_at,
|
||||
m.filename, m.user_id, u.display_name, m.description,
|
||||
COALESCE((SELECT SUM(vote) FROM kml_votes WHERE kml_id = m.id), 0) as votes
|
||||
FROM completed_trips ct
|
||||
@@ -731,51 +734,3 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// HandleTripComplete records a completed trip
|
||||
func HandleTripComplete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := getUserID(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
RouteName string `json:"route_name"`
|
||||
StartAddress string `json:"start_address"`
|
||||
EndAddress string `json:"end_address"`
|
||||
KmlFilename string `json:"kml_filename"`
|
||||
KmlOwnerID string `json:"kml_owner_id"`
|
||||
Distance float64 `json:"distance"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var kmlID interface{} = nil
|
||||
if req.Type == "kml" {
|
||||
var id int
|
||||
err := db.QueryRow("SELECT id FROM kml_metadata WHERE user_id = ? AND filename = ?", req.KmlOwnerID, req.KmlFilename).Scan(&id)
|
||||
if err == nil {
|
||||
kmlID = id
|
||||
}
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO completed_trips (user_id, trip_type, route_name, start_address, end_address, kml_id, distance)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, userID, req.Type, req.RouteName, req.StartAddress, req.EndAddress, kmlID, req.Distance)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to save completed trip: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -96,9 +96,16 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
var metadata TripState
|
||||
if err := json.NewDecoder(r.Body).Decode(&metadata); err != nil {
|
||||
// Fallback for legacy calls or if no metadata is sent
|
||||
// But we expect metadata now
|
||||
fmt.Printf("[API Trip] Warning: Failed to decode metadata: %v\n", err)
|
||||
}
|
||||
|
||||
userID, _ := getUserID(r.Context())
|
||||
sm := getStepManager(userID)
|
||||
sm.StartNewTrip()
|
||||
sm.StartNewTrip(metadata)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
@@ -140,9 +147,6 @@ func main() {
|
||||
// 7. User Profile Endpoint
|
||||
http.HandleFunc("/api/user/profile", RequireAuth(HandleUserProfile))
|
||||
|
||||
// 8. Trip Completion Endpoint
|
||||
http.HandleFunc("/api/trip/complete", RequireAuth(HandleTripComplete))
|
||||
|
||||
// 9. Start Server
|
||||
binding := "0.0.0.0:8080"
|
||||
fmt.Printf("Server starting on http://%s\n", binding)
|
||||
|
||||
@@ -7,11 +7,21 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const METERS_PER_STEP = 0.762 // Match frontend
|
||||
|
||||
type TripState struct {
|
||||
StartDate string `json:"start_date"` // YYYY-MM-DD
|
||||
StartTime time.Time `json:"start_time"` // Exact start time
|
||||
StartDayInitialSteps int `json:"start_day_initial_steps"` // Steps on the tracker when trip started
|
||||
DailyCache map[string]int `json:"daily_cache"` // Cache of steps for past days
|
||||
|
||||
// Trip Metadata
|
||||
TripType string `json:"trip_type"`
|
||||
RouteName string `json:"route_name"`
|
||||
StartAddress string `json:"start_address"`
|
||||
EndAddress string `json:"end_address"`
|
||||
KmlID *int `json:"kml_id"`
|
||||
TotalDistance float64 `json:"total_distance"` // in km
|
||||
}
|
||||
|
||||
type StepManager struct {
|
||||
@@ -47,13 +57,20 @@ func (sm *StepManager) LoadTripState() error {
|
||||
|
||||
func (sm *StepManager) loadTripStateLocked() error {
|
||||
var startTime time.Time
|
||||
var kmlID sql.NullInt64
|
||||
err := db.QueryRow(`
|
||||
SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time
|
||||
SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time,
|
||||
trip_type, route_name, start_address, end_address, kml_id, total_distance
|
||||
FROM trips WHERE user_id = ?
|
||||
`, sm.userID).Scan(
|
||||
&sm.tripState.StartDate, &startTime, &sm.tripState.StartDayInitialSteps,
|
||||
&sm.previousTotalSteps, &sm.targetTotalSteps, &sm.lastSyncTime, &sm.nextSyncTime,
|
||||
&sm.tripState.TripType, &sm.tripState.RouteName, &sm.tripState.StartAddress, &sm.tripState.EndAddress, &kmlID, &sm.tripState.TotalDistance,
|
||||
)
|
||||
if err == nil && kmlID.Valid {
|
||||
id := int(kmlID.Int64)
|
||||
sm.tripState.KmlID = &id
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Initialize with defaults if no trip exists
|
||||
@@ -94,8 +111,9 @@ func (sm *StepManager) SaveTripState() {
|
||||
|
||||
func (sm *StepManager) saveTripStateLocked() {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO trips (user_id, start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO trips (user_id, start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time,
|
||||
trip_type, route_name, start_address, end_address, kml_id, total_distance)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
start_date = VALUES(start_date),
|
||||
start_time = VALUES(start_time),
|
||||
@@ -103,9 +121,16 @@ func (sm *StepManager) saveTripStateLocked() {
|
||||
previous_total_steps = VALUES(previous_total_steps),
|
||||
target_total_steps = VALUES(target_total_steps),
|
||||
last_sync_time = VALUES(last_sync_time),
|
||||
next_sync_time = VALUES(next_sync_time)
|
||||
next_sync_time = VALUES(next_sync_time),
|
||||
trip_type = VALUES(trip_type),
|
||||
route_name = VALUES(route_name),
|
||||
start_address = VALUES(start_address),
|
||||
end_address = VALUES(end_address),
|
||||
kml_id = VALUES(kml_id),
|
||||
total_distance = VALUES(total_distance)
|
||||
`, sm.userID, sm.tripState.StartDate, sm.tripState.StartTime, sm.tripState.StartDayInitialSteps,
|
||||
sm.previousTotalSteps, sm.targetTotalSteps, sm.lastSyncTime, sm.nextSyncTime)
|
||||
sm.previousTotalSteps, sm.targetTotalSteps, sm.lastSyncTime, sm.nextSyncTime,
|
||||
sm.tripState.TripType, sm.tripState.RouteName, sm.tripState.StartAddress, sm.tripState.EndAddress, sm.tripState.KmlID, sm.tripState.TotalDistance)
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving trip state: %v\n", err)
|
||||
}
|
||||
@@ -123,7 +148,7 @@ func (sm *StepManager) saveTripStateLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *StepManager) StartNewTrip() {
|
||||
func (sm *StepManager) StartNewTrip(metadata TripState) {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
now := time.Now()
|
||||
@@ -137,6 +162,12 @@ func (sm *StepManager) StartNewTrip() {
|
||||
StartTime: now,
|
||||
StartDayInitialSteps: initialSteps,
|
||||
DailyCache: make(map[string]int),
|
||||
TripType: metadata.TripType,
|
||||
RouteName: metadata.RouteName,
|
||||
StartAddress: metadata.StartAddress,
|
||||
EndAddress: metadata.EndAddress,
|
||||
KmlID: metadata.KmlID,
|
||||
TotalDistance: metadata.TotalDistance,
|
||||
}
|
||||
// On new trip, previous total is 0
|
||||
sm.previousTotalSteps = 0
|
||||
@@ -244,8 +275,6 @@ func (sm *StepManager) performSync(interval time.Duration) {
|
||||
sm.lastSyncTime = time.Now()
|
||||
sm.nextSyncTime = time.Now().Add(interval)
|
||||
|
||||
// Save final state with explicit NOW() for last_sync_time to ensure DB consistency
|
||||
// next_sync_time is also calculated in SQL relative to the same NOW()
|
||||
_, err = db.Exec(`
|
||||
UPDATE trips SET
|
||||
last_sync_time = NOW(),
|
||||
@@ -258,9 +287,42 @@ func (sm *StepManager) performSync(interval time.Duration) {
|
||||
fmt.Printf("Error saving sync completion: %v\n", err)
|
||||
}
|
||||
|
||||
// Check for Trip Completion
|
||||
if sm.tripState.TotalDistance > 0 {
|
||||
currentDistance := float64(sm.targetTotalSteps) * METERS_PER_STEP / 1000.0
|
||||
if currentDistance >= sm.tripState.TotalDistance {
|
||||
fmt.Printf("[StepManager] Trip Fulfillment Detected! %.2f / %.2f km\n", currentDistance, sm.tripState.TotalDistance)
|
||||
sm.recordTripCompletionLocked()
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Sync Complete. Total Trip Steps: %d\n", sm.targetTotalSteps)
|
||||
}
|
||||
|
||||
func (sm *StepManager) recordTripCompletionLocked() {
|
||||
// 1. Insert into completed_trips
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO completed_trips (user_id, trip_type, route_name, start_address, end_address, kml_id, distance)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, sm.userID, sm.tripState.TripType, sm.tripState.RouteName, sm.tripState.StartAddress, sm.tripState.EndAddress, sm.tripState.KmlID, sm.tripState.TotalDistance)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("[StepManager] Error recording completion: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Delete from active trips
|
||||
_, err = db.Exec("DELETE FROM trips WHERE user_id = ?", sm.userID)
|
||||
if err != nil {
|
||||
fmt.Printf("[StepManager] Error clearing active trip: %v\n", err)
|
||||
}
|
||||
|
||||
// 3. Reset local state
|
||||
sm.tripState.StartDate = ""
|
||||
sm.previousTotalSteps = 0
|
||||
sm.targetTotalSteps = 0
|
||||
}
|
||||
|
||||
// calculateSmoothedTokenAt returns the interpolated step count at a given time
|
||||
func (sm *StepManager) calculateSmoothedTokenAt(t time.Time) int {
|
||||
totalDuration := sm.nextSyncTime.Sub(sm.lastSyncTime)
|
||||
|
||||
Reference in New Issue
Block a user