use axum::{ debug_handler, extract::{Path, State}, response::Response, }; use interim_models::{ accessors::{Accessor, Actor, portal::PortalAccessor}, field::Field, }; use interim_pgtypes::{escape_identifier, pg_class::PgClass}; use serde::Deserialize; use sqlx::{postgres::types::Oid, query}; use uuid::Uuid; use validator::Validate; use crate::{ app::{App, AppDbConn}, errors::{AppError, bad_request}, extractors::ValidatedForm, navigator::{Navigator, NavigatorPage}, 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, Validate)] pub(super) struct FormBody { field_id: Uuid, /// Expects "true" for truthy, else falsy. delete_data: String, } /// HTTP POST handler for removing an existing [`Field`]. /// /// This handler expects 3 path parameters with the structure described by /// [`PathParams`]. #[debug_handler(state = App)] pub(super) async fn post( AppDbConn(mut app_db): AppDbConn, State(mut pooler): State, CurrentUser(user): CurrentUser, navigator: Navigator, Path(PathParams { portal_id, rel_oid, workspace_id, }): Path, ValidatedForm(FormBody { delete_data, field_id, }): ValidatedForm, ) -> Result { // FIXME CSRF // Ensure field exists and belongs to portal. // TODO: This leaks information if it fails. Instead, return // `Err(AccessError::NotFound)` if not found. let field = Field::belonging_to_portal(portal_id) .with_id(field_id) .fetch_one(&mut app_db) .await?; let mut workspace_client = pooler .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; // For authorization only. let _portal = PortalAccessor::new() .id(portal_id) .as_actor(Actor::User(user.id)) .verify_workspace_id(workspace_id) .verify_rel_oid(Oid(rel_oid)) .verify_rel_ownership() .using_rel(&rel) .using_app_db(&mut app_db) .using_workspace_client(&mut workspace_client) .fetch_one() .await?; if delete_data == "true" && field.name.starts_with('_') { return Err(bad_request!("cannot delete data for a system column")); } field.delete(&mut app_db).await?; if delete_data == "true" { query(&format!( "alter table {ident} drop column if exists {col_esc}", ident = rel.get_identifier(), col_esc = escape_identifier(&field.name), )) .execute(workspace_client.get_conn()) .await?; } Ok(navigator .portal_page() .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) .portal_id(portal_id) .build()? .redirect_to()) }