This commit is contained in:
Jonas Rabenstein 2026-08-13 02:43:17 +02:00
commit 859603399a
4 changed files with 1284 additions and 313 deletions

View file

@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
anyhow = "1"
axum = "0.8"
axum = { version = "0.8", features = [ "macros" ] }
axum-extra = { version = "0.10", features = ["cookie"] }
chrono = { version = "0.4", features = ["serde"] }
dotenvy = "0.15"

181
src/db.rs
View file

@ -8,16 +8,25 @@ use crate::model::DetectedCrossing;
pub fn hash_token(token: &str) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
hasher.finalize().to_vec()
}
pub async fn create_session(db: &PgPool, athlete_id: i64, token: &str) -> Result<()> {
pub async fn create_session(
db: &PgPool,
athlete_id: i64,
token: &str,
) -> Result<()> {
let hash = hash_token(token);
sqlx::query(
r#"
INSERT INTO sessions (token_hash, athlete_id)
INSERT INTO sessions (
token_hash,
athlete_id
)
VALUES ($1, $2)
"#,
)
@ -29,14 +38,20 @@ pub async fn create_session(db: &PgPool, athlete_id: i64, token: &str) -> Result
Ok(())
}
pub async fn athlete_for_session(db: &PgPool, token: &str) -> Result<Option<(i64, String)>> {
pub async fn athlete_for_session(
db: &PgPool,
token: &str,
) -> Result<Option<(i64, String)>> {
let hash = hash_token(token);
let row = sqlx::query(
r#"
SELECT a.id, a.display_name
SELECT
a.id,
a.display_name
FROM sessions s
JOIN athletes a ON a.id = s.athlete_id
JOIN athletes a
ON a.id = s.athlete_id
WHERE s.token_hash = $1
"#,
)
@ -47,8 +62,11 @@ pub async fn athlete_for_session(db: &PgPool, token: &str) -> Result<Option<(i64
use sqlx::Row;
if let Some(row) = row {
let id: i64 = row.try_get("id")?;
let name: String = row.try_get("display_name")?;
let id: i64 =
row.try_get("id")?;
let name: String =
row.try_get("display_name")?;
sqlx::query(
r#"
@ -67,7 +85,90 @@ pub async fn athlete_for_session(db: &PgPool, token: &str) -> Result<Option<(i64
}
}
pub async fn delete_old_oauth_states(db: &PgPool) -> Result<()> {
#[derive(Debug, Clone)]
pub struct SessionAthlete {
pub id: i64,
pub intervals_athlete_id: String,
pub display_name: String,
pub access_token: String,
}
pub async fn session_athlete(
db: &PgPool,
token: &str,
) -> Result<Option<SessionAthlete>> {
let hash = hash_token(token);
let row = sqlx::query(
r#"
SELECT
a.id,
a.intervals_athlete_id,
a.display_name,
a.access_token
FROM sessions s
JOIN athletes a
ON a.id = s.athlete_id
WHERE s.token_hash = $1
"#,
)
.bind(&hash)
.fetch_optional(db)
.await?;
use sqlx::Row;
let Some(row) = row else {
return Ok(None);
};
sqlx::query(
r#"
UPDATE sessions
SET last_seen_at = now()
WHERE token_hash = $1
"#,
)
.bind(&hash)
.execute(db)
.await?;
Ok(Some(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")?,
}))
}
/// Return the start time of the most recently imported activity
/// for an athlete.
///
/// This is used as the default `oldest` value on the sync page.
pub async fn last_synced_activity_time(
db: &PgPool,
athlete_id: i64,
) -> Result<Option<DateTime<Utc>>> {
let row = sqlx::query(
r#"
SELECT MAX(start_time) AS last_synced
FROM activities
WHERE athlete_id = $1
AND processed_at IS NOT NULL
"#,
)
.bind(athlete_id)
.fetch_one(db)
.await?;
use sqlx::Row;
Ok(row.try_get("last_synced")?)
}
pub async fn delete_old_oauth_states(
db: &PgPool,
) -> Result<()> {
sqlx::query(
r#"
DELETE FROM oauth_states
@ -80,6 +181,40 @@ pub async fn delete_old_oauth_states(db: &PgPool) -> Result<()> {
Ok(())
}
pub async fn ensure_dev_athlete(
db: &PgPool,
intervals_athlete_id: &str,
display_name: &str,
api_key: &str,
) -> Result<i64> {
let row = sqlx::query(
r#"
INSERT INTO athletes (
intervals_athlete_id,
display_name,
access_token,
scopes
)
VALUES ($1, $2, $3, 'dev')
ON CONFLICT (intervals_athlete_id)
DO UPDATE SET
display_name = EXCLUDED.display_name,
access_token = EXCLUDED.access_token,
updated_at = now()
RETURNING id
"#,
)
.bind(intervals_athlete_id)
.bind(display_name)
.bind(api_key)
.fetch_one(db)
.await?;
use sqlx::Row;
Ok(row.try_get("id")?)
}
pub async fn ensure_activity(
tx: &mut Transaction<'_, Postgres>,
athlete_id: i64,
@ -96,8 +231,17 @@ pub async fn ensure_activity(
activity_json,
processed_at
)
VALUES ($1, $2, $3, $4, NULL)
ON CONFLICT (athlete_id, intervals_activity_id)
VALUES (
$1,
$2,
$3,
$4,
NULL
)
ON CONFLICT (
athlete_id,
intervals_activity_id
)
DO UPDATE SET
start_time = EXCLUDED.start_time,
activity_json = EXCLUDED.activity_json,
@ -113,6 +257,7 @@ pub async fn ensure_activity(
.await?;
use sqlx::Row;
Ok(row.try_get("id")?)
}
@ -154,7 +299,10 @@ pub async fn save_crossing(
$1,
$2,
$3,
ST_SetSRID(ST_MakePoint($4, $5), 4326),
ST_SetSRID(
ST_MakePoint($4, $5),
4326
),
$6,
$7,
$8
@ -192,14 +340,3 @@ pub async fn mark_activity_processed(
Ok(())
}
pub async fn delete_session(db: &PgPool, token: &str) -> Result<()> {
let hash = hash_token(token);
sqlx::query("DELETE FROM sessions WHERE token_hash = $1")
.bind(&hash)
.execute(db)
.await?;
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use chrono::{DateTime, Utc};