show all known athletes for session on sync page
This commit is contained in:
parent
30542807b8
commit
50cd57417d
2 changed files with 113 additions and 29 deletions
48
src/db.rs
48
src/db.rs
|
|
@ -122,6 +122,54 @@ 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.
|
||||
///
|
||||
|
|
|
|||
94
src/main.rs
94
src/main.rs
|
|
@ -161,11 +161,18 @@ async fn sync_index(
|
|||
State(state): State<AppState>,
|
||||
jar: axum_extra::extract::cookie::CookieJar,
|
||||
) -> Result<Html<String>, (axum::http::StatusCode, String)> {
|
||||
let session = current_session_athlete(&state, &jar)
|
||||
.await
|
||||
.map_err(http_error)?;
|
||||
let cookie = jar
|
||||
.get("county_session")
|
||||
.ok_or_else(|| http_error(anyhow!("not authenticated")))?;
|
||||
|
||||
let html = format!(
|
||||
let athletes = db::session_accessible_athletes(
|
||||
&state.db,
|
||||
cookie.value(),
|
||||
)
|
||||
.await
|
||||
.map_err(http_error)?;
|
||||
|
||||
let mut html = String::from(
|
||||
r#"<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
|
|
@ -174,63 +181,92 @@ async fn sync_index(
|
|||
content="width=device-width,initial-scale=1">
|
||||
<title>Sync · Landkreis-Sprints</title>
|
||||
<style>
|
||||
body {{
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 900px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #111827;
|
||||
color: #f9fafb;
|
||||
}}
|
||||
a {{
|
||||
}
|
||||
a {
|
||||
color: #93c5fd;
|
||||
}}
|
||||
.card {{
|
||||
}
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}}
|
||||
.button {{
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: .6rem .9rem;
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}}
|
||||
.small {{
|
||||
}
|
||||
.small {
|
||||
color: #9ca3af;
|
||||
}}
|
||||
}
|
||||
.notice {
|
||||
background: #3b2f12;
|
||||
border: 1px solid #6b551c;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Synchronisation</h1>
|
||||
"#,
|
||||
);
|
||||
|
||||
<div class="card">
|
||||
if athletes.is_empty() {
|
||||
html.push_str(
|
||||
r#"<div class="card notice">
|
||||
Keine für diesen Authentication-Token bekannten Athleten.
|
||||
</div>"#,
|
||||
);
|
||||
} else {
|
||||
html.push_str(&format!(
|
||||
r#"<div class="card small">
|
||||
{} Athlet{} für diesen Authentication-Token bekannt.
|
||||
</div>
|
||||
<div class="grid">"#,
|
||||
athletes.len(),
|
||||
if athletes.len() == 1 { "" } else { "en" },
|
||||
));
|
||||
|
||||
for athlete in athletes {
|
||||
html.push_str(&format!(
|
||||
r#"<div class="card">
|
||||
<h2>{}</h2>
|
||||
<div class="small">
|
||||
Intervals.icu Athlete: {}
|
||||
</div>
|
||||
|
||||
<div class="small">Intervals.icu: {}</div>
|
||||
<p>
|
||||
<a class="button"
|
||||
href="/sync/{}">
|
||||
<a class="button" href="/sync/{}">
|
||||
Aktivitäten anzeigen
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>"#,
|
||||
html::escape(&athlete.display_name),
|
||||
html::escape(&athlete.intervals_athlete_id),
|
||||
url_segment(&athlete.intervals_athlete_id),
|
||||
));
|
||||
}
|
||||
|
||||
<p>
|
||||
html.push_str("</div>");
|
||||
}
|
||||
|
||||
html.push_str(
|
||||
r#"<p>
|
||||
<a href="/">← Leaderboard</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>"#,
|
||||
html::escape(&session.display_name),
|
||||
html::escape(&session.intervals_athlete_id),
|
||||
url_segment(&session.intervals_athlete_id),
|
||||
);
|
||||
|
||||
Ok(Html(html))
|
||||
|
|
|
|||
Loading…
Reference in a new issue