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
|
/// Return the start time of the most recently imported activity
|
||||||
/// for an athlete.
|
/// for an athlete.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
88
src/main.rs
88
src/main.rs
|
|
@ -161,11 +161,18 @@ 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 session = current_session_athlete(&state, &jar)
|
let cookie = 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 html = format!(
|
let mut html = String::from(
|
||||||
r#"<!doctype html>
|
r#"<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -174,63 +181,92 @@ async fn sync_index(
|
||||||
content="width=device-width,initial-scale=1">
|
content="width=device-width,initial-scale=1">
|
||||||
<title>Sync · Landkreis-Sprints</title>
|
<title>Sync · Landkreis-Sprints</title>
|
||||||
<style>
|
<style>
|
||||||
body {{
|
body {
|
||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
max-width: 900px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #111827;
|
background: #111827;
|
||||||
color: #f9fafb;
|
color: #f9fafb;
|
||||||
}}
|
}
|
||||||
a {{
|
a {
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
}}
|
}
|
||||||
.card {{
|
.card {
|
||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}}
|
}
|
||||||
.button {{
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #2563eb;
|
background: #2563eb;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: .6rem .9rem;
|
padding: .6rem .9rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 0;
|
}
|
||||||
cursor: pointer;
|
.small {
|
||||||
}}
|
|
||||||
.small {{
|
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}}
|
}
|
||||||
|
.notice {
|
||||||
|
background: #3b2f12;
|
||||||
|
border: 1px solid #6b551c;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Synchronisation</h1>
|
<h1>Synchronisation</h1>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
<div class="card">
|
if athletes.is_empty() {
|
||||||
<h2>{}</h2>
|
html.push_str(
|
||||||
<div class="small">
|
r#"<div class="card notice">
|
||||||
Intervals.icu Athlete: {}
|
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>
|
||||||
|
<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: {}</div>
|
||||||
<p>
|
<p>
|
||||||
<a class="button"
|
<a class="button" href="/sync/{}">
|
||||||
href="/sync/{}">
|
|
||||||
Aktivitäten anzeigen
|
Aktivitäten anzeigen
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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>
|
<a href="/">← Leaderboard</a>
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>"#,
|
</html>"#,
|
||||||
html::escape(&session.display_name),
|
|
||||||
html::escape(&session.intervals_athlete_id),
|
|
||||||
url_segment(&session.intervals_athlete_id),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Html(html))
|
Ok(Html(html))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue