1
0

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:
Carter
2023-11-25 20:48:51 -08:00
committed by GitHub
parent bad350e49b
commit 0efbbed5e2
8 changed files with 228 additions and 11 deletions

View File

@@ -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,
}
}
}

View File

@@ -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,
}
}
}

View File

@@ -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,