- Extract JDBC driver/plugin logic from Tauri commands to dbx-core::jdbc - Add web API routes for JDBC drivers, JDBC plugin, Agent JAR import, system fonts - Upgrade zip crate from v2 to v4 in dbx-core - Replace http.ts stubs with real HTTP calls (support File and string paths) - Remove isWeb restrictions in DriverStoreDialog, add browser file picker support - Fix Mac traffic light inset incorrectly applied on web version - Show driver management toolbar button on web version
305 lines
17 KiB
Rust
305 lines
17 KiB
Rust
mod auth;
|
|
mod error;
|
|
mod routes;
|
|
mod sse;
|
|
mod state;
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
|
|
use argon2::password_hash::rand_core::OsRng;
|
|
use argon2::password_hash::SaltString;
|
|
use argon2::{Argon2, PasswordHasher};
|
|
use axum::extract::DefaultBodyLimit;
|
|
use axum::middleware;
|
|
use axum::routing::{delete, get, post};
|
|
use axum::Router;
|
|
use dbx_core::connection::AppState;
|
|
use dbx_core::storage::Storage;
|
|
use tokio::sync::RwLock;
|
|
use tower_http::cors::{Any, CorsLayer};
|
|
|
|
use state::WebState;
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| "dbx_web=info,tower_http=info".parse().unwrap()),
|
|
)
|
|
.init();
|
|
|
|
rustls::crypto::aws_lc_rs::default_provider().install_default().expect("Failed to install rustls crypto provider");
|
|
|
|
// Data directory
|
|
let data_dir = std::env::var("DBX_DATA_DIR").map(std::path::PathBuf::from).unwrap_or_else(|_| {
|
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
|
std::path::PathBuf::from(home).join(".dbx-web")
|
|
});
|
|
std::fs::create_dir_all(&data_dir).expect("Failed to create data directory");
|
|
|
|
let app_state = {
|
|
let db_path = data_dir.join("dbx.db");
|
|
let storage = Storage::open(&db_path).await.expect("Failed to open storage");
|
|
storage.migrate_from_json(&data_dir).await.expect("Failed to migrate JSON data");
|
|
Arc::new(AppState::new_with_plugin_dir_and_app_version(
|
|
storage,
|
|
data_dir.join("plugins"),
|
|
env!("CARGO_PKG_VERSION"),
|
|
))
|
|
};
|
|
|
|
// Password hash: env var takes priority, then database
|
|
let password_hash = if let Some(pw) = std::env::var("DBX_PASSWORD").ok() {
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
Some(Argon2::default().hash_password(pw.as_bytes(), &salt).expect("Failed to hash password").to_string())
|
|
} else {
|
|
app_state.storage.load_password_hash().await.unwrap_or(None)
|
|
};
|
|
|
|
let web_state = Arc::new(WebState {
|
|
app: app_state,
|
|
data_dir,
|
|
password_hash: RwLock::new(password_hash),
|
|
sessions: RwLock::new(HashSet::new()),
|
|
sse_channels: RwLock::new(HashMap::new()),
|
|
login_rate_limit: tokio::sync::Mutex::new(state::LoginRateLimit { fail_count: 0, locked_until: None }),
|
|
});
|
|
|
|
// CORS
|
|
let cors = CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any);
|
|
|
|
// API routes
|
|
let api = Router::new()
|
|
// Auth
|
|
.route("/auth/login", post(auth::login))
|
|
.route("/auth/check", get(auth::check))
|
|
.route("/auth/setup", post(auth::setup))
|
|
.route("/auth/change-password", post(auth::change_password))
|
|
.route("/auth/logout", post(auth::logout))
|
|
// Connection
|
|
.route("/connection/test", post(routes::connection::test_connection))
|
|
.route("/connection/connect", post(routes::connection::connect_db))
|
|
.route("/connection/disconnect", post(routes::connection::disconnect_db))
|
|
.route("/connection/save", post(routes::connection::save_connections))
|
|
.route("/connection/list", get(routes::connection::load_connections))
|
|
.route("/plugins", get(routes::plugins::list_plugins))
|
|
// JDBC
|
|
.route("/jdbc/drivers", get(routes::jdbc::list_jdbc_drivers).post(routes::jdbc::import_jdbc_drivers))
|
|
.route("/jdbc/drivers/{name}", delete(routes::jdbc::delete_jdbc_driver))
|
|
.route("/jdbc/plugin/status", get(routes::jdbc::get_jdbc_plugin_status))
|
|
.route("/jdbc/plugin/install", post(routes::jdbc::install_jdbc_plugin))
|
|
.route("/jdbc/plugin/install-local", post(routes::jdbc::install_jdbc_plugin_local))
|
|
.route("/jdbc/plugin/uninstall", post(routes::jdbc::uninstall_jdbc_plugin))
|
|
// System
|
|
.route("/system/fonts", get(routes::jdbc::list_system_fonts))
|
|
// Agent drivers
|
|
.route("/agents/installed-local", get(routes::agents::list_installed_agents_local))
|
|
.route("/agents/installed", get(routes::agents::list_installed_agents))
|
|
.route("/agents/storage-usage", get(routes::agents::get_driver_store_usage))
|
|
.route("/agents/install", post(routes::agents::install_agent))
|
|
.route("/agents/upgrade-all", post(routes::agents::upgrade_all_agents))
|
|
.route("/agents/uninstall", post(routes::agents::uninstall_agent))
|
|
.route("/agents/import-offline", post(routes::agents::import_agents_from_zip))
|
|
.route("/agents/import-jar", post(routes::agents::import_agent_jar))
|
|
.route(
|
|
"/agents/java-runtime",
|
|
get(routes::agents::get_agent_java_runtime_config).post(routes::agents::set_agent_java_runtime_config),
|
|
)
|
|
.route("/agents/invalidate-registry-cache", post(routes::agents::invalidate_agent_registry_cache))
|
|
.route("/agents/reinstall-jre", post(routes::agents::reinstall_jre))
|
|
.route("/agents/uninstall-jre", post(routes::agents::uninstall_jre))
|
|
.route("/agents/progress/{operationId}", get(routes::agents::agent_progress))
|
|
// Schema
|
|
.route("/schema/databases", get(routes::schema::list_databases))
|
|
.route("/schema/schemas", get(routes::schema::list_schemas))
|
|
.route("/schema/tables", get(routes::schema::list_tables))
|
|
.route("/schema/objects", get(routes::schema::list_objects))
|
|
.route("/schema/object-source", get(routes::schema::get_object_source))
|
|
.route("/schema/columns", get(routes::schema::list_columns))
|
|
.route("/schema/indexes", get(routes::schema::list_indexes))
|
|
.route("/schema/foreign-keys", get(routes::schema::list_foreign_keys))
|
|
.route("/schema/triggers", get(routes::schema::list_triggers))
|
|
.route("/schema/ddl", get(routes::schema::get_ddl))
|
|
.route("/schema-diff/prepare", post(routes::schema_diff::prepare_schema_diff))
|
|
.route("/schema-diff/generate-sync-sql", post(routes::schema_diff::generate_schema_sync_sql))
|
|
.route(
|
|
"/schema/cache",
|
|
post(routes::schema_cache::save_schema_cache).get(routes::schema_cache::load_schema_cache),
|
|
)
|
|
.route("/schema/cache-prefix", delete(routes::schema_cache::delete_schema_cache_prefix))
|
|
// Query
|
|
.route("/query/execute", post(routes::query::execute_query))
|
|
.route("/query/execute-multi", post(routes::query::execute_multi))
|
|
.route("/query/execute-batch", post(routes::query::execute_batch))
|
|
.route("/query/execute-script", post(routes::query::execute_script))
|
|
.route("/query/execute-in-transaction", post(routes::query::execute_in_transaction))
|
|
.route("/query/analyze-sql-references", post(routes::query::analyze_sql_references))
|
|
.route("/query/find-statement-at-cursor", post(routes::query::find_statement_at_cursor))
|
|
.route("/query/prepare-pagination-plan", post(routes::query::prepare_query_pagination_execution_plan))
|
|
.route("/query/build-sorted-sql", post(routes::query::build_sorted_query_sql))
|
|
.route("/query/build-explain-sql", post(routes::query::build_explain_sql))
|
|
.route("/query/build-dropped-file-preview-sql", post(routes::query::build_dropped_file_preview_sql))
|
|
.route("/query/build-table-select-sql", post(routes::query::build_table_select_sql))
|
|
.route("/query/build-database-search-sql", post(routes::query::build_database_search_sql))
|
|
.route("/query/build-search-result-where", post(routes::query::build_search_result_where))
|
|
.route("/query/build-rename-object-sql", post(routes::query::build_rename_object_sql))
|
|
.route("/query/build-create-database-sql", post(routes::query::build_create_database_sql))
|
|
.route("/query/build-duckdb-attach-database-sql", post(routes::query::build_duckdb_attach_database_sql))
|
|
.route("/query/build-drop-object-sql", post(routes::query::build_drop_object_sql))
|
|
.route("/query/build-drop-table-sql", post(routes::query::build_drop_table_sql))
|
|
.route("/query/build-empty-table-sql", post(routes::query::build_empty_table_sql))
|
|
.route("/query/build-truncate-table-sql", post(routes::query::build_truncate_table_sql))
|
|
.route("/query/build-drop-database-sql", post(routes::query::build_drop_database_sql))
|
|
.route("/query/build-create-schema-sql", post(routes::query::build_create_schema_sql))
|
|
.route("/query/build-drop-schema-sql", post(routes::query::build_drop_schema_sql))
|
|
.route("/query/build-duplicate-table-structure-sql", post(routes::query::build_duplicate_table_structure_sql))
|
|
.route(
|
|
"/query/build-executable-object-source-statements",
|
|
post(routes::query::build_executable_object_source_statements),
|
|
)
|
|
.route("/query/build-executable-object-source-sql", post(routes::query::build_executable_object_source_sql))
|
|
.route(
|
|
"/query/build-routine-rename-object-source-statements",
|
|
post(routes::query::build_routine_rename_object_source_statements),
|
|
)
|
|
.route("/query/build-view-ddl-sql", post(routes::query::build_view_ddl_sql))
|
|
.route("/query/build-table-structure-change-sql", post(routes::query::build_table_structure_change_sql))
|
|
.route("/query/build-create-table-sql", post(routes::query::build_create_table_sql))
|
|
.route("/query/analyze-editability", post(routes::query::analyze_editable_query_editability))
|
|
.route("/query/prepare-data-grid-save", post(routes::query::prepare_data_grid_save))
|
|
.route(
|
|
"/query/build-data-grid-copy-update-statements",
|
|
post(routes::query::build_data_grid_copy_update_statements),
|
|
)
|
|
.route(
|
|
"/query/build-data-grid-copy-insert-statement",
|
|
post(routes::query::build_data_grid_copy_insert_statement),
|
|
)
|
|
.route(
|
|
"/query/build-data-grid-context-filter-condition",
|
|
post(routes::query::build_data_grid_context_filter_condition),
|
|
)
|
|
.route(
|
|
"/query/build-data-grid-column-value-filter-condition",
|
|
post(routes::query::build_data_grid_column_value_filter_condition),
|
|
)
|
|
.route("/query/build-data-grid-count-sql", post(routes::query::build_data_grid_count_sql))
|
|
.route("/query/build-hive-table-properties-sql", post(routes::query::build_hive_table_properties_sql))
|
|
.route("/query/build-export-insert-statements", post(routes::query::build_export_insert_statements))
|
|
.route("/query/build-export-sql-insert", post(routes::query::build_export_sql_insert))
|
|
.route("/query/build-database-sql-export", post(routes::query::build_database_sql_export))
|
|
.route("/data-compare/prepare", post(routes::data_compare::prepare_data_compare))
|
|
.route("/data-compare/prepare-from-tables", post(routes::data_compare::prepare_data_compare_from_tables))
|
|
.route("/query/cancel", post(routes::query::cancel_query))
|
|
.route("/query/close-session", post(routes::query::close_query_session))
|
|
.route("/query/close-client-session", post(routes::query::close_client_connection_session))
|
|
.route("/export/query-result-json", post(routes::text_export::export_query_result_json))
|
|
.route("/export/query-result-markdown", post(routes::text_export::export_query_result_markdown))
|
|
// Redis
|
|
.route("/redis/list-databases", post(routes::redis::list_databases))
|
|
.route("/redis/scan-keys", post(routes::redis::scan_keys))
|
|
.route("/redis/scan-values", post(routes::redis::scan_values))
|
|
.route("/redis/get-value", post(routes::redis::get_value))
|
|
.route("/redis/set-string", post(routes::redis::set_string))
|
|
.route("/redis/delete-key", post(routes::redis::delete_key))
|
|
.route("/redis/hash-set", post(routes::redis::hash_set))
|
|
.route("/redis/hash-del", post(routes::redis::hash_del))
|
|
.route("/redis/list-push", post(routes::redis::list_push))
|
|
.route("/redis/list-set", post(routes::redis::list_set))
|
|
.route("/redis/list-remove", post(routes::redis::list_remove))
|
|
.route("/redis/set-add", post(routes::redis::set_add))
|
|
.route("/redis/set-remove", post(routes::redis::set_remove))
|
|
.route("/redis/delete-keys", post(routes::redis::delete_keys))
|
|
.route("/redis/flush-db", post(routes::redis::flush_db))
|
|
.route("/redis/execute-command", post(routes::redis::execute_command))
|
|
// MongoDB
|
|
.route("/mongo/list-databases", post(routes::mongo::list_databases))
|
|
.route("/mongo/list-collections", post(routes::mongo::list_collections))
|
|
.route("/mongo/find-documents", post(routes::mongo::find_documents))
|
|
.route("/mongo/insert-document", post(routes::mongo::insert_document))
|
|
.route("/mongo/update-document", post(routes::mongo::update_document))
|
|
.route("/mongo/delete-document", post(routes::mongo::delete_document))
|
|
// History
|
|
.route("/history", get(routes::history::load_history).delete(routes::history::clear_history))
|
|
.route("/history/save", post(routes::history::save_history))
|
|
.route("/history/{id}", delete(routes::history::delete_history_entry))
|
|
// Saved SQL
|
|
.route(
|
|
"/saved-sql",
|
|
get(routes::saved_sql::load_saved_sql_library).post(routes::saved_sql::save_saved_sql_file),
|
|
)
|
|
.route("/saved-sql/{id}", delete(routes::saved_sql::delete_saved_sql_file))
|
|
.route("/saved-sql/folders", post(routes::saved_sql::save_saved_sql_folder))
|
|
.route("/saved-sql/folders/{id}", delete(routes::saved_sql::delete_saved_sql_folder))
|
|
// AI
|
|
.route("/ai/config", post(routes::ai::save_ai_config).get(routes::ai::load_ai_config))
|
|
.route("/ai/conversation", post(routes::ai::save_ai_conversation))
|
|
.route("/ai/conversations", get(routes::ai::load_ai_conversations))
|
|
.route("/ai/conversation/{id}", delete(routes::ai::delete_ai_conversation))
|
|
.route("/ai/complete", post(routes::ai::ai_complete))
|
|
.route("/ai/stream", post(routes::ai::ai_stream))
|
|
.route("/ai/cancel-stream", post(routes::ai::ai_cancel_stream))
|
|
.route("/ai/test-connection", post(routes::ai::ai_test_connection))
|
|
.route("/ai/models", post(routes::ai::ai_list_models))
|
|
// Transfer
|
|
.route("/transfer/start", post(routes::transfer::start_transfer))
|
|
.route("/transfer/progress/{transferId}", get(routes::transfer::transfer_progress))
|
|
.route("/transfer/cancel", post(routes::transfer::cancel_transfer))
|
|
// Database export
|
|
.route("/export/database", post(routes::database_export::start_database_export))
|
|
.route("/export/database/progress/{exportId}", get(routes::database_export::database_export_progress))
|
|
.route("/export/database/cancel", post(routes::database_export::cancel_database_export))
|
|
// SQL file
|
|
.route("/sql-file/preview", post(routes::sql_file::preview_sql_file))
|
|
.route("/sql-file/execute", post(routes::sql_file::execute_sql_file))
|
|
.route("/sql-file/progress/{executionId}", get(routes::sql_file::sql_file_progress))
|
|
.route("/sql-file/cancel", post(routes::sql_file::cancel_sql_file))
|
|
// Table import
|
|
.route("/import/preview", post(routes::table_import::preview_import))
|
|
.route("/import/execute", post(routes::table_import::execute_import))
|
|
.route("/import/progress/{importId}", get(routes::table_import::import_progress))
|
|
.route("/import/cancel", post(routes::table_import::cancel_import))
|
|
// Update
|
|
.route("/version", get(routes::update::get_version))
|
|
.route("/update/check", get(routes::update::check_for_updates))
|
|
// Layout
|
|
.route("/layout/sidebar", post(routes::layout::save_sidebar_layout).get(routes::layout::load_sidebar_layout))
|
|
// App settings
|
|
.route(
|
|
"/app-settings/pinned-tree-node-ids",
|
|
get(routes::app_settings::load_pinned_tree_node_ids).post(routes::app_settings::save_pinned_tree_node_ids),
|
|
)
|
|
.layer(middleware::from_fn_with_state(web_state.clone(), auth::auth_middleware))
|
|
.with_state(web_state.clone());
|
|
|
|
// Build app
|
|
let mut app = Router::new()
|
|
.nest("/api", api)
|
|
.layer(DefaultBodyLimit::max(300 * 1024 * 1024))
|
|
.layer(tower_http::trace::TraceLayer::new_for_http())
|
|
.layer(cors);
|
|
|
|
// Static file serving
|
|
if let Ok(static_dir) = std::env::var("DBX_STATIC_DIR") {
|
|
use tower_http::services::{ServeDir, ServeFile};
|
|
let index_path = format!("{}/index.html", static_dir);
|
|
let serve_dir = ServeDir::new(&static_dir).not_found_service(ServeFile::new(&index_path));
|
|
app = app.fallback_service(serve_dir);
|
|
}
|
|
|
|
// Bind address
|
|
let port: u16 = std::env::var("DBX_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(4224);
|
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
|
|
|
tracing::info!("DBX Web server starting on http://{}", addr);
|
|
if std::env::var("DBX_PASSWORD").is_ok() {
|
|
tracing::info!("Password protection is enabled");
|
|
}
|
|
|
|
let listener = tokio::net::TcpListener::bind(addr).await.expect("Failed to bind address");
|
|
axum::serve(listener, app).await.expect("Server error");
|
|
}
|