export works from web
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
Reference in New Issue
Block a user