move to monorepo dir

This commit is contained in:
Jai A
2024-10-16 14:11:42 -07:00
parent ff7975773e
commit e3a3379615
756 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
use crate::auth::validate::get_user_record_from_bearer_token;
use crate::database::redis::RedisPool;
use crate::models::analytics::Download;
use crate::models::ids::ProjectId;
use crate::models::pats::Scopes;
use crate::queue::analytics::AnalyticsQueue;
use crate::queue::maxmind::MaxMindIndexer;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::search::SearchConfig;
use crate::util::date::get_current_tenths_of_ms;
use crate::util::guards::admin_key_guard;
use actix_web::{patch, post, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::sync::Arc;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("admin")
.service(count_download)
.service(force_reindex),
);
}
#[derive(Deserialize)]
pub struct DownloadBody {
pub url: String,
pub project_id: ProjectId,
pub version_name: String,
pub ip: String,
pub headers: HashMap<String, String>,
}
// This is an internal route, cannot be used without key
#[patch("/_count-download", guard = "admin_key_guard")]
#[allow(clippy::too_many_arguments)]
pub async fn count_download(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
maxmind: web::Data<Arc<MaxMindIndexer>>,
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
session_queue: web::Data<AuthQueue>,
download_body: web::Json<DownloadBody>,
) -> Result<HttpResponse, ApiError> {
let token = download_body
.headers
.iter()
.find(|x| x.0.to_lowercase() == "authorization")
.map(|x| &**x.1);
let user = get_user_record_from_bearer_token(&req, token, &**pool, &redis, &session_queue)
.await
.ok()
.flatten();
let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into();
let id_option = crate::models::ids::base62_impl::parse_base62(&download_body.version_name)
.ok()
.map(|x| x as i64);
let (version_id, project_id) = if let Some(version) = sqlx::query!(
"
SELECT v.id id, v.mod_id mod_id FROM files f
INNER JOIN versions v ON v.id = f.version_id
WHERE f.url = $1
",
download_body.url,
)
.fetch_optional(pool.as_ref())
.await?
{
(version.id, version.mod_id)
} else if let Some(version) = sqlx::query!(
"
SELECT id, mod_id FROM versions
WHERE ((version_number = $1 OR id = $3) AND mod_id = $2)
",
download_body.version_name,
project_id as crate::database::models::ids::ProjectId,
id_option
)
.fetch_optional(pool.as_ref())
.await?
{
(version.id, version.mod_id)
} else {
return Err(ApiError::InvalidInput(
"Specified version does not exist!".to_string(),
));
};
let url = url::Url::parse(&download_body.url)
.map_err(|_| ApiError::InvalidInput("invalid download URL specified!".to_string()))?;
let ip = crate::routes::analytics::convert_to_ip_v6(&download_body.ip)
.unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped());
analytics_queue.add_download(Download {
recorded: get_current_tenths_of_ms(),
domain: url.host_str().unwrap_or_default().to_string(),
site_path: url.path().to_string(),
user_id: user
.and_then(|(scopes, x)| {
if scopes.contains(Scopes::PERFORM_ANALYTICS) {
Some(x.id.0 as u64)
} else {
None
}
})
.unwrap_or(0),
project_id: project_id as u64,
version_id: version_id as u64,
ip,
country: maxmind.query(ip).await.unwrap_or_default(),
user_agent: download_body
.headers
.get("user-agent")
.cloned()
.unwrap_or_default(),
headers: download_body
.headers
.clone()
.into_iter()
.filter(|x| !crate::routes::analytics::FILTERED_HEADERS.contains(&&*x.0.to_lowercase()))
.collect(),
});
Ok(HttpResponse::NoContent().body(""))
}
#[post("/_force_reindex", guard = "admin_key_guard")]
pub async fn force_reindex(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
config: web::Data<SearchConfig>,
) -> Result<HttpResponse, ApiError> {
use crate::search::indexing::index_projects;
let redis = redis.get_ref();
index_projects(pool.as_ref().clone(), redis.clone(), &config).await?;
Ok(HttpResponse::NoContent().finish())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
use crate::auth::get_user_from_headers;
use crate::database::redis::RedisPool;
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use actix_web::{post, web, HttpRequest, HttpResponse};
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("gdpr").service(export));
}
#[post("/export")]
pub async fn export(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_ACCESS]),
)
.await?
.1;
let user_id = user.id.into();
let collection_ids = crate::database::models::User::get_collections(user_id, &**pool).await?;
let collections =
crate::database::models::Collection::get_many(&collection_ids, &**pool, &redis)
.await?
.into_iter()
.map(crate::models::collections::Collection::from)
.collect::<Vec<_>>();
let follows = crate::database::models::User::get_follows(user_id, &**pool)
.await?
.into_iter()
.map(crate::models::ids::ProjectId::from)
.collect::<Vec<_>>();
let projects = crate::database::models::User::get_projects(user_id, &**pool, &redis)
.await?
.into_iter()
.map(crate::models::ids::ProjectId::from)
.collect::<Vec<_>>();
let org_ids = crate::database::models::User::get_organizations(user_id, &**pool).await?;
let orgs = crate::database::models::organization_item::Organization::get_many_ids(
&org_ids, &**pool, &redis,
)
.await?
.into_iter()
// TODO: add team members
.map(|x| crate::models::organizations::Organization::from(x, vec![]))
.collect::<Vec<_>>();
let notifs = crate::database::models::notification_item::Notification::get_many_user(
user_id, &**pool, &redis,
)
.await?
.into_iter()
.map(crate::models::notifications::Notification::from)
.collect::<Vec<_>>();
let oauth_clients =
crate::database::models::oauth_client_item::OAuthClient::get_all_user_clients(
user_id, &**pool,
)
.await?
.into_iter()
.map(crate::models::oauth_clients::OAuthClient::from)
.collect::<Vec<_>>();
let oauth_authorizations = crate::database::models::oauth_client_authorization_item::OAuthClientAuthorization::get_all_for_user(
user_id, &**pool,
)
.await?
.into_iter()
.map(crate::models::oauth_clients::OAuthClientAuthorization::from)
.collect::<Vec<_>>();
let pat_ids = crate::database::models::pat_item::PersonalAccessToken::get_user_pats(
user_id, &**pool, &redis,
)
.await?;
let pats = crate::database::models::pat_item::PersonalAccessToken::get_many_ids(
&pat_ids, &**pool, &redis,
)
.await?
.into_iter()
.map(|x| crate::models::pats::PersonalAccessToken::from(x, false))
.collect::<Vec<_>>();
let payout_ids =
crate::database::models::payout_item::Payout::get_all_for_user(user_id, &**pool).await?;
let payouts = crate::database::models::payout_item::Payout::get_many(&payout_ids, &**pool)
.await?
.into_iter()
.map(crate::models::payouts::Payout::from)
.collect::<Vec<_>>();
let report_ids =
crate::database::models::user_item::User::get_reports(user_id, &**pool).await?;
let reports = crate::database::models::report_item::Report::get_many(&report_ids, &**pool)
.await?
.into_iter()
.map(crate::models::reports::Report::from)
.collect::<Vec<_>>();
let message_ids = sqlx::query!(
"
SELECT id FROM threads_messages WHERE author_id = $1 AND hide_identity = FALSE
",
user_id.0
)
.fetch_all(pool.as_ref())
.await?
.into_iter()
.map(|x| crate::database::models::ids::ThreadMessageId(x.id))
.collect::<Vec<_>>();
let messages =
crate::database::models::thread_item::ThreadMessage::get_many(&message_ids, &**pool)
.await?
.into_iter()
.map(|x| crate::models::threads::ThreadMessage::from(x, &user))
.collect::<Vec<_>>();
let uploaded_images_ids = sqlx::query!(
"SELECT id FROM uploaded_images WHERE owner_id = $1",
user_id.0
)
.fetch_all(pool.as_ref())
.await?
.into_iter()
.map(|x| crate::database::models::ids::ImageId(x.id))
.collect::<Vec<_>>();
let uploaded_images =
crate::database::models::image_item::Image::get_many(&uploaded_images_ids, &**pool, &redis)
.await?
.into_iter()
.map(crate::models::images::Image::from)
.collect::<Vec<_>>();
let subscriptions =
crate::database::models::user_subscription_item::UserSubscriptionItem::get_all_user(
user_id, &**pool,
)
.await?
.into_iter()
.map(crate::models::billing::UserSubscription::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(serde_json::json!({
"user": user,
"collections": collections,
"follows": follows,
"projects": projects,
"orgs": orgs,
"notifs": notifs,
"oauth_clients": oauth_clients,
"oauth_authorizations": oauth_authorizations,
"pats": pats,
"payouts": payouts,
"reports": reports,
"messages": messages,
"uploaded_images": uploaded_images,
"subscriptions": subscriptions,
})))
}

View File

@@ -0,0 +1,26 @@
pub(crate) mod admin;
pub mod billing;
pub mod flows;
pub mod gdpr;
pub mod moderation;
pub mod pats;
pub mod session;
use super::v3::oauth_clients;
pub use super::ApiError;
use crate::util::cors::default_cors;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
actix_web::web::scope("_internal")
.wrap(default_cors())
.configure(admin::config)
.configure(oauth_clients::config)
.configure(session::config)
.configure(flows::config)
.configure(pats::config)
.configure(moderation::config)
.configure(billing::config)
.configure(gdpr::config),
);
}

View File

@@ -0,0 +1,313 @@
use super::ApiError;
use crate::database;
use crate::database::redis::RedisPool;
use crate::models::ids::random_base62;
use crate::models::projects::ProjectStatus;
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
use crate::queue::session::AuthQueue;
use crate::{auth::check_is_moderator_from_headers, models::pats::Scopes};
use actix_web::{web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
use std::collections::HashMap;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("moderation/projects", web::get().to(get_projects));
cfg.route("moderation/project/{id}", web::get().to(get_project_meta));
cfg.route("moderation/project", web::post().to(set_project_meta));
}
#[derive(Deserialize)]
pub struct ResultCount {
#[serde(default = "default_count")]
pub count: i16,
}
fn default_count() -> i16 {
100
}
pub async fn get_projects(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
count: web::Query<ResultCount>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_READ]),
)
.await?;
use futures::stream::TryStreamExt;
let project_ids = sqlx::query!(
"
SELECT id FROM mods
WHERE status = $1
ORDER BY queued ASC
LIMIT $2;
",
ProjectStatus::Processing.as_str(),
count.count as i64
)
.fetch(&**pool)
.map_ok(|m| database::models::ProjectId(m.id))
.try_collect::<Vec<database::models::ProjectId>>()
.await?;
let projects: Vec<_> = database::Project::get_many_ids(&project_ids, &**pool, &redis)
.await?
.into_iter()
.map(crate::models::projects::Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))
}
pub async fn get_project_meta(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
info: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_READ]),
)
.await?;
let project_id = info.into_inner().0;
let project = database::models::Project::get(&project_id, &**pool, &redis).await?;
if let Some(project) = project {
let rows = sqlx::query!(
"
SELECT
f.metadata, v.id version_id
FROM versions v
INNER JOIN files f ON f.version_id = v.id
WHERE v.mod_id = $1
",
project.inner.id.0
)
.fetch_all(&**pool)
.await?;
let mut merged = MissingMetadata {
identified: HashMap::new(),
flame_files: HashMap::new(),
unknown_files: HashMap::new(),
};
let mut check_hashes = Vec::new();
let mut check_flames = Vec::new();
for row in rows {
if let Some(metadata) = row
.metadata
.and_then(|x| serde_json::from_value::<MissingMetadata>(x).ok())
{
merged.identified.extend(metadata.identified);
merged.flame_files.extend(metadata.flame_files);
merged.unknown_files.extend(metadata.unknown_files);
check_hashes.extend(merged.flame_files.keys().cloned());
check_hashes.extend(merged.unknown_files.keys().cloned());
check_flames.extend(merged.flame_files.values().map(|x| x.id as i32));
}
}
let rows = sqlx::query!(
"
SELECT encode(mef.sha1, 'escape') sha1, mel.status status
FROM moderation_external_files mef
INNER JOIN moderation_external_licenses mel ON mef.external_license_id = mel.id
WHERE mef.sha1 = ANY($1)
",
&check_hashes
.iter()
.map(|x| x.as_bytes().to_vec())
.collect::<Vec<_>>()
)
.fetch_all(&**pool)
.await?;
for row in rows {
if let Some(sha1) = row.sha1 {
if let Some(val) = merged.flame_files.remove(&sha1) {
merged.identified.insert(
sha1,
IdentifiedFile {
file_name: val.file_name,
status: ApprovalType::from_string(&row.status)
.unwrap_or(ApprovalType::Unidentified),
},
);
} else if let Some(val) = merged.unknown_files.remove(&sha1) {
merged.identified.insert(
sha1,
IdentifiedFile {
file_name: val,
status: ApprovalType::from_string(&row.status)
.unwrap_or(ApprovalType::Unidentified),
},
);
}
}
}
let rows = sqlx::query!(
"
SELECT mel.id, mel.flame_project_id, mel.status status
FROM moderation_external_licenses mel
WHERE mel.flame_project_id = ANY($1)
",
&check_flames,
)
.fetch_all(&**pool)
.await?;
for row in rows {
if let Some(sha1) = merged
.flame_files
.iter()
.find(|x| Some(x.1.id as i32) == row.flame_project_id)
.map(|x| x.0.clone())
{
if let Some(val) = merged.flame_files.remove(&sha1) {
merged.identified.insert(
sha1,
IdentifiedFile {
file_name: val.file_name.clone(),
status: ApprovalType::from_string(&row.status)
.unwrap_or(ApprovalType::Unidentified),
},
);
}
}
}
Ok(HttpResponse::Ok().json(merged))
} else {
Err(ApiError::NotFound)
}
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Judgement {
Flame {
id: i32,
status: ApprovalType,
link: String,
title: String,
},
Unknown {
status: ApprovalType,
proof: Option<String>,
link: Option<String>,
title: Option<String>,
},
}
pub async fn set_project_meta(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
judgements: web::Json<HashMap<String, Judgement>>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PROJECT_READ]),
)
.await?;
let mut transaction = pool.begin().await?;
let mut ids = Vec::new();
let mut titles = Vec::new();
let mut statuses = Vec::new();
let mut links = Vec::new();
let mut proofs = Vec::new();
let mut flame_ids = Vec::new();
let mut file_hashes = Vec::new();
for (hash, judgement) in judgements.0 {
let id = random_base62(8);
let (title, status, link, proof, flame_id) = match judgement {
Judgement::Flame {
id,
status,
link,
title,
} => (
Some(title),
status,
Some(link),
Some("See Flame page/license for permission".to_string()),
Some(id),
),
Judgement::Unknown {
status,
proof,
link,
title,
} => (title, status, link, proof, None),
};
ids.push(id as i64);
titles.push(title);
statuses.push(status.as_str());
links.push(link);
proofs.push(proof);
flame_ids.push(flame_id);
file_hashes.push(hash);
}
sqlx::query(
"
INSERT INTO moderation_external_licenses (id, title, status, link, proof, flame_project_id)
SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::varchar[], $4::varchar[], $5::varchar[], $6::integer[])
"
)
.bind(&ids[..])
.bind(&titles[..])
.bind(&statuses[..])
.bind(&links[..])
.bind(&proofs[..])
.bind(&flame_ids[..])
.execute(&mut *transaction)
.await?;
sqlx::query(
"
INSERT INTO moderation_external_files (sha1, external_license_id)
SELECT * FROM UNNEST ($1::bytea[], $2::bigint[])
ON CONFLICT (sha1)
DO NOTHING
",
)
.bind(&file_hashes[..])
.bind(&ids[..])
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().finish())
}

View File

@@ -0,0 +1,282 @@
use crate::database;
use crate::database::models::generate_pat_id;
use crate::auth::get_user_from_headers;
use crate::routes::ApiError;
use crate::database::redis::RedisPool;
use actix_web::web::{self, Data};
use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use rand::distributions::Alphanumeric;
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use crate::models::pats::{PersonalAccessToken, Scopes};
use crate::queue::session::AuthQueue;
use crate::util::validate::validation_errors_to_string;
use serde::Deserialize;
use sqlx::postgres::PgPool;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
#[get("pat")]
pub async fn get_pats(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_READ]),
)
.await?
.1;
let pat_ids = database::models::pat_item::PersonalAccessToken::get_user_pats(
user.id.into(),
&**pool,
&redis,
)
.await?;
let pats =
database::models::pat_item::PersonalAccessToken::get_many_ids(&pat_ids, &**pool, &redis)
.await?;
Ok(HttpResponse::Ok().json(
pats.into_iter()
.map(|x| PersonalAccessToken::from(x, false))
.collect::<Vec<_>>(),
))
}
#[derive(Deserialize, Validate)]
pub struct NewPersonalAccessToken {
pub scopes: Scopes,
#[validate(length(min = 3, max = 255))]
pub name: String,
pub expires: DateTime<Utc>,
}
#[post("pat")]
pub async fn create_pat(
req: HttpRequest,
info: web::Json<NewPersonalAccessToken>,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
info.0
.validate()
.map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?;
if info.scopes.is_restricted() {
return Err(ApiError::InvalidInput(
"Invalid scopes requested!".to_string(),
));
}
if info.expires < Utc::now() {
return Err(ApiError::InvalidInput(
"Expire date must be in the future!".to_string(),
));
}
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_CREATE]),
)
.await?
.1;
let mut transaction = pool.begin().await?;
let id = generate_pat_id(&mut transaction).await?;
let token = ChaCha20Rng::from_entropy()
.sample_iter(&Alphanumeric)
.take(60)
.map(char::from)
.collect::<String>();
let token = format!("mrp_{}", token);
let name = info.name.clone();
database::models::pat_item::PersonalAccessToken {
id,
name: name.clone(),
access_token: token.clone(),
scopes: info.scopes,
user_id: user.id.into(),
created: Utc::now(),
expires: info.expires,
last_used: None,
}
.insert(&mut transaction)
.await?;
transaction.commit().await?;
database::models::pat_item::PersonalAccessToken::clear_cache(
vec![(None, None, Some(user.id.into()))],
&redis,
)
.await?;
Ok(HttpResponse::Ok().json(PersonalAccessToken {
id: id.into(),
name,
access_token: Some(token),
scopes: info.scopes,
user_id: user.id,
created: Utc::now(),
expires: info.expires,
last_used: None,
}))
}
#[derive(Deserialize, Validate)]
pub struct ModifyPersonalAccessToken {
pub scopes: Option<Scopes>,
#[validate(length(min = 3, max = 255))]
pub name: Option<String>,
pub expires: Option<DateTime<Utc>>,
}
#[patch("pat/{id}")]
pub async fn edit_pat(
req: HttpRequest,
id: web::Path<(String,)>,
info: web::Json<ModifyPersonalAccessToken>,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_WRITE]),
)
.await?
.1;
let id = id.into_inner().0;
let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?;
if let Some(pat) = pat {
if pat.user_id == user.id.into() {
let mut transaction = pool.begin().await?;
if let Some(scopes) = &info.scopes {
if scopes.is_restricted() {
return Err(ApiError::InvalidInput(
"Invalid scopes requested!".to_string(),
));
}
sqlx::query!(
"
UPDATE pats
SET scopes = $1
WHERE id = $2
",
scopes.bits() as i64,
pat.id.0
)
.execute(&mut *transaction)
.await?;
}
if let Some(name) = &info.name {
sqlx::query!(
"
UPDATE pats
SET name = $1
WHERE id = $2
",
name,
pat.id.0
)
.execute(&mut *transaction)
.await?;
}
if let Some(expires) = &info.expires {
if expires < &Utc::now() {
return Err(ApiError::InvalidInput(
"Expire date must be in the future!".to_string(),
));
}
sqlx::query!(
"
UPDATE pats
SET expires = $1
WHERE id = $2
",
expires,
pat.id.0
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
database::models::pat_item::PersonalAccessToken::clear_cache(
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
&redis,
)
.await?;
}
}
Ok(HttpResponse::NoContent().finish())
}
#[delete("pat/{id}")]
pub async fn delete_pat(
req: HttpRequest,
id: web::Path<(String,)>,
pool: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAT_DELETE]),
)
.await?
.1;
let id = id.into_inner().0;
let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?;
if let Some(pat) = pat {
if pat.user_id == user.id.into() {
let mut transaction = pool.begin().await?;
database::models::pat_item::PersonalAccessToken::remove(pat.id, &mut transaction)
.await?;
transaction.commit().await?;
database::models::pat_item::PersonalAccessToken::clear_cache(
vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))],
&redis,
)
.await?;
}
}
Ok(HttpResponse::NoContent().finish())
}

View File

@@ -0,0 +1,252 @@
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::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::{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<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())
.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: &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,
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::<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,
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?;
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, 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?;
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,
))
}
}