anti cheat: don't trust the client, move trip completions to server
All checks were successful
pedestrian-simulator / build (push) Successful in 1m11s

This commit is contained in:
2026-01-14 17:17:58 -07:00
parent f0172afb1e
commit 16c6c9c074
5 changed files with 121 additions and 83 deletions

View File

@@ -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)