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, State(mut base_pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, Path(ListRelationsPagePath { base_id }): Path, ) -> Result { // 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 = 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 = all_rels .into_iter() .filter(|rel| { let privileges: HashSet = 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, } 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, State(mut base_pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, Path(params): Path, ) -> Result { 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, ¤t_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::>() .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, rows: Vec, } 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()) }