use axum::http::HeaderMap; use crate::{ AppState, auth::GetAuthorizationRequest, definitions::{common::Empty, table_definition::GetTableCatalogRequest}, pages::table_scope, services::authenticated_request, }; use super::{draft::{RelationTableOption, TableDraft}, state::AddTablePageState}; /// Loads everything the builder needs around the draft: the column-type /// vocabulary, the profiles that can be picked, and — for whichever profile the /// table will belong to — the tables that are link targets and the names the /// new table may not reuse. /// /// The vocabulary, link targets and reserved names always come from the live /// backend, never from the posted form, so they cannot be spoofed by a crafted /// request. pub(crate) async fn load_page( state: AppState, headers: &HeaderMap, mut draft: TableDraft, status: Option, error: Option, ) -> Result { let authorization_request = authenticated_request(headers, GetAuthorizationRequest {}) .map_err(|_| LoadError::Unauthenticated)?; let mut auth = state.auth; let authorization = auth .get_authorization(authorization_request) .await .map_err(|error| match error.code() { tonic::Code::Unauthenticated => LoadError::Unauthenticated, _ => LoadError::Backend(error.message().to_string()), })? .into_inner(); if !crate::authz::can_manage(&authorization, crate::authz::STRUCT_TABLE) { return Err(LoadError::Forbidden); } let mut exchange_rates = state.exchange_rates.clone(); let rate_sources = exchange_rates .list_rate_sources( authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?, ) .await .map_err(|error| LoadError::Backend(error.message().to_string()))? .into_inner() .sources .into_iter() .map(|source| source.id) .collect(); let mut definitions = state.definitions; // What a column may be is the backend's to say; the picker and every rule // the draft applies are read from this. draft.columns.catalog = crate::schema::column_catalog( definitions .list_column_types( authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?, ) .await .map_err(|error| LoadError::Backend(error.message().to_string()))? .into_inner() .column_types, ); // The panel holds a column to the rules of the table it is being described // for, so it is told what that table is on every render rather than left to // read a copy that a `refresh` could have moved on from. draft.columns.global = draft.global; draft.columns.table_name = draft.table_name.clone(); let tree = definitions .get_profile_tree( authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?, ) .await .map_err(|error| LoadError::Backend(error.message().to_string()))? .into_inner(); // The global scope is the catalog's answer, here as everywhere else, so a // shared table the admin panel lists is a shared table this page can link // to — see `crate::pages::table_scope`. Which profile holds the shared // tables is the server's to say, and the tree above says it. let shared_profile = tree.shared_profile_name.clone(); let catalog_tables = definitions .get_table_catalog( authenticated_request(headers, GetTableCatalogRequest { profile_name: None, }) .map_err(|_| LoadError::Unauthenticated)?, ) .await .map_err(|error| LoadError::Backend(error.message().to_string()))? .into_inner() .tables; let effective_profile = draft.effective_profile_name(); // A global table is a link target from every scope, and a global table // being created may only link to another global one. let tables = if draft.global { table_scope::global_tables(&catalog_tables) } else { table_scope::linkable_tables(&tree.profiles, &catalog_tables, &effective_profile) }; let table_options = tables .into_iter() // The profile's ledger accounts are not a link target of their own: an // ACCOUNTING column is how a row is posted to one. .filter(|table| table.name != crate::schema::LEDGER_ACCOUNTS_TABLE) .map(|table| RelationTableOption { name: table.name, global: table.global, system: table.table_kind == "system", }) .collect::>(); // Every table the new one would sit beside holds its name — including the // shared ones, which exist in every profile, and including in a profile // that does not exist yet. A global table would land in every profile at // once, so for that scope every name in the deployment is taken. draft.existing_profile_tables = if draft.global { tree.profiles .iter() .flat_map(|profile| profile.tables.iter()) .map(|table| table.name.clone()) .chain(catalog_tables.iter().map(|table| table.name.clone())) .collect::>() .into_iter() .collect() } else { table_options .iter() .map(|table| table.name.clone()) .collect() }; draft.set_available_relation_table_options(table_options); Ok(AddTablePageState { nav: crate::ui::Nav::from_authorization(headers, "admin", &authorization), shared_profile, profiles: tree .profiles .into_iter() .map(|profile| profile.name) .collect(), rate_sources, draft, status, error, }) } pub(crate) enum LoadError { Unauthenticated, Forbidden, Backend(String), }