use std::sync::Arc;
use async_trait::async_trait;
use axum::Json;
use axum::Router;
use axum::extract::State;
use axum::routing::post;
use serde::Deserialize;
use cheers_core::{DeviceBinding, DeviceId, Error, RefreshError, StoreError, TokenMinter, UserId};
use cheers_server::{
BindingResolver, RefreshStore, RevocationWriter, SessionAuthority, UserStore,
};
use crate::error::RouteError;
use crate::me::{SessionDirectory, SessionRecorder};
use crate::session::SessionBody;
pub struct RefreshAuthState<M, R, U, W> {
pub authority: Arc<SessionAuthority<M, R, U, W>>,
pub bindings: Arc<dyn BindingResolver>,
pub recorder: Arc<dyn SessionRecorder>,
}
impl<M, R, U, W> std::fmt::Debug for RefreshAuthState<M, R, U, W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RefreshAuthState").finish_non_exhaustive()
}
}
pub struct DirectoryBindings<D: ?Sized>(pub Arc<D>);
impl<D: ?Sized> std::fmt::Debug for DirectoryBindings<D> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DirectoryBindings").finish_non_exhaustive()
}
}
#[async_trait]
impl<D: SessionDirectory + ?Sized> BindingResolver for DirectoryBindings<D> {
async fn resolve_binding(
&self,
user_id: &UserId,
device_id: &DeviceId,
now: i64,
) -> Result<Option<DeviceBinding>, StoreError> {
Ok(self
.0
.list_sessions(user_id, now)
.await?
.into_iter()
.find(|row| &row.device_id == device_id)
.map(|row| row.binding))
}
}
pub fn router<M, R, U, W>(state: Arc<RefreshAuthState<M, R, U, W>>) -> Router
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
{
Router::new()
.route("/refresh", post(refresh::<M, R, U, W>))
.with_state(state)
}
#[derive(Debug, Clone, Deserialize)]
pub struct RefreshBody {
pub refresh_token: String,
}
pub async fn refresh<M, R, U, W>(
State(state): State<Arc<RefreshAuthState<M, R, U, W>>>,
Json(body): Json<RefreshBody>,
) -> Result<Json<SessionBody>, RouteError>
where
M: TokenMinter + Send + Sync + 'static,
R: RefreshStore + Send + Sync + 'static,
U: UserStore + Send + Sync + 'static,
W: RevocationWriter + Send + Sync + 'static,
{
let now = now_unix();
let session = state
.authority
.rotate(&body.refresh_token, state.bindings.as_ref(), now)
.await
.map_err(map_rotate_error)?;
if let Err(e) = state.recorder.record_new_session(&session).await {
tracing::warn!(error = %e, "cheers-axum refresh: re-recording rotated session failed");
}
Ok(Json(SessionBody::from_new_session(session)))
}
fn map_rotate_error(err: Error) -> RouteError {
match err {
Error::Refresh(RefreshError::Store(e)) => RouteError::Store(e.to_string()),
Error::Refresh(_) => RouteError::Unauthorized,
other => other.into(),
}
}
fn now_unix() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or_default()
}