diff --git a/client/testfixture/config.textproto b/client/testfixture/config.textproto index 01b59959..9a99426e 100644 --- a/client/testfixture/config.textproto +++ b/client/testfixture/config.textproto @@ -27,16 +27,6 @@ auth { } jobs {} record_apis: [{ - name: "_user_avatar" - table_name: "_user_avatar" - conflict_resolution: REPLACE - excluded_columns: ["updated"] - autofill_missing_user_id_columns: true - acl_authenticated: [CREATE, UPDATE, DELETE] - create_access_rule: "_REQ_.user IS NULL OR _REQ_.user = _USER_.id" - update_access_rule: "_ROW_.user = _USER_.id" - delete_access_rule: "_ROW_.user = _USER_.id" -}, { name: "simple_strict_table" table_name: "simple_strict_table" acl_authenticated: [CREATE, READ, UPDATE, DELETE] diff --git a/docs/src/content/docs/documentation/APIs/record_apis.mdx b/docs/src/content/docs/documentation/APIs/record_apis.mdx index aa36b7f2..c2dbfc58 100644 --- a/docs/src/content/docs/documentation/APIs/record_apis.mdx +++ b/docs/src/content/docs/documentation/APIs/record_apis.mdx @@ -25,21 +25,17 @@ multiple times as different API endpoints. Editing the configuration file directly, you can set up as many as you like allowing for some extra flexibility around permissions and visibility. -An example API setup for managing user avatars: +An example API setup for managing user profiles: ```json record_apis: [ { - name: "_user_avatar" - table_name: "_user_avatar" + name: "profiles" + table_name: "profiles" conflict_resolution: REPLACE - autofill_missing_user_id_columns: true - acl_world: [READ] - acl_authenticated: [CREATE, READ, UPDATE, DELETE] - create_access_rule: "_REQ_.user IS NULL OR _REQ_.user = _USER_.id" - update_access_rule: "_ROW_.user = _USER_.id" - delete_access_rule: "_ROW_.user = _USER_.id" - } + acl_authenticated: [READ, CREATE] + create_access_rule: "_REQ_.user = _USER_.id" + }, ] ``` diff --git a/examples/blog/traildepot/config.textproto b/examples/blog/traildepot/config.textproto index a3b46afc..6d1d3173 100644 --- a/examples/blog/traildepot/config.textproto +++ b/examples/blog/traildepot/config.textproto @@ -6,17 +6,6 @@ server { } auth {} record_apis: [ - { - name: "_user_avatar" - table_name: "_user_avatar" - conflict_resolution: REPLACE - autofill_missing_user_id_columns: true - acl_world: [READ] - acl_authenticated: [CREATE, READ, UPDATE, DELETE] - create_access_rule: "_REQ_.user IS NULL OR _REQ_.user = _USER_.id" - update_access_rule: "_ROW_.user = _USER_.id" - delete_access_rule: "_ROW_.user = _USER_.id" - }, { name: "profiles" table_name: "profiles" diff --git a/examples/coffee-vector-search/traildepot/config.textproto b/examples/coffee-vector-search/traildepot/config.textproto index b4d8676f..680d791c 100644 --- a/examples/coffee-vector-search/traildepot/config.textproto +++ b/examples/coffee-vector-search/traildepot/config.textproto @@ -23,15 +23,4 @@ auth { refresh_token_ttl_sec: 2592000 } record_apis: [ - { - name: "_user_avatar" - table_name: "_user_avatar" - conflict_resolution: REPLACE - autofill_missing_user_id_columns: true - acl_world: [READ] - acl_authenticated: [CREATE, READ, UPDATE, DELETE] - create_access_rule: "_REQ_.user IS NULL OR _REQ_.user = _USER_.id" - update_access_rule: "_ROW_.user = _USER_.id" - delete_access_rule: "_ROW_.user = _USER_.id" - } ] diff --git a/examples/collab-clicker-ssr/traildepot/config.textproto b/examples/collab-clicker-ssr/traildepot/config.textproto index 10433071..b55bf1db 100644 --- a/examples/collab-clicker-ssr/traildepot/config.textproto +++ b/examples/collab-clicker-ssr/traildepot/config.textproto @@ -23,17 +23,6 @@ auth { refresh_token_ttl_sec: 2592000 } record_apis: [ - { - name: "_user_avatar" - table_name: "_user_avatar" - conflict_resolution: REPLACE - autofill_missing_user_id_columns: true - acl_world: [READ] - acl_authenticated: [CREATE, READ, UPDATE, DELETE] - create_access_rule: "_REQ_.user IS NULL OR _REQ_.user = _USER_.id" - update_access_rule: "_ROW_.user = _USER_.id" - delete_access_rule: "_ROW_.user = _USER_.id" - }, { name: "counter" table_name: "counter" diff --git a/examples/data-cli-tutorial/traildepot/config.textproto b/examples/data-cli-tutorial/traildepot/config.textproto index fabb5f14..e620a641 100644 --- a/examples/data-cli-tutorial/traildepot/config.textproto +++ b/examples/data-cli-tutorial/traildepot/config.textproto @@ -10,17 +10,6 @@ auth { refresh_token_ttl_sec: 2592000 } record_apis: [ - { - name: "_user_avatar" - table_name: "_user_avatar" - conflict_resolution: REPLACE - autofill_missing_user_id_columns: true - acl_world: [READ] - acl_authenticated: [CREATE, READ, UPDATE, DELETE] - create_access_rule: "_REQ_.user IS NULL OR _REQ_.user = _USER_.id" - update_access_rule: "_ROW_.user = _USER_.id" - delete_access_rule: "_ROW_.user = _USER_.id" - }, { name: "movies" table_name: "movies" diff --git a/trailbase-core/src/auth/api/avatar.rs b/trailbase-core/src/auth/api/avatar.rs index 34e8d9c3..ddfb27bd 100644 --- a/trailbase-core/src/auth/api/avatar.rs +++ b/trailbase-core/src/auth/api/avatar.rs @@ -86,7 +86,8 @@ pub async fn create_avatar_handler( )], }; - // TODO: Better input validation, i.e. ensure only one "file" provided. + // TODO: Better input validation, i.e. ensure only one "file" provided. Consider only supporting + // multipart form. if records_and_files.len() != 1 { return Err(AuthError::BadRequest("expected one file")); } @@ -151,7 +152,7 @@ lazy_static! { #[cfg(test)] mod tests { - use axum::extract::{FromRequest, Path, Query, State}; + use axum::extract::{FromRequest, Path, State}; use axum::http; use axum::response::Response; use axum_test::multipart::{MultipartForm, Part}; @@ -161,27 +162,16 @@ mod tests { use crate::app_state::*; use crate::auth::api::login::login_with_password; use crate::auth::user::{DbUser, User}; - use crate::constants::{AVATAR_TABLE, USER_TABLE}; + use crate::constants::USER_TABLE; use crate::extract::Either; - use crate::records::create_record::{ - CreateRecordQuery, CreateRecordResponse, create_record_handler, - }; - use crate::test::unpack_json_response; - use crate::util::{b64_to_uuid, id_to_b64, uuid_to_b64}; + use crate::util::{id_to_b64, uuid_to_b64}; type Request = http::Request; const COL_NAME: &str = "file"; - const AVATAR_COLLECTION_NAME: &str = AVATAR_TABLE; - async fn build_upload_avatar_form_req( - user: &uuid::Uuid, - filename: &str, - body_slice: &[u8], - ) -> Request { - let user_id = uuid_to_b64(&user); - - let form = MultipartForm::new().add_text("user", user_id).add_part( + async fn build_upload_avatar_form_req(filename: &str, body_slice: &[u8]) -> Request { + let form = MultipartForm::new().add_part( COL_NAME, Part::bytes(body_slice.to_vec()).file_name(filename), ); @@ -197,29 +187,21 @@ mod tests { async fn upload_avatar( state: &AppState, - user: Option, + user: User, body: &[u8], ) -> Result { - let user_id = user.as_ref().unwrap().uuid; - let response: CreateRecordResponse = unpack_json_response( - create_record_handler( - State(state.clone()), - Path(AVATAR_COLLECTION_NAME.to_string()), - Query(CreateRecordQuery::default()), - user, - Either::from_request( - build_upload_avatar_form_req(&user_id, "foo.html", body).await, - &(), - ) + let user_id = user.uuid; + + create_avatar_handler( + State(state.clone()), + user, + Either::from_request(build_upload_avatar_form_req("foo.html", body).await, &()) .await .unwrap(), - ) - .await?, ) - .await - .unwrap(); + .await?; - return Ok(b64_to_uuid(&response.ids[0])?); + return Ok(user_id); } async fn download_avatar(state: &AppState, record_id: &[u8; 16]) -> Response { @@ -263,15 +245,10 @@ mod tests { const PNG0: &[u8] = b"\x89PNG\x0d\x0a\x1a\x0b"; const PNG1: &[u8] = b"\x89PNG\x0d\x0a\x1a\x0c"; - let record_id = upload_avatar( - &state, - User::from_auth_token(&state, &user_x_token.auth_token), - PNG0, - ) - .await - .unwrap(); + let user = User::from_auth_token(&state, &user_x_token.auth_token).unwrap(); + let user_id = upload_avatar(&state, user.clone(), PNG0).await.unwrap(); - let response = download_avatar(&state, &record_id.into_bytes()).await; + let response = download_avatar(&state, &user_id.into_bytes()).await; assert_eq!( axum::body::to_bytes(response.into_body(), usize::MAX) .await @@ -280,14 +257,8 @@ mod tests { ); // Test replacement - let record_id = upload_avatar( - &state, - User::from_auth_token(&state, &user_x_token.auth_token), - PNG1, - ) - .await - .unwrap(); - let response = download_avatar(&state, &record_id.into_bytes()).await; + let user_id = upload_avatar(&state, user.clone(), PNG1).await.unwrap(); + let response = download_avatar(&state, &user_id.into_bytes()).await; assert_eq!( axum::body::to_bytes(response.into_body(), usize::MAX) .await @@ -299,7 +270,7 @@ mod tests { assert!( upload_avatar( &state, - User::from_auth_token(&state, &user_x_token.auth_token), + User::from_auth_token(&state, &user_x_token.auth_token).unwrap(), b"Body 0", ) .await diff --git a/trailbase-core/src/config.rs b/trailbase-core/src/config.rs index 41e5322a..45b56b42 100644 --- a/trailbase-core/src/config.rs +++ b/trailbase-core/src/config.rs @@ -76,7 +76,7 @@ pub mod proto { use crate::DESCRIPTOR_POOL; use crate::config::ConfigError; use crate::constants::{ - AVATAR_TABLE, DEFAULT_AUTH_TOKEN_TTL, DEFAULT_REFRESH_TOKEN_TTL, LOGS_RETENTION_DEFAULT, + DEFAULT_AUTH_TOKEN_TTL, DEFAULT_REFRESH_TOKEN_TTL, LOGS_RETENTION_DEFAULT, }; use crate::email; @@ -114,7 +114,7 @@ pub mod proto { // NOTE: It's arguable if copying custom defaults into the config is the cleanest approach, // however it lets us tie into the set update-config Admin UI flow to let users change the // templates. - let mut config = Config { + let config = Config { server: ServerConfig { application_name: Some("TrailBase".to_string()), site_url: None, @@ -135,27 +135,6 @@ pub mod proto { ..Default::default() }; - config.record_apis = vec![RecordApiConfig { - name: Some(AVATAR_TABLE.to_string()), - table_name: Some(AVATAR_TABLE.to_string()), - conflict_resolution: Some(ConflictResolutionStrategy::Replace.into()), - autofill_missing_user_id_columns: Some(true), - enable_subscriptions: None, - acl_world: vec![], - acl_authenticated: vec![ - PermissionFlag::Create as i32, - PermissionFlag::Update as i32, - PermissionFlag::Delete as i32, - ], - excluded_columns: vec!["updated".to_string()], - read_access_rule: None, - create_access_rule: Some("_REQ_.user IS NULL OR _REQ_.user = _USER_.id".to_string()), - update_access_rule: Some("_ROW_.user = _USER_.id".to_string()), - delete_access_rule: Some("_ROW_.user = _USER_.id".to_string()), - schema_access_rule: None, - expand: vec![], - }]; - return config; }