use super::system::AppState;
use crate::error::DbError;
use crate::server::auth::Claims;
use crate::server::authorization::{AuthorizationService, PermissionAction};
use crate::sync::{LogEntry, Operation};
use axum::{
extract::{Extension, Path, State},
http::HeaderMap,
response::Json,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct AuthParams {
pub token: String,
pub htmx: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub token: String,
}
#[derive(Debug, Deserialize)]
pub struct ChangePasswordRequest {
pub current_password: String,
pub new_password: String,
}
#[derive(Debug, Serialize)]
pub struct ChangePasswordResponse {
pub status: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateApiKeyRequest {
pub name: String,
#[serde(default)]
pub roles: Vec<String>,
pub scoped_databases: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct CreateApiKeyResponse {
pub id: String,
pub name: String,
pub key: String, pub created_at: String,
pub roles: Vec<String>,
pub scoped_databases: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct ListApiKeysResponse {
pub keys: Vec<crate::server::auth::ApiKeyListItem>,
}
#[derive(Debug, Serialize)]
pub struct DeleteApiKeyResponse {
pub deleted: bool,
}
pub async fn change_password_handler(
State(state): State<AppState>,
axum::Extension(claims): axum::Extension<crate::server::auth::Claims>,
Json(req): Json<ChangePasswordRequest>,
) -> Result<Json<ChangePasswordResponse>, DbError> {
let db = state.storage.get_database("_system")?;
let collection = db.system_collection("_admins")?;
let doc = match collection.get(&claims.sub) {
Ok(d) => d,
Err(DbError::DocumentNotFound(_)) => {
return Err(DbError::BadRequest("User not found".to_string()));
}
Err(e) => return Err(e),
};
let user: crate::server::auth::User = serde_json::from_value(doc.to_value())
.map_err(|_| DbError::InternalError("Corrupted user data".to_string()))?;
if !crate::server::auth::verify_password_blocking(&req.current_password, &user.password_hash)
.await
{
return Err(DbError::BadRequest(
"Current password is incorrect".to_string(),
));
}
if req.new_password.len() < 12 {
return Err(DbError::BadRequest(
"Password must be at least 12 characters".to_string(),
));
}
let new_hash = crate::server::auth::hash_password_blocking(&req.new_password).await?;
let updated_user = crate::server::auth::User {
username: user.username.clone(),
password_hash: new_hash,
};
let updated_value = serde_json::to_value(&updated_user)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
collection.update(&claims.sub, updated_value.clone())?;
if let Some(ref log) = state.replication_log {
let entry = LogEntry {
sequence: 0, node_id: "".to_string(), database: "_system".to_string(),
collection: "_admins".to_string(),
operation: Operation::Update,
key: claims.sub.clone(),
data: serde_json::to_vec(&updated_value).ok(),
timestamp: chrono::Utc::now().timestamp_millis() as u64,
origin_sequence: None,
};
let _ = log.append(entry);
}
Ok(Json(ChangePasswordResponse {
status: "password_updated".to_string(),
}))
}
pub async fn create_api_key_handler(
State(state): State<AppState>,
Extension(claims): Extension<Claims>,
Json(req): Json<CreateApiKeyRequest>,
) -> Result<Json<CreateApiKeyResponse>, DbError> {
AuthorizationService::check_permission(&claims, &state, PermissionAction::Admin, None).await?;
let (raw_key, key_hash) = crate::server::auth::AuthService::generate_api_key();
let id = uuid::Uuid::new_v4().to_string();
let created_at = chrono::Utc::now().to_rfc3339();
let db = state.storage.get_database("_system")?;
if let Err(DbError::CollectionNotFound(_)) =
db.system_collection(crate::server::auth::API_KEYS_COLL)
{
db.create_collection(crate::server::auth::API_KEYS_COLL.to_string(), None)?;
}
let collection = db.system_collection(crate::server::auth::API_KEYS_COLL)?;
if req.roles.is_empty() {
return Err(DbError::BadRequest(
"API keys must declare at least one role (empty roles no longer default to admin)"
.to_string(),
));
}
let roles = req.roles.clone();
let api_key = crate::server::auth::ApiKey {
id: id.clone(),
name: req.name.clone(),
key_hash,
created_at: created_at.clone(),
roles: roles.clone(),
scoped_databases: req.scoped_databases.clone(),
expires_at: None,
};
let doc_value = serde_json::to_value(&api_key)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
collection.insert(doc_value.clone())?;
crate::server::auth::api_key_cache().insert(api_key.clone());
if let Some(ref log) = state.replication_log {
let entry = LogEntry {
sequence: 0,
node_id: "".to_string(),
database: "_system".to_string(),
collection: crate::server::auth::API_KEYS_COLL.to_string(),
operation: Operation::Insert,
key: id.clone(),
data: serde_json::to_vec(&doc_value).ok(),
timestamp: chrono::Utc::now().timestamp_millis() as u64,
origin_sequence: None,
};
let _ = log.append(entry);
}
tracing::info!("API key '{}' created", req.name);
Ok(Json(CreateApiKeyResponse {
id,
name: req.name,
key: raw_key,
created_at,
roles,
scoped_databases: req.scoped_databases,
}))
}
pub async fn list_api_keys_handler(
State(state): State<AppState>,
Extension(claims): Extension<Claims>,
) -> Result<Json<ListApiKeysResponse>, DbError> {
AuthorizationService::check_permission(&claims, &state, PermissionAction::Admin, None).await?;
let db = state.storage.get_database("_system")?;
let collection = match db.system_collection(crate::server::auth::API_KEYS_COLL) {
Ok(c) => c,
Err(DbError::CollectionNotFound(_)) => {
return Ok(Json(ListApiKeysResponse { keys: vec![] }));
}
Err(e) => return Err(e),
};
let mut keys = Vec::new();
for doc in collection.scan(None) {
let api_key: crate::server::auth::ApiKey = serde_json::from_value(doc.to_value())
.map_err(|_| DbError::InternalError("Corrupted API key data".to_string()))?;
keys.push(crate::server::auth::ApiKeyListItem {
id: api_key.id,
name: api_key.name,
created_at: api_key.created_at,
roles: api_key.roles,
scoped_databases: api_key.scoped_databases,
});
}
Ok(Json(ListApiKeysResponse { keys }))
}
pub async fn delete_api_key_handler(
State(state): State<AppState>,
Extension(claims): Extension<Claims>,
Path(key_id): Path<String>,
) -> Result<Json<DeleteApiKeyResponse>, DbError> {
AuthorizationService::check_permission(&claims, &state, PermissionAction::Admin, None).await?;
let db = state.storage.get_database("_system")?;
let collection = db.system_collection(crate::server::auth::API_KEYS_COLL)?;
collection.delete(&key_id)?;
crate::server::auth::api_key_cache().remove_by_id(&key_id);
if let Some(ref log) = state.replication_log {
let entry = LogEntry {
sequence: 0,
node_id: "".to_string(),
database: "_system".to_string(),
collection: crate::server::auth::API_KEYS_COLL.to_string(),
operation: Operation::Delete,
key: key_id.clone(),
data: None,
timestamp: chrono::Utc::now().timestamp_millis() as u64,
origin_sequence: None,
};
let _ = log.append(entry);
}
tracing::info!("API key '{}' deleted", key_id);
Ok(Json(DeleteApiKeyResponse { deleted: true }))
}
pub async fn login_handler(
State(state): State<AppState>,
peer: Result<
axum::extract::ConnectInfo<std::net::SocketAddr>,
axum::extract::rejection::ExtensionRejection,
>,
headers: HeaderMap,
Json(req): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, DbError> {
let socket_ip = peer
.ok()
.map(|axum::extract::ConnectInfo(addr)| addr.ip().to_string());
let client_ip = if crate::server::auth::trust_proxy_headers() {
headers
.get("X-Forwarded-For")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
.or_else(|| {
headers
.get("X-Real-IP")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
})
.or(socket_ip)
.unwrap_or_else(|| "unknown".to_string())
} else {
socket_ip.unwrap_or_else(|| "unknown".to_string())
};
let rate_bucket = format!("{}|{}", client_ip, req.username);
crate::server::auth::check_rate_limit(&rate_bucket)?;
let db = state.storage.get_database("_system")?;
let collection = match db.system_collection("_admins") {
Ok(c) => c,
Err(DbError::CollectionNotFound(_)) => {
tracing::warn!("_admins collection not found, initializing...");
crate::server::auth::AuthService::init(
&state.storage,
state.replication_log.as_deref(),
state.storage.data_dir(),
)?;
db.system_collection("_admins")?
}
Err(e) => return Err(e),
};
if collection.count() == 0 {
tracing::warn!("_admins collection empty, creating default admin...");
crate::server::auth::AuthService::init(
&state.storage,
state.replication_log.as_deref(),
state.storage.data_dir(),
)?;
}
let doc = match collection.get(&req.username) {
Ok(d) => d,
Err(DbError::DocumentNotFound(_)) => {
crate::server::auth::record_login_failure(&rate_bucket);
return Err(DbError::BadRequest("Invalid credentials".to_string()));
}
Err(e) => return Err(e),
};
let user: crate::server::auth::User = serde_json::from_value(doc.to_value()).map_err(|e| {
tracing::error!("Failed to deserialize user '{}': {}", req.username, e);
DbError::InternalError("Corrupted user data".to_string())
})?;
if !crate::server::auth::verify_password_blocking(&req.password, &user.password_hash).await {
crate::server::auth::record_login_failure(&rate_bucket);
tracing::warn!(
"Password verification failed for user '{}' from {}",
req.username,
client_ip
);
return Err(DbError::BadRequest("Invalid credentials".to_string()));
}
crate::server::auth::clear_login_failures(&rate_bucket);
let roles = crate::server::auth::AuthService::get_user_roles(&state.storage, &user.username);
let token =
crate::server::auth::AuthService::create_jwt_with_roles(&user.username, roles, None)?;
Ok(Json(LoginResponse { token }))
}
#[derive(Debug, Serialize)]
pub struct LiveQueryTokenResponse {
pub token: String,
pub expires_in: u32, }
pub async fn livequery_token_handler(
Extension(claims): Extension<crate::server::auth::Claims>,
) -> Result<Json<LiveQueryTokenResponse>, DbError> {
let token = crate::server::auth::AuthService::create_livequery_jwt(
&claims.sub,
claims.roles.clone(),
claims.scoped_databases.clone(),
)?;
Ok(Json(LiveQueryTokenResponse {
token,
expires_in: 2,
}))
}