allow importing accessible foreign athletes

This commit is contained in:
OpenAI 2026-08-13 10:03:35 +00:00 committed by Jonas Rabenstein
commit 30542807b8
2 changed files with 99 additions and 10 deletions

View file

@ -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<i64> {
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,

View file

@ -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<String> {
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<bool> {
let row = sqlx::query(
r#"