diff --git a/src/db.rs b/src/db.rs index 198ac33..51ef3e6 100644 --- a/src/db.rs +++ b/src/db.rs @@ -122,54 +122,6 @@ pub async fn session_athlete(db: &PgPool, token: &str) -> Result Result> { - let hash = hash_token(token); - - let rows = sqlx::query( - r#" - SELECT DISTINCT - a.id, - a.intervals_athlete_id, - a.display_name, - a.access_token - FROM sessions s - JOIN athletes current_athlete - ON current_athlete.id = s.athlete_id - JOIN athletes a - ON a.access_token = current_athlete.access_token - WHERE s.token_hash = $1 - AND s.created_at > now() - interval '1 day' - ORDER BY a.display_name, a.intervals_athlete_id - "#, - ) - .bind(&hash) - .fetch_all(db) - .await?; - - use sqlx::Row; - - rows.into_iter() - .map(|row| { - Ok(SessionAthlete { - id: row.try_get("id")?, - intervals_athlete_id: row.try_get("intervals_athlete_id")?, - display_name: row.try_get("display_name")?, - access_token: row.try_get("access_token")?, - }) - }) - .collect() -} - /// Return the start time of the most recently imported activity /// for an athlete. /// diff --git a/src/intervals.rs b/src/intervals.rs index 6cf6a86..581ce8e 100644 --- a/src/intervals.rs +++ b/src/intervals.rs @@ -210,6 +210,24 @@ impl IntervalsClient { self.get_json(url).await } + /// List athletes the API-key caller is following or coaching, + /// including the caller. + /// + /// GET /api/v1/athletes + /// + /// Intervals.icu requires API-key authentication for this endpoint; + /// bearer/OAuth authentication is not supported. + pub async fn athletes(&self) -> Result { + if !matches!(self.authentication, Some(Authentication::ApiKey(_))) { + return Err(anyhow!( + "Intervals.icu GET /api/v1/athletes requires API-key authentication" + )); + } + + let url = self.api_url("athletes"); + self.get_json(url).await + } + /// Get a specific athlete. pub async fn athlete(&self, athlete_id: &str) -> Result { let url = self.api_url(&format!("athlete/{athlete_id}")); diff --git a/src/main.rs b/src/main.rs index 7c124ee..d7935da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -161,17 +161,20 @@ async fn sync_index( State(state): State, jar: axum_extra::extract::cookie::CookieJar, ) -> Result, (axum::http::StatusCode, String)> { - let cookie = jar - .get("county_session") - .ok_or_else(|| http_error(anyhow!("not authenticated")))?; - - let athletes = db::session_accessible_athletes( - &state.db, - cookie.value(), - ) + let session = current_session_athlete(&state, &jar) .await .map_err(http_error)?; + let client = + IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); + let athletes = client + .athletes() + .await + .map_err(http_error)?; + let athletes = athletes + .as_array() + .ok_or_else(|| http_error(anyhow!("athletes response is not an array")))?; + let mut html = String::from( r#" @@ -228,20 +231,35 @@ a { if athletes.is_empty() { html.push_str( r#"
- Keine für diesen Authentication-Token bekannten Athleten. + Keine für diesen API-Key zugänglichen Athleten.
"#, ); } else { html.push_str(&format!( r#"
- {} Athlet{} für diesen Authentication-Token bekannt. + {} zugängliche{} Athlet{} gefunden.
"#, athletes.len(), + if athletes.len() == 1 { "" } else { "n" }, if athletes.len() == 1 { "" } else { "en" }, )); for athlete in athletes { + let athlete_id = athlete + .get("id") + .and_then(Value::as_str) + .or_else(|| athlete.get("id").and_then(Value::as_i64).map(|_| "")) + .unwrap_or(""); + if athlete_id.is_empty() { + continue; + } + let display_name = athlete + .get("name") + .or_else(|| athlete.get("display_name")) + .and_then(Value::as_str) + .unwrap_or(athlete_id); + html.push_str(&format!( r#"

{}

@@ -252,9 +270,9 @@ a {

"#, - html::escape(&athlete.display_name), - html::escape(&athlete.intervals_athlete_id), - url_segment(&athlete.intervals_athlete_id), + html::escape(display_name), + html::escape(athlete_id), + url_segment(athlete_id), )); }