Minos push (#589) (#590)

* Minos push (#589)

* moving to other computer

* working redirection

* incomplete pat setup

* no more errors

* new migrations

* fixed bugs; added user check

* pats

* resized pats

* removed testing callback

* lowered kratos_id size

* metadata support

* google not working

* refactoring

* restructured github_id

* kratos-id optional, legacy accounts connect

* default picture

* merge mistake

* clippy

* sqlx-data.json

* env vars, clippy

* merge error

* scopes into an i64, name

* requested changes

* removed banning

* partial completion of github flow

* revision

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Wyatt Verchere
2023-05-31 16:03:08 -07:00
committed by GitHub
parent 2eb51edfb6
commit fe25cd3bec
19 changed files with 1781 additions and 738 deletions

View File

@@ -1,9 +1,12 @@
use crate::database::models::user_item;
use crate::models::ids::ProjectId;
use crate::models::projects::MonetizationStatus;
use crate::models::users::User;
use crate::routes::ApiError;
use crate::util::auth::{link_or_insert_new_user, MinosNewUser};
use crate::util::guards::admin_key_guard;
use crate::DownloadQueue;
use actix_web::{patch, post, web, HttpResponse};
use actix_web::{get, patch, post, web, HttpResponse};
use chrono::{DateTime, SecondsFormat, Utc};
use rust_decimal::Decimal;
use serde::Deserialize;
@@ -16,10 +19,38 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("admin")
.service(count_download)
.service(add_minos_user)
.service(get_legacy_account)
.service(process_payout),
);
}
// Adds a Minos user to the database
// This is an internal endpoint, and should not be used by applications, only by the Minos backend
#[post("_minos-user-callback", guard = "admin_key_guard")]
pub async fn add_minos_user(
minos_user: web::Json<MinosNewUser>, // getting directly from Kratos rather than Minos, so unparse
client: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let minos_new_user = minos_user.into_inner();
let mut transaction = client.begin().await?;
link_or_insert_new_user(&mut transaction, minos_new_user).await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().finish())
}
#[get("_legacy_account/{github_id}", guard = "admin_key_guard")]
pub async fn get_legacy_account(
pool: web::Data<PgPool>,
github_id: web::Path<i32>,
) -> Result<HttpResponse, ApiError> {
let github_id = github_id.into_inner();
let user = user_item::User::get_from_github_id(github_id as u64, &**pool).await?;
let user: Option<User> = user.map(|u| u.into());
Ok(HttpResponse::Ok().json(user))
}
#[derive(Deserialize)]
pub struct DownloadBody {
pub url: String,

View File

@@ -1,28 +1,30 @@
/*!
This auth module is primarily for use within the main website. Applications interacting with the
authenticated API (a very small portion - notifications, private projects, editing/creating projects
and versions) should either retrieve the Modrinth GitHub token through the site, or create a personal
app token for use with Modrinth.
This auth module is how we allow for authentication within the Modrinth sphere.
It uses a self-hosted Ory Kratos instance on the backend, powered by our Minos backend.
JUst as a summary: Don't implement this flow in your application! Instead, use a personal access token
or create your own GitHub OAuth2 application.
Applications interacting with the authenticated API (a very small portion - notifications, private projects, editing/creating projects
and versions) should include the Ory authentication cookie in their requests. This cookie is set by the Ory Kratos instance and Minos provides function to access these.
This system will be revisited and allow easier interaction with the authenticated API once we roll
out our own authentication system.
In addition, you can use a logged-in-account to generate a PAT.
This token can be passed in as a Bearer token in the Authorization header, as an alternative to a cookie.
This is useful for applications that don't have a frontend, or for applications that need to access the authenticated API on behalf of a user.
Just as a summary: Don't implement this flow in your application!
*/
use crate::database::models::{generate_state_id, User};
use crate::database::models::{self, generate_state_id};
use crate::models::error::ApiError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::ids::DecodingError;
use crate::models::users::{Badges, Role};
use crate::parse_strings_from_var;
use crate::util::auth::get_github_user_from_token;
use crate::util::auth::{get_minos_user_from_cookies, AuthenticationError};
use actix_web::http::StatusCode;
use actix_web::web::{scope, Data, Query, ServiceConfig};
use actix_web::{get, HttpResponse};
use actix_web::{get, HttpRequest, HttpResponse};
use chrono::Utc;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use thiserror::Error;
@@ -41,8 +43,8 @@ pub enum AuthorizationError {
Database(#[from] crate::database::models::DatabaseError),
#[error("Error while parsing JSON: {0}")]
SerDe(#[from] serde_json::Error),
#[error("Error while communicating to GitHub OAuth2")]
Github(#[from] reqwest::Error),
#[error("Error with communicating to Minos")]
Minos(#[from] reqwest::Error),
#[error("Invalid Authentication credentials")]
InvalidCredentials,
#[error("Authentication Error: {0}")]
@@ -51,8 +53,8 @@ pub enum AuthorizationError {
Decoding(#[from] DecodingError),
#[error("Invalid callback URL specified")]
Url,
#[error("User is not allowed to access Modrinth services")]
Banned,
#[error("User exists in Minos but not in Labrinth")]
DatabaseMismatch,
}
impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode {
@@ -61,12 +63,12 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY,
AuthorizationError::Minos(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
AuthorizationError::Decoding(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Authentication(..) => StatusCode::UNAUTHORIZED,
AuthorizationError::Url => StatusCode::BAD_REQUEST,
AuthorizationError::Banned => StatusCode::FORBIDDEN,
AuthorizationError::DatabaseMismatch => StatusCode::INTERNAL_SERVER_ERROR,
}
}
@@ -77,12 +79,12 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::SqlxDatabase(..) => "database_error",
AuthorizationError::Database(..) => "database_error",
AuthorizationError::SerDe(..) => "invalid_input",
AuthorizationError::Github(..) => "github_error",
AuthorizationError::Minos(..) => "network_error",
AuthorizationError::InvalidCredentials => "invalid_credentials",
AuthorizationError::Decoding(..) => "decoding_error",
AuthorizationError::Authentication(..) => "authentication_error",
AuthorizationError::Url => "url_error",
AuthorizationError::Banned => "user_banned",
AuthorizationError::DatabaseMismatch => "database_mismatch",
},
description: &self.to_string(),
})
@@ -93,31 +95,22 @@ impl actix_web::ResponseError for AuthorizationError {
pub struct AuthorizationInit {
pub url: String,
}
#[derive(Serialize, Deserialize)]
pub struct Authorization {
pub code: String,
pub struct StateResponse {
pub state: String,
}
#[derive(Serialize, Deserialize)]
pub struct AccessToken {
pub access_token: String,
pub scope: String,
pub token_type: String,
}
//http://localhost:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods
// Init link takes us to Minos API and calls back to callback endpoint with a code and state
//http://<URL>:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods
#[get("init")]
pub async fn init(
Query(info): Query<AuthorizationInit>,
Query(info): Query<AuthorizationInit>, // callback url
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
let domain = url.domain().ok_or(AuthorizationError::Url)?;
let domain = url.host_str().ok_or(AuthorizationError::Url)?; // TODO: change back to .domain() (host_str is so we can use 127.0.0.1)
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" {
return Err(AuthorizationError::Url);
}
@@ -139,14 +132,13 @@ pub async fn init(
transaction.commit().await?;
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
let kratos_url = dotenvy::var("KRATOS_URL")?;
let labrinth_url = dotenvy::var("BIND_ADDR")?;
let url = format!(
"https://github.com/login/oauth/authorize?client_id={}&state={}&scope={}",
client_id,
to_base62(state.0 as u64),
"read%3Auser"
// Callback URL of initialization is /callback below.
"{kratos_url}/self-service/login/browser?return_to=http://{labrinth_url}/v2/auth/callback?state={}",
to_base62(state.0 as u64)
);
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*url))
.json(AuthorizationInit { url }))
@@ -154,11 +146,12 @@ pub async fn init(
#[get("callback")]
pub async fn auth_callback(
Query(info): Query<Authorization>,
req: HttpRequest,
Query(state): Query<StateResponse>,
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
let mut transaction = client.begin().await?;
let state_id = parse_base62(&info.state)?;
let state_id: u64 = parse_base62(&state.state)?;
let result_option = sqlx::query!(
"
@@ -170,119 +163,51 @@ pub async fn auth_callback(
.fetch_optional(&mut *transaction)
.await?;
// Extract cookie header from request
let cookie_header = req.headers().get("Cookie");
if let Some(result) = result_option {
let duration: chrono::Duration = result.expires - Utc::now();
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentials);
}
sqlx::query!(
"
DELETE FROM states
WHERE id = $1
",
state_id as i64
)
.execute(&mut *transaction)
.await?;
let client_id = dotenvy::var("GITHUB_CLIENT_ID")?;
let client_secret = dotenvy::var("GITHUB_CLIENT_SECRET")?;
let url = format!(
"https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}",
client_id, client_secret, info.code
);
let token: AccessToken = reqwest::Client::new()
.post(&url)
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await?
.json()
if let Some(cookie_header) = cookie_header {
// Extract cookie header to get authenticated user from Minos
let duration: chrono::Duration = result.expires - Utc::now();
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentials);
}
sqlx::query!(
"
DELETE FROM states
WHERE id = $1
",
state_id as i64
)
.execute(&mut *transaction)
.await?;
let user = get_github_user_from_token(&token.access_token).await?;
let user_result = User::get_from_github_id(user.id, &mut *transaction).await?;
match user_result {
Some(_) => {}
None => {
let banned_user = sqlx::query!(
"SELECT user FROM banned_users WHERE github_id = $1",
user.id as i64
)
.fetch_optional(&mut *transaction)
.await?;
if banned_user.is_some() {
return Err(AuthorizationError::Banned);
}
let user_id = crate::database::models::generate_user_id(&mut transaction).await?;
let mut username_increment: i32 = 0;
let mut username = None;
while username.is_none() {
let test_username = format!(
"{}{}",
&user.login,
if username_increment > 0 {
username_increment.to_string()
} else {
"".to_string()
}
);
let new_id = crate::database::models::User::get_id_from_username_or_id(
&test_username,
&**client,
)
// Attempt to create a minos user from the cookie header- if this fails, the user is invalid
let minos_user = get_minos_user_from_cookies(
cookie_header
.to_str()
.map_err(|_| AuthenticationError::InvalidCredentials)?,
)
.await?;
let user_result =
models::User::get_from_minos_kratos_id(minos_user.id.clone(), &mut transaction)
.await?;
if new_id.is_none() {
username = Some(test_username);
} else {
username_increment += 1;
}
}
if let Some(username) = username {
User {
id: user_id,
github_id: Some(user.id as i64),
username,
name: user.name,
email: user.email,
avatar_url: Some(user.avatar_url),
bio: user.bio,
created: Utc::now(),
role: Role::Developer.to_string(),
badges: Badges::default(),
balance: Decimal::ZERO,
payout_wallet: None,
payout_wallet_type: None,
payout_address: None,
}
.insert(&mut transaction)
.await?;
}
// Cookies exist, but user does not exist in database, meaning they are invalid
if user_result.is_none() {
return Err(AuthorizationError::DatabaseMismatch);
}
}
transaction.commit().await?;
transaction.commit().await?;
let redirect_url = if result.url.contains('?') {
format!("{}&code={}", result.url, token.access_token)
// Cookie is attached now, so redirect to the original URL
// Do not re-append cookie header, as it is not needed,
// because all redirects are to various modrinth.com subdomains
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*result.url))
.json(AuthorizationInit { url: result.url }))
} else {
format!("{}?code={}", result.url, token.access_token)
};
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*redirect_url))
.json(AuthorizationInit { url: redirect_url }))
Err(AuthorizationError::InvalidCredentials)
}
} else {
Err(AuthorizationError::InvalidCredentials)
}

View File

@@ -3,6 +3,7 @@ mod auth;
mod midas;
mod moderation;
mod notifications;
mod pats;
pub(crate) mod project_creation;
mod projects;
mod reports;
@@ -25,6 +26,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
.configure(midas::config)
.configure(moderation::config)
.configure(notifications::config)
.configure(pats::config)
.configure(project_creation::config)
.configure(projects::config)
.configure(reports::config)

View File

@@ -2,17 +2,12 @@ use super::ApiError;
use crate::database;
use crate::models::projects::ProjectStatus;
use crate::util::auth::check_is_moderator_from_headers;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("moderation")
.service(get_projects)
.service(ban_user)
.service(unban_user),
);
cfg.service(web::scope("moderation").service(get_projects));
}
#[derive(Deserialize)]
@@ -58,38 +53,3 @@ pub async fn get_projects(
Ok(HttpResponse::Ok().json(projects))
}
#[derive(Deserialize)]
pub struct BanUser {
pub id: i64,
}
#[get("ban")]
pub async fn ban_user(
req: HttpRequest,
pool: web::Data<PgPool>,
id: web::Query<BanUser>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
sqlx::query!("INSERT INTO banned_users (github_id) VALUES ($1);", id.id)
.execute(&**pool)
.await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("ban")]
pub async fn unban_user(
req: HttpRequest,
pool: web::Data<PgPool>,
id: web::Query<BanUser>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
sqlx::query!("DELETE FROM banned_users WHERE github_id = $1;", id.id)
.execute(&**pool)
.await?;
Ok(HttpResponse::NoContent().body(""))
}

233
src/routes/v2/pats.rs Normal file
View File

@@ -0,0 +1,233 @@
/*!
Current edition of Ory kratos does not support PAT access of data, so this module is how we allow for PAT authentication.
Just as a summary: Don't implement this flow in your application!
*/
use crate::database;
use crate::database::models::generate_pat_id;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::pat::{generate_pat, PersonalAccessToken};
use actix_web::web::{self, Data, Query};
use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse};
use chrono::{Duration, Utc};
use serde::Deserialize;
use sqlx::postgres::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get_pats);
cfg.service(create_pat);
cfg.service(edit_pat);
cfg.service(delete_pat);
}
#[derive(Deserialize)]
pub struct CreatePersonalAccessToken {
pub scope: i64, // todo: should be a vec of enum
pub name: Option<String>,
pub expire_in_days: i64, // resets expiry to expire_in_days days from now
}
#[derive(Deserialize)]
pub struct ModifyPersonalAccessToken {
#[serde(default, with = "::serde_with::rust::double_option")]
pub name: Option<Option<String>>,
pub expire_in_days: Option<i64>, // resets expiry to expire_in_days days from now
}
// GET /pat
// Get all personal access tokens for the given user. Minos/Kratos cookie must be attached for it to work.
// Does not return the actual access token, only the ID + metadata.
#[get("pat")]
pub async fn get_pats(req: HttpRequest, pool: Data<PgPool>) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
let pats = sqlx::query!(
"
SELECT id, name, user_id, scope, expires_at
FROM pats
WHERE user_id = $1
",
db_user_id.0
)
.fetch_all(&**pool)
.await?;
let pats = pats
.into_iter()
.map(|pat| PersonalAccessToken {
id: to_base62(pat.id as u64),
scope: pat.scope,
name: pat.name,
expires_at: pat.expires_at,
access_token: None,
user_id: UserId(pat.user_id as u64),
})
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(pats))
}
// POST /pat
// Create a new personal access token for the given user. Minos/Kratos cookie must be attached for it to work.
// All PAT tokens are base62 encoded, and are prefixed with "mod_"
#[post("pat")]
pub async fn create_pat(
req: HttpRequest,
Query(info): Query<CreatePersonalAccessToken>, // callback url
pool: Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
let mut transaction: sqlx::Transaction<sqlx::Postgres> = pool.begin().await?;
let pat = generate_pat_id(&mut transaction).await?;
let access_token = generate_pat(&mut transaction).await?;
let expiry = Utc::now().naive_utc() + Duration::days(info.expire_in_days);
if info.expire_in_days <= 0 {
return Err(ApiError::InvalidInput(
"'expire_in_days' must be greater than 0".to_string(),
));
}
sqlx::query!(
"
INSERT INTO pats (id, name, access_token, user_id, scope, expires_at)
VALUES ($1, $2, $3, $4, $5, $6)
",
pat.0,
info.name,
access_token,
db_user_id.0,
info.scope,
expiry
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().json(PersonalAccessToken {
id: to_base62(pat.0 as u64),
access_token: Some(access_token),
name: info.name,
scope: info.scope,
user_id: user.id,
expires_at: expiry,
}))
}
// PATCH /pat/(id)
// Edit an access token of id "id" for the given user.
// 'None' will mean not edited. Minos/Kratos cookie or PAT must be attached for it to work.
#[patch("pat/{id}")]
pub async fn edit_pat(
req: HttpRequest,
id: web::Path<String>,
Query(info): Query<ModifyPersonalAccessToken>, // callback url
pool: Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let pat_id = database::models::PatId(parse_base62(&id)? as i64);
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
if let Some(expire_in_days) = info.expire_in_days {
if expire_in_days <= 0 {
return Err(ApiError::InvalidInput(
"'expire_in_days' must be greater than 0".to_string(),
));
}
}
// Get the singular PAT and user combination (failing immediately if it doesn't exist)
let mut transaction = pool.begin().await?;
let row = sqlx::query!(
"
SELECT id, name, scope, user_id, expires_at FROM pats
WHERE id = $1 AND user_id = $2
",
pat_id.0,
db_user_id.0 // included for safety
)
.fetch_one(&**pool)
.await?;
let pat = PersonalAccessToken {
id: to_base62(row.id as u64),
access_token: None,
user_id: UserId::from(db_user_id),
name: info.name.unwrap_or(row.name),
scope: row.scope,
expires_at: info
.expire_in_days
.map(|d| Utc::now().naive_utc() + Duration::days(d))
.unwrap_or(row.expires_at),
};
sqlx::query!(
"
UPDATE pats SET
name = $1,
expires_at = $2
WHERE id = $3
",
pat.name,
pat.expires_at,
parse_base62(&pat.id)? as i64
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().json(pat))
}
// DELETE /pat
// Delete a personal access token for the given user. Minos/Kratos cookie must be attached for it to work.
#[delete("pat/{id}")]
pub async fn delete_pat(
req: HttpRequest,
id: web::Path<String>,
pool: Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let pat_id = database::models::PatId(parse_base62(&id)? as i64);
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
// Get the singular PAT and user combination (failing immediately if it doesn't exist)
// This is to prevent users from deleting other users' PATs
let pat_id = sqlx::query!(
"
SELECT id FROM pats
WHERE id = $1 AND user_id = $2
",
pat_id.0,
db_user_id.0
)
.fetch_one(&**pool)
.await?
.id;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
DELETE FROM pats
WHERE id = $1
",
pat_id,
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().finish())
}

View File

@@ -10,7 +10,7 @@ use crate::models::projects::{
use crate::models::threads::ThreadType;
use crate::models::users::UserId;
use crate::search::indexing::IndexingError;
use crate::util::auth::{get_user_from_headers, AuthenticationError};
use crate::util::auth::{get_user_from_headers_transaction, AuthenticationError};
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use actix_multipart::{Field, Multipart};
@@ -341,7 +341,7 @@ async fn project_create_inner(
let cdn_url = dotenvy::var("CDN_URL")?;
// The currently logged in user
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let current_user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?;
let project_id: ProjectId = models::generate_project_id(transaction).await?.into();

View File

@@ -3,7 +3,9 @@ use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId
use crate::models::reports::{ItemType, Report};
use crate::models::threads::{MessageBody, ThreadType};
use crate::routes::ApiError;
use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers, get_user_from_headers_transaction,
};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
@@ -36,7 +38,7 @@ pub async fn report_create(
) -> Result<HttpResponse, ApiError> {
let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let current_user = get_user_from_headers_transaction(req.headers(), &mut transaction).await?;
let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await {

View File

@@ -12,7 +12,7 @@ use crate::models::projects::{
VersionId, VersionStatus, VersionType,
};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::util::auth::get_user_from_headers_transaction;
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use crate::validate::{validate_file, ValidationResult};
@@ -127,7 +127,7 @@ async fn version_create_inner(
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
let all_loaders = models::categories::Loader::list(&mut *transaction).await?;
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?;
let mut error = None;
while let Some(item) = payload.next().await {
@@ -479,7 +479,7 @@ async fn upload_file_to_version_inner(
let mut initial_file_data: Option<InitialFileData> = None;
let mut file_builders: Vec<VersionFileBuilder> = Vec::new();
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?;
let result = models::Version::get_full(version_id, &**client).await?;