use anyhow::{Context as _, Result}; use askama::Template; use axum::{ extract::{Path, State}, http::{header::CACHE_CONTROL, HeaderValue}, response::{Html, IntoResponse as _, Response}, routing::get, Router, }; use deadpool_postgres::{tokio_postgres::Row, GenericClient}; use diesel::prelude::*; use mdengine::{ class_privileges_for_grantees, pg_attribute::{attributes_for_rel, PgAttribute}, pg_class::{self, PgClass}, }; use serde::Deserialize; use tower::ServiceBuilder; use tower_http::{ services::{ServeDir, ServeFile}, set_header::SetResponseHeaderLayer, }; use crate::{ abstract_::{diesel_set_user_id, escape_identifier}, app_error::AppError, app_state::{AppState, DieselConn, PgConn}, auth, data_layer::{Field, FieldOptionsBuilder, ToHtmlString as _, Value}, settings::Settings, users::CurrentUser, }; const FRONTEND_ROW_LIMIT: i64 = 1000; pub fn new_router(state: AppState) -> Router<()> { let base_path = state.settings.base_path.clone(); let app = Router::new() .route("/", get(landing_page)) .route("/c/{oid}/viewer", get(viewer_page)) .nest("/auth", auth::new_router()) .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("no-cache"), )) .fallback_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"), )) .service( ServeDir::new("static").not_found_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("no-cache"), )) .service(ServeFile::new("static/_404.html")), ), ), ) .with_state(state); if base_path.is_empty() { app } else { Router::new().nest(&base_path, app).fallback_service( ServeDir::new("static").not_found_service(ServeFile::new("static/_404.html")), ) } } async fn landing_page( State(Settings { base_path, pg_user_role_prefix, .. }): State, DieselConn(db_conn): DieselConn, CurrentUser(current_user): CurrentUser, ) -> Result { let grantees = vec![format!( "{}{}", pg_user_role_prefix, current_user.id.simple() )]; let visible_tables = db_conn .interact(move |conn| -> Result> { diesel_set_user_id(&pg_user_role_prefix, current_user.id, conn)?; let privileges = class_privileges_for_grantees(grantees) .load(conn) .context("error reading classes")?; Ok(privileges.into_iter().map(|value| value.class).collect()) }) .await .unwrap()?; #[derive(Template)] #[template(path = "tmp.html")] struct ResponseTemplate { base_path: String, relations: Vec, } Ok(Html( ResponseTemplate { base_path, relations: visible_tables, } .render()?, ) .into_response()) } #[derive(Deserialize)] struct ViewerPagePath { oid: u32, } async fn viewer_page( State(Settings { base_path, pg_user_role_prefix, .. }): State, DieselConn(diesel_conn): DieselConn, PgConn(pg_client): PgConn, CurrentUser(current_user): CurrentUser, Path(params): Path, ) -> Result { pg_client .query( &format!( "SET ROLE {};", escape_identifier(&format!( "{}{}", pg_user_role_prefix, current_user.id.simple() )), ), &[], ) .await?; // FIXME: Ensure user has access to relation // One-off helper struct to hold Diesel results struct RelMeta { class: PgClass, attrs: Vec, } let RelMeta { class, attrs } = diesel_conn .interact(move |conn| -> Result<_> { Ok(RelMeta { class: pg_class::table .filter(pg_class::dsl::oid.eq(params.oid)) .select(PgClass::as_select()) .first(conn)?, attrs: attributes_for_rel(params.oid).load(conn)?, }) }) .await .unwrap()?; let query = [ "SELECT", &attrs .iter() .map(|attr| attr.attname.clone()) .collect::>() .join(", "), "FROM", &escape_identifier(&class.relname), "LIMIT", &FRONTEND_ROW_LIMIT.to_string(), ";", ] .join(" "); let rows = pg_client.query(&query, &[]).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()) }