Skip to main content

prospero_api/auth/
handlers.rs

1//! `/api/session` — dashboard sign-in, whoami and sign-out (#2).
2
3use axum::Json;
4use axum::extract::State;
5use axum::http::{HeaderMap, StatusCode, header};
6use axum::response::{IntoResponse, Response};
7use prospero_types::{SessionInfo, SignInBody};
8
9use super::session::{self, SESSION_TTL_SECS};
10use super::{Principal, Via, resolve_principal};
11use crate::AppState;
12use crate::error::ApiError;
13
14fn info(p: &Principal) -> SessionInfo {
15    SessionInfo::Token {
16        token_name: p.token_name.clone(),
17        scope: p.scope,
18        expires_at: p
19            .expires_unix
20            .and_then(|s| chrono::DateTime::from_timestamp(s, 0))
21            .map(|d| d.to_rfc3339()),
22    }
23}
24
25/// `GET /api/session` — the current credential, or `{"auth":"disabled"}`.
26pub async fn get_session(
27    State(st): State<AppState>,
28    headers: HeaderMap,
29) -> Result<Json<SessionInfo>, ApiError> {
30    if !st.auth.is_enabled() {
31        return Ok(Json(SessionInfo::Disabled));
32    }
33    let p = resolve_principal(&st.auth, &headers).ok_or(ApiError::Unauthorized)?;
34    Ok(Json(info(&p)))
35}
36
37/// `POST /api/session` — exchange a token for a session cookie.
38pub async fn post_session(
39    State(st): State<AppState>,
40    headers: HeaderMap,
41    Json(body): Json<SignInBody>,
42) -> Result<Response, ApiError> {
43    let Some(tokens) = st.auth.tokens() else {
44        return Err(ApiError::NotFound("authentication is disabled".into()));
45    };
46    let entry = tokens
47        .authenticate(&body.token)
48        .ok_or(ApiError::Unauthorized)?;
49    let expires = chrono::Utc::now().timestamp() + SESSION_TTL_SECS;
50    let value = session::sign(st.auth.session_key(), entry, expires);
51    let secure = st.auth.cookie_secure() || session::is_https(&headers);
52    let principal = Principal {
53        token_name: entry.name.clone(),
54        scope: entry.scope,
55        via: Via::Cookie,
56        expires_unix: Some(expires),
57    };
58    tracing::info!(target: "prospero_audit", actor = %entry.name, "dashboard sign-in");
59    let mut response = Json(info(&principal)).into_response();
60    response.headers_mut().insert(
61        header::SET_COOKIE,
62        session::set_cookie(&value, SESSION_TTL_SECS, secure),
63    );
64    Ok(response)
65}
66
67/// `DELETE /api/session` — clear the cookie.
68pub async fn delete_session(State(st): State<AppState>, headers: HeaderMap) -> Response {
69    let secure = st.auth.cookie_secure() || session::is_https(&headers);
70    (
71        StatusCode::NO_CONTENT,
72        [(header::SET_COOKIE, session::clear_cookie(secure))],
73    )
74        .into_response()
75}