use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models::session_item::Session as DBSession; use crate::database::models::session_item::SessionBuilder; use crate::database::models::UserId; use crate::models::pats::Scopes; use crate::models::sessions::Session; use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::env::parse_var; use actix_web::http::header::AUTHORIZATION; use actix_web::web::{scope, Data, ServiceConfig}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use chrono::Utc; use rand::distributions::Alphanumeric; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use sqlx::PgPool; use woothee::parser::Parser; pub fn config(cfg: &mut ServiceConfig) { cfg.service( scope("session") .service(list) .service(delete) .service(refresh), ); } pub struct SessionMetadata { pub city: Option, pub country: Option, pub ip: String, pub os: Option, pub platform: Option, pub user_agent: String, } pub async fn get_session_metadata( req: &HttpRequest, ) -> Result { let conn_info = req.connection_info().clone(); let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { if let Some(header) = req.headers().get("CF-Connecting-IP") { header.to_str().ok() } else { conn_info.peer_addr() } } else { conn_info.peer_addr() }; let country = req .headers() .get("cf-ipcountry") .and_then(|x| x.to_str().ok()); let city = req.headers().get("cf-ipcity").and_then(|x| x.to_str().ok()); let user_agent = req .headers() .get("user-agent") .and_then(|x| x.to_str().ok()) .ok_or_else(|| AuthenticationError::InvalidCredentials)?; let parser = Parser::new(); let info = parser.parse(user_agent); let os = if let Some(info) = info { Some((info.os, info.name)) } else { None }; Ok(SessionMetadata { os: os.map(|x| x.0.to_string()), platform: os.map(|x| x.1.to_string()), city: city.map(|x| x.to_string()), country: country.map(|x| x.to_string()), ip: ip_addr .ok_or_else(|| AuthenticationError::InvalidCredentials)? .to_string(), user_agent: user_agent.to_string(), }) } pub async fn issue_session( req: HttpRequest, user_id: UserId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, redis: &deadpool_redis::Pool, ) -> Result { let metadata = get_session_metadata(&req).await?; let session = ChaCha20Rng::from_entropy() .sample_iter(&Alphanumeric) .take(60) .map(char::from) .collect::(); let session = format!("mra_{session}"); let id = SessionBuilder { session, user_id, os: metadata.os, platform: metadata.platform, city: metadata.city, country: metadata.country, ip: metadata.ip, user_agent: metadata.user_agent, } .insert(transaction) .await?; let session = DBSession::get_id(id, &mut *transaction, redis) .await? .ok_or_else(|| AuthenticationError::InvalidCredentials)?; DBSession::clear_cache( vec![( Some(session.id), Some(session.session.clone()), Some(session.user_id), )], redis, ) .await?; Ok(session) } #[get("list")] pub async fn list( req: HttpRequest, pool: Data, redis: Data, session_queue: Data, ) -> Result { let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::SESSION_READ]), ) .await? .1; let session = req .headers() .get(AUTHORIZATION) .and_then(|x| x.to_str().ok()) .ok_or_else(|| AuthenticationError::InvalidCredentials)?; let session_ids = DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis).await?; let sessions = DBSession::get_many_ids(&session_ids, &**pool, &redis) .await? .into_iter() .filter(|x| x.expires > Utc::now()) .map(|x| Session::from(x, false, Some(session))) .collect::>(); Ok(HttpResponse::Ok().json(sessions)) } #[delete("{id}")] pub async fn delete( info: web::Path<(String,)>, req: HttpRequest, pool: Data, redis: Data, session_queue: Data, ) -> Result { let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::SESSION_DELETE]), ) .await? .1; let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?; if let Some(session) = session { if session.user_id == current_user.id.into() { let mut transaction = pool.begin().await?; DBSession::remove(session.id, &mut transaction).await?; DBSession::clear_cache( vec![( Some(session.id), Some(session.session), Some(session.user_id), )], &redis, ) .await?; transaction.commit().await?; } } Ok(HttpResponse::NoContent().body("")) } #[post("refresh")] pub async fn refresh( req: HttpRequest, pool: Data, redis: Data, session_queue: Data, ) -> Result { let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue, None) .await? .1; let session = req .headers() .get(AUTHORIZATION) .and_then(|x| x.to_str().ok()) .ok_or_else(|| ApiError::Authentication(AuthenticationError::InvalidCredentials))?; let session = DBSession::get(session, &**pool, &redis).await?; if let Some(session) = session { if current_user.id != session.user_id.into() || session.refresh_expires < Utc::now() { return Err(ApiError::Authentication( AuthenticationError::InvalidCredentials, )); } let mut transaction = pool.begin().await?; DBSession::remove(session.id, &mut transaction).await?; let new_session = issue_session(req, session.user_id, &mut transaction, &redis).await?; DBSession::clear_cache( vec![( Some(session.id), Some(session.session), Some(session.user_id), )], &redis, ) .await?; transaction.commit().await?; Ok(HttpResponse::Ok().json(Session::from(new_session, true, None))) } else { Err(ApiError::Authentication( AuthenticationError::InvalidCredentials, )) } }