use bb8_redis::redis::{AsyncCommands, LposOptions};
use cc_utils::prelude::*;
use chrono::{DateTime, Duration, Utc, serde::ts_seconds};
use passwords::PasswordGenerator;
use serde::{Deserialize, Serialize};
use sha3::{Digest, Sha3_256};
const TOKEN_LENGTH: usize = 64;
const TOKEN_PREFIX: &str = "user_tokens";
pub const MAX_TOKENS_PER_USER: isize = 3;
pub const DAYS_VALID: i64 = 28;
pub type UserId = u64;
#[derive(Deserialize, Serialize)]
pub struct UserToken {
pub user_id: UserId,
token_str: String,
#[serde(with = "ts_seconds")]
birth: DateTime<Utc>,
}
impl UserToken {
pub fn new(id: UserId) -> MResult<Self> { generate_token(id) }
}
pub type ApiToken = String;
pub fn hash_password(user_password: &[u8], user_salt: &[u8]) -> Vec<u8> {
let mut hasher = Sha3_256::new();
hasher.update([user_password, user_salt].concat());
hasher.finalize().to_vec()
}
pub fn hashes_eq(user_password: &[u8], salt_from_db: &[u8], hash_from_db: &[u8]) -> bool {
hash_password(user_password, salt_from_db).eq(hash_from_db)
}
pub fn get_user_tokens_list_name(user_id: UserId) -> String {
format!("{}:id{}", TOKEN_PREFIX, user_id)
}
pub async fn log_in(
user_login: String,
salt_db: &[u8],
hash_db: &[u8],
possible_user_id: UserId,
cacher: &bb8_redis::bb8::Pool<bb8_redis::RedisConnectionManager>,
) -> MResult<UserToken> {
if !hashes_eq(user_login.as_bytes(), salt_db, hash_db) { return Err("Hashes are not equal.".into()) } ;
let utl_name = get_user_tokens_list_name(possible_user_id);
let mut cacher_conn = cacher.get().await?;
let user_tokens_list_len: isize = cacher_conn.llen(&utl_name).await?;
let token = generate_token(possible_user_id)?;
if user_tokens_list_len >= MAX_TOKENS_PER_USER { cacher_conn.ltrim(&utl_name, 0, MAX_TOKENS_PER_USER - 1).await?; }
cacher_conn.lpush(&utl_name, &serde_json::to_string(&token)?).await?;
Ok(token)
}
pub async fn check_token(
token: &ApiToken,
cacher: &bb8_redis::bb8::Pool<bb8_redis::RedisConnectionManager>,
) -> MResult<UserId> {
let token_data = serde_json::from_str::<UserToken>(&token)?;
let user_tokens_list = get_user_tokens_list_name(token_data.user_id);
let mut cacher_conn = cacher.get().await?;
let idx: Option<i32> = cacher_conn.lpos(&user_tokens_list, &token, LposOptions::default()).await?;
if idx.is_none() { return Err("There is no such tokens.".into()) }
let duration: Duration = Utc::now() - token_data.birth;
if duration.num_days() >= DAYS_VALID {
cacher_conn.lrem(user_tokens_list, 1, &token).await?;
return Err("The token is expired.".into())
}
Ok(token_data.user_id)
}
pub async fn check_and_remove_token(
token: &ApiToken,
cacher: &bb8_redis::bb8::Pool<bb8_redis::RedisConnectionManager>,
) -> MResult<()> {
let token_data = serde_json::from_str::<UserToken>(&token)?;
let user_tokens_list = get_user_tokens_list_name(token_data.user_id);
let mut cacher_conn = cacher.get().await?;
let idx: Option<i32> = cacher_conn.lpos(&user_tokens_list, &token, LposOptions::default()).await?;
if idx.is_none() { return Err("There is no such tokens.".into()) }
cacher_conn.lrem(user_tokens_list, 1, &token).await?;
Ok(())
}
fn get_password_generator(length: usize) -> PasswordGenerator {
PasswordGenerator {
length,
numbers: true,
lowercase_letters: true,
uppercase_letters: true,
symbols: true,
strict: true,
exclude_similar_characters: true,
spaces: false,
}
}
pub fn generate_token(user_id: UserId) -> MResult<UserToken> {
Ok(UserToken {
user_id,
token_str: get_password_generator(TOKEN_LENGTH).generate_one()?,
birth: Utc::now(),
})
}
pub fn generate_salt() -> MResult<String> {
Ok(get_password_generator(16).generate_one()?)
}