use crate::Authenticatable;
use larust_core::AppError;
use larust_http::session::Session;
const SESSION_KEY: &str = "_auth_user_id";
fn session_key(guard: &str) -> String {
if guard == "web" {
SESSION_KEY.to_string()
} else {
format!("_auth_{guard}_user_id")
}
}
pub async fn login<U: Authenticatable>(session: &Session, user: &U) -> Result<(), AppError> {
session
.cycle_id()
.await
.map_err(|source| AppError::Internal(Box::new(source)))?;
session
.insert(&session_key(U::GUARD), user.auth_id())
.await
.map_err(|source| AppError::Internal(Box::new(source)))
}
pub async fn logout(session: &Session) -> Result<(), AppError> {
session
.flush()
.await
.map_err(|source| AppError::Internal(Box::new(source)))
}
pub async fn logout_for<U: Authenticatable>(session: &Session) -> Result<(), AppError> {
session
.remove::<i64>(&session_key(U::GUARD))
.await
.map(|_| ())
.map_err(|source| AppError::Internal(Box::new(source)))
}
pub async fn id(session: &Session) -> Result<Option<i64>, AppError> {
session
.get::<i64>(SESSION_KEY)
.await
.map_err(|source| AppError::Internal(Box::new(source)))
}
pub async fn id_for<U: Authenticatable>(session: &Session) -> Result<Option<i64>, AppError> {
session
.get::<i64>(&session_key(U::GUARD))
.await
.map_err(|source| AppError::Internal(Box::new(source)))
}
pub async fn check(session: &Session) -> Result<bool, AppError> {
Ok(id(session).await?.is_some())
}
pub async fn check_for<U: Authenticatable>(session: &Session) -> Result<bool, AppError> {
Ok(id_for::<U>(session).await?.is_some())
}
pub async fn user<U: Authenticatable>(session: &Session) -> Result<Option<U>, AppError> {
match id_for::<U>(session).await? {
Some(user_id) => U::find_for_auth(user_id).await,
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tower_sessions::MemoryStore;
#[derive(Clone)]
struct TestUser {
id: i64,
}
impl Authenticatable for TestUser {
fn auth_id(&self) -> i64 {
self.id
}
async fn find_for_auth(id: i64) -> Result<Option<Self>, AppError> {
Ok((id == 1).then_some(TestUser { id }))
}
}
#[derive(Clone)]
struct AdminUser {
id: i64,
}
impl Authenticatable for AdminUser {
const GUARD: &'static str = "admin";
fn auth_id(&self) -> i64 {
self.id
}
async fn find_for_auth(id: i64) -> Result<Option<Self>, AppError> {
Ok((id == 1).then_some(AdminUser { id }))
}
}
fn new_session() -> Session {
Session::new(None, Arc::new(MemoryStore::default()), None)
}
#[tokio::test]
async fn login_sets_id_and_check_reflects_it() {
let session = new_session();
assert!(!check(&session).await.unwrap());
login(&session, &TestUser { id: 1 }).await.unwrap();
assert_eq!(id(&session).await.unwrap(), Some(1));
assert!(check(&session).await.unwrap());
}
#[tokio::test]
async fn logout_clears_the_whole_session() {
let session = new_session();
login(&session, &TestUser { id: 1 }).await.unwrap();
logout(&session).await.unwrap();
assert_eq!(id(&session).await.unwrap(), None);
assert!(!check(&session).await.unwrap());
}
#[tokio::test]
async fn user_resolves_the_logged_in_user() {
let session = new_session();
login(&session, &TestUser { id: 1 }).await.unwrap();
let resolved = user::<TestUser>(&session).await.unwrap();
assert_eq!(resolved.map(|u| u.id), Some(1));
}
#[tokio::test]
async fn user_returns_none_when_logged_out() {
let session = new_session();
assert_eq!(
user::<TestUser>(&session).await.unwrap().map(|u| u.id),
None
);
}
#[tokio::test]
async fn user_returns_none_for_an_id_that_no_longer_resolves() {
let session = new_session();
login(&session, &TestUser { id: 999 }).await.unwrap();
let resolved = user::<TestUser>(&session).await.unwrap();
assert!(resolved.is_none());
}
#[tokio::test]
async fn two_guards_stay_logged_in_independently_in_the_same_session() {
let session = new_session();
login(&session, &TestUser { id: 1 }).await.unwrap();
login(&session, &AdminUser { id: 1 }).await.unwrap();
assert!(check(&session).await.unwrap());
assert!(check_for::<AdminUser>(&session).await.unwrap());
assert_eq!(
user::<TestUser>(&session).await.unwrap().map(|u| u.id),
Some(1)
);
assert_eq!(
user::<AdminUser>(&session).await.unwrap().map(|u| u.id),
Some(1)
);
}
#[tokio::test]
async fn logout_for_only_clears_its_own_guard() {
let session = new_session();
login(&session, &TestUser { id: 1 }).await.unwrap();
login(&session, &AdminUser { id: 1 }).await.unwrap();
logout_for::<AdminUser>(&session).await.unwrap();
assert!(!check_for::<AdminUser>(&session).await.unwrap());
assert!(check(&session).await.unwrap());
}
#[tokio::test]
async fn a_guard_that_never_logged_in_reads_as_logged_out() {
let session = new_session();
login(&session, &TestUser { id: 1 }).await.unwrap();
assert!(!check_for::<AdminUser>(&session).await.unwrap());
assert_eq!(id_for::<AdminUser>(&session).await.unwrap(), None);
}
}