1use crate::auth_api::{error, internal, secure_cookies, with_cookie};
6use crate::operator::{Authenticated, Operator, session_cookie};
7use crate::secret::unix_now;
8use axum::Router;
9use axum::extract::{Request, State};
10use axum::http::{StatusCode, header};
11use axum::middleware::{self, Next};
12use axum::response::Response;
13use std::sync::Arc;
14use std::time::Duration;
15
16pub fn operator_only<S: Clone + Send + Sync + 'static>(router: Router<S>, operator: Arc<Operator>) -> Router<S> {
20 router
21 .layer(middleware::from_fn_with_state(operator.clone(), require_operator))
22 .layer(middleware::from_fn_with_state(operator, crate::origin::browser_rules))
23}
24
25pub async fn require_operator(State(operator): State<Arc<Operator>>, mut req: Request, next: Next) -> Response {
33 let (token, session) = match operator.authenticate_cookies(req.headers(), unix_now()) {
34 Ok(Some(found)) => found,
35 Ok(None) => return unauthenticated(),
36 Err(err) => return internal(err),
37 };
38 let slid = session.slid;
39 let session_id = session.session_id.clone();
40 req.extensions_mut().insert(session);
41 let response = next.run(req).await;
42 let resend = slid
43 && !response.headers().contains_key(header::SET_COOKIE)
44 && matches!(operator.session_expires_at(&session_id, unix_now()), Ok(Some(_)));
45 if resend {
46 with_cookie(response, &session_cookie(&token, secure_cookies(&operator)))
47 } else {
48 response
49 }
50}
51
52fn unauthenticated() -> Response {
53 error(StatusCode::UNAUTHORIZED, "unauthenticated", "sign in first")
54}
55
56pub async fn require_step_up(req: Request, next: Next) -> Response {
61 let fresh = req
62 .extensions()
63 .get::<Authenticated>()
64 .is_some_and(|session| session.stepped_up(unix_now()));
65 if !fresh {
66 return step_up_required();
67 }
68 next.run(req).await
69}
70
71pub fn step_up_required() -> Response {
74 error(
75 StatusCode::FORBIDDEN,
76 "step_up_required",
77 "confirm your password or a passkey again (POST /api/auth/step-up/password or /api/auth/step-up/passkey/start)",
78 )
79}
80
81pub const SESSION_RECHECK: Duration = Duration::from_secs(60 * 60);
86
87pub async fn session_ended(operator: Arc<Operator>, session: Authenticated) {
94 let mut ends = operator.session_ends();
95 let Ok(Some(mut expires_at)) = operator.session_expires_at(&session.session_id, unix_now()) else {
97 return;
98 };
99 loop {
100 let left = Duration::from_secs(expires_at.saturating_sub(unix_now()).max(0) as u64);
101 tokio::select! {
102 changed = ends.changed() => {
103 if changed.is_err() {
104 return;
105 }
106 }
107 () = tokio::time::sleep(left.min(SESSION_RECHECK)) => {}
108 }
109 match operator.session_expires_at(&session.session_id, unix_now()) {
110 Ok(Some(later)) => expires_at = later,
111 Ok(None) | Err(_) => return,
112 }
113 }
114}