use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Router, extract::FromRequestParts};
use serde::Deserialize;
use super::WebPush;
use super::store::BrowserSubscription;
use crate::error::{AutumnError, AutumnResult};
use crate::state::AppState;
pub const VAPID_PUBLIC_KEY_PATH: &str = "/push/vapid-public-key";
pub const SUBSCRIBE_PATH: &str = "/push/subscribe";
pub const UNSUBSCRIBE_PATH: &str = "/push/unsubscribe";
pub fn router() -> Router<AppState> {
Router::new()
.route(VAPID_PUBLIC_KEY_PATH, get(vapid_public_key))
.route(SUBSCRIBE_PATH, post(subscribe))
.route(UNSUBSCRIBE_PATH, post(unsubscribe))
}
#[derive(Debug, Deserialize)]
struct UnsubscribeBody {
endpoint: String,
}
pub const CSRF_TOKEN_HEADER: &str = "x-autumn-push-csrf-token";
pub const CSRF_TOKEN_HEADER_NAME_HEADER: &str = "x-autumn-push-csrf-header";
async fn vapid_public_key(
push: WebPush,
csrf: Option<crate::security::CsrfToken>,
csrf_header: Option<axum::Extension<crate::security::CsrfTokenHeader>>,
) -> impl IntoResponse {
let csrf_headers = csrf.map(|token| {
let header_name =
csrf_header.map_or_else(|| "X-CSRF-Token".to_owned(), |axum::Extension(name)| name.0);
(token.token().to_owned(), header_name)
});
let mut response = match push.vapid_public_key() {
Ok(key) => (
StatusCode::OK,
[
("content-type", "text/plain; charset=utf-8"),
("cache-control", "no-store"),
],
key,
)
.into_response(),
Err(e) => (
StatusCode::SERVICE_UNAVAILABLE,
[("cache-control", "no-store")],
e.to_string(),
)
.into_response(),
};
if let Some((token, header_name)) = csrf_headers {
let headers = response.headers_mut();
if let (Ok(token), Ok(name)) = (
axum::http::HeaderValue::from_str(&token),
axum::http::HeaderValue::from_str(&header_name),
) {
headers.insert(CSRF_TOKEN_HEADER, token);
headers.insert(CSRF_TOKEN_HEADER_NAME_HEADER, name);
}
}
response
}
async fn subscribe(
State(state): State<AppState>,
push: WebPush,
parts: RequestPrincipal,
Json(subscription): Json<BrowserSubscription>,
) -> AutumnResult<StatusCode> {
let principal = parts.resolve(&state).await?;
push.subscribe(principal, &subscription).await?;
Ok(StatusCode::NO_CONTENT)
}
async fn unsubscribe(
State(state): State<AppState>,
push: WebPush,
parts: RequestPrincipal,
Json(body): Json<UnsubscribeBody>,
) -> AutumnResult<StatusCode> {
let principal = parts.resolve(&state).await?;
push.unsubscribe(principal, &body.endpoint).await?;
Ok(StatusCode::NO_CONTENT)
}
struct RequestPrincipal(axum::http::request::Parts);
impl FromRequestParts<AppState> for RequestPrincipal {
type Rejection = std::convert::Infallible;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &AppState,
) -> Result<Self, Self::Rejection> {
Ok(Self(parts.clone()))
}
}
impl RequestPrincipal {
async fn resolve(mut self, state: &AppState) -> AutumnResult<String> {
if let Some(actor) = crate::current::Current::scoped_actor() {
return Ok(actor);
}
let session_key = state.config().auth.session_key.clone();
let Ok(session) = crate::session::Session::from_request_parts(&mut self.0, state).await;
if let Some(user_id) = session.get(&session_key).await {
return Ok(user_id);
}
Err(AutumnError::unauthorized_msg(
"a push subscription must belong to a signed-in user",
))
}
}