phonograph/src/routes/relations.rs

171 lines
4.9 KiB
Rust
Raw Normal View History

2025-05-26 22:08:21 -07:00
use std::collections::HashSet;
use anyhow::Context as _;
use askama::Template;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse as _, Response},
};
use serde::Deserialize;
use sqlx::{
postgres::{types::Oid, PgRow},
query,
};
use uuid::Uuid;
use crate::{
app_error::AppError,
app_state::AppDbConn,
base_pooler::{self, BasePooler},
bases::Base,
data_layer::{Field, FieldOptionsBuilder, ToHtmlString as _, Value},
db_conns::{escape_identifier, init_role},
pg_acls::{PgAclItem, PgPrivilegeType},
pg_attributes::fetch_attributes_for_rel,
pg_classes::{PgClass, PgRelKind},
pg_roles::{PgRole, RoleTree},
settings::Settings,
users::CurrentUser,
};
#[derive(Deserialize)]
pub struct ListRelationsPagePath {
base_id: Uuid,
}
pub async fn list_relations_page(
State(Settings { base_path, .. }): State<Settings>,
State(mut base_pooler): State<BasePooler>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(ListRelationsPagePath { base_id }): Path<ListRelationsPagePath>,
) -> Result<Response, AppError> {
// FIXME auth
let base = Base::fetch_by_id(base_id, &mut *app_db)
.await?
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
let mut client = base_pooler.acquire_for(base_id).await?;
let rolname = format!("{}{}", &base.user_role_prefix, current_user.id.simple());
init_role(&rolname, &mut client).await?;
let roles = PgRole::fetch_by_names_any(vec![rolname], &mut *client).await?;
let role = roles.first().context("role not found in pg_roles")?;
let granted_role_tree = RoleTree::fetch_granted(role.oid, &mut *client)
.await?
.context("unable to construct role tree")?;
let granted_roles: HashSet<String> = granted_role_tree
.flatten_inherited()
.into_iter()
.map(|role| role.rolname.clone())
.collect();
let all_rels = PgClass::fetch_all_by_kind_any([PgRelKind::OrdinaryTable], &mut *client).await?;
let accessible_rels: Vec<PgClass> = all_rels
.into_iter()
.filter(|rel| {
let privileges: HashSet<PgPrivilegeType> = rel
.relacl
.clone()
.unwrap_or(vec![])
.into_iter()
.filter(|item| granted_roles.contains(&item.grantee))
.flat_map(|item| item.privileges)
.map(|privilege| privilege.privilege)
.collect();
privileges.contains(&PgPrivilegeType::Select)
})
.collect();
#[derive(Template)]
#[template(path = "list_rels.html")]
struct ResponseTemplate {
base_path: String,
base: Base,
rels: Vec<PgClass>,
}
Ok(Html(
ResponseTemplate {
base,
base_path,
rels: accessible_rels,
}
.render()?,
)
.into_response())
}
#[derive(Deserialize)]
pub struct ViewerPagePath {
base_id: Uuid,
class_oid: u32,
}
pub async fn viewer_page(
State(Settings { base_path, .. }): State<Settings>,
State(mut base_pooler): State<BasePooler>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(params): Path<ViewerPagePath>,
) -> Result<Response, AppError> {
let base = Base::fetch_by_id(params.base_id, &mut *app_db)
.await?
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
let mut client = base_pooler.acquire_for(params.base_id).await?;
init_role(
&format!("{}{}", &base.user_role_prefix, &current_user.id.simple()),
&mut client,
)
.await?;
// FIXME: Ensure user has access to database and relation
let class = query!(
"select relname from pg_class where oid = $1",
Oid(params.class_oid)
)
.fetch_optional(&mut *client)
.await?
.ok_or(AppError::NotFound(
"no relation found with that oid".to_owned(),
))?;
let attrs = fetch_attributes_for_rel(Oid(params.class_oid), &mut *client).await?;
const FRONTEND_ROW_LIMIT: i64 = 1000;
let rows = query(&format!(
"select {} from {} limit $1",
attrs
.iter()
.map(|attr| attr.attname.clone())
.collect::<Vec<_>>()
.join(", "),
escape_identifier(&class.relname),
))
.bind(FRONTEND_ROW_LIMIT)
.fetch_all(&mut *client)
.await?;
#[derive(Template)]
#[template(path = "class-viewer.html")]
struct ResponseTemplate {
base_path: String,
fields: Vec<Field>,
rows: Vec<PgRow>,
}
Ok(Html(
ResponseTemplate {
base_path,
fields: attrs
.into_iter()
.map(|attr| Field {
options: FieldOptionsBuilder::default().build().unwrap(),
name: attr.attname,
})
.collect(),
rows,
}
.render()?,
)
.into_response())
}