fix(server): accept nil user_id placeholder in push; use received_at for leaderboard (#73, #74)
Build and Deploy / build-and-push (push) Successful in 3m37s
Build and Deploy / build-and-push (push) Successful in 3m37s
- sync.rs: replace Uuid::nil() placeholder with the authenticated user's real UUID before the mismatch check so desktop client pushes no longer fail with 400 user_id mismatch (#73) - replays.rs: use server-computed received_at instead of client-supplied header.recorded_at when updating leaderboard recorded_at to prevent timestamp spoofing (#74) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -156,6 +156,8 @@ pub async fn upload(
|
||||
|
||||
// Update leaderboard best score/time for opted-in users when this replay
|
||||
// beats their existing best. Only classic mode counts for the leaderboard.
|
||||
// Use `received_at` (server-computed) rather than `header.recorded_at`
|
||||
// (client-supplied) so clients cannot spoof the timestamp.
|
||||
if header.mode == "Classic" {
|
||||
sqlx::query!(
|
||||
r#"UPDATE leaderboard
|
||||
@@ -170,7 +172,7 @@ pub async fn upload(
|
||||
)"#,
|
||||
header.final_score,
|
||||
header.time_seconds,
|
||||
header.recorded_at,
|
||||
received_at,
|
||||
user.user_id,
|
||||
header.final_score,
|
||||
header.final_score,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use axum::{Json, extract::State};
|
||||
use chrono::Utc;
|
||||
use sqlx::SqlitePool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use solitaire_sync::{
|
||||
AchievementRecord, PlayerProgress, StatsSnapshot, SyncPayload, SyncResponse, merge,
|
||||
@@ -142,10 +143,19 @@ pub async fn pull(
|
||||
pub async fn push(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
Json(client_payload): Json<SyncPayload>,
|
||||
Json(mut client_payload): Json<SyncPayload>,
|
||||
) -> Result<Json<SyncResponse>, AppError> {
|
||||
// Reject payloads that claim to belong to a different user.
|
||||
if client_payload.user_id.to_string() != user.user_id {
|
||||
let user_uuid: Uuid = user
|
||||
.user_id
|
||||
.parse()
|
||||
.map_err(|_| AppError::Internal("invalid user_id UUID in JWT".into()))?;
|
||||
|
||||
// The desktop client always sends Uuid::nil() as a placeholder for the
|
||||
// authenticated user's real ID (see build_payload docstring). Replace it
|
||||
// here. Reject payloads that explicitly claim a different user's identity.
|
||||
if client_payload.user_id == Uuid::nil() {
|
||||
client_payload.user_id = user_uuid;
|
||||
} else if client_payload.user_id != user_uuid {
|
||||
return Err(AppError::BadRequest("user_id mismatch".into()));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user