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::{query, query_scalar}; use uuid::Uuid; use crate::{ app_error::AppError, app_state::AppDbConn, base_pooler::BasePooler, base_user_perms::sync_perms_for_base, bases::Base, db_conns::{escape_identifier, init_role}, settings::Settings, users::CurrentUser, }; pub async fn list_bases_page( State(settings): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, ) -> Result { let bases = Base::fetch_by_perm_any(current_user.id, vec!["configure", "connect"], &mut *app_db) .await?; #[derive(Template)] #[template(path = "list_bases.html")] struct ResponseTemplate { bases: Vec, settings: Settings, } Ok(Html(ResponseTemplate { bases, settings }.render()?).into_response()) } pub async fn add_base_page( State(settings): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, ) -> Result { // FIXME: CSRF let base = Base::insertable_builder() .url("".to_owned()) .owner_id(current_user.id) .build()? .insert(&mut *app_db) .await?; query!( " insert into base_user_perms (id, base_id, user_id, perm) values ($1, $2, $3, 'configure')", Uuid::now_v7(), base.id, current_user.id ) .execute(&mut *app_db) .await?; Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base.id)).into_response()) } #[derive(Deserialize)] pub struct BaseConfigPagePath { base_id: Uuid, } pub async fn base_config_page_get( State(settings): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, Path(params): Path, ) -> Result { // FIXME: auth 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()))?; #[derive(Template)] #[template(path = "base_config.html")] struct ResponseTemplate { base: Base, settings: Settings, } Ok(Html(ResponseTemplate { base, settings }.render()?).into_response()) } #[derive(Deserialize)] pub struct BaseConfigPageForm { name: String, url: String, } pub async fn base_config_page_post( State(settings): State, State(mut base_pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, Path(BaseConfigPagePath { base_id }): Path, Form(form): Form, ) -> Result { // FIXME: CSRF // 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()))?; query!( "update bases set name = $1, url = $2 where id = $3", &form.name, &form.url, &base_id ) .execute(&mut *app_db) .await?; if form.url != base.url { base_pooler.close_for(base_id).await?; let mut client = base_pooler.acquire_for(base.id).await?; let rolname = format!("{}{}", base.user_role_prefix, current_user.id.simple()); // Bootstrap user role with database connect privilege. If the user was // able to successfully authenticate a connection string, it should be // safe to say that they should be allowed to connect as an Interim // user. init_role(&rolname, &mut client).await?; let db_name: String = query_scalar!("select current_database()") .fetch_one(&mut *client) .await? .context("unable to select current_database()")?; query!("reset role").execute(&mut *client).await?; query(&format!( "grant connect on database {} to {}", escape_identifier(&db_name), escape_identifier(&rolname) )) .execute(&mut *client) .await?; sync_perms_for_base(base.id, &mut app_db, &mut client).await?; } Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base_id)).into_response()) }