dev api key

This commit is contained in:
Jonas Rabenstein 2026-08-12 14:02:12 +02:00
commit 89091a03d3
11 changed files with 791 additions and 762 deletions

View file

@ -1,81 +1,71 @@
use axum::{
extract::State,
http::StatusCode,
Json,
};
use axum::{extract::State, http::StatusCode, response::IntoResponse};
use serde::Deserialize;
use crate::{
AppState,
model::WebhookEnvelope,
};
use crate::AppState;
#[derive(Debug, Deserialize)]
pub struct WebhookPayload {
pub secret: String,
#[serde(default)]
pub athlete_id: Option<String>,
#[serde(default)]
pub activity_id: Option<String>,
}
pub async fn receive(
State(state): State<AppState>,
Json(payload): Json<WebhookEnvelope>,
) -> Result<&'static str, StatusCode> {
if payload.secret != state.config.intervals_webhook_secret {
tracing::warn!("invalid Intervals.icu webhook secret");
return Err(StatusCode::UNAUTHORIZED);
axum::Json(payload): axum::Json<WebhookPayload>,
) -> impl IntoResponse {
let Some(expected_secret) = state.config.intervals_webhook_secret.as_deref() else {
tracing::error!("Intervals.icu webhook secret is not configured");
return (
StatusCode::INTERNAL_SERVER_ERROR,
"webhook secret not configured",
)
.into_response();
};
if payload.secret != expected_secret {
tracing::warn!("received webhook with invalid secret");
return (StatusCode::UNAUTHORIZED, "invalid webhook secret").into_response();
}
for event in payload.events {
match event.event_type.as_str() {
"ACTIVITY_UPLOADED" |
"ACTIVITY_ANALYZED" => {
let activity_id = event
.activity
.as_ref()
.and_then(|activity| {
activity.get("id")
})
.and_then(|id| {
id.as_str()
});
let Some(activity_id) = payload.activity_id else {
tracing::warn!("received webhook without activity_id");
let Some(activity_id) = activity_id else {
tracing::warn!(
athlete_id = %event.athlete_id,
"activity webhook without activity id"
);
return (StatusCode::BAD_REQUEST, "missing activity_id").into_response();
};
continue;
};
let athlete_id = payload
.athlete_id
.or_else(|| std::env::var("INTERVALS_ATHLETE_ID").ok());
if let Err(error) =
crate::process_activity(
state.clone(),
event.athlete_id.clone(),
activity_id.to_owned(),
).await
{
tracing::error!(
%error,
athlete_id = %event.athlete_id,
activity_id = %activity_id,
"activity processing failed"
);
let Some(athlete_id) = athlete_id else {
tracing::error!(
"webhook has no athlete_id and \
INTERVALS_ATHLETE_ID is not configured"
);
// Return 500 so Intervals.icu can retry the webhook.
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
return (StatusCode::BAD_REQUEST, "missing athlete_id").into_response();
};
"APP_SCOPE_CHANGED" => {
tracing::info!(
athlete_id = %event.athlete_id,
"OAuth scopes changed"
);
}
let state_clone = state.clone();
_ => {
tracing::debug!(
event_type = %event.event_type,
"ignoring Intervals.icu webhook"
);
}
tokio::spawn(async move {
if let Err(error) =
crate::process_activity(state_clone, athlete_id, activity_id.clone()).await
{
tracing::error!(
%error,
activity_id = %activity_id,
"failed to process webhook activity"
);
}
}
});
// Intervals.icu has historically retried on 204; use an ordinary 200.
Ok("ok")
(StatusCode::ACCEPTED, "activity queued").into_response()
}