//! Signing in and out. use crate::auth::{CurrentUser, clear_session_cookie, session_cookie}; use crate::db::User; use crate::error::ApiError; use crate::state::AppState; use axum::Json; use axum::extract::State; use axum::http::HeaderMap; use axum::http::header::USER_AGENT; use axum::response::{IntoResponse, Response}; use axum_extra::extract::CookieJar; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct LoginRequest { /// The DAV root, e.g. `https://example.com/dav.php/`. pub server_url: String, pub username: String, pub password: String, } /// What a signed-in client is told about itself. /// /// The [`User`] row is returned directly rather than copied into a parallel /// response struct. It holds nothing secret — the password lives encrypted in /// its own table and never travels — and v1's four near-identical wire structs /// are a warning about what maintaining a second shape costs. #[derive(Serialize)] pub struct SessionResponse { pub user: User, } /// `POST /api/auth/login` /// /// Verification is a real request to the CalDAV server: if it will list your /// calendars, you are who you say you are. There is no Runway password to /// forget, and revoking an account on the CalDAV server revokes it here. pub async fn login( State(state): State, jar: CookieJar, headers: HeaderMap, Json(request): Json, ) -> Result { if request.username.is_empty() || request.password.is_empty() { return Err(ApiError::BadRequest( "a username and password are both required".to_owned(), )); } let user_agent = headers .get(USER_AGENT) .and_then(|value| value.to_str().ok()); let (user, token) = state .auth .login_with_caldav( request.server_url.trim(), request.username.trim(), &request.password, user_agent, ) .await?; // The token goes in an HttpOnly cookie and nowhere else. It is deliberately // absent from the body: anything in the body is readable by script, which // is the property the cookie exists to avoid. let jar = jar.add(session_cookie( token, state.auth.session_lifetime(), state.config.secure_cookies, )); Ok((jar, Json(SessionResponse { user })).into_response()) } /// `POST /api/auth/logout` /// /// Ends the session server-side as well as clearing the cookie. Clearing only /// the cookie would leave a token that still works for anyone holding a copy. pub async fn logout(State(state): State, jar: CookieJar) -> Result { if let Some(cookie) = jar.get(crate::auth::SESSION_COOKIE) { state.auth.logout(cookie.value()).await?; } let jar = jar.add(clear_session_cookie(state.config.secure_cookies)); Ok((jar, Json(serde_json::json!({ "status": "signed out" }))).into_response()) } /// `GET /api/auth/session` /// /// Who the caller is. Used on startup to decide whether to show the login /// screen, which replaces v1's separate token-verification endpoint. pub async fn current_session( CurrentUser { user, .. }: CurrentUser, ) -> Result, ApiError> { Ok(Json(SessionResponse { user })) }