You've already forked AstralRinth
forked from didirus/AstralRinth
* 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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
233
src/routes/v2/pats.rs
Normal 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())
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user