use crate::auth::{AuthError, AuthResult, AuthUser, WebSocketAuthenticator};
use crate::connection::WebSocketConnection;
use async_trait::async_trait;
use reinhardt_auth::sessions::{Session, SessionBackend, SessionError};
use std::sync::Arc;
const DEFAULT_COOKIE_NAME: &str = "sessionid";
const DEFAULT_TIMEOUT: u64 = 1800;
#[derive(Debug, Clone)]
pub struct PagesAuthUser {
pub user_id: String,
pub username: String,
pub permissions: Vec<String>,
pub is_superuser: bool,
}
impl AuthUser for PagesAuthUser {
fn id(&self) -> &str {
&self.user_id
}
fn username(&self) -> &str {
&self.username
}
fn is_authenticated(&self) -> bool {
!self.user_id.is_empty()
}
fn has_permission(&self, permission: &str) -> bool {
self.is_superuser || self.permissions.contains(&permission.to_string())
}
}
pub struct PagesAuthenticator<B: SessionBackend> {
session_backend: B,
cookie_name: String,
timeout: Option<u64>,
}
impl<B: SessionBackend> PagesAuthenticator<B> {
pub fn new(session_backend: B) -> Self {
Self {
session_backend,
cookie_name: DEFAULT_COOKIE_NAME.to_string(),
timeout: Some(DEFAULT_TIMEOUT),
}
}
pub fn with_cookie_name(mut self, name: impl Into<String>) -> Self {
self.cookie_name = name.into();
self
}
pub fn with_timeout(mut self, timeout: Option<u64>) -> Self {
self.timeout = timeout;
self
}
pub async fn authenticate_from_cookies(&self, cookies: &str) -> AuthResult<Box<dyn AuthUser>> {
let session_id = self.extract_session_id(cookies)?;
let mut session = Session::from_key(self.session_backend.clone(), session_id)
.await
.map_err(Self::map_session_error)?;
if let Some(timeout) = self.timeout {
session.set_timeout(timeout);
}
session
.validate_timeout()
.map_err(|_| AuthError::TokenExpired)?;
let user_id: String = session
.get("_auth_user_id")
.map_err(|e| AuthError::AuthenticationFailed(format!("Failed to read user ID: {}", e)))?
.ok_or_else(|| {
AuthError::AuthenticationFailed("User ID not found in session".to_string())
})?;
let username: String = session
.get("_auth_user_name")
.map_err(|e| {
AuthError::AuthenticationFailed(format!("Failed to read username: {}", e))
})?
.unwrap_or_else(|| user_id.clone());
let is_superuser: bool = session
.get("_auth_user_is_superuser")
.map_err(|e| {
AuthError::AuthenticationFailed(format!("Failed to read superuser flag: {}", e))
})?
.unwrap_or(false);
let permissions: Vec<String> = session
.get("_auth_user_permissions")
.map_err(|e| {
AuthError::AuthenticationFailed(format!("Failed to read permissions: {}", e))
})?
.unwrap_or_default();
Ok(Box::new(PagesAuthUser {
user_id,
username,
permissions,
is_superuser,
}))
}
fn extract_session_id(&self, cookies: &str) -> AuthResult<String> {
for cookie in cookies.split(';') {
let cookie = cookie.trim();
if let Some((name, value)) = cookie.split_once('=')
&& name.trim() == self.cookie_name
{
return Ok(value.trim().to_string());
}
}
Err(AuthError::AuthenticationFailed(format!(
"Session ID not found in cookies (looking for '{}')",
self.cookie_name
)))
}
fn map_session_error(error: SessionError) -> AuthError {
match error {
SessionError::SessionExpired => AuthError::TokenExpired,
SessionError::CacheError(msg) => {
AuthError::AuthenticationFailed(format!("Session store error: {}", msg))
}
SessionError::SerializationError(msg) => {
AuthError::AuthenticationFailed(format!("Session data error: {}", msg))
}
_ => AuthError::AuthenticationFailed(format!("Session error: {}", error)),
}
}
}
#[async_trait]
impl<B: SessionBackend + 'static> WebSocketAuthenticator for PagesAuthenticator<B> {
async fn authenticate(
&self,
_connection: &Arc<WebSocketConnection>,
credentials: &str,
) -> AuthResult<Box<dyn AuthUser>> {
self.authenticate_from_cookies(credentials).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use reinhardt_auth::sessions::InMemorySessionBackend;
use rstest::{fixture, rstest};
#[fixture]
fn backend() -> InMemorySessionBackend {
InMemorySessionBackend::new()
}
#[rstest]
fn test_pages_auth_user_creation() {
let user = PagesAuthUser {
user_id: "user_1".to_string(),
username: "alice".to_string(),
permissions: vec!["chat.read".to_string(), "chat.write".to_string()],
is_superuser: false,
};
assert_eq!(user.id(), "user_1");
assert_eq!(user.username(), "alice");
assert!(user.is_authenticated());
assert!(user.has_permission("chat.read"));
assert!(user.has_permission("chat.write"));
assert!(!user.has_permission("admin.delete"));
}
#[rstest]
fn test_pages_auth_user_superuser() {
let user = PagesAuthUser {
user_id: "admin_1".to_string(),
username: "admin".to_string(),
permissions: vec![],
is_superuser: true,
};
assert!(user.has_permission("any.permission"));
assert!(user.has_permission("admin.delete"));
}
#[rstest]
fn test_extract_session_id_success(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend);
let cookies = "sessionid=abc123; csrftoken=xyz789";
let session_id = auth.extract_session_id(cookies).unwrap();
assert_eq!(session_id, "abc123");
}
#[rstest]
fn test_extract_session_id_with_spaces(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend);
let cookies = "sessionid = abc123 ; csrftoken = xyz789";
let session_id = auth.extract_session_id(cookies).unwrap();
assert_eq!(session_id, "abc123");
}
#[rstest]
fn test_extract_session_id_not_found(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend);
let cookies = "csrftoken=xyz789; other=value";
let result = auth.extract_session_id(cookies);
assert!(result.is_err());
}
#[rstest]
fn test_extract_session_id_empty_cookies(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend);
let cookies = "";
let result = auth.extract_session_id(cookies);
assert!(result.is_err());
}
#[rstest]
fn test_custom_cookie_name(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend).with_cookie_name("my_session");
let cookies = "my_session=custom123; sessionid=default456";
let session_id = auth.extract_session_id(cookies).unwrap();
assert_eq!(session_id, "custom123");
}
#[rstest]
fn test_custom_cookie_name_not_found(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend).with_cookie_name("my_session");
let cookies = "sessionid=abc123";
let result = auth.extract_session_id(cookies);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
AuthError::AuthenticationFailed(msg) => {
assert!(msg.contains("my_session"));
}
_ => panic!("Expected AuthenticationFailed error"),
}
}
#[rstest]
#[tokio::test]
async fn test_authenticate_valid_session(backend: InMemorySessionBackend) {
let mut session = Session::new(backend.clone());
session.set("_auth_user_id", "user_123").unwrap();
session.set("_auth_user_name", "testuser").unwrap();
session.set("_auth_user_is_superuser", false).unwrap();
session
.set(
"_auth_user_permissions",
vec!["read".to_string(), "write".to_string()],
)
.unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend);
let cookies = format!("sessionid={}; csrftoken=xyz789", session_key);
let user = auth.authenticate_from_cookies(&cookies).await.unwrap();
assert_eq!(user.id(), "user_123");
assert_eq!(user.username(), "testuser");
assert!(user.is_authenticated());
assert!(user.has_permission("read"));
assert!(user.has_permission("write"));
assert!(!user.has_permission("admin"));
}
#[rstest]
#[tokio::test]
async fn test_authenticate_missing_user_id(backend: InMemorySessionBackend) {
let mut session = Session::new(backend.clone());
session.set("_auth_user_name", "testuser").unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend);
let cookies = format!("sessionid={}", session_key);
let result = auth.authenticate_from_cookies(&cookies).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
AuthError::AuthenticationFailed(msg) => {
assert!(msg.contains("User ID not found"));
}
_ => panic!("Expected AuthenticationFailed error"),
}
}
#[rstest]
#[tokio::test]
async fn test_authenticate_superuser_permissions(backend: InMemorySessionBackend) {
let mut session = Session::new(backend.clone());
session.set("_auth_user_id", "admin_1").unwrap();
session.set("_auth_user_name", "admin").unwrap();
session.set("_auth_user_is_superuser", true).unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend);
let cookies = format!("sessionid={}", session_key);
let user = auth.authenticate_from_cookies(&cookies).await.unwrap();
assert!(user.has_permission("any.permission"));
assert!(user.has_permission("admin.delete"));
assert!(user.has_permission("chat.write"));
}
#[rstest]
#[tokio::test]
async fn test_authenticate_default_values(backend: InMemorySessionBackend) {
let mut session = Session::new(backend.clone());
session.set("_auth_user_id", "minimal_user").unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend);
let cookies = format!("sessionid={}", session_key);
let user = auth.authenticate_from_cookies(&cookies).await.unwrap();
assert_eq!(user.username(), "minimal_user");
assert!(!user.has_permission("admin.access"));
}
#[rstest]
#[tokio::test]
async fn test_authenticate_missing_session_id(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend);
let cookies = "csrftoken=xyz789";
let result = auth.authenticate_from_cookies(cookies).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
AuthError::AuthenticationFailed(msg) => {
assert!(msg.contains("Session ID not found"));
}
_ => panic!("Expected AuthenticationFailed error"),
}
}
#[rstest]
#[tokio::test]
async fn test_authenticate_nonexistent_session(backend: InMemorySessionBackend) {
let auth = PagesAuthenticator::new(backend);
let cookies = "sessionid=nonexistent_session_id";
let result = auth.authenticate_from_cookies(cookies).await;
assert!(result.is_err());
}
#[rstest]
#[tokio::test]
async fn test_custom_timeout(backend: InMemorySessionBackend) {
let mut session = Session::new(backend.clone());
session.set("_auth_user_id", "user_123").unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend).with_timeout(Some(86400)); let cookies = format!("sessionid={}", session_key);
let result = auth.authenticate_from_cookies(&cookies).await;
assert!(result.is_ok());
}
#[rstest]
#[tokio::test]
async fn test_disabled_timeout(backend: InMemorySessionBackend) {
let mut session = Session::new(backend.clone());
session.set("_auth_user_id", "user_123").unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend).with_timeout(None);
let cookies = format!("sessionid={}", session_key);
let result = auth.authenticate_from_cookies(&cookies).await;
assert!(result.is_ok());
}
#[rstest]
#[tokio::test]
async fn test_websocket_authenticator_trait(backend: InMemorySessionBackend) {
use tokio::sync::mpsc;
let mut session = Session::new(backend.clone());
session.set("_auth_user_id", "ws_user").unwrap();
session.set("_auth_user_name", "websocket_user").unwrap();
session.save().await.unwrap();
let session_key = session.session_key().unwrap().to_string();
let auth = PagesAuthenticator::new(backend);
let (tx, _rx) = mpsc::unbounded_channel();
let conn = Arc::new(WebSocketConnection::new("conn_1".to_string(), tx));
let cookies = format!("sessionid={}; csrftoken=abc", session_key);
let user = auth.authenticate(&conn, &cookies).await.unwrap();
assert_eq!(user.id(), "ws_user");
assert_eq!(user.username(), "websocket_user");
}
}