sql-web 0.1.0

A web-based database browser for SQLite, MySQL, and PostgreSQL written in Rust using Rocket and SQLx.
use crate::config::{DatabaseConfig, DatabaseManager, DatabasePool};
use crate::models::FlashMessage as Flash;
use crate::routes::index::AuthGuard;
use crate::template::{IntoTemplateResponse, TemplateResponse};
use askama::Template;
use rocket::form::Form;
use rocket::http::Status;
use rocket::response::Redirect;
use rocket::{get, post, State};

#[derive(Template)]
#[template(path = "add_index.html")]
pub struct AddIndexTemplate {
    pub table_name: String,
    pub columns: Vec<String>,
    pub database_name: String,
    pub readonly: bool,
    pub flash_messages: Vec<Flash>,
    pub version: String,
    pub error: String,
}

#[derive(Template)]
#[template(path = "drop_index.html")]
pub struct DropIndexTemplate {
    pub table_name: String,
    pub indexes: Vec<String>,
    pub database_name: String,
    pub readonly: bool,
    pub flash_messages: Vec<Flash>,
    pub version: String,
    pub error: String,
}

#[derive(rocket::FromForm)]
pub struct AddIndexForm {
    pub index_name: String,
    pub columns: Vec<String>,
    pub unique: Option<bool>,
}

#[derive(rocket::FromForm)]
pub struct DropIndexForm {
    pub index_name: String,
}

#[get("/table/<table_name>/add-index")]
pub async fn add_index(
    table_name: String,
    db: &State<DatabasePool>,
    config: &State<DatabaseConfig>,
    _auth: AuthGuard,
) -> Result<TemplateResponse<AddIndexTemplate>, Status> {
    if config.readonly {
        return Err(Status::Forbidden);
    }

    let pool = &db.0;
    let manager = DatabaseManager::new(pool, config.inner().clone());

    let database_info = manager
        .get_database_info()
        .await
        .map_err(|_| Status::InternalServerError)?;

    let table_info = manager
        .get_table_info(&table_name)
        .await
        .map_err(|_| Status::InternalServerError)?;

    let columns: Vec<String> = table_info.columns.iter().map(|c| c.name.clone()).collect();

    Ok(AddIndexTemplate {
        table_name: table_name.clone(),
        columns,
        database_name: database_info.base_name(),
        readonly: database_info.readonly,
        flash_messages: vec![],
        version: "0.1.0".to_string(),
        error: String::new(),
    }
    .into_template_response())
}

#[post("/table/<table_name>/add-index", data = "<form>")]
pub async fn add_index_execute(
    table_name: String,
    form: Form<AddIndexForm>,
    db: &State<DatabasePool>,
    config: &State<DatabaseConfig>,
    _auth: AuthGuard,
) -> Result<Redirect, Status> {
    if config.readonly {
        return Err(Status::Forbidden);
    }

    let pool = &db.0;
    let manager = DatabaseManager::new(pool, config.inner().clone());

    let index_name = form.index_name.trim();
    if index_name.is_empty() || form.columns.is_empty() {
        return Err(Status::BadRequest);
    }

    let unique_clause = if form.unique.unwrap_or(false) {
        "UNIQUE "
    } else {
        ""
    };

    let columns_str = form.columns.join(", ");

    let sql = format!(
        "CREATE {}INDEX \"{}\" ON \"{}\" ({})",
        unique_clause, index_name, table_name, columns_str
    );

    match manager.execute_query(&sql).await {
        Ok(_) => Ok(Redirect::to(format!("/table/{}/structure", table_name))),
        Err(_) => Err(Status::InternalServerError),
    }
}

#[get("/table/<table_name>/drop-index")]
pub async fn drop_index(
    table_name: String,
    db: &State<DatabasePool>,
    config: &State<DatabaseConfig>,
    _auth: AuthGuard,
) -> Result<TemplateResponse<DropIndexTemplate>, Status> {
    if config.readonly {
        return Err(Status::Forbidden);
    }

    let pool = &db.0;
    let manager = DatabaseManager::new(pool, config.inner().clone());

    let database_info = manager
        .get_database_info()
        .await
        .map_err(|_| Status::InternalServerError)?;

    let indexes = manager
        .get_indexes(&table_name)
        .await
        .unwrap_or_else(|_| vec![]);

    let index_names: Vec<String> = indexes.iter().map(|i| i.name.clone()).collect();

    Ok(DropIndexTemplate {
        table_name: table_name.clone(),
        indexes: index_names,
        database_name: database_info.base_name(),
        readonly: database_info.readonly,
        flash_messages: vec![],
        version: "0.1.0".to_string(),
        error: String::new(),
    }
    .into_template_response())
}

#[post("/table/<table_name>/drop-index", data = "<form>")]
pub async fn drop_index_execute(
    table_name: String,
    form: Form<DropIndexForm>,
    db: &State<DatabasePool>,
    config: &State<DatabaseConfig>,
    _auth: AuthGuard,
) -> Result<Redirect, Status> {
    if config.readonly {
        return Err(Status::Forbidden);
    }

    let pool = &db.0;
    let manager = DatabaseManager::new(pool, config.inner().clone());

    let sql = format!("DROP INDEX \"{}\"", form.index_name);

    match manager.execute_query(&sql).await {
        Ok(_) => Ok(Redirect::to(format!("/table/{}/structure", table_name))),
        Err(_) => Err(Status::InternalServerError),
    }
}