use axum::{ extract::{Path, State}, response::IntoResponse, }; use interim_models::workspace_user_perm::{self, WorkspaceUserPerm}; use interim_pgtypes::escape_identifier; use serde::Deserialize; use sqlx::query; use uuid::Uuid; use crate::{ app::AppDbConn, errors::{AppError, forbidden}, navigator::Navigator, settings::Settings, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, }; #[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 { // 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")); } let mut workspace_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 table_owner_rolname = format!("table_owner_{0}", Uuid::new_v4().simple()); query(&format!( "create role {0}", escape_identifier(&table_owner_rolname), )) .execute(workspace_client.get_conn()) .await?; query(&format!( "grant {0} to {1} with admin option", escape_identifier(&table_owner_rolname), escape_identifier(&format!( "{0}{1}", settings.db_role_prefix, user.id.simple() )) )) .execute(workspace_client.get_conn()) .await?; query(&format!( "grant create, usage on schema {0} to {1}", escape_identifier(&settings.phono_table_namespace), escape_identifier(&table_owner_rolname), )) .execute(workspace_client.get_conn()) .await?; const TABLE_NAME: &str = "untitled"; query(&format!( r#" create table {0}.{1} ( _id uuid primary key not null default uuidv7(), _created_by text not null default current_user, _created_at timestamptz not null default now(), _form_session uuid, _form_backlink_portal uuid, _form_backlink_row uuid, notes text ) "#, escape_identifier(&settings.phono_table_namespace), escape_identifier(TABLE_NAME), )) .execute(workspace_client.get_conn()) .await?; query(&format!( "alter table {0}.{1} owner to {2}", escape_identifier(&settings.phono_table_namespace), escape_identifier(TABLE_NAME), escape_identifier(&table_owner_rolname) )) .execute(workspace_client.get_conn()) .await?; Ok(navigator.workspace_page(workspace_id).redirect_to()) }