2025-02-26 13:10:48 -08:00
|
|
|
use chrono::{DateTime, TimeDelta, Utc};
|
|
|
|
use deadpool_diesel::postgres::Connection;
|
|
|
|
use diesel::{
|
2025-03-14 13:04:57 -07:00
|
|
|
dsl::{auto_type, AsSelect, Gt, Select},
|
2025-02-26 13:10:48 -08:00
|
|
|
pg::Pg,
|
|
|
|
prelude::*,
|
|
|
|
};
|
2025-02-26 13:10:50 -08:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
2025-02-26 13:10:48 -08:00
|
|
|
use crate::{app_error::AppError, schema::csrf_tokens::dsl::*};
|
2025-02-26 13:10:50 -08:00
|
|
|
|
2025-03-14 13:04:57 -07:00
|
|
|
const TOKEN_PREFIX: &str = "csrf-";
|
2025-02-26 13:10:50 -08:00
|
|
|
|
2025-02-26 13:10:48 -08:00
|
|
|
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
|
|
|
#[diesel(table_name = crate::schema::csrf_tokens)]
|
|
|
|
#[diesel(check_for_backend(Pg))]
|
|
|
|
pub struct CsrfToken {
|
|
|
|
pub id: Uuid,
|
|
|
|
pub user_id: Option<Uuid>,
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CsrfToken {
|
|
|
|
fn all() -> Select<csrf_tokens, AsSelect<CsrfToken, Pg>> {
|
|
|
|
csrf_tokens.select(Self::as_select())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_not_expired() -> Gt<created_at, DateTime<Utc>> {
|
|
|
|
let ttl = TimeDelta::hours(24);
|
|
|
|
let min_created_at: DateTime<Utc> = Utc::now() - ttl;
|
|
|
|
created_at.gt(min_created_at)
|
|
|
|
}
|
|
|
|
|
2025-03-14 13:04:57 -07:00
|
|
|
#[auto_type(no_type_alias)]
|
|
|
|
pub fn with_user_id<'a>(token_user_id: &'a Option<Uuid>) -> _ {
|
2025-02-26 13:10:48 -08:00
|
|
|
user_id.is_not_distinct_from(token_user_id)
|
|
|
|
}
|
|
|
|
|
2025-03-14 13:04:57 -07:00
|
|
|
#[auto_type(no_type_alias)]
|
|
|
|
pub fn with_token_id<'a>(token_id: &'a Uuid) -> _ {
|
2025-02-26 13:10:48 -08:00
|
|
|
id.eq(token_id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-14 13:04:57 -07:00
|
|
|
/// Convenience function for creating new CSRF token rows in the database.
|
2025-02-26 13:10:48 -08:00
|
|
|
pub async fn generate_csrf_token(
|
|
|
|
db_conn: &Connection,
|
|
|
|
with_user_id: Option<Uuid>,
|
2025-02-26 13:10:50 -08:00
|
|
|
) -> Result<String, AppError> {
|
2025-02-26 13:10:48 -08:00
|
|
|
let token_id = Uuid::new_v4();
|
|
|
|
db_conn
|
2025-02-26 13:10:50 -08:00
|
|
|
.interact(move |conn| {
|
2025-02-26 13:10:48 -08:00
|
|
|
diesel::insert_into(csrf_tokens)
|
2025-02-26 13:10:50 -08:00
|
|
|
.values((
|
2025-02-26 13:10:48 -08:00
|
|
|
id.eq(token_id),
|
|
|
|
user_id.eq(with_user_id),
|
|
|
|
created_at.eq(diesel::dsl::now),
|
2025-02-26 13:10:50 -08:00
|
|
|
))
|
|
|
|
.execute(conn)
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
.unwrap()?;
|
2025-03-14 13:04:57 -07:00
|
|
|
Ok(format!("{}{}", TOKEN_PREFIX, token_id.simple()))
|
2025-02-26 13:10:50 -08:00
|
|
|
}
|
|
|
|
|
2025-03-14 13:04:57 -07:00
|
|
|
/// Convenience function for validating CSRF tokens against the database.
|
2025-02-26 13:10:48 -08:00
|
|
|
pub async fn validate_csrf_token(
|
|
|
|
db_conn: &Connection,
|
2025-02-26 13:10:50 -08:00
|
|
|
token: &str,
|
2025-02-26 13:10:48 -08:00
|
|
|
with_user_id: Option<Uuid>,
|
2025-02-26 13:10:50 -08:00
|
|
|
) -> Result<bool, AppError> {
|
2025-02-26 13:10:48 -08:00
|
|
|
let token_id = match Uuid::try_parse(&token[TOKEN_PREFIX.len()..]) {
|
|
|
|
Ok(token_id) => token_id,
|
2025-02-26 13:10:50 -08:00
|
|
|
Err(_) => return Ok(false),
|
|
|
|
};
|
2025-02-26 13:10:48 -08:00
|
|
|
Ok(db_conn
|
2025-02-26 13:10:50 -08:00
|
|
|
.interact(move |conn| {
|
2025-02-26 13:10:48 -08:00
|
|
|
CsrfToken::all()
|
2025-03-14 13:04:57 -07:00
|
|
|
.filter(CsrfToken::with_token_id(&token_id))
|
|
|
|
.filter(CsrfToken::with_user_id(&with_user_id))
|
2025-02-26 13:10:48 -08:00
|
|
|
.filter(CsrfToken::is_not_expired())
|
2025-02-26 13:10:50 -08:00
|
|
|
.first(conn)
|
|
|
|
.optional()
|
|
|
|
})
|
|
|
|
.await
|
2025-02-26 13:10:48 -08:00
|
|
|
.unwrap()?
|
|
|
|
.is_some())
|
2025-02-26 13:10:50 -08:00
|
|
|
}
|