//! # User tokens management use crate::connections::redis_connection; use crate::constants::{ AUTH_TOKEN_DURATION, AUTH_TOKEN_HB_MIN_INTERVAL, AUTH_TOKEN_MAX_INACTIVITY, }; use crate::models::{User, UserID}; use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest}; use std::future::{Ready, ready}; #[derive(thiserror::Error, Debug)] enum LoginTokenServiceError { #[error("Malformed login token!")] MalformedToken, } #[derive(Debug, Clone)] pub struct LoginTokenValue(String); impl LoginTokenValue { pub fn parse(&self) -> anyhow::Result<(UserID, &str)> { let (id, key) = self .0 .split_once('-') .ok_or(LoginTokenServiceError::MalformedToken)?; Ok((UserID(id.parse()?), key)) } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct LoginToken { #[serde(rename = "e")] expire: u64, #[serde(rename = "h")] hb: u64, #[serde(rename = "t")] key: String, #[serde(rename = "u")] pub user_id: UserID, } impl LoginToken { fn new(user_id: UserID) -> Self { Self { expire: time() + AUTH_TOKEN_DURATION.as_secs(), hb: time(), key: rand_str(40), user_id, } } fn is_expired(&self) -> bool { self.expire < time() || self.hb + AUTH_TOKEN_MAX_INACTIVITY.as_secs() < time() } fn refresh_hb(&mut self) -> bool { if self.hb + AUTH_TOKEN_HB_MIN_INTERVAL.as_secs() < time() { self.hb = time(); return true; } false } /// Get the token returned to the user fn key(&self) -> String { format!("{}-{}", self.user_id.0, self.key) } } /// Get redis key for a given user fn redis_key(user_id: &UserID) -> String { format!("tok-{}", user_id.0) } /// Get the tokens of a user async fn get_user_tokens(user_id: UserID) -> anyhow::Result> { Ok(redis_connection::get_value(&redis_key(&user_id)) .await? .unwrap_or_default()) } async fn set_user_tokens(user_id: UserID, mut tokens: Vec) -> anyhow::Result<()> { tokens.retain(|t| !t.is_expired()); redis_connection::set_value(&redis_key(&user_id), &tokens, AUTH_TOKEN_MAX_INACTIVITY).await } /// Generate a new login token pub async fn gen_new_token(user: &User) -> anyhow::Result { let mut tokens = get_user_tokens(user.id()).await?; let token = LoginToken::new(user.id()); let key = token.key(); tokens.push(token); set_user_tokens(user.id(), tokens).await?; Ok(key) } /// Delete a login token (disconnect a client) pub async fn delete_token(token: &LoginTokenValue) -> anyhow::Result<()> { let (user_id, key) = token.parse()?; let mut tokens = get_user_tokens(user_id).await?; tokens.retain(|t| t.key != key); set_user_tokens(user_id, tokens).await } /// Remove all the tokens of a user (disconnect it from all devices) pub async fn disconnect_user_from_all_devices(user_id: UserID) -> anyhow::Result<()> { set_user_tokens(user_id, vec![]).await } /// Get a user information from its token async fn load_token_info(token: &LoginTokenValue) -> anyhow::Result> { let (user_id, key) = token.parse()?; let mut user_tokens = get_user_tokens(user_id).await?; let token = match user_tokens.iter_mut().find(|t| t.key == key) { Some(t) => t, None => { log::error!("Could not find token for key '{key}' (missing token)"); return Ok(None); } }; if token.is_expired() { log::error!("Could not find token for key '{key}' (token expired)"); return Ok(None); } // Check if heartbeat must be updated let clone = token.clone(); if token.refresh_hb() { set_user_tokens(clone.user_id, user_tokens).await?; } Ok(Some(clone)) } impl FromRequest for LoginTokenValue { type Error = actix_web::Error; type Future = Ready>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { ready(match req.headers().get("X-Auth-Token") { Some(v) => Ok(LoginTokenValue(v.to_str().unwrap_or("").to_string())), None => Err(actix_web::error::ErrorBadRequest( "Missing auth token header!", )), }) } } impl FromRequest for LoginToken { type Error = actix_web::Error; type Future = futures_util::future::LocalBoxFuture<'static, Result>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let req = req.clone(); Box::pin(async move { let token = LoginTokenValue::extract(&req).await?; let token = match load_token_info(&token).await { Err(e) => { log::error!("Failed to load auth token! {e}"); return Err(actix_web::error::ErrorPreconditionFailed( "Failed to check auth token!", )); } Ok(None) => { return Err(actix_web::error::ErrorPreconditionFailed( "Invalid auth token!", )); } Ok(Some(t)) => t, }; Ok(token) }) } }