add logout

This commit is contained in:
Hamcha 2023-07-02 16:04:56 +02:00
parent 1e10162fb3
commit a1036aa02a
Signed by: hamcha
GPG key ID: 1669C533B8CF6D89
4 changed files with 83 additions and 7 deletions

View file

@ -2,7 +2,11 @@ use anyhow::Result;
use axum::{ use axum::{
async_trait, async_trait,
extract::{FromRequestParts, State}, extract::{FromRequestParts, State},
http::{header::COOKIE, request::Parts, HeaderValue, Request, StatusCode}, http::{
header::{COOKIE, SET_COOKIE},
request::Parts,
HeaderValue, Request, StatusCode,
},
middleware::Next, middleware::Next,
response::Response, response::Response,
Extension, Extension,
@ -45,6 +49,23 @@ where
} }
} }
pub struct RequireSession(pub Session);
#[async_trait]
impl<S> FromRequestParts<S> for RequireSession
where
S: Send + Sync,
{
type Rejection = AppError<'static>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match Extension::<Session>::from_request_parts(parts, state).await {
Ok(Extension(session)) => Ok(RequireSession(session)),
_ => Err(INVALID_SESSION),
}
}
}
pub async fn refresh_sessions<B>( pub async fn refresh_sessions<B>(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
mut req: Request<B>, mut req: Request<B>,
@ -69,8 +90,22 @@ pub async fn refresh_sessions<B>(
.ok() .ok()
{ {
let extensions = req.extensions_mut(); let extensions = req.extensions_mut();
extensions.insert(session); extensions.insert(session.clone());
extensions.insert(user); extensions.insert(user);
let mut response = next.run(req).await;
// Only set the session cookie if it hasn't been set yet (eg. logout)
let headers = response.headers_mut();
if !headers.contains_key(SET_COOKIE) {
headers.insert(
SET_COOKIE,
session
.cookie(state.config.domain().as_str(), state.config.secure())
.parse()
.unwrap(),
);
}
return response;
} }
} }
} }

View file

@ -144,6 +144,13 @@ impl Session {
} }
} }
pub async fn destroy(self: Self, pool: &Pool<Postgres>) -> Result<()> {
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
.execute(pool)
.await?;
Ok(())
}
pub async fn prune_dead(pool: &Pool<Postgres>) -> Result<u64> { pub async fn prune_dead(pool: &Pool<Postgres>) -> Result<u64> {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
let result = sqlx::query!("DELETE FROM sessions WHERE expires_at < $1", now) let result = sqlx::query!("DELETE FROM sessions WHERE expires_at < $1", now)
@ -158,6 +165,21 @@ impl Session {
.secure(secure) .secure(secure)
.http_only(!secure) .http_only(!secure)
.path("/") .path("/")
.expires(
cookie::time::OffsetDateTime::from_unix_timestamp(self.expires_at.timestamp())
.unwrap(),
)
.finish()
.to_string()
}
pub fn cookie_for_delete(domain: &str, secure: bool) -> String {
Cookie::build("session", "")
.domain(domain)
.secure(secure)
.http_only(!secure)
.path("/")
.max_age(cookie::time::Duration::seconds(0))
.finish() .finish()
.to_string() .to_string()
} }

View file

@ -41,6 +41,7 @@ async fn main() -> Result<()> {
let app = Router::new() let app = Router::new()
.route("/auth/login", post(routes::auth::login)) .route("/auth/login", post(routes::auth::login))
.route("/auth/logout", post(routes::auth::logout))
.route("/me", get(routes::auth::me)) .route("/me", get(routes::auth::me))
.route("/pages/:site/:slug", get(routes::content::page)) .route("/pages/:site/:slug", get(routes::content::page))
.route("/admin/bootstrap", post(routes::admin::bootstrap)) .route("/admin/bootstrap", post(routes::admin::bootstrap))

View file

@ -2,7 +2,7 @@ use axum::{
extract::State, extract::State,
http::{header::SET_COOKIE, StatusCode}, http::{header::SET_COOKIE, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Extension, Json, Json,
}; };
use chrono::Duration; use chrono::Duration;
use serde::Deserialize; use serde::Deserialize;
@ -10,7 +10,12 @@ use serde_json::json;
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
auth::{hash::verify, http::RequireUser, session::Session, user::User}, auth::{
hash::verify,
http::{RequireSession, RequireUser},
session::Session,
user::User,
},
error::AppError, error::AppError,
state::AppState, state::AppState,
}; };
@ -56,12 +61,10 @@ pub async fn login(
let mut response: Response = let mut response: Response =
Json(json!({ "session_token": token, "expires_at": session.expires_at })).into_response(); Json(json!({ "session_token": token, "expires_at": session.expires_at })).into_response();
let secure = state.config.secure();
response.headers_mut().insert( response.headers_mut().insert(
SET_COOKIE, SET_COOKIE,
session session
.cookie(state.config.domain().as_str(), secure) .cookie(state.config.domain().as_str(), state.config.secure())
.parse()?, .parse()?,
); );
@ -71,3 +74,18 @@ pub async fn login(
pub async fn me(RequireUser(user): RequireUser) -> Result<String, AppError<'static>> { pub async fn me(RequireUser(user): RequireUser) -> Result<String, AppError<'static>> {
Ok(user.name) Ok(user.name)
} }
pub async fn logout(
State(state): State<Arc<AppState>>,
RequireSession(session): RequireSession,
) -> Result<impl IntoResponse, AppError<'static>> {
session.destroy(&state.database).await?;
let mut response: Response = Json(json!({ "ok": true })).into_response();
response.headers_mut().insert(
SET_COOKIE,
Session::cookie_for_delete(state.config.domain().as_str(), state.config.secure())
.parse()?,
);
Ok(response)
}