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:
@@ -969,9 +969,21 @@ async function calculateAndStartRoute(startAddress, endAddress, isRestoring = fa
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Initialize step reference
|
// Initialize step reference
|
||||||
|
// Initialize/Reset Server Trip
|
||||||
// Only start a new trip on server if this is a fresh start, not a restore
|
// Only start a new trip on server if this is a fresh start, not a restore
|
||||||
if (!isRestoring) {
|
if (!isRestoring) {
|
||||||
fetch('/api/trip', { method: 'POST' }).catch(console.error);
|
const tripMetadata = {
|
||||||
|
trip_type: 'address',
|
||||||
|
route_name: `${leg.start_address} to ${leg.end_address}`,
|
||||||
|
start_address: leg.start_address,
|
||||||
|
end_address: leg.end_address,
|
||||||
|
total_distance: routeTotalDistance / 1000 // In km
|
||||||
|
};
|
||||||
|
fetch('/api/trip', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(tripMetadata)
|
||||||
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset display steps for visual consistency
|
// Reset display steps for visual consistency
|
||||||
@@ -2126,7 +2138,17 @@ function startFromCustomRoute(pathData, routeName, isRestoring = false, markers
|
|||||||
|
|
||||||
// Initialize/Reset Server Trip
|
// Initialize/Reset Server Trip
|
||||||
if (!isRestoring) {
|
if (!isRestoring) {
|
||||||
fetch('/api/trip', { method: 'POST' }).catch(console.error);
|
const tripMetadata = {
|
||||||
|
trip_type: 'kml',
|
||||||
|
route_name: routeName,
|
||||||
|
kml_id: currentKmlFile ? currentKmlFile.id : null,
|
||||||
|
total_distance: routeTotalDistance / 1000 // In km
|
||||||
|
};
|
||||||
|
fetch('/api/trip', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(tripMetadata)
|
||||||
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset display steps
|
// Reset display steps
|
||||||
@@ -2274,26 +2296,13 @@ async function triggerCelebration(finalDistance) {
|
|||||||
overlay.classList.add('active');
|
overlay.classList.add('active');
|
||||||
createConfetti();
|
createConfetti();
|
||||||
|
|
||||||
// Report to backend
|
// Clean up local state
|
||||||
if (currentTripDetails) {
|
localStorage.removeItem(LOCATION_STORAGE);
|
||||||
try {
|
currentTripDetails = null;
|
||||||
await fetch('/api/trip/complete', {
|
masterPath = [];
|
||||||
method: 'POST',
|
routeTotalDistance = 0;
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// We keep window.hasCelebrated = true to prevent immediate re-trigger
|
||||||
body: JSON.stringify({
|
// but starting a new trip will reset it.
|
||||||
type: currentTripDetails.type,
|
|
||||||
route_name: currentTripDetails.route_name,
|
|
||||||
start_address: currentTripDetails.start_address || '',
|
|
||||||
end_address: currentTripDetails.end_address || '',
|
|
||||||
kml_filename: currentTripDetails.kml_filename || '',
|
|
||||||
kml_owner_id: currentTripDetails.kml_owner_id || '',
|
|
||||||
distance: currentTripDetails.distance
|
|
||||||
})
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to record trip completion:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createConfetti() {
|
function createConfetti() {
|
||||||
|
|||||||
@@ -134,5 +134,13 @@ func createTables() {
|
|||||||
// Migration for KML description
|
// Migration for KML description
|
||||||
db.Exec("ALTER TABLE kml_metadata ADD COLUMN IF NOT EXISTS description TEXT")
|
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")
|
log.Println("Database tables initialized")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,7 +687,10 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Fetch completed trips
|
// Fetch completed trips
|
||||||
rows, err := db.Query(`
|
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,
|
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
|
COALESCE((SELECT SUM(vote) FROM kml_votes WHERE kml_id = m.id), 0) as votes
|
||||||
FROM completed_trips ct
|
FROM completed_trips ct
|
||||||
@@ -731,51 +734,3 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleTripComplete records a completed trip
|
// 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
|
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())
|
userID, _ := getUserID(r.Context())
|
||||||
sm := getStepManager(userID)
|
sm := getStepManager(userID)
|
||||||
sm.StartNewTrip()
|
sm.StartNewTrip(metadata)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -140,9 +147,6 @@ func main() {
|
|||||||
// 7. User Profile Endpoint
|
// 7. User Profile Endpoint
|
||||||
http.HandleFunc("/api/user/profile", RequireAuth(HandleUserProfile))
|
http.HandleFunc("/api/user/profile", RequireAuth(HandleUserProfile))
|
||||||
|
|
||||||
// 8. Trip Completion Endpoint
|
|
||||||
http.HandleFunc("/api/trip/complete", RequireAuth(HandleTripComplete))
|
|
||||||
|
|
||||||
// 9. Start Server
|
// 9. Start Server
|
||||||
binding := "0.0.0.0:8080"
|
binding := "0.0.0.0:8080"
|
||||||
fmt.Printf("Server starting on http://%s\n", binding)
|
fmt.Printf("Server starting on http://%s\n", binding)
|
||||||
|
|||||||
@@ -7,11 +7,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const METERS_PER_STEP = 0.762 // Match frontend
|
||||||
|
|
||||||
type TripState struct {
|
type TripState struct {
|
||||||
StartDate string `json:"start_date"` // YYYY-MM-DD
|
StartDate string `json:"start_date"` // YYYY-MM-DD
|
||||||
StartTime time.Time `json:"start_time"` // Exact start time
|
StartTime time.Time `json:"start_time"` // Exact start time
|
||||||
StartDayInitialSteps int `json:"start_day_initial_steps"` // Steps on the tracker when trip started
|
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
|
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 {
|
type StepManager struct {
|
||||||
@@ -47,13 +57,20 @@ func (sm *StepManager) LoadTripState() error {
|
|||||||
|
|
||||||
func (sm *StepManager) loadTripStateLocked() error {
|
func (sm *StepManager) loadTripStateLocked() error {
|
||||||
var startTime time.Time
|
var startTime time.Time
|
||||||
|
var kmlID sql.NullInt64
|
||||||
err := db.QueryRow(`
|
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 = ?
|
FROM trips WHERE user_id = ?
|
||||||
`, sm.userID).Scan(
|
`, sm.userID).Scan(
|
||||||
&sm.tripState.StartDate, &startTime, &sm.tripState.StartDayInitialSteps,
|
&sm.tripState.StartDate, &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, &kmlID, &sm.tripState.TotalDistance,
|
||||||
)
|
)
|
||||||
|
if err == nil && kmlID.Valid {
|
||||||
|
id := int(kmlID.Int64)
|
||||||
|
sm.tripState.KmlID = &id
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// Initialize with defaults if no trip exists
|
// Initialize with defaults if no trip exists
|
||||||
@@ -94,8 +111,9 @@ func (sm *StepManager) SaveTripState() {
|
|||||||
|
|
||||||
func (sm *StepManager) saveTripStateLocked() {
|
func (sm *StepManager) saveTripStateLocked() {
|
||||||
_, err := db.Exec(`
|
_, 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)
|
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 (?, ?, ?, ?, ?, ?, ?, ?)
|
trip_type, route_name, start_address, end_address, kml_id, total_distance)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
start_date = VALUES(start_date),
|
start_date = VALUES(start_date),
|
||||||
start_time = VALUES(start_time),
|
start_time = VALUES(start_time),
|
||||||
@@ -103,9 +121,16 @@ func (sm *StepManager) saveTripStateLocked() {
|
|||||||
previous_total_steps = VALUES(previous_total_steps),
|
previous_total_steps = VALUES(previous_total_steps),
|
||||||
target_total_steps = VALUES(target_total_steps),
|
target_total_steps = VALUES(target_total_steps),
|
||||||
last_sync_time = VALUES(last_sync_time),
|
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.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 {
|
if err != nil {
|
||||||
fmt.Printf("Error saving trip state: %v\n", err)
|
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()
|
sm.mu.Lock()
|
||||||
defer sm.mu.Unlock()
|
defer sm.mu.Unlock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -137,6 +162,12 @@ func (sm *StepManager) StartNewTrip() {
|
|||||||
StartTime: now,
|
StartTime: now,
|
||||||
StartDayInitialSteps: initialSteps,
|
StartDayInitialSteps: initialSteps,
|
||||||
DailyCache: make(map[string]int),
|
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
|
// On new trip, previous total is 0
|
||||||
sm.previousTotalSteps = 0
|
sm.previousTotalSteps = 0
|
||||||
@@ -244,8 +275,6 @@ func (sm *StepManager) performSync(interval time.Duration) {
|
|||||||
sm.lastSyncTime = time.Now()
|
sm.lastSyncTime = time.Now()
|
||||||
sm.nextSyncTime = time.Now().Add(interval)
|
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(`
|
_, err = db.Exec(`
|
||||||
UPDATE trips SET
|
UPDATE trips SET
|
||||||
last_sync_time = NOW(),
|
last_sync_time = NOW(),
|
||||||
@@ -258,9 +287,42 @@ func (sm *StepManager) performSync(interval time.Duration) {
|
|||||||
fmt.Printf("Error saving sync completion: %v\n", err)
|
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)
|
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
|
// calculateSmoothedTokenAt returns the interpolated step count at a given time
|
||||||
func (sm *StepManager) calculateSmoothedTokenAt(t time.Time) int {
|
func (sm *StepManager) calculateSmoothedTokenAt(t time.Time) int {
|
||||||
totalDuration := sm.nextSyncTime.Sub(sm.lastSyncTime)
|
totalDuration := sm.nextSyncTime.Sub(sm.lastSyncTime)
|
||||||
|
|||||||
Reference in New Issue
Block a user