From 30542807b819138fbfb01cba3bdc0aa5a1ee35ec Mon Sep 17 00:00:00 2001 From: OpenAI Date: Thu, 13 Aug 2026 10:03:35 +0000 Subject: [PATCH] allow importing accessible foreign athletes --- src/db.rs | 35 ++++++++++++++++++++++++ src/main.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/db.rs b/src/db.rs index 760e9b6..51ef3e6 100644 --- a/src/db.rs +++ b/src/db.rs @@ -232,6 +232,41 @@ pub async fn upsert_oauth_athlete( Ok(row.try_get("id")?) } + +/// Ensure that an athlete accessible through the current session exists +/// locally so activities can reference it. The session credential itself is +/// intentionally not stored here; callers continue to use their authenticated +/// IntervalsClient for API requests. +pub async fn ensure_local_athlete( + db: &PgPool, + intervals_athlete_id: &str, + display_name: &str, +) -> Result { + let row = sqlx::query( + r#" + INSERT INTO athletes ( + intervals_athlete_id, + display_name, + access_token, + scopes + ) + VALUES ($1, $2, '', 'SESSION_ACCESS') + ON CONFLICT (intervals_athlete_id) + DO UPDATE SET + display_name = EXCLUDED.display_name, + updated_at = now() + RETURNING id + "#, + ) + .bind(intervals_athlete_id) + .bind(display_name) + .fetch_one(db) + .await?; + + use sqlx::Row; + Ok(row.try_get("id")?) +} + pub async fn ensure_activity( tx: &mut Transaction<'_, Postgres>, athlete_id: i64, diff --git a/src/main.rs b/src/main.rs index 758274d..bd77d9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -254,16 +254,20 @@ async fn sync_athlete( * tracked per local athlete row, so they're resolved against * athlete_id, not the session. */ - let target_athlete = local_athlete(&state.db, &athlete_id) + let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); + + let target_display_name = ensure_accessible_athlete( + &state, + &client, + &athlete_id, + ) + .await + .map_err(http_error)?; + + let target_athlete_id = local_athlete(&state.db, &athlete_id) .await - .map_err(http_error)?; - - let target_athlete_id = target_athlete.as_ref().map(|(id, _)| *id); - - let target_display_name = target_athlete - .as_ref() - .map(|(_, name)| name.clone()) - .unwrap_or_else(|| athlete_id.clone()); + .map_err(http_error)? + .map(|(id, _)| id); /* * Default: @@ -285,8 +289,6 @@ async fn sync_athlete( let newest = parse_datetime_local(query.newest.as_deref()).unwrap_or_else(|| Utc::now()); - let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); - let activities = client .activities( &athlete_id, @@ -714,6 +716,14 @@ async fn sync_import_activity( let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); + ensure_accessible_athlete( + &state, + &client, + &athlete_id, + ) + .await + .map_err(http_error)?; + match process_activity_with_client( state.clone(), athlete_id.clone(), @@ -812,6 +822,14 @@ async fn sync_import_visible( let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); + ensure_accessible_athlete( + &state, + &client, + &athlete_id, + ) + .await + .map_err(http_error)?; + let target_athlete_id = local_athlete(&state.db, &athlete_id) .await .map_err(http_error)? @@ -894,6 +912,42 @@ Aktivitäten werden importiert … ))) } +/// Verify that the current authentication token can access an athlete and +/// ensure that the athlete exists in the local database. This is required for +/// importing activities belonging to athletes that have never authenticated +/// with county-sprints themselves. +async fn ensure_accessible_athlete( + state: &AppState, + client: &IntervalsClient, + athlete_id: &str, +) -> Result { + let athlete = client + .athlete(athlete_id) + .await + .with_context(|| { + format!( + "cannot access Intervals.icu athlete {} with current authentication token", + athlete_id + ) + })?; + + let display_name = athlete + .get("name") + .or_else(|| athlete.get("display_name")) + .and_then(Value::as_str) + .unwrap_or(athlete_id) + .to_string(); + + db::ensure_local_athlete( + &state.db, + athlete_id, + &display_name, + ) + .await?; + + Ok(display_name) +} + async fn activity_exists(db: &PgPool, athlete_id: i64, activity_id: &str) -> Result { let row = sqlx::query( r#"