export works from web

This commit is contained in:
Priec
2026-08-15 19:05:59 +02:00
parent 10d9784b9b
commit e3104a367e
11 changed files with 199 additions and 92 deletions

View File

@@ -3,7 +3,9 @@ use axum::http::HeaderMap;
use crate::{
AppState,
auth::GetAuthorizationRequest,
definitions::common::Empty,
definitions::{common::Empty, table_definition::GetTableCatalogRequest},
i18n::Locale,
pages::{GLOBAL_SCOPE, table_scope},
services::authenticated_request,
};
@@ -12,8 +14,14 @@ pub(crate) struct Catalog {
pub authorization: crate::auth::AuthorizationSnapshot,
}
/// One entry of the scope selector: the global scope, or a profile.
pub(crate) struct Profile {
/// What the form posts, and what the backend resolves the table in —
/// `__global` for the shared tables, the schema name otherwise.
pub name: String,
/// What the selector shows. The global scope is not a schema a user names,
/// so it is labelled in their language instead.
pub label: String,
pub tables: Vec<String>,
}
@@ -45,35 +53,67 @@ pub(crate) async fn load_catalog(
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner();
let mut profiles = tree
.profiles
// A global table is shared by every profile, so the tree repeats it under
// each one. Listing it there would offer the same table once per profile
// and make "the tables of this profile" mean two different things; it is
// offered under the global scope instead, which — like every other page —
// is read from the catalog rather than derived from the tree. See
// `crate::pages::table_scope`.
let global = table_scope::global_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,
)
.into_iter()
.map(|table| table.name)
.filter(|table| {
crate::authz::permits_table(&authorization, GLOBAL_SCOPE, table, required_action)
})
.collect::<Vec<_>>();
let locale = Locale::from_headers(headers);
let mut profiles = Vec::new();
if !global.is_empty() {
profiles.push(Profile {
name: GLOBAL_SCOPE.to_string(),
label: scope_label(locale, GLOBAL_SCOPE),
tables: global,
});
}
profiles.extend(tree.profiles.into_iter().filter_map(|profile| {
let tables = profile
.tables
.into_iter()
.filter_map(|profile| {
let tables = profile
.tables
.into_iter()
.filter(|table| {
crate::authz::permits_table(
&authorization,
&profile.name,
&table.name,
required_action,
)
})
.map(|table| table.name)
.collect::<Vec<_>>();
(!tables.is_empty()).then_some(Profile {
name: profile.name,
tables,
})
.filter(|table| !table.global)
.filter(|table| {
crate::authz::permits_table(
&authorization,
&profile.name,
&table.name,
required_action,
)
})
.map(|table| table.name)
.collect::<Vec<_>>();
(!tables.is_empty()).then_some(Profile {
label: profile.name.clone(),
name: profile.name,
tables,
})
}));
// An insert-only role is deliberately absent from GetProfileTree because
// that listing is filtered by read permission. Exact table grants still
// carry enough information to offer their target here. Wildcard-only
// insert roles can type a target manually and the backend remains the
// authoritative permission check.
// carry enough information to offer their target here. A wildcard-only
// insert role has no table name to offer, so its scope stays empty and the
// backend remains the authoritative permission check.
for permission in authorization
.permissions
.iter()
@@ -98,6 +138,7 @@ pub(crate) async fn load_catalog(
} else {
profiles.push(Profile {
name: profile_name.to_string(),
label: scope_label(locale, profile_name),
tables: vec![table_name.to_string()],
});
}
@@ -108,6 +149,16 @@ pub(crate) async fn load_catalog(
})
}
/// A profile shows its schema name; the global scope is not one a user named,
/// so it is labelled in their language.
fn scope_label(locale: Locale, name: &str) -> String {
if name == GLOBAL_SCOPE {
crate::tr!(locale, "admin-global-label")
} else {
name.to_string()
}
}
pub(crate) enum LoadError {
Unauthenticated,
Forbidden,

View File

@@ -1,9 +1,11 @@
use axum::{
Form,
extract::State,
http::{HeaderMap, HeaderValue, header},
response::{Html, IntoResponse, Redirect, Response},
};
// The table checkboxes post `table_names` once per checked box, and
// `axum::Form` (serde_urlencoded) cannot decode repeated keys into a `Vec`.
use axum_extra::extract::Form;
use crate::{
AppState,

View File

@@ -4,8 +4,10 @@ use crate::{i18n::Locale, tr};
pub(crate) struct ExportForm {
#[serde(default)]
pub profile_name: String,
/// One entry per checked table. The form posts the key once per checked
/// box, which only `axum_extra`'s `Form` decodes into a `Vec`.
#[serde(default)]
pub table_names: String,
pub table_names: Vec<String>,
/// An unchecked checkbox is not posted at all, so its absence is the
/// `false` and any value it does carry is the `true`.
#[serde(default)]
@@ -25,8 +27,8 @@ impl ExportForm {
}
let tables = self
.table_names
.split(',')
.map(str::trim)
.iter()
.map(|table| table.trim())
.filter(|table| !table.is_empty())
.map(str::to_string)
.collect::<Vec<_>>();

View File

@@ -1,11 +1,13 @@
use std::collections::{HashMap, HashSet};
use axum::{
Form,
extract::State,
http::HeaderMap,
response::{Html, IntoResponse, Redirect, Response},
};
// The table checkboxes post `table_names` once per checked box, and
// `axum::Form` (serde_urlencoded) cannot decode repeated keys into a `Vec`.
use axum_extra::extract::Form;
use crate::{
AppState,

View File

@@ -4,8 +4,10 @@ use crate::{i18n::Locale, tr};
pub(crate) struct ImportForm {
#[serde(default)]
pub profile_name: String,
/// One entry per checked table. The form posts the key once per checked
/// box, which only `axum_extra`'s `Form` decodes into a `Vec`.
#[serde(default)]
pub table_names: String,
pub table_names: Vec<String>,
#[serde(default)]
pub csv_data: String,
}
@@ -25,8 +27,8 @@ impl ImportForm {
}
let tables = self
.table_names
.split(',')
.map(str::trim)
.iter()
.map(|table| table.trim())
.filter(|table| !table.is_empty())
.map(str::to_string)
.collect::<Vec<_>>();