use crate::server::app::AppState;
use crate::server::body::{parse_form, percent_decode};
use crate::server::errors::{config_failure, error, method_not_allowed};
use axum::body::Bytes;
use axum::extract::State;
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post};
use axum::{Json, Router};
use nomoreide_core::config::DatabaseDef;
use nomoreide_core::db;
use serde_json::json;
const ENGINES: [&str; 3] = ["postgres", "mysql", "sqlite"];
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/api/databases", get(list).post(register))
.route(
"/api/databases/detect",
get(detect).delete(remove).fallback(method_not_allowed),
)
.route(
"/api/databases/test",
post(test).delete(remove).fallback(method_not_allowed),
)
.route(
"/api/databases/:name",
delete(remove).fallback(method_not_allowed),
)
.route(
"/api/databases/:name/write-access",
post(write_access).fallback(method_not_allowed),
)
}
async fn list(State(state): State<AppState>) -> Response {
match state.config_store.load().await {
Ok(config) => Json(json!({ "ok": true, "connections": db::list_connections(&config) }))
.into_response(),
Err(reason) => config_failure(&reason),
}
}
async fn detect(State(state): State<AppState>) -> Response {
match state.config_store.load().await {
Ok(config) => Json(json!({ "ok": true, "detected": db::detect_from_env(&config).await }))
.into_response(),
Err(reason) => config_failure(&reason),
}
}
async fn register(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let name = match required(&form, "name") {
Ok(name) => name,
Err(reason) => return throw(&reason),
};
let engine = match required(&form, "engine").and_then(|value| parse_engine(&value)) {
Ok(engine) => engine,
Err(reason) => return throw(&reason),
};
let mut url = match required(&form, "url") {
Ok(url) => url,
Err(reason) => return throw(&reason),
};
let config = match state.config_store.load().await {
Ok(config) => config,
Err(reason) => return config_failure(&reason),
};
let existing = config
.databases
.iter()
.find(|database| database.name == name)
.cloned();
if let Some(existing) = &existing {
url = db::merge_stored_password(engine, &url, &existing.url);
}
let definition = DatabaseDef {
name,
engine: engine.to_string(),
url,
write_unlocked: existing.and_then(|existing| existing.write_unlocked),
project_path: optional(&form, "projectPath"),
};
match state.config_store.register_database(definition).await {
Ok(config) => Json(json!({
"ok": true,
"databases": config
.databases
.iter()
.map(|database| json!({ "name": database.name, "engine": database.engine }))
.collect::<Vec<_>>(),
}))
.into_response(),
Err(reason) => config_failure(&reason),
}
}
async fn test(body: Bytes) -> Response {
let form = parse_form(&body);
let engine = match required(&form, "engine").and_then(|value| parse_engine(&value)) {
Ok(engine) => engine,
Err(reason) => return throw(&reason),
};
let url = match required(&form, "url") {
Ok(url) => url,
Err(reason) => return throw(&reason),
};
match db::test_connection(engine, &url).await {
Ok(()) => Json(json!({ "ok": true })).into_response(),
Err(reason) => Json(json!({
"ok": false,
"error": db::redact_database_error(engine, &url, &reason),
}))
.into_response(),
}
}
async fn remove(State(state): State<AppState>, uri: Uri) -> Response {
let Some(name) = name_from(&uri) else {
return error(StatusCode::NOT_FOUND, "Not found");
};
match state.config_store.remove_database(&name).await {
Ok(_) => Json(json!({ "ok": true })).into_response(),
Err(reason) => config_failure(&reason),
}
}
async fn write_access(State(state): State<AppState>, uri: Uri, body: Bytes) -> Response {
let Some(name) = name_from(&uri) else {
return error(StatusCode::NOT_FOUND, "Not found");
};
let form = parse_form(&body);
let unlocked = match required(&form, "unlocked") {
Ok(value) => value == "true",
Err(reason) => return throw(&reason),
};
match state
.config_store
.set_database_write_access(&name, unlocked)
.await
{
Ok(_) => Json(json!({ "ok": true, "writeUnlocked": unlocked })).into_response(),
Err(reason) => config_failure(&reason),
}
}
fn name_from(uri: &Uri) -> Option<String> {
let path = uri.path().strip_prefix("/api/databases/")?;
let segment = path.split('/').next()?;
if segment.is_empty() {
return None;
}
Some(percent_decode(segment))
}
fn required(form: &std::collections::HashMap<String, String>, key: &str) -> Result<String, String> {
form.get(key)
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| format!("{key} is required"))
}
fn optional(form: &std::collections::HashMap<String, String>, key: &str) -> Option<String> {
form.get(key)
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(str::to_string)
}
fn parse_engine(value: &str) -> Result<&'static str, String> {
ENGINES
.iter()
.find(|engine| **engine == value)
.copied()
.ok_or_else(|| {
format!(
"Unsupported engine \"{value}\". Use one of: {}.",
ENGINES.join(", ")
)
})
}
fn throw(message: &str) -> Response {
error(StatusCode::INTERNAL_SERVER_ERROR, message)
}