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
|
/// Return the start time of the most recently imported activity
|
||||||
/// for an athlete.
|
/// for an athlete.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,24 @@ impl IntervalsClient {
|
||||||
self.get_json(url).await
|
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.
|
/// Get a specific athlete.
|
||||||
pub async fn athlete(&self, athlete_id: &str) -> Result<Value> {
|
pub async fn athlete(&self, athlete_id: &str) -> Result<Value> {
|
||||||
let url = self.api_url(&format!("athlete/{athlete_id}"));
|
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>,
|
State(state): State<AppState>,
|
||||||
jar: axum_extra::extract::cookie::CookieJar,
|
jar: axum_extra::extract::cookie::CookieJar,
|
||||||
) -> Result<Html<String>, (axum::http::StatusCode, String)> {
|
) -> Result<Html<String>, (axum::http::StatusCode, String)> {
|
||||||
let cookie = jar
|
let session = current_session_athlete(&state, &jar)
|
||||||
.get("county_session")
|
|
||||||
.ok_or_else(|| http_error(anyhow!("not authenticated")))?;
|
|
||||||
|
|
||||||
let athletes = db::session_accessible_athletes(
|
|
||||||
&state.db,
|
|
||||||
cookie.value(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(http_error)?;
|
.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(
|
let mut html = String::from(
|
||||||
r#"<!doctype html>
|
r#"<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
|
|
@ -228,20 +231,35 @@ a {
|
||||||
if athletes.is_empty() {
|
if athletes.is_empty() {
|
||||||
html.push_str(
|
html.push_str(
|
||||||
r#"<div class="card notice">
|
r#"<div class="card notice">
|
||||||
Keine für diesen Authentication-Token bekannten Athleten.
|
Keine für diesen API-Key zugänglichen Athleten.
|
||||||
</div>"#,
|
</div>"#,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
r#"<div class="card small">
|
r#"<div class="card small">
|
||||||
{} Athlet{} für diesen Authentication-Token bekannt.
|
{} zugängliche{} Athlet{} gefunden.
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">"#,
|
<div class="grid">"#,
|
||||||
athletes.len(),
|
athletes.len(),
|
||||||
|
if athletes.len() == 1 { "" } else { "n" },
|
||||||
if athletes.len() == 1 { "" } else { "en" },
|
if athletes.len() == 1 { "" } else { "en" },
|
||||||
));
|
));
|
||||||
|
|
||||||
for athlete in athletes {
|
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!(
|
html.push_str(&format!(
|
||||||
r#"<div class="card">
|
r#"<div class="card">
|
||||||
<h2>{}</h2>
|
<h2>{}</h2>
|
||||||
|
|
@ -252,9 +270,9 @@ a {
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>"#,
|
</div>"#,
|
||||||
html::escape(&athlete.display_name),
|
html::escape(display_name),
|
||||||
html::escape(&athlete.intervals_athlete_id),
|
html::escape(athlete_id),
|
||||||
url_segment(&athlete.intervals_athlete_id),
|
url_segment(athlete_id),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue