use axum::http::HeaderMap; use crate::{ AppState, auth::GetAuthorizationRequest, definitions::common::Empty, 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 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, ); 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(); let effective_profile = draft.effective_profile_name(); if draft.global { let global_tables = tree .profiles .iter() .flat_map(|profile| profile.tables.iter()) .filter(|table| table.global) .map(|table| (table.name.clone(), table.table_kind.clone())) .collect::>() .into_iter() .map(|(name, table_kind)| RelationTableOption { name, global: true, system: table_kind == "system", }) .collect::>(); draft.existing_profile_tables = tree .profiles .iter() .flat_map(|profile| profile.tables.iter()) .map(|table| table.name.clone()) .collect::>() .into_iter() .collect(); draft.set_available_relation_table_options(global_tables); } else { match tree .profiles .iter() .find(|profile| profile.name == effective_profile) { // An existing profile: its tables are the link targets, and their // names are reserved against duplicate table creation. Some(profile) => { let table_options = profile .tables .iter() .filter(|table| table.name != "accounts") .map(|table| RelationTableOption { name: table.name.clone(), global: table.global, system: table.table_kind == "system", }) .collect::>(); draft.existing_profile_tables = table_options .iter() .map(|table| table.name.clone()) .collect(); draft.set_available_relation_table_options(table_options); } // A brand-new (or not-yet-named) profile has nothing to link to. None => { draft.existing_profile_tables.clear(); draft.relation_tables.clear(); draft.relation_table_options.clear(); } }} Ok(AddTablePageState { nav: crate::ui::Nav::new(headers, "admin").with_authorization(&authorization), profiles: tree .profiles .into_iter() .map(|profile| profile.name) .collect(), draft, status, error, }) } pub(crate) enum LoadError { Unauthenticated, Forbidden, Backend(String), }