mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bf0d74330 |
Generated
+2
@@ -4523,6 +4523,7 @@ dependencies = [
|
||||
"const_format",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"derive_more 2.0.1",
|
||||
"dotenv-build",
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -4558,6 +4559,7 @@ dependencies = [
|
||||
"rust_iso3166",
|
||||
"rustls 0.23.32",
|
||||
"rusty-money",
|
||||
"secrecy",
|
||||
"sentry",
|
||||
"sentry-actix",
|
||||
"serde",
|
||||
|
||||
@@ -16,10 +16,12 @@ actix-http = { workspace = true, optional = true }
|
||||
actix-multipart = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
derive_more = { workspace = true, features = ["debug"] }
|
||||
actix-web-prom = { workspace = true, features = ["process"] }
|
||||
actix-ws = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
argon2 = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
ariadne = { workspace = true }
|
||||
async-stripe = { workspace = true, features = [
|
||||
"billing",
|
||||
|
||||
@@ -8,7 +8,10 @@ pub use checks::{
|
||||
filter_visible_projects,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use validate::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
pub use validate::{
|
||||
check_is_moderator_from_headers, get_user_from_headers,
|
||||
get_user_from_headers_v2,
|
||||
};
|
||||
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::models::error::ApiError;
|
||||
@@ -20,8 +23,6 @@ use thiserror::Error;
|
||||
pub enum AuthenticationError {
|
||||
#[error(transparent)]
|
||||
Internal(#[from] eyre::Report),
|
||||
#[error("Environment Error")]
|
||||
Env(#[from] dotenvy::Error),
|
||||
#[error("An unknown database error occurred: {0}")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
@@ -58,7 +59,6 @@ impl actix_web::ResponseError for AuthenticationError {
|
||||
AuthenticationError::Internal(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
AuthenticationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthenticationError::Sqlx(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AuthenticationError::Database(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
@@ -93,7 +93,6 @@ impl AuthenticationError {
|
||||
pub fn error_name(&self) -> &'static str {
|
||||
match self {
|
||||
AuthenticationError::Internal(..) => "internal_error",
|
||||
AuthenticationError::Env(..) => "environment_error",
|
||||
AuthenticationError::Sqlx(..) => "database_error",
|
||||
AuthenticationError::Database(..) => "database_error",
|
||||
AuthenticationError::SerDe(..) => "invalid_input",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::auth::get_user_from_headers_v2;
|
||||
use crate::auth::oauth::uris::{OAuthRedirectUris, ValidatedRedirectUri};
|
||||
use crate::auth::validate::extract_authorization_header;
|
||||
use crate::database::models::flow_item::DBFlow;
|
||||
@@ -12,10 +12,9 @@ use crate::database::models::{
|
||||
generate_oauth_client_authorization_id,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models;
|
||||
use crate::models::ids::OAuthClientId;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::{App, models};
|
||||
use actix_web::http::header::{CACHE_CONTROL, LOCATION, PRAGMA};
|
||||
use actix_web::web::{Data, Query, ServiceConfig};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
||||
@@ -59,21 +58,16 @@ pub struct OAuthClientAccessRequest {
|
||||
|
||||
#[get("authorize")]
|
||||
pub async fn init_oauth(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
Query(oauth_info): Query<OAuthInit>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::USER_AUTH_WRITE,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let user =
|
||||
get_user_from_headers_v2(&app, &req, &**pool, Scopes::USER_AUTH_WRITE)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let client_id = oauth_info.client_id.into();
|
||||
let client = DBOAuthClient::get(client_id, &**pool).await?;
|
||||
@@ -172,33 +166,22 @@ pub struct RespondToOAuthClientScopes {
|
||||
|
||||
#[post("accept")]
|
||||
pub async fn accept_client_scopes(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
accept_body: web::Json<RespondToOAuthClientScopes>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
accept_or_reject_client_scopes(
|
||||
true,
|
||||
req,
|
||||
accept_body,
|
||||
pool,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
accept_or_reject_client_scopes(true, app, req, accept_body, pool).await
|
||||
}
|
||||
|
||||
#[post("reject")]
|
||||
pub async fn reject_client_scopes(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
body: web::Json<RespondToOAuthClientScopes>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
accept_or_reject_client_scopes(false, req, body, pool, redis, session_queue)
|
||||
.await
|
||||
accept_or_reject_client_scopes(false, app, req, body, pool).await
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -314,26 +297,20 @@ pub async fn request_token(
|
||||
|
||||
pub async fn accept_or_reject_client_scopes(
|
||||
accept: bool,
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
body: web::Json<RespondToOAuthClientScopes>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
let current_user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let current_user =
|
||||
get_user_from_headers_v2(&app, &req, &**pool, Scopes::SESSION_ACCESS)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let flow = DBFlow::take_if(
|
||||
&body.flow,
|
||||
|f| matches!(f, DBFlow::InitOAuthAppApproval { .. }),
|
||||
&redis,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
if let Some(DBFlow::InitOAuthAppApproval {
|
||||
@@ -379,7 +356,7 @@ pub async fn accept_or_reject_client_scopes(
|
||||
scopes,
|
||||
redirect_uris,
|
||||
state,
|
||||
&redis,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::AuthProvider;
|
||||
use crate::App;
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::database::models::{DBUser, user_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
@@ -11,10 +12,9 @@ use actix_web::http::header::{AUTHORIZATION, HeaderValue};
|
||||
use chrono::Utc;
|
||||
|
||||
pub async fn get_maybe_user_from_headers<'a, E>(
|
||||
app: &App,
|
||||
req: &HttpRequest,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<Option<(Scopes, User)>, AuthenticationError>
|
||||
where
|
||||
@@ -25,14 +25,8 @@ where
|
||||
}
|
||||
|
||||
// Fetch DB user record and minos user from headers
|
||||
let Some((scopes, db_user)) = get_user_record_from_bearer_token(
|
||||
req,
|
||||
None,
|
||||
executor,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
.await?
|
||||
let Some((scopes, db_user)) =
|
||||
get_user_record_from_bearer_token(app, req, None, executor).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -45,24 +39,18 @@ where
|
||||
}
|
||||
|
||||
pub async fn get_full_user_from_headers<'a, E>(
|
||||
app: &App,
|
||||
req: &HttpRequest,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<(Scopes, DBUser), AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let (scopes, db_user) = get_user_record_from_bearer_token(
|
||||
req,
|
||||
None,
|
||||
executor,
|
||||
redis,
|
||||
session_queue,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let (scopes, db_user) =
|
||||
get_user_record_from_bearer_token(app, req, None, executor)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
if !scopes.contains(required_scopes) {
|
||||
return Err(AuthenticationError::InvalidCredentials);
|
||||
@@ -74,31 +62,42 @@ where
|
||||
pub async fn get_user_from_headers<'a, E>(
|
||||
req: &HttpRequest,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
_redis: &RedisPool,
|
||||
_session_queue: &AuthQueue,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<(Scopes, User), AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let (scopes, db_user) = get_full_user_from_headers(
|
||||
get_user_from_headers_v2(
|
||||
crate::APP.get().unwrap(),
|
||||
req,
|
||||
executor,
|
||||
redis,
|
||||
session_queue,
|
||||
required_scopes,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_from_headers_v2<'a, E>(
|
||||
app: &App,
|
||||
req: &HttpRequest,
|
||||
executor: E,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<(Scopes, User), AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let (scopes, db_user) =
|
||||
get_full_user_from_headers(app, req, executor, required_scopes).await?;
|
||||
|
||||
Ok((scopes, User::from_full(db_user)))
|
||||
}
|
||||
|
||||
pub async fn get_user_record_from_bearer_token<'a, 'b, E>(
|
||||
app: &App,
|
||||
req: &HttpRequest,
|
||||
token: Option<&str>,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
) -> Result<Option<(Scopes, user_item::DBUser)>, AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
@@ -113,7 +112,9 @@ where
|
||||
Some(("mrp", _)) => {
|
||||
let pat =
|
||||
crate::database::models::pat_item::DBPersonalAccessToken::get(
|
||||
token, executor, redis,
|
||||
token,
|
||||
executor,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
@@ -122,17 +123,23 @@ where
|
||||
return Err(AuthenticationError::InvalidCredentials);
|
||||
}
|
||||
|
||||
let user =
|
||||
user_item::DBUser::get_id(pat.user_id, executor, redis).await?;
|
||||
let user = user_item::DBUser::get_id(
|
||||
pat.user_id,
|
||||
executor,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
session_queue.add_pat(pat.id).await;
|
||||
app.state.auth_queue.add_pat(pat.id).await;
|
||||
|
||||
user.map(|x| (pat.scopes, x))
|
||||
}
|
||||
Some(("mra", _)) => {
|
||||
let session =
|
||||
crate::database::models::session_item::DBSession::get(
|
||||
token, executor, redis,
|
||||
token,
|
||||
executor,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
@@ -141,19 +148,21 @@ where
|
||||
return Err(AuthenticationError::InvalidCredentials);
|
||||
}
|
||||
|
||||
let user =
|
||||
user_item::DBUser::get_id(session.user_id, executor, redis)
|
||||
.await?;
|
||||
let user = user_item::DBUser::get_id(
|
||||
session.user_id,
|
||||
executor,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?;
|
||||
if req
|
||||
.headers()
|
||||
.get("x-ratelimit-key")
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.is_none_or(|x| x != rate_limit_ignore)
|
||||
.is_none_or(|x| x != &app.env.rate_limit_ignore_key)
|
||||
{
|
||||
let metadata = get_session_metadata(req).await?;
|
||||
session_queue.add_session(session.id, metadata).await;
|
||||
app.state.auth_queue.add_session(session.id, metadata).await;
|
||||
}
|
||||
|
||||
user.map(|x| (Scopes::all(), x))
|
||||
@@ -174,23 +183,26 @@ where
|
||||
let user = user_item::DBUser::get_id(
|
||||
access_token.user_id,
|
||||
executor,
|
||||
redis,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
session_queue.add_oauth_access_token(access_token.id).await;
|
||||
app.state
|
||||
.auth_queue
|
||||
.add_oauth_access_token(access_token.id)
|
||||
.await;
|
||||
|
||||
user.map(|u| (access_token.scopes, u))
|
||||
}
|
||||
Some(("github" | "gho" | "ghp", _)) => {
|
||||
let user = AuthProvider::GitHub.get_user(token).await?;
|
||||
let user = AuthProvider::GitHub.get_user(&app.env, token).await?;
|
||||
let id =
|
||||
AuthProvider::GitHub.get_user_id(&user.id, executor).await?;
|
||||
|
||||
let user = user_item::DBUser::get_id(
|
||||
id.ok_or_else(|| AuthenticationError::InvalidCredentials)?,
|
||||
executor,
|
||||
redis,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -219,24 +231,17 @@ pub fn extract_authorization_header(
|
||||
}
|
||||
|
||||
pub async fn check_is_moderator_from_headers<'a, 'b, E>(
|
||||
app: &App,
|
||||
req: &HttpRequest,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
session_queue: &AuthQueue,
|
||||
required_scopes: Scopes,
|
||||
) -> Result<User, AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let user = get_user_from_headers(
|
||||
req,
|
||||
executor,
|
||||
redis,
|
||||
session_queue,
|
||||
required_scopes,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let user = get_user_from_headers_v2(app, req, executor, required_scopes)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
if user.role.is_mod() {
|
||||
Ok(user)
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut};
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct ReadOnlyPgPool(PgPool);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use std::time::Duration;
|
||||
const DEFAULT_EXPIRY: i64 = 60 * 60 * 12; // 12 hours
|
||||
const ACTUAL_EXPIRY: i64 = 60 * 30; // 30 minutes
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RedisPool {
|
||||
pub url: String,
|
||||
pub pool: deadpool_redis::Pool,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::web;
|
||||
@@ -8,7 +8,8 @@ use queue::{
|
||||
analytics::AnalyticsQueue, email::EmailQueue, payouts::PayoutsQueue,
|
||||
session::AuthQueue, socket::ActiveSockets,
|
||||
};
|
||||
use sqlx::Postgres;
|
||||
use secrecy::SecretString;
|
||||
use sqlx::{PgPool, Postgres};
|
||||
use tracing::{info, warn};
|
||||
|
||||
extern crate clickhouse as clickhouse_crate;
|
||||
@@ -50,6 +51,7 @@ pub struct Pepper {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LabrinthConfig {
|
||||
pub app: App,
|
||||
pub pool: sqlx::Pool<Postgres>,
|
||||
pub ro_pool: ReadOnlyPgPool,
|
||||
pub redis_pool: RedisPool,
|
||||
@@ -72,8 +74,52 @@ pub struct LabrinthConfig {
|
||||
pub gotenberg_client: GotenbergClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct App {
|
||||
pub env: AppEnv,
|
||||
pub state: AppState,
|
||||
}
|
||||
|
||||
#[derive(derive_more::Debug, Clone)]
|
||||
pub struct AppEnv {
|
||||
pub postgres: PgPool,
|
||||
pub postgres_ro: PgPool,
|
||||
pub redis: RedisPool,
|
||||
#[debug(skip)]
|
||||
pub clickhouse: ::clickhouse::Client,
|
||||
// env vars
|
||||
pub self_addr: String,
|
||||
pub rate_limit_ignore_key: String,
|
||||
pub github_client_id: String,
|
||||
pub github_client_secret: SecretString,
|
||||
pub discord_client_id: String,
|
||||
pub discord_client_secret: SecretString,
|
||||
pub microsoft_client_id: String,
|
||||
pub microsoft_client_secret: SecretString,
|
||||
pub gitlab_client_id: String,
|
||||
pub gitlab_client_secret: SecretString,
|
||||
pub google_client_id: String,
|
||||
pub google_client_secret: SecretString,
|
||||
pub steam_api_key: SecretString,
|
||||
pub paypal_api_url: String,
|
||||
pub paypal_client_id: String,
|
||||
pub paypal_client_secret: SecretString,
|
||||
pub sendy_url: String,
|
||||
pub sendy_list_id: String,
|
||||
pub sendy_api_key: SecretString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
// TODO: fully own the `AuthQueue`
|
||||
pub auth_queue: web::Data<AuthQueue>,
|
||||
}
|
||||
|
||||
pub static APP: OnceLock<App> = OnceLock::new();
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn app_setup(
|
||||
env: AppEnv,
|
||||
pool: sqlx::Pool<Postgres>,
|
||||
ro_pool: ReadOnlyPgPool,
|
||||
redis_pool: RedisPool,
|
||||
@@ -92,6 +138,16 @@ pub fn app_setup(
|
||||
dotenvy::var("BIND_ADDR").unwrap()
|
||||
);
|
||||
|
||||
let session_queue = web::Data::new(AuthQueue::new());
|
||||
let state = AppState {
|
||||
auth_queue: session_queue.clone(),
|
||||
};
|
||||
let app = App { env, state };
|
||||
|
||||
if APP.set(app.clone()).is_err() {
|
||||
warn!("Setting `ENV` multiple times");
|
||||
}
|
||||
|
||||
let automated_moderation_queue =
|
||||
web::Data::new(AutomatedModerationQueue::default());
|
||||
|
||||
@@ -207,8 +263,6 @@ pub fn app_setup(
|
||||
});
|
||||
}
|
||||
|
||||
let session_queue = web::Data::new(AuthQueue::new());
|
||||
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
let session_queue_ref = session_queue.clone();
|
||||
@@ -270,6 +324,7 @@ pub fn app_setup(
|
||||
}
|
||||
|
||||
LabrinthConfig {
|
||||
app,
|
||||
pool,
|
||||
ro_pool,
|
||||
redis_pool,
|
||||
|
||||
@@ -4,7 +4,6 @@ use actix_web::{App, HttpServer};
|
||||
use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use clap::Parser;
|
||||
|
||||
use labrinth::app_config;
|
||||
use labrinth::background_task::BackgroundTask;
|
||||
use labrinth::database::redis::RedisPool;
|
||||
use labrinth::file_hosting::{S3BucketConfig, S3Host};
|
||||
@@ -15,7 +14,9 @@ use labrinth::util::env::parse_var;
|
||||
use labrinth::util::gotenberg::GotenbergClient;
|
||||
use labrinth::util::ratelimit::rate_limit_middleware;
|
||||
use labrinth::utoipa_app_config;
|
||||
use labrinth::{AppEnv, app_config};
|
||||
use labrinth::{check_env_vars, clickhouse, database, file_hosting};
|
||||
use modrinth_util::env_var;
|
||||
use std::ffi::CStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::{Instrument, error, info, info_span};
|
||||
@@ -198,6 +199,14 @@ async fn main() -> std::io::Result<()> {
|
||||
.expect("Failed to register jemalloc metrics");
|
||||
|
||||
let labrinth_config = labrinth::app_setup(
|
||||
AppEnv {
|
||||
self_addr: env_var("SELF_ADDR").unwrap(),
|
||||
rate_limit_ignore_key: env_var("RATE_LIMIT_IGNORE_KEY").unwrap(),
|
||||
postgres: pool.clone(),
|
||||
postgres_ro: ro_pool.clone(),
|
||||
redis: redis_pool.clone(),
|
||||
clickhouse: clickhouse.clone(),
|
||||
},
|
||||
pool.clone(),
|
||||
ro_pool.clone(),
|
||||
redis_pool.clone(),
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::models::payouts::{
|
||||
use crate::models::projects::MonetizationStatus;
|
||||
use crate::queue::payouts::mural::MuralPayoutRequest;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::env::env_var;
|
||||
use crate::util::error::Context;
|
||||
use crate::util::webhook::{
|
||||
PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
|
||||
@@ -20,6 +19,7 @@ use chrono::{DateTime, Datelike, Duration, NaiveTime, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use eyre::{Result, eyre};
|
||||
use futures::TryStreamExt;
|
||||
use modrinth_util::env_var;
|
||||
use muralpay::MuralPay;
|
||||
use reqwest::Method;
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
@@ -11,6 +11,7 @@ use sqlx::PgPool;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthQueue {
|
||||
session_queue: Mutex<HashMap<DBSessionId, SessionMetadata>>,
|
||||
pat_queue: Mutex<HashSet<DBPatId>>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::App;
|
||||
use crate::auth::{get_user_from_headers, get_user_from_headers_v2};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::analytics::{PageView, Playtime};
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -49,22 +50,16 @@ pub struct UrlInput {
|
||||
#[post("view")]
|
||||
pub async fn page_view_ingest(
|
||||
req: HttpRequest,
|
||||
app: web::Data<App>,
|
||||
maxmind: web::Data<MaxMind>,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
url_input: web::Json<UrlInput>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::empty(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
let user = get_user_from_headers_v2(&app, &req, &**pool, Scopes::empty())
|
||||
.await
|
||||
.ok();
|
||||
let conn_info = req.connection_info().peer_addr().map(|x| x.to_string());
|
||||
|
||||
let url = Url::parse(&url_input.url).map_err(|_| {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::App;
|
||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
@@ -7,7 +8,6 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::queue::analytics::AnalyticsQueue;
|
||||
use crate::queue::moderation::AUTOMOD_ID;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::search::SearchConfig;
|
||||
use crate::util::date::get_current_tenths_of_ms;
|
||||
@@ -46,11 +46,10 @@ pub struct DownloadBody {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn count_download(
|
||||
req: HttpRequest,
|
||||
app: web::Data<App>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
maxmind: web::Data<MaxMind>,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
download_body: web::Json<DownloadBody>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let token = download_body
|
||||
@@ -59,16 +58,10 @@ pub async fn count_download(
|
||||
.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 user = get_user_record_from_bearer_token(&app, &req, token, &**pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let project_id: crate::database::models::ids::DBProjectId =
|
||||
download_body.project_id.into();
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::util::error::Context;
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{App, AppEnv};
|
||||
use actix_web::web::{Data, Query, ServiceConfig, scope};
|
||||
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web};
|
||||
use argon2::password_hash::SaltString;
|
||||
@@ -33,6 +34,7 @@ use lettre::message::Mailbox;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::collections::HashMap;
|
||||
@@ -254,43 +256,44 @@ impl TempUser {
|
||||
impl AuthProvider {
|
||||
pub fn get_redirect_url(
|
||||
&self,
|
||||
app: &App,
|
||||
state: String,
|
||||
) -> Result<String, AuthenticationError> {
|
||||
let self_addr = dotenvy::var("SELF_ADDR")?;
|
||||
let self_addr = &app.env.self_addr;
|
||||
let raw_redirect_uri = format!("{self_addr}/v2/auth/callback");
|
||||
let redirect_uri = urlencoding::encode(&raw_redirect_uri);
|
||||
|
||||
Ok(match self {
|
||||
AuthProvider::GitHub => {
|
||||
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
|
||||
let client_id = &app.env.github_client_id;
|
||||
|
||||
format!(
|
||||
"https://github.com/login/oauth/authorize?client_id={client_id}&prompt=select_account&state={state}&scope=read%3Auser%20user%3Aemail&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Discord => {
|
||||
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?;
|
||||
let client_id = &app.env.discord_client_id;
|
||||
|
||||
format!(
|
||||
"https://discord.com/api/oauth2/authorize?client_id={client_id}&state={state}&response_type=code&scope=identify%20email&redirect_uri={redirect_uri}"
|
||||
)
|
||||
}
|
||||
AuthProvider::Microsoft => {
|
||||
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?;
|
||||
let client_id = &app.env.microsoft_client_id;
|
||||
|
||||
format!(
|
||||
"https://login.live.com/oauth20_authorize.srf?client_id={client_id}&response_type=code&scope=user.read&state={state}&prompt=select_account&redirect_uri={redirect_uri}"
|
||||
)
|
||||
}
|
||||
AuthProvider::GitLab => {
|
||||
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
|
||||
let client_id = &app.env.gitlab_client_id;
|
||||
|
||||
format!(
|
||||
"https://gitlab.com/oauth/authorize?client_id={client_id}&state={state}&scope=read_user+profile+email&response_type=code&redirect_uri={redirect_uri}",
|
||||
)
|
||||
}
|
||||
AuthProvider::Google => {
|
||||
let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?;
|
||||
let client_id = &app.env.google_client_id;
|
||||
|
||||
format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&scope={}&response_type=code&redirect_uri={}",
|
||||
@@ -316,8 +319,8 @@ impl AuthProvider {
|
||||
)
|
||||
}
|
||||
AuthProvider::PayPal => {
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?;
|
||||
let api_url = &app.env.paypal_api_url;
|
||||
let client_id = &app.env.paypal_client_id;
|
||||
|
||||
let auth_url = if api_url.contains("sandbox") {
|
||||
"sandbox.paypal.com"
|
||||
@@ -337,10 +340,10 @@ impl AuthProvider {
|
||||
|
||||
pub async fn get_token(
|
||||
&self,
|
||||
app: &App,
|
||||
query: HashMap<String, String>,
|
||||
) -> Result<String, AuthenticationError> {
|
||||
let redirect_uri =
|
||||
format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?);
|
||||
let redirect_uri = format!("{}/v2/auth/callback", &app.env.self_addr);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AccessToken {
|
||||
@@ -352,8 +355,9 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?;
|
||||
let client_id = &app.env.github_client_id;
|
||||
let client_secret =
|
||||
&app.env.github_client_secret.expose_secret();
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}"
|
||||
@@ -373,11 +377,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = dotenvy::var("DISCORD_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("DISCORD_CLIENT_SECRET")?;
|
||||
let client_id = &app.env.discord_client_id;
|
||||
let client_secret =
|
||||
app.env.discord_client_secret.expose_secret();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_id", client_id.as_str());
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
@@ -398,11 +403,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = dotenvy::var("MICROSOFT_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("MICROSOFT_CLIENT_SECRET")?;
|
||||
let client_id = &app.env.microsoft_client_id;
|
||||
let client_secret =
|
||||
app.env.microsoft_client_secret.expose_secret();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_id", client_id.as_str());
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
@@ -423,11 +429,12 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = dotenvy::var("GITLAB_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("GITLAB_CLIENT_SECRET")?;
|
||||
let client_id = &app.env.gitlab_client_id;
|
||||
let client_secret =
|
||||
app.env.gitlab_client_secret.expose_secret();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_id", client_id.as_str());
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
@@ -448,12 +455,13 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let client_id = dotenvy::var("GOOGLE_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("GOOGLE_CLIENT_SECRET")?;
|
||||
let client_id = &app.env.google_client_id;
|
||||
let client_secret =
|
||||
app.env.google_client_secret.expose_secret();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("client_id", &*client_id);
|
||||
map.insert("client_secret", &*client_secret);
|
||||
map.insert("client_id", client_id.as_str());
|
||||
map.insert("client_secret", client_secret);
|
||||
map.insert("code", code);
|
||||
map.insert("grant_type", "authorization_code");
|
||||
map.insert("redirect_uri", &redirect_uri);
|
||||
@@ -528,9 +536,10 @@ impl AuthProvider {
|
||||
let code = query
|
||||
.get("code")
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
let client_id = dotenvy::var("PAYPAL_CLIENT_ID")?;
|
||||
let client_secret = dotenvy::var("PAYPAL_CLIENT_SECRET")?;
|
||||
let api_url = &app.env.paypal_api_url;
|
||||
let client_id = &app.env.paypal_client_id;
|
||||
let client_secret =
|
||||
&app.env.paypal_client_secret.expose_secret();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("code", code.as_str());
|
||||
@@ -562,6 +571,7 @@ impl AuthProvider {
|
||||
|
||||
pub async fn get_user(
|
||||
&self,
|
||||
env: &AppEnv,
|
||||
token: &str,
|
||||
) -> Result<TempUser, AuthenticationError> {
|
||||
let res = match self {
|
||||
@@ -579,9 +589,7 @@ impl AuthProvider {
|
||||
.get("x-oauth-client-id")
|
||||
.and_then(|x| x.to_str().ok());
|
||||
|
||||
if client_id
|
||||
!= Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap())
|
||||
{
|
||||
if client_id != Some(&env.github_client_id) {
|
||||
return Err(AuthenticationError::InvalidClientId);
|
||||
}
|
||||
}
|
||||
@@ -731,7 +739,7 @@ impl AuthProvider {
|
||||
}
|
||||
}
|
||||
AuthProvider::Steam => {
|
||||
let api_key = dotenvy::var("STEAM_API_KEY")?;
|
||||
let api_key = env.steam_api_key.expose_secret();
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SteamResponse {
|
||||
@@ -796,7 +804,7 @@ impl AuthProvider {
|
||||
pub country: String,
|
||||
}
|
||||
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
let api_url = &env.paypal_api_url;
|
||||
|
||||
let paypal_user: PayPalUser = reqwest::Client::new()
|
||||
.get(format!(
|
||||
@@ -1063,11 +1071,10 @@ pub struct Authorization {
|
||||
// http://localhost:8000/auth/init?url=https://modrinth.com
|
||||
#[get("init")]
|
||||
pub async fn init(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
Query(info): Query<AuthorizationInit>, // callback url
|
||||
client: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, AuthenticationError> {
|
||||
// If a user is logging into an OAuth method while already logged in,
|
||||
// this may be present.
|
||||
@@ -1076,11 +1083,10 @@ pub async fn init(
|
||||
// logged in.
|
||||
let existing_user_id = if let Some(auth_token) = &info.auth_token {
|
||||
get_user_record_from_bearer_token(
|
||||
&app,
|
||||
&req,
|
||||
Some(auth_token),
|
||||
&**client,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -1110,11 +1116,10 @@ pub async fn init(
|
||||
|
||||
let user_id = if let Some(token) = info.token {
|
||||
let (_, user) = get_user_record_from_bearer_token(
|
||||
&app,
|
||||
&req,
|
||||
Some(&token),
|
||||
&**client,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
@@ -1130,10 +1135,10 @@ pub async fn init(
|
||||
provider: info.provider,
|
||||
existing_user_id,
|
||||
}
|
||||
.insert(Duration::minutes(30), &redis)
|
||||
.insert(Duration::minutes(30), &app.env.redis)
|
||||
.await?;
|
||||
|
||||
let url = info.provider.get_redirect_url(state)?;
|
||||
let url = info.provider.get_redirect_url(&app, state)?;
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", &*url))
|
||||
.json(serde_json::json!({ "url": url })))
|
||||
@@ -1142,6 +1147,7 @@ pub async fn init(
|
||||
#[get("callback")]
|
||||
pub async fn auth_callback(
|
||||
req: HttpRequest,
|
||||
app: Data<App>,
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
client: Data<PgPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
@@ -1177,11 +1183,11 @@ pub async fn auth_callback(
|
||||
.wrap_err("failed to remove flow")?;
|
||||
|
||||
let token = provider
|
||||
.get_token(query)
|
||||
.get_token(&app, query)
|
||||
.await
|
||||
.wrap_err("failed to get token from provider")?;
|
||||
let oauth_user = provider
|
||||
.get_user(&token)
|
||||
.get_user(&app.env, &token)
|
||||
.await
|
||||
.wrap_err("failed to get user from provider")?;
|
||||
|
||||
@@ -1393,11 +1399,12 @@ pub async fn delete_auth_provider(
|
||||
}
|
||||
|
||||
pub async fn check_sendy_subscription(
|
||||
app: &App,
|
||||
email: &str,
|
||||
) -> Result<bool, AuthenticationError> {
|
||||
let url = dotenvy::var("SENDY_URL")?;
|
||||
let id = dotenvy::var("SENDY_LIST_ID")?;
|
||||
let api_key = dotenvy::var("SENDY_API_KEY")?;
|
||||
let url = &app.env.sendy_url;
|
||||
let id = &app.env.sendy_list_id;
|
||||
let api_key = app.env.sendy_api_key.expose_secret();
|
||||
|
||||
if url.is_empty() || url == "none" {
|
||||
tracing::info!(
|
||||
@@ -1934,21 +1941,15 @@ pub struct Remove2FA {
|
||||
|
||||
#[delete("2fa")]
|
||||
pub async fn remove_2fa(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
login: web::Json<Remove2FA>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (scopes, user) = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
None,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
let (scopes, user) =
|
||||
get_user_record_from_bearer_token(&app, &req, None, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
if !scopes.contains(Scopes::USER_AUTH_WRITE) {
|
||||
return Err(ApiError::Authentication(
|
||||
@@ -1967,7 +1968,7 @@ pub async fn remove_2fa(
|
||||
})?,
|
||||
true,
|
||||
user.id,
|
||||
&redis,
|
||||
&app.env.redis,
|
||||
&pool,
|
||||
&mut transaction,
|
||||
)
|
||||
@@ -2002,12 +2003,15 @@ pub async fn remove_2fa(
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::TwoFactorRemoved,
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.insert(user.id, &mut transaction, &app.env.redis)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
|
||||
.await?;
|
||||
crate::database::models::DBUser::clear_caches(
|
||||
&[(user.id, None)],
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
@@ -2116,18 +2120,18 @@ pub struct ChangePassword {
|
||||
|
||||
#[patch("password")]
|
||||
pub async fn change_password(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
change_password: web::Json<ChangePassword>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = if let Some(flow) = &change_password.flow {
|
||||
let flow = DBFlow::get(flow, &redis).await?;
|
||||
let flow = DBFlow::get(flow, &app.env.redis).await?;
|
||||
|
||||
if let Some(DBFlow::ForgotPassword { user_id }) = flow {
|
||||
let user = crate::database::models::DBUser::get_id(
|
||||
user_id, &**pool, &redis,
|
||||
user_id,
|
||||
&app.env.postgres,
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
@@ -2146,11 +2150,10 @@ pub async fn change_password(
|
||||
user
|
||||
} else {
|
||||
let (scopes, user) = get_user_record_from_bearer_token(
|
||||
&app,
|
||||
&req,
|
||||
None,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
&app.env.postgres,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
@@ -2178,7 +2181,7 @@ pub async fn change_password(
|
||||
user
|
||||
};
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut transaction = app.env.postgres.begin().await?;
|
||||
|
||||
let update_password = if let Some(new_password) =
|
||||
&change_password.new_password
|
||||
@@ -2236,26 +2239,29 @@ pub async fn change_password(
|
||||
.await?;
|
||||
|
||||
if let Some(flow) = &change_password.flow {
|
||||
DBFlow::remove(flow, &redis).await?;
|
||||
DBFlow::remove(flow, &app.env.redis).await?;
|
||||
}
|
||||
|
||||
if update_password.is_some() {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::PasswordChanged,
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.insert(user.id, &mut transaction, &app.env.redis)
|
||||
.await?;
|
||||
} else {
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::PasswordRemoved,
|
||||
}
|
||||
.insert(user.id, &mut transaction, &redis)
|
||||
.insert(user.id, &mut transaction, &app.env.redis)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
crate::database::models::DBUser::clear_caches(&[(user.id, None)], &redis)
|
||||
.await?;
|
||||
crate::database::models::DBUser::clear_caches(
|
||||
&[(user.id, None)],
|
||||
&app.env.redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
@@ -2532,16 +2538,13 @@ pub async fn subscribe_newsletter(
|
||||
|
||||
#[get("email/subscribe")]
|
||||
pub async fn get_newsletter_subscription_status(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_full_user_from_headers(
|
||||
&app,
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
&app.env.postgres,
|
||||
Scopes::USER_READ,
|
||||
)
|
||||
.await?
|
||||
@@ -2549,7 +2552,7 @@ pub async fn get_newsletter_subscription_status(
|
||||
|
||||
let is_subscribed = user.is_subscribed_to_newsletter
|
||||
|| if let Some(email) = user.email {
|
||||
check_sendy_subscription(&email).await?
|
||||
check_sendy_subscription(&app, &email).await?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use super::ApiError;
|
||||
use crate::database;
|
||||
use crate::database::models::{DBOrganization, DBTeamId, DBTeamMember, DBUser};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::{OrganizationId, TeamId};
|
||||
use crate::models::projects::{Project, ProjectStatus};
|
||||
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::util::error::Context;
|
||||
use crate::{App, database};
|
||||
use crate::{auth::check_is_moderator_from_headers, models::pats::Scopes};
|
||||
use actix_web::{HttpRequest, get, post, web};
|
||||
use ariadne::ids::{UserId, random_base62};
|
||||
@@ -76,30 +75,24 @@ pub enum Ownership {
|
||||
)]
|
||||
#[get("/projects")]
|
||||
async fn get_projects(
|
||||
app: web::Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
request_opts: web::Query<ProjectsRequestOptions>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<Vec<FetchedProject>>, ApiError> {
|
||||
get_projects_internal(req, pool, redis, request_opts, session_queue).await
|
||||
get_projects_internal(app, req, pool, redis, request_opts).await
|
||||
}
|
||||
|
||||
pub async fn get_projects_internal(
|
||||
app: web::Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
request_opts: web::Query<ProjectsRequestOptions>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<Vec<FetchedProject>>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
check_is_moderator_from_headers(&app, &req, &**pool, Scopes::PROJECT_READ)
|
||||
.await?;
|
||||
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
@@ -207,19 +200,13 @@ pub async fn get_projects_internal(
|
||||
#[get("/project/{id}")]
|
||||
async fn get_project_meta(
|
||||
req: HttpRequest,
|
||||
app: web::Data<App>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
info: web::Path<(String,)>,
|
||||
) -> Result<web::Json<MissingMetadata>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
check_is_moderator_from_headers(&app, &req, &**pool, Scopes::PROJECT_READ)
|
||||
.await?;
|
||||
|
||||
let project_id = info.into_inner().0;
|
||||
let project =
|
||||
@@ -361,19 +348,12 @@ pub enum Judgement {
|
||||
#[post("/project")]
|
||||
async fn set_project_meta(
|
||||
req: HttpRequest,
|
||||
app: web::Data<App>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
judgements: web::Json<HashMap<String, Judgement>>,
|
||||
) -> Result<(), ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
check_is_moderator_from_headers(&app, &req, &**pool, Scopes::PROJECT_READ)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ pub fn config(cfg: &mut ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionMetadata {
|
||||
pub city: Option<String>,
|
||||
pub country: Option<String>,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::App;
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||
use crate::database::models::friend_item::DBFriend;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::User;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::queue::socket::{
|
||||
ActiveSocket, ActiveSockets, SocketId, TunnelSocketType,
|
||||
};
|
||||
@@ -45,19 +45,18 @@ struct LauncherHeartbeatInit {
|
||||
#[get("launcher_socket")]
|
||||
pub async fn ws_init(
|
||||
req: HttpRequest,
|
||||
app: Data<App>,
|
||||
pool: Data<PgPool>,
|
||||
web::Query(auth): web::Query<LauncherHeartbeatInit>,
|
||||
body: Payload,
|
||||
db: Data<ActiveSockets>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (scopes, db_user) = get_user_record_from_bearer_token(
|
||||
&app,
|
||||
&req,
|
||||
Some(&auth.code),
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::App;
|
||||
use crate::models::projects::Project;
|
||||
use crate::models::v2::projects::LegacyProject;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::internal;
|
||||
use crate::{database::redis::RedisPool, routes::v2_reroute};
|
||||
use actix_web::{HttpRequest, HttpResponse, get, web};
|
||||
@@ -24,13 +24,14 @@ fn default_count() -> u16 {
|
||||
|
||||
#[get("projects")]
|
||||
pub async fn get_projects(
|
||||
app: web::Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
count: web::Query<ResultCount>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let response = internal::moderation::get_projects_internal(
|
||||
app,
|
||||
req,
|
||||
pool.clone(),
|
||||
redis.clone(),
|
||||
@@ -38,7 +39,6 @@ pub async fn get_projects(
|
||||
count: count.count,
|
||||
offset: 0,
|
||||
}),
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
.map(|resp| HttpResponse::Ok().json(resp))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::App;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::reports::Report;
|
||||
use crate::models::v2::reports::LegacyReport;
|
||||
@@ -181,13 +182,13 @@ pub async fn report_edit(
|
||||
#[delete("report/{id}")]
|
||||
pub async fn report_delete(
|
||||
req: HttpRequest,
|
||||
app: web::Data<App>,
|
||||
pool: web::Data<PgPool>,
|
||||
info: web::Path<(crate::models::ids::ReportId,)>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
// Returns NoContent, so no need to convert
|
||||
v3::reports::report_delete(req, pool, info, redis, session_queue)
|
||||
v3::reports::report_delete(req, app, pool, info, redis)
|
||||
.await
|
||||
.or_else(v2_reroute::flatten_404_error)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::App;
|
||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||
use crate::auth::{AuthenticationError, get_user_from_headers};
|
||||
use crate::database::models::payout_item::DBPayout;
|
||||
@@ -439,25 +440,21 @@ pub struct WithdrawalFees {
|
||||
#[utoipa::path]
|
||||
#[post("/fees")]
|
||||
pub async fn calculate_fees(
|
||||
app: web::Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
body: web::Json<Withdrawal>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
payouts_queue: web::Data<PayoutsQueue>,
|
||||
) -> Result<web::Json<WithdrawalFees>, ApiError> {
|
||||
// even though we don't use the user, we ensure they're logged in to make API calls
|
||||
let (_, _user) = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
None,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::Authentication(AuthenticationError::InvalidCredentials)
|
||||
})?;
|
||||
let (_, _user) =
|
||||
get_user_record_from_bearer_token(&app, &req, None, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::Authentication(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
)
|
||||
})?;
|
||||
|
||||
let fees = payouts_queue
|
||||
.calculate_fees(&body.method, &body.method_id, body.amount)
|
||||
@@ -472,25 +469,22 @@ pub async fn calculate_fees(
|
||||
#[utoipa::path]
|
||||
#[post("")]
|
||||
pub async fn create_payout(
|
||||
app: web::Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
body: web::Json<Withdrawal>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
payouts_queue: web::Data<PayoutsQueue>,
|
||||
gotenberg: web::Data<GotenbergClient>,
|
||||
) -> Result<(), ApiError> {
|
||||
let (scopes, user) = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
None,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::Authentication(AuthenticationError::InvalidCredentials)
|
||||
})?;
|
||||
let (scopes, user) =
|
||||
get_user_record_from_bearer_token(&app, &req, None, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::Authentication(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
)
|
||||
})?;
|
||||
|
||||
if !scopes.contains(Scopes::PAYOUTS_WRITE) {
|
||||
return Err(ApiError::Authentication(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::database;
|
||||
use crate::database::models::image_item;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::thread_item::{
|
||||
@@ -17,6 +16,7 @@ use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::img;
|
||||
use crate::util::routes::read_typed_from_payload;
|
||||
use crate::{App, database};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use ariadne::ids::UserId;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
@@ -513,19 +513,13 @@ pub async fn report_edit(
|
||||
|
||||
pub async fn report_delete(
|
||||
req: HttpRequest,
|
||||
app: web::Data<App>,
|
||||
pool: web::Data<PgPool>,
|
||||
info: web::Path<(crate::models::ids::ReportId,)>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::REPORT_DELETE,
|
||||
)
|
||||
.await?;
|
||||
check_is_moderator_from_headers(&app, &req, &**pool, Scopes::REPORT_DELETE)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::App;
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::auth::validate::get_maybe_user_from_headers;
|
||||
use crate::database::models::shared_instance_item::{
|
||||
@@ -150,19 +151,18 @@ pub async fn shared_instance_list(
|
||||
}
|
||||
|
||||
pub async fn shared_instance_get(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
info: web::Path<(SharedInstanceId,)>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0.into();
|
||||
|
||||
let user = get_maybe_user_from_headers(
|
||||
&app,
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SHARED_INSTANCE_READ,
|
||||
)
|
||||
.await?
|
||||
@@ -357,19 +357,18 @@ pub async fn shared_instance_delete(
|
||||
}
|
||||
|
||||
pub async fn shared_instance_version_list(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
info: web::Path<(SharedInstanceId,)>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0.into();
|
||||
|
||||
let user = get_maybe_user_from_headers(
|
||||
&app,
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SHARED_INSTANCE_READ,
|
||||
)
|
||||
.await?
|
||||
@@ -403,19 +402,18 @@ pub async fn shared_instance_version_list(
|
||||
}
|
||||
|
||||
pub async fn shared_instance_version_get(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
info: web::Path<(SharedInstanceVersionId,)>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let version_id = info.into_inner().0.into();
|
||||
|
||||
let user = get_maybe_user_from_headers(
|
||||
&app,
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SHARED_INSTANCE_READ,
|
||||
)
|
||||
.await?
|
||||
@@ -562,20 +560,19 @@ async fn delete_instance_version(
|
||||
}
|
||||
|
||||
pub async fn shared_instance_version_download(
|
||||
app: Data<App>,
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
info: web::Path<(SharedInstanceVersionId,)>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<Redirect, ApiError> {
|
||||
let version_id = info.into_inner().0.into();
|
||||
|
||||
let user = get_maybe_user_from_headers(
|
||||
&app,
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SHARED_INSTANCE_VERSION_READ,
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use eyre::{Context, eyre};
|
||||
|
||||
pub fn env_var(key: &str) -> eyre::Result<String> {
|
||||
dotenvy::var(key)
|
||||
.wrap_err_with(|| eyre!("missing environment variable `{key}`"))
|
||||
}
|
||||
|
||||
pub fn parse_var<T: FromStr>(var: &str) -> Option<T> {
|
||||
dotenvy::var(var).ok().and_then(|i| i.parse().ok())
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::PayoutId;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError};
|
||||
use crate::util::env::env_var;
|
||||
use crate::util::error::Context;
|
||||
use actix_web::http::header::HeaderName;
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
use modrinth_util::env_var;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
Reference in New Issue
Block a user