use axum::{ extract::{Path, State}, response::IntoResponse, }; use interim_pgtypes::escape_identifier; use serde::Deserialize; use sqlx::query; use uuid::Uuid; use crate::{ app::AppDbConn, errors::AppError, navigator::Navigator, settings::Settings, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_utils::{ TABLE_OWNER_ROLE_PREFIX, TABLE_READER_ROLE_PREFIX, TABLE_WRITER_ROLE_PREFIX, }, }; #[derive(Debug, Deserialize)] pub(super) struct PathParams { workspace_id: Uuid, } /// HTTP POST handler for creating a managed Postgres table within a workspace /// database. Upon success, it redirects the client back to the workspace /// homepage, which is expected to display a list of available tables including /// the newly created one. /// /// This handler expects 1 path parameter named `workspace_id` which should /// deserialize to a UUID. pub(super) async fn post( State(settings): State, State(mut pooler): State, CurrentUser(user): CurrentUser, navigator: Navigator, AppDbConn(mut app_db): AppDbConn, Path(PathParams { workspace_id }): Path, ) -> Result { // FIXME: CSRF, Check workspace authorization. const NAME_LEN_WORDS: usize = 3; let table_name = interim_namegen::default_generator() .with_separator('_') .generate_name(NAME_LEN_WORDS); let mut root_client = pooler // FIXME: Should this be scoped down to the unprivileged role after // setting up the table owner? .acquire_for(workspace_id, RoleAssignment::Root) .await?; let user_rolname = format!( "{prefix}{user_id}", prefix = settings.db_role_prefix, user_id = user.id.simple() ); let rolname_uuid = Uuid::new_v4().simple(); let rolname_table_owner = format!("{TABLE_OWNER_ROLE_PREFIX}{rolname_uuid}"); let rolname_table_reader = format!("{TABLE_READER_ROLE_PREFIX}{rolname_uuid}"); let rolname_table_writer = format!("{TABLE_WRITER_ROLE_PREFIX}{rolname_uuid}"); for rolname in [ &rolname_table_owner, &rolname_table_reader, &rolname_table_writer, ] { query(&format!("create role {0}", escape_identifier(rolname))) .execute(root_client.get_conn()) .await?; query(&format!( "grant {0} to {1} with admin option", escape_identifier(rolname), escape_identifier(&user_rolname) )) .execute(root_client.get_conn()) .await?; } query(&format!( r#" create table {0}.{1} ( _id uuid primary key not null default uuidv7(), _created_by text default current_user, _created_at timestamptz not null default now() ) "#, escape_identifier(&settings.phono_table_namespace), escape_identifier(&table_name), )) .execute(root_client.get_conn()) .await?; query(&format!( "alter table {nsp}.{ident} owner to {owner}", nsp = escape_identifier(&settings.phono_table_namespace), ident = escape_identifier(&table_name), owner = escape_identifier(&rolname_table_owner), )) .execute(root_client.get_conn()) .await?; query(&format!( "grant select on {0}.{1} to {2}", escape_identifier(&settings.phono_table_namespace), escape_identifier(&table_name), escape_identifier(&rolname_table_reader), )) .execute(root_client.get_conn()) .await?; query(&format!( "grant delete, truncate on {0}.{1} to {2}", escape_identifier(&settings.phono_table_namespace), escape_identifier(&table_name), escape_identifier(&rolname_table_writer), )) .execute(root_client.get_conn()) .await?; Ok(navigator.workspace_page(workspace_id).redirect_to()) }