use crate::auth_api::{error, internal, secure_cookies, with_cookie};
use crate::operator::{Authenticated, Operator, session_cookie};
use crate::secret::unix_now;
use axum::Router;
use axum::extract::{Request, State};
use axum::http::{StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::Response;
use std::sync::Arc;
use std::time::Duration;
pub fn operator_only<S: Clone + Send + Sync + 'static>(router: Router<S>, operator: Arc<Operator>) -> Router<S> {
router
.layer(middleware::from_fn_with_state(operator.clone(), require_operator))
.layer(middleware::from_fn_with_state(operator, crate::origin::browser_rules))
}
pub async fn require_operator(State(operator): State<Arc<Operator>>, mut req: Request, next: Next) -> Response {
let (token, session) = match operator.authenticate_cookies(req.headers(), unix_now()) {
Ok(Some(found)) => found,
Ok(None) => return unauthenticated(),
Err(err) => return internal(err),
};
let slid = session.slid;
let session_id = session.session_id.clone();
req.extensions_mut().insert(session);
let response = next.run(req).await;
let resend = slid
&& !response.headers().contains_key(header::SET_COOKIE)
&& matches!(operator.session_expires_at(&session_id, unix_now()), Ok(Some(_)));
if resend {
with_cookie(response, &session_cookie(&token, secure_cookies(&operator)))
} else {
response
}
}
fn unauthenticated() -> Response {
error(StatusCode::UNAUTHORIZED, "unauthenticated", "sign in first")
}
pub async fn require_step_up(req: Request, next: Next) -> Response {
let fresh = req
.extensions()
.get::<Authenticated>()
.is_some_and(|session| session.stepped_up(unix_now()));
if !fresh {
return step_up_required();
}
next.run(req).await
}
pub fn step_up_required() -> Response {
error(
StatusCode::FORBIDDEN,
"step_up_required",
"confirm your password or a passkey again (POST /api/auth/step-up/password or /api/auth/step-up/passkey/start)",
)
}
pub const SESSION_RECHECK: Duration = Duration::from_secs(60 * 60);
pub async fn session_ended(operator: Arc<Operator>, session: Authenticated) {
let mut ends = operator.session_ends();
let Ok(Some(mut expires_at)) = operator.session_expires_at(&session.session_id, unix_now()) else {
return;
};
loop {
let left = Duration::from_secs(expires_at.saturating_sub(unix_now()).max(0) as u64);
tokio::select! {
changed = ends.changed() => {
if changed.is_err() {
return;
}
}
() = tokio::time::sleep(left.min(SESSION_RECHECK)) => {}
}
match operator.session_expires_at(&session.session_id, unix_now()) {
Ok(Some(later)) => expires_at = later,
Ok(None) | Err(_) => return,
}
}
}