use std::sync::Arc;
use axum_extra::extract::PrivateCookieJar;
use super::session::{SessionResolution, SessionResolver};
use super::traits::{RefreshTokenResolver, SessionStore, SvAware};
use crate::oauth::AuthClient;
use crate::session_version::{SV_CACHE_KEY_PREFIX, SV_CACHE_TTL, SessionVersionCache};
use crate::types::SessionId;
pub struct SvAwareSessionResolver<S: SessionStore, R: RefreshTokenResolver, C: SessionVersionCache> {
base: SessionResolver<S>,
store: Arc<S>,
refresh_resolver: Arc<R>,
auth_client: Arc<AuthClient>,
cache: Arc<C>,
}
impl<S, R, C> Clone for SvAwareSessionResolver<S, R, C>
where
S: SessionStore,
R: RefreshTokenResolver,
C: SessionVersionCache,
{
fn clone(&self) -> Self {
Self {
base: self.base.clone(),
store: Arc::clone(&self.store),
refresh_resolver: Arc::clone(&self.refresh_resolver),
auth_client: Arc::clone(&self.auth_client),
cache: Arc::clone(&self.cache),
}
}
}
impl<S, R, C> SvAwareSessionResolver<S, R, C>
where
S: SessionStore,
R: RefreshTokenResolver,
C: SessionVersionCache,
{
pub(super) fn new(
base: SessionResolver<S>,
store: Arc<S>,
refresh_resolver: Arc<R>,
auth_client: Arc<AuthClient>,
cache: Arc<C>,
) -> Self {
Self {
base,
store,
refresh_resolver,
auth_client,
cache,
}
}
pub async fn resolve(
&self,
jar: &PrivateCookieJar,
) -> Result<SessionResolution<S::AuthContext>, S::Error> {
let resolution = self.base.resolve(jar).await?;
let session = match resolution {
SessionResolution::Authenticated(s) => s,
other => return Ok(other),
};
let Some(token_sv) = session.sv() else {
return Ok(SessionResolution::Authenticated(session));
};
let cache_key = format!("{SV_CACHE_KEY_PREFIX}{}", session.ppnum_id());
match self.cache.get(&cache_key).await {
Some(cached_sv) if token_sv >= cached_sv => {
Ok(SessionResolution::Authenticated(session))
}
_ => self.refresh_and_recheck(jar, &cache_key).await,
}
}
async fn refresh_and_recheck(
&self,
jar: &PrivateCookieJar,
cache_key: &str,
) -> Result<SessionResolution<S::AuthContext>, S::Error> {
let Some(cookie) = jar.get(self.base.cookie_name()) else {
return Ok(SessionResolution::Expired);
};
let session_id = SessionId(cookie.value().to_string());
let refresh_token = match self
.refresh_resolver
.resolve_refresh_token(&session_id)
.await
{
Ok(Some(rt)) => rt,
Ok(None) => {
tracing::debug!(
session_id = %session_id,
"no refresh_token available — surface as Expired"
);
return Ok(SessionResolution::Expired);
}
Err(e) => {
tracing::warn!(
session_id = %session_id,
error = %e,
"refresh_token lookup failed — surface as Expired"
);
return Ok(SessionResolution::Expired);
}
};
let token_response = match self.auth_client.refresh_token(&refresh_token).await {
Ok(t) => t,
Err(e) => {
tracing::info!(
session_id = %session_id,
error = %e,
"token refresh failed (likely revoked refresh_token) — Expired"
);
return Ok(SessionResolution::Expired);
}
};
let new_sv = match self
.auth_client
.get_user_info(&token_response.access_token)
.await
{
Ok(info) => match info.session_version {
Some(sv) => sv,
None => {
tracing::error!(
session_id = %session_id,
"Human session refreshed but userinfo.session_version=None — Expired",
);
return Ok(SessionResolution::Expired);
}
},
Err(e) => {
tracing::warn!(
session_id = %session_id,
error = %e,
"post-refresh userinfo failed — Expired"
);
return Ok(SessionResolution::Expired);
}
};
if let Err(e) = self.store.update_sv(&session_id, new_sv).await {
tracing::warn!(
session_id = %session_id,
new_sv = new_sv,
error = %e,
"update_sv failed after refresh — Expired",
);
return Ok(SessionResolution::Expired);
}
self.cache.set(cache_key, new_sv, SV_CACHE_TTL).await;
match self.store.find(&session_id).await? {
Some(refreshed) => Ok(SessionResolution::Authenticated(refreshed)),
None => Ok(SessionResolution::Expired),
}
}
}