use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::session::errors::SessionError;
use crate::storage::CacheData;
use crate::userdb::User as DbUser;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub account: String,
pub label: String,
pub is_admin: bool,
pub sequence_number: Option<i64>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl From<DbUser> for User {
fn from(db_user: DbUser) -> Self {
Self {
id: db_user.id,
account: db_user.account,
label: db_user.label,
is_admin: db_user.is_admin,
sequence_number: db_user.sequence_number,
created_at: db_user.created_at,
updated_at: db_user.updated_at,
}
}
}
impl From<User> for DbUser {
fn from(session_user: User) -> Self {
Self {
id: session_user.id,
account: session_user.account,
label: session_user.label,
is_admin: session_user.is_admin,
sequence_number: session_user.sequence_number,
created_at: session_user.created_at,
updated_at: session_user.updated_at,
}
}
}
impl User {
pub fn has_admin_privileges(&self) -> bool {
self.is_admin || self.sequence_number == Some(1)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct StoredSession {
pub(super) user_id: String,
pub(super) csrf_token: String,
pub(super) expires_at: DateTime<Utc>,
pub(super) ttl: u64,
}
impl From<StoredSession> for CacheData {
fn from(data: StoredSession) -> Self {
Self {
value: serde_json::to_string(&data).expect("Failed to serialize StoredSession"),
}
}
}
impl TryFrom<CacheData> for StoredSession {
type Error = SessionError;
fn try_from(data: CacheData) -> Result<Self, Self::Error> {
serde_json::from_str(&data.value).map_err(|e| SessionError::Storage(e.to_string()))
}
}
#[derive(Debug, Clone)]
pub struct CsrfToken(String);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CsrfHeaderVerified(pub bool);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AuthenticationStatus(pub bool);
impl std::fmt::Display for AuthenticationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::Display for CsrfHeaderVerified {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl CsrfToken {
pub fn new(token: String) -> Self {
Self(token)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UserId(String);
impl UserId {
pub fn new(id: String) -> Result<Self, crate::session::SessionError> {
use crate::session::SessionError;
if id.is_empty() {
return Err(SessionError::Validation(
"User ID cannot be empty".to_string(),
));
}
if id.len() > 255 {
return Err(SessionError::Validation("User ID too long".to_string()));
}
if !id.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '@' | '+' | '(' | ')')
}) {
return Err(SessionError::Validation(
"User ID contains invalid characters".to_string(),
));
}
if id.contains("..") || id.contains("--") || id.contains("__") {
return Err(SessionError::Validation(
"User ID contains dangerous character sequences".to_string(),
));
}
Ok(UserId(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct SessionId(String);
impl SessionId {
pub fn new(id: String) -> Result<Self, crate::session::SessionError> {
use crate::session::SessionError;
if id.is_empty() {
return Err(SessionError::Validation(
"Session ID cannot be empty".to_string(),
));
}
if id.len() < 10 {
return Err(SessionError::Validation("Session ID too short".to_string()));
}
if id.len() > 256 {
return Err(SessionError::Validation("Session ID too long".to_string()));
}
if !id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~'))
{
return Err(SessionError::Validation(
"Session ID contains invalid characters".to_string(),
));
}
Ok(SessionId(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SessionCookie(String);
impl SessionCookie {
pub fn new(cookie: String) -> Result<Self, crate::session::SessionError> {
use crate::session::SessionError;
if cookie.is_empty() {
return Err(SessionError::Cookie(
"Session cookie cannot be empty".to_string(),
));
}
if cookie.len() < 10 {
return Err(SessionError::Cookie("Session cookie too short".to_string()));
}
if cookie.len() > 1024 {
return Err(SessionError::Cookie("Session cookie too long".to_string()));
}
if !cookie
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '=' | '.' | '+' | '/'))
{
return Err(SessionError::Cookie(
"Session cookie contains invalid characters".to_string(),
));
}
Ok(SessionCookie(cookie))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests;