use std::collections::HashMap; use axum::{ Json, debug_handler, extract::{Path, State}, response::{IntoResponse as _, Response}, }; // [`axum_extra`]'s form extractor is preferred: // https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform use axum_extra::extract::Form; use interim_models::{ datum::Datum, portal::Portal, workspace::Workspace, workspace_user_perm::{self, WorkspaceUserPerm}, }; use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; use serde::Deserialize; use serde_json::json; use sqlx::{postgres::types::Oid, query}; use uuid::Uuid; use crate::{ app::{App, AppDbConn}, errors::{AppError, forbidden}, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, }; #[derive(Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, rel_oid: u32, workspace_id: Uuid, } #[derive(Debug, Deserialize)] pub(super) struct FormBody { column: String, pkeys: HashMap, value: Datum, } /// HTTP POST handler for updating a single value in a backing Postgres table. /// /// This handler expects 3 path parameters with the structure described by /// [`PathParams`]. #[debug_handler(state = App)] pub(super) async fn post( State(mut workspace_pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(user): CurrentUser, Path(PathParams { portal_id, rel_oid, workspace_id, }): Path, Form(form): Form, ) -> Result { // Check workspace authorization. let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id) .fetch_all(&mut app_db) .await?; if workspace_perms.iter().all(|p| { p.workspace_id != workspace_id || p.perm != workspace_user_perm::PermissionValue::Connect }) { return Err(forbidden!("access denied to workspace")); } // FIXME ensure workspace corresponds to rel/portal, and that user has // permission to access/alter both as needed. // Prevent users from modifying Phonograph metadata columns. if form.column.starts_with('_') { return Err(forbidden!("access denied to update system metadata column")); } let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; let workspace = Workspace::with_id(portal.workspace_id) .fetch_one(&mut app_db) .await?; let mut workspace_client = workspace_pooler .acquire_for(workspace.id, RoleAssignment::User(user.id)) .await?; let rel = PgClass::with_oid(portal.class_oid) .fetch_one(&mut workspace_client) .await?; let pkey_attrs = PgAttribute::pkeys_for_rel(Oid(rel_oid)) .fetch_all(&mut workspace_client) .await?; // TODO: simplify pkey management form.pkeys .get(&pkey_attrs.first().unwrap().attname) .unwrap() .clone() .bind_onto(form.value.bind_onto(query(&format!( "update {ident} set {value_col} = $1 where {pkey_col} = $2", ident = rel.get_identifier(), value_col = escape_identifier(&form.column), pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname), )))) .execute(workspace_client.get_conn()) .await?; Ok(Json(json!({ "ok": true })).into_response()) }