Add fields to OAuth (#769)
* Add url and description fields to OAuthClient model * Add OAuth client icon editing and deleting endpoints * updated query data * fix missed queries * sqlx prep * update with tests builds
This commit is contained in:
parent
bad350e49b
commit
0efbbed5e2
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE created_by = $1",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE created_by = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -40,11 +40,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "url?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "description?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "uri_ids?",
|
||||
"type_info": "Int8Array"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 10,
|
||||
"name": "uri_vals?",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
@ -62,9 +72,11 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8bfb350d4f539a110b05f42812ea2593a1556ef214f3bed519de6b6e21c7d477"
|
||||
"hash": "467df7989a1f4f5f3bc3409a4060e89dcfdd5a5ab9c7a96dc4008e74e06be914"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth_clients\n SET name = $1, icon_url = $2, max_scopes = $3\n WHERE (id = $4)\n ",
|
||||
"query": "\n UPDATE oauth_clients\n SET name = $1, icon_url = $2, max_scopes = $3, url = $4, description = $5\n WHERE (id = $6)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@ -8,10 +8,12 @@
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e8cc8895ebc8b1904a43e00f1e123f75ffdaebc76d67a5d35218fa9273d46d53"
|
||||
"hash": "781f54f36b8713bd36f5f89c98f8c89e94cc7c54e0b7302c877586186bb128a2"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE clients.id = ANY($1::bigint[])",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE clients.id = ANY($1::bigint[])",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -40,11 +40,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "url?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "description?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "uri_ids?",
|
||||
"type_info": "Int8Array"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 10,
|
||||
"name": "uri_vals?",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
@ -62,9 +72,11 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fdfb2433a8e407d42cec1791d67549ab5c23306758168af38f955c06d251b0b7"
|
||||
"hash": "93190af0fcfe2e795ed6b90eece7c567b8724509d1fdbd5cc75f516c1811c9d3"
|
||||
}
|
||||
7
migrations/20231122230639_oauth_client_metadata.sql
Normal file
7
migrations/20231122230639_oauth_client_metadata.sql
Normal file
@ -0,0 +1,7 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE
|
||||
oauth_clients
|
||||
ADD
|
||||
COLUMN url text NULL,
|
||||
ADD
|
||||
COLUMN description text NULL;
|
||||
@ -23,6 +23,8 @@ pub struct OAuthClient {
|
||||
pub redirect_uris: Vec<OAuthRedirectUri>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub created_by: UserId,
|
||||
pub url: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
struct ClientQueryResult {
|
||||
@ -33,6 +35,8 @@ struct ClientQueryResult {
|
||||
secret_hash: String,
|
||||
created: DateTime<Utc>,
|
||||
created_by: i64,
|
||||
url: Option<String>,
|
||||
description: Option<String>,
|
||||
uri_ids: Option<Vec<i64>>,
|
||||
uri_vals: Option<Vec<String>>,
|
||||
}
|
||||
@ -53,6 +57,8 @@ macro_rules! select_clients_with_predicate {
|
||||
clients.secret_hash as "secret_hash!",
|
||||
clients.created as "created!",
|
||||
clients.created_by as "created_by!",
|
||||
clients.url as "url?",
|
||||
clients.description as "description?",
|
||||
uris.uri_ids as "uri_ids?",
|
||||
uris.uri_vals as "uri_vals?"
|
||||
FROM oauth_clients clients
|
||||
@ -155,12 +161,14 @@ impl OAuthClient {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE oauth_clients
|
||||
SET name = $1, icon_url = $2, max_scopes = $3
|
||||
WHERE (id = $4)
|
||||
SET name = $1, icon_url = $2, max_scopes = $3, url = $4, description = $5
|
||||
WHERE (id = $6)
|
||||
",
|
||||
self.name,
|
||||
self.icon_url,
|
||||
self.max_scopes.to_postgres(),
|
||||
self.url,
|
||||
self.description,
|
||||
self.id.0,
|
||||
)
|
||||
.execute(exec)
|
||||
@ -240,6 +248,8 @@ impl From<ClientQueryResult> for OAuthClient {
|
||||
redirect_uris: redirects,
|
||||
created: r.created,
|
||||
created_by: UserId(r.created_by),
|
||||
url: r.url,
|
||||
description: r.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +54,13 @@ pub struct OAuthClient {
|
||||
|
||||
// The user that created (and thus controls) this client
|
||||
pub created_by: UserId,
|
||||
|
||||
// When this client was created
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
// (optional) Metadata about the client
|
||||
pub url: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
@ -88,6 +95,9 @@ impl From<DBOAuthClient> for OAuthClient {
|
||||
max_scopes: value.max_scopes,
|
||||
redirect_uris: value.redirect_uris.into_iter().map(|r| r.into()).collect(),
|
||||
created_by: value.created_by.into(),
|
||||
created: value.created,
|
||||
url: value.url,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::{collections::HashSet, fmt::Display};
|
||||
use std::{collections::HashSet, fmt::Display, sync::Arc};
|
||||
|
||||
use actix_web::{
|
||||
delete, get, patch, post,
|
||||
@ -16,7 +16,9 @@ use validator::Validate;
|
||||
use super::ApiError;
|
||||
use crate::{
|
||||
auth::checks::ValidateAllAuthorized,
|
||||
file_hosting::FileHost,
|
||||
models::{ids::base62_impl::parse_base62, oauth_clients::DeleteOAuthClientQueryParam},
|
||||
util::routes::read_from_payload,
|
||||
};
|
||||
use crate::{
|
||||
auth::{checks::ValidateAuthorized, get_user_from_headers},
|
||||
@ -50,6 +52,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.service(oauth_client_create)
|
||||
.service(oauth_client_edit)
|
||||
.service(oauth_client_delete)
|
||||
.service(oauth_client_icon_edit)
|
||||
.service(oauth_client_icon_delete)
|
||||
.service(get_client)
|
||||
.service(get_clients)
|
||||
.service(get_user_oauth_authorizations),
|
||||
@ -145,6 +149,15 @@ pub struct NewOAuthApp {
|
||||
pub max_scopes: Scopes,
|
||||
|
||||
pub redirect_uris: Vec<String>,
|
||||
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 255)
|
||||
)]
|
||||
pub url: Option<String>,
|
||||
|
||||
#[validate(length(max = 255))]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[post("app")]
|
||||
@ -187,6 +200,8 @@ pub async fn oauth_client_create<'a>(
|
||||
redirect_uris,
|
||||
created: Utc::now(),
|
||||
created_by: current_user.id.into(),
|
||||
url: new_oauth_app.url.clone(),
|
||||
description: new_oauth_app.description.clone(),
|
||||
secret_hash: client_secret_hash,
|
||||
};
|
||||
client.clone().insert(&mut transaction).await?;
|
||||
@ -248,6 +263,15 @@ pub struct OAuthClientEdit {
|
||||
|
||||
#[validate(length(min = 1))]
|
||||
pub redirect_uris: Option<Vec<String>>,
|
||||
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 255)
|
||||
)]
|
||||
pub url: Option<Option<String>>,
|
||||
|
||||
#[validate(length(max = 255))]
|
||||
pub description: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[patch("app/{id}")]
|
||||
@ -289,6 +313,8 @@ pub async fn oauth_client_edit(
|
||||
icon_url,
|
||||
max_scopes,
|
||||
redirect_uris,
|
||||
url,
|
||||
description,
|
||||
} = client_updates.into_inner();
|
||||
if let Some(name) = name {
|
||||
updated_client.name = name;
|
||||
@ -302,6 +328,14 @@ pub async fn oauth_client_edit(
|
||||
updated_client.max_scopes = max_scopes;
|
||||
}
|
||||
|
||||
if let Some(url) = url {
|
||||
updated_client.url = url;
|
||||
}
|
||||
|
||||
if let Some(description) = description {
|
||||
updated_client.description = description;
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
updated_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
@ -319,6 +353,130 @@ pub async fn oauth_client_edit(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
#[patch("app/{id}/icon")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn oauth_client_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
client_id: web::Path<ApiOAuthClientId>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let client = OAuthClient::get((*client_id).into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified client does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
client.validate_authorized(Some(&user))?;
|
||||
|
||||
if let Some(ref icon) = client.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", client_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = Some(format!("{}/{}", cdn_url, upload_data.file_name));
|
||||
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("app/{id}/icon")]
|
||||
pub async fn oauth_client_icon_delete(
|
||||
req: HttpRequest,
|
||||
client_id: web::Path<ApiOAuthClientId>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let client = OAuthClient::get((*client_id).into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified client does not exist!".to_string())
|
||||
})?;
|
||||
client.validate_authorized(Some(&user))?;
|
||||
|
||||
if let Some(ref icon) = client.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = None;
|
||||
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[get("authorizations")]
|
||||
pub async fn get_user_oauth_authorizations(
|
||||
req: HttpRequest,
|
||||
|
||||
@ -41,6 +41,8 @@ async fn can_create_edit_get_oauth_client() {
|
||||
let client_id = get_json_val_str(creation_result.client.id);
|
||||
|
||||
let icon_url = Some("https://modrinth.com/icon".to_string());
|
||||
let url = Some("https://modrinth.com".to_string());
|
||||
let description = Some("test description".to_string());
|
||||
let edited_redirect_uris = vec![
|
||||
redirect_uris[0].clone(),
|
||||
"https://modrinth.com/b".to_string(),
|
||||
@ -50,6 +52,8 @@ async fn can_create_edit_get_oauth_client() {
|
||||
icon_url: Some(icon_url.clone()),
|
||||
max_scopes: None,
|
||||
redirect_uris: Some(edited_redirect_uris.clone()),
|
||||
url: Some(url.clone()),
|
||||
description: Some(description.clone()),
|
||||
};
|
||||
let resp = env
|
||||
.api
|
||||
@ -63,6 +67,8 @@ async fn can_create_edit_get_oauth_client() {
|
||||
.await;
|
||||
assert_eq!(1, clients.len());
|
||||
assert_eq!(icon_url, clients[0].icon_url);
|
||||
assert_eq!(url, clients[0].url);
|
||||
assert_eq!(description, clients[0].description);
|
||||
assert_eq!(client_name, clients[0].name);
|
||||
assert_eq!(2, clients[0].redirect_uris.len());
|
||||
assert_eq!(edited_redirect_uris[0], clients[0].redirect_uris[0].uri);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user