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
|
// Update leaderboard best score/time for opted-in users when this replay
|
||||||
// beats their existing best. Only classic mode counts for the leaderboard.
|
// 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" {
|
if header.mode == "Classic" {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"UPDATE leaderboard
|
r#"UPDATE leaderboard
|
||||||
@@ -170,7 +172,7 @@ pub async fn upload(
|
|||||||
)"#,
|
)"#,
|
||||||
header.final_score,
|
header.final_score,
|
||||||
header.time_seconds,
|
header.time_seconds,
|
||||||
header.recorded_at,
|
received_at,
|
||||||
user.user_id,
|
user.user_id,
|
||||||
header.final_score,
|
header.final_score,
|
||||||
header.final_score,
|
header.final_score,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
use axum::{Json, extract::State};
|
use axum::{Json, extract::State};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use solitaire_sync::{
|
use solitaire_sync::{
|
||||||
AchievementRecord, PlayerProgress, StatsSnapshot, SyncPayload, SyncResponse, merge,
|
AchievementRecord, PlayerProgress, StatsSnapshot, SyncPayload, SyncResponse, merge,
|
||||||
@@ -142,10 +143,19 @@ pub async fn pull(
|
|||||||
pub async fn push(
|
pub async fn push(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
Json(client_payload): Json<SyncPayload>,
|
Json(mut client_payload): Json<SyncPayload>,
|
||||||
) -> Result<Json<SyncResponse>, AppError> {
|
) -> Result<Json<SyncResponse>, AppError> {
|
||||||
// Reject payloads that claim to belong to a different user.
|
let user_uuid: Uuid = user
|
||||||
if client_payload.user_id.to_string() != user.user_id {
|
.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()));
|
return Err(AppError::BadRequest("user_id mismatch".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user