phonograph/interim-server/src/routes/relations.rs
2025-07-08 14:37:03 -07:00

224 lines
6.7 KiB
Rust

use std::collections::{HashMap, HashSet};
use anyhow::Context as _;
use askama::Template;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse as _, Redirect, Response},
};
use axum_extra::extract::Form;
use serde::Deserialize;
use sqlx::postgres::types::Oid;
use uuid::Uuid;
use crate::{
app_error::{not_found, AppError},
app_state::AppDbConn,
base_pooler::BasePooler,
bases::Base,
db_conns::init_role,
pg_acls::PgPrivilegeType,
pg_classes::{PgClass, PgRelKind},
pg_roles::{user_id_from_rolname, PgRole, RoleTree},
rel_invitations::RelInvitation,
settings::Settings,
users::{CurrentUser, User},
};
#[derive(Deserialize)]
pub struct ListRelationsPagePath {
base_id: Uuid,
}
pub async fn list_relations_page(
State(settings): 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_default()
.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: Base,
rels: Vec<PgClass>,
settings: Settings,
}
Ok(Html(
ResponseTemplate {
base,
rels: accessible_rels,
settings,
}
.render()?,
)
.into_response())
}
#[derive(Deserialize)]
pub struct RelPagePath {
base_id: Uuid,
class_oid: u32,
}
pub async fn rel_index_page(
State(settings): State<Settings>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
) -> Result<Response, AppError> {
todo!();
}
pub async fn rel_rbac_page(
State(settings): State<Settings>,
State(mut base_pooler): State<BasePooler>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
) -> Result<Response, AppError> {
// FIXME: auth
let base = Base::fetch_by_id(base_id, &mut *app_db)
.await?
.ok_or(not_found!("no base found with id {}", base_id))?;
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 class = PgClass::fetch_by_oid(Oid(class_oid), &mut *client)
.await?
.ok_or(not_found!("no relation found with oid {}", class_oid))?;
let user_ids: Vec<Uuid> = class
.relacl
.clone()
.unwrap_or_default()
.iter()
.filter_map(|item| user_id_from_rolname(&item.grantee, &base.user_role_prefix).ok())
.collect();
let all_users = User::fetch_by_ids_any(user_ids, &mut *app_db).await?;
let interim_users: HashMap<String, User> = all_users
.into_iter()
.map(|user| {
(
format!("{}{}", base.user_role_prefix, user.id.simple()),
user,
)
})
.collect();
let all_invites = RelInvitation::fetch_by_class_oid(Oid(class_oid), &mut *app_db).await?;
let mut invites_by_email: HashMap<String, Vec<RelInvitation>> = HashMap::new();
for invite in all_invites {
let entry = invites_by_email.entry(invite.email.clone()).or_default();
entry.push(invite);
}
#[derive(Template)]
#[template(path = "rel_rbac.html")]
struct ResponseTemplate {
base: Base,
interim_users: HashMap<String, User>,
invites_by_email: HashMap<String, Vec<RelInvitation>>,
pg_class: PgClass,
settings: Settings,
}
Ok(Html(
ResponseTemplate {
base,
interim_users,
invites_by_email,
pg_class: class,
settings,
}
.render()?,
)
.into_response())
}
pub async fn rel_rbac_invite_page_get(
State(settings): State<Settings>,
) -> Result<Response, AppError> {
#[derive(Template)]
#[template(path = "rbac_invite.html")]
struct ResponseTemplate {
settings: Settings,
}
Ok(Html(ResponseTemplate { settings }.render()?).into_response())
}
#[derive(Deserialize)]
pub struct RbacInvitePagePostForm {
email: String,
}
pub async fn rel_rbac_invite_page_post(
State(settings): State<Settings>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
Form(form): Form<RbacInvitePagePostForm>,
) -> Result<Response, AppError> {
// FIXME auth
// FIXME form validation
for privilege in [
PgPrivilegeType::Select,
PgPrivilegeType::Insert,
PgPrivilegeType::Update,
PgPrivilegeType::Delete,
PgPrivilegeType::Truncate,
PgPrivilegeType::References,
PgPrivilegeType::Trigger,
] {
RelInvitation::upsertable()
.email(form.email.clone())
.base_id(base_id)
.class_oid(Oid(class_oid))
.privilege(privilege)
.created_by(current_user.id)
.build()?
.upsert(&mut *app_db)
.await?;
}
Ok(Redirect::to(&format!(
"{0}/d/{base_id}/r/{class_oid}/rbac",
settings.root_path
))
.into_response())
}