163 lines
5.9 KiB
Rust
163 lines
5.9 KiB
Rust
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<String>,
|
|
error: Option<String>,
|
|
) -> Result<AddTablePageState, LoadError> {
|
|
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::<Vec<_>>();
|
|
|
|
// 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::<std::collections::BTreeSet<_>>()
|
|
.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),
|
|
}
|