You've already forked AstralRinth
forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
147
apps/labrinth/src/routes/internal/admin.rs
Normal file
147
apps/labrinth/src/routes/internal/admin.rs
Normal 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())
|
||||
}
|
||||
1847
apps/labrinth/src/routes/internal/billing.rs
Normal file
1847
apps/labrinth/src/routes/internal/billing.rs
Normal file
File diff suppressed because it is too large
Load Diff
2372
apps/labrinth/src/routes/internal/flows.rs
Normal file
2372
apps/labrinth/src/routes/internal/flows.rs
Normal file
File diff suppressed because it is too large
Load Diff
177
apps/labrinth/src/routes/internal/gdpr.rs
Normal file
177
apps/labrinth/src/routes/internal/gdpr.rs
Normal 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,
|
||||
})))
|
||||
}
|
||||
26
apps/labrinth/src/routes/internal/mod.rs
Normal file
26
apps/labrinth/src/routes/internal/mod.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
313
apps/labrinth/src/routes/internal/moderation.rs
Normal file
313
apps/labrinth/src/routes/internal/moderation.rs
Normal 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())
|
||||
}
|
||||
282
apps/labrinth/src/routes/internal/pats.rs
Normal file
282
apps/labrinth/src/routes/internal/pats.rs
Normal 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())
|
||||
}
|
||||
252
apps/labrinth/src/routes/internal/session.rs
Normal file
252
apps/labrinth/src/routes/internal/session.rs
Normal 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,
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user