You've already forked pages
forked from didirus/AstralRinth
* Update Rust version * Update async-compression 0.4.25 -> 0.4.27 * Update async-tungstenite 0.29.1 -> 0.30.0 * Update bytemuck 1.23.0 -> 1.23.1 * Update clap 4.5.40 -> 4.5.43 * Update deadpool-redis 0.21.1 -> 0.22.0 and redis 0.31.0 -> 0.32.4 * Update enumset 1.1.6 -> 1.1.7 * Update hyper-util 0.1.14 -> 0.1.16 * Update indexmap 2.9.0 -> 2.10.0 * Update indicatif 0.17.11 -> 0.18.0 * Update jemalloc_pprof 0.7.0 -> 0.8.1 * Update lettre 0.11.17 -> 0.11.18 * Update meilisearch-sdk 0.28.0 -> 0.29.1 * Update notify 8.0.0 -> 8.2.0 and notify-debouncer-mini 0.6.0 -> 0.7.0 * Update quick-xml 0.37.5 -> 0.38.1 * Fix theseus lint * Update reqwest 0.12.20 -> 0.12.22 * Cargo fmt in theseus * Update rgb 0.8.50 -> 0.8.52 * Update sentry 0.41.0 -> 0.42.0 and sentry-actix 0.41.0 -> 0.42.0 * Update serde_json 1.0.140 -> 1.0.142 * Update serde_with 3.13.0 -> 3.14.0 * Update spdx 0.10.8 -> 0.10.9 * Update sysinfo 0.35.2 -> 0.36.1 * Update tauri suite * Fix build by updating mappings * Update tokio 1.45.1 -> 1.47.1 and tokio-util 0.7.15 -> 0.7.16 * Update tracing-actix-web 0.7.18 -> 0.7.19 * Update zip 4.2.0 -> 4.3.0 * Misc Cargo.lock updates * Update Dockerfiles
267 lines
7.0 KiB
Rust
267 lines
7.0 KiB
Rust
use crate::auth::{AuthenticationError, get_user_from_headers};
|
|
use crate::database::models::DBUserId;
|
|
use crate::database::models::session_item::DBSession;
|
|
use crate::database::models::session_item::SessionBuilder;
|
|
use crate::database::redis::RedisPool;
|
|
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::{Data, ServiceConfig, scope};
|
|
use actix_web::{HttpRequest, HttpResponse, delete, get, post, web};
|
|
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<String>,
|
|
pub country: Option<String>,
|
|
pub ip: String,
|
|
|
|
pub os: Option<String>,
|
|
pub platform: Option<String>,
|
|
pub user_agent: String,
|
|
}
|
|
|
|
pub async fn get_session_metadata(
|
|
req: &HttpRequest,
|
|
) -> Result<SessionMetadata, AuthenticationError> {
|
|
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())
|
|
.unwrap_or("No user agent");
|
|
|
|
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: DBUserId,
|
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
redis: &RedisPool,
|
|
) -> Result<DBSession, AuthenticationError> {
|
|
let metadata = get_session_metadata(&req).await?;
|
|
|
|
let session = ChaCha20Rng::from_entropy()
|
|
.sample_iter(&Alphanumeric)
|
|
.take(60)
|
|
.map(char::from)
|
|
.collect::<String>();
|
|
|
|
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<PgPool>,
|
|
redis: Data<RedisPool>,
|
|
session_queue: Data<AuthQueue>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
let current_user = get_user_from_headers(
|
|
&req,
|
|
&**pool,
|
|
&redis,
|
|
&session_queue,
|
|
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::<Vec<_>>();
|
|
|
|
Ok(HttpResponse::Ok().json(sessions))
|
|
}
|
|
|
|
#[delete("{id}")]
|
|
pub async fn delete(
|
|
info: web::Path<(String,)>,
|
|
req: HttpRequest,
|
|
pool: Data<PgPool>,
|
|
redis: Data<RedisPool>,
|
|
session_queue: Data<AuthQueue>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
let current_user = get_user_from_headers(
|
|
&req,
|
|
&**pool,
|
|
&redis,
|
|
&session_queue,
|
|
Scopes::SESSION_DELETE,
|
|
)
|
|
.await?
|
|
.1;
|
|
|
|
let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
|
|
|
|
if let Some(session) = session
|
|
&& session.user_id == current_user.id.into()
|
|
{
|
|
let mut transaction = pool.begin().await?;
|
|
DBSession::remove(session.id, &mut transaction).await?;
|
|
transaction.commit().await?;
|
|
DBSession::clear_cache(
|
|
vec![(
|
|
Some(session.id),
|
|
Some(session.session),
|
|
Some(session.user_id),
|
|
)],
|
|
&redis,
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
Ok(HttpResponse::NoContent().body(""))
|
|
}
|
|
|
|
#[post("refresh")]
|
|
pub async fn refresh(
|
|
req: HttpRequest,
|
|
pool: Data<PgPool>,
|
|
redis: Data<RedisPool>,
|
|
session_queue: Data<AuthQueue>,
|
|
) -> Result<HttpResponse, ApiError> {
|
|
let current_user = get_user_from_headers(
|
|
&req,
|
|
&**pool,
|
|
&redis,
|
|
&session_queue,
|
|
Scopes::empty(),
|
|
)
|
|
.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?;
|
|
transaction.commit().await?;
|
|
DBSession::clear_cache(
|
|
vec![(
|
|
Some(session.id),
|
|
Some(session.session),
|
|
Some(session.user_id),
|
|
)],
|
|
&redis,
|
|
)
|
|
.await?;
|
|
|
|
Ok(HttpResponse::Ok().json(Session::from(new_session, true, None)))
|
|
} else {
|
|
Err(ApiError::Authentication(
|
|
AuthenticationError::InvalidCredentials,
|
|
))
|
|
}
|
|
}
|