sync: show the target athlete's name, not the session's own
/sync/{athlete_id} always rendered 'Sync: <session.display_name>',
even when athlete_id (now possibly a foreign athlete, see the
previous 'allow syncing athletes other than the current session'
change) differed from the session's own athlete. The header now
This commit is contained in:
parent
660fc1783e
commit
b26486c110
3 changed files with 49 additions and 61 deletions
48
src/db.rs
48
src/db.rs
|
|
@ -122,54 +122,6 @@ pub async fn session_athlete(db: &PgPool, token: &str) -> Result<Option<SessionA
|
|||
}))
|
||||
}
|
||||
|
||||
/// Return all locally known athletes that use the same credential as the
|
||||
/// current session.
|
||||
///
|
||||
/// Intervals.icu does not expose an API endpoint that enumerates every
|
||||
/// athlete a credential can access. Therefore this is the set of athletes
|
||||
/// already known to this application for the credential. The owner of the
|
||||
/// credential is included naturally because the login creates that row.
|
||||
pub async fn session_accessible_athletes(
|
||||
db: &PgPool,
|
||||
token: &str,
|
||||
) -> Result<Vec<SessionAthlete>> {
|
||||
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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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<Value> {
|
||||
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<Value> {
|
||||
let url = self.api_url(&format!("athlete/{athlete_id}"));
|
||||
|
|
|
|||
44
src/main.rs
44
src/main.rs
|
|
@ -161,17 +161,20 @@ async fn sync_index(
|
|||
State(state): State<AppState>,
|
||||
jar: axum_extra::extract::cookie::CookieJar,
|
||||
) -> Result<Html<String>, (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#"<!doctype html>
|
||||
<html lang="de">
|
||||
|
|
@ -228,20 +231,35 @@ a {
|
|||
if athletes.is_empty() {
|
||||
html.push_str(
|
||||
r#"<div class="card notice">
|
||||
Keine für diesen Authentication-Token bekannten Athleten.
|
||||
Keine für diesen API-Key zugänglichen Athleten.
|
||||
</div>"#,
|
||||
);
|
||||
} else {
|
||||
html.push_str(&format!(
|
||||
r#"<div class="card small">
|
||||
{} Athlet{} für diesen Authentication-Token bekannt.
|
||||
{} zugängliche{} Athlet{} gefunden.
|
||||
</div>
|
||||
<div class="grid">"#,
|
||||
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#"<div class="card">
|
||||
<h2>{}</h2>
|
||||
|
|
@ -252,9 +270,9 @@ a {
|
|||
</a>
|
||||
</p>
|
||||
</div>"#,
|
||||
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),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue