Files
komp_ac/web/src/pages/add_table/loader.rs
2026-08-12 17:46:58 +02:00

137 lines
4.8 KiB
Rust

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<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 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::<std::collections::BTreeMap<_, _>>()
.into_iter()
.map(|(name, table_kind)| RelationTableOption {
name,
global: true,
system: table_kind == "system",
})
.collect::<Vec<_>>();
draft.existing_profile_tables = tree
.profiles
.iter()
.flat_map(|profile| profile.tables.iter())
.map(|table| table.name.clone())
.collect::<std::collections::BTreeSet<_>>()
.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::<Vec<_>>();
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),
}