Land password-vault dedicated workbench and mnote-vault-core/CLI, agent token read path design, vault transport split, and retire obsolete filetree smokes. Ignore local vault reimport scripts that trip secret scanners.
151 lines
4.8 KiB
Rust
151 lines
4.8 KiB
Rust
use crate::context::RequestContext;
|
|
use axum::http::StatusCode;
|
|
use axum::http::{HeaderName, HeaderValue};
|
|
use axum::response::{IntoResponse, Response};
|
|
use axum::Json;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ErrorBody {
|
|
pub ok: bool,
|
|
pub code: &'static str,
|
|
pub message: String,
|
|
pub request_id: Option<String>,
|
|
pub trace_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub details: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WebError {
|
|
status: StatusCode,
|
|
code: &'static str,
|
|
message: String,
|
|
request_context: Option<RequestContext>,
|
|
headers: Vec<(&'static str, String)>,
|
|
details: Option<Value>,
|
|
}
|
|
|
|
impl WebError {
|
|
pub fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
|
|
Self {
|
|
status,
|
|
code,
|
|
message: message.into(),
|
|
request_context: None,
|
|
headers: Vec::new(),
|
|
details: None,
|
|
}
|
|
}
|
|
|
|
pub fn bad_request(message: impl Into<String>) -> Self {
|
|
Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
|
|
}
|
|
|
|
pub fn bad_request_code(code: &'static str, message: impl Into<String>) -> Self {
|
|
Self::new(StatusCode::BAD_REQUEST, code, message)
|
|
}
|
|
|
|
pub fn internal(message: impl Into<String>) -> Self {
|
|
Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
|
|
}
|
|
|
|
pub fn bad_gateway_code(code: &'static str, message: impl Into<String>) -> Self {
|
|
Self::new(StatusCode::BAD_GATEWAY, code, message)
|
|
}
|
|
|
|
pub fn service_unavailable_code(code: &'static str, message: impl Into<String>) -> Self {
|
|
Self::new(StatusCode::SERVICE_UNAVAILABLE, code, message)
|
|
}
|
|
|
|
pub fn gateway_timeout_code(code: &'static str, message: impl Into<String>) -> Self {
|
|
Self::new(StatusCode::GATEWAY_TIMEOUT, code, message)
|
|
}
|
|
|
|
pub fn with_context(mut self, request_context: &RequestContext) -> Self {
|
|
self.request_context = Some(request_context.clone());
|
|
self
|
|
}
|
|
|
|
pub fn with_header(mut self, name: &'static str, value: impl Into<String>) -> Self {
|
|
self.headers.push((name, value.into()));
|
|
self
|
|
}
|
|
|
|
pub fn with_details(mut self, details: Value) -> Self {
|
|
self.details = Some(details);
|
|
self
|
|
}
|
|
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
|
|
pub fn code(&self) -> &'static str {
|
|
self.code
|
|
}
|
|
|
|
pub fn status(&self) -> StatusCode {
|
|
self.status
|
|
}
|
|
}
|
|
|
|
/// Map independent vault-core errors into HTTP WebError (12-2 P1a shared read path).
|
|
impl From<mnote_vault_core::VaultError> for WebError {
|
|
fn from(err: mnote_vault_core::VaultError) -> Self {
|
|
use mnote_vault_core::VaultStatus;
|
|
let status = match err.status {
|
|
VaultStatus::BadRequest => StatusCode::BAD_REQUEST,
|
|
VaultStatus::Unauthorized => StatusCode::UNAUTHORIZED,
|
|
VaultStatus::Forbidden => StatusCode::FORBIDDEN,
|
|
VaultStatus::NotFound => StatusCode::NOT_FOUND,
|
|
VaultStatus::Conflict => StatusCode::CONFLICT,
|
|
VaultStatus::Locked => StatusCode::from_u16(423).unwrap_or(StatusCode::FORBIDDEN),
|
|
VaultStatus::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
|
|
VaultStatus::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
|
};
|
|
let mut web = WebError::new(status, err.code, err.message);
|
|
if let Some(details) = err.details {
|
|
web = web.with_details(details);
|
|
}
|
|
web
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for WebError {
|
|
fn into_response(self) -> Response {
|
|
let body = ErrorBody {
|
|
ok: false,
|
|
code: self.code,
|
|
message: self.message,
|
|
request_id: self
|
|
.request_context
|
|
.as_ref()
|
|
.map(|context| context.trace.request_id.clone()),
|
|
trace_id: self
|
|
.request_context
|
|
.as_ref()
|
|
.map(|context| context.trace.trace_id.clone()),
|
|
details: self.details,
|
|
};
|
|
let mut response = (self.status, Json(body)).into_response();
|
|
if let Ok(name) = HeaderName::from_lowercase(b"x-error-code") {
|
|
if let Ok(value) = HeaderValue::from_str(self.code) {
|
|
response.headers_mut().insert(name, value);
|
|
}
|
|
}
|
|
for (name, value) in self.headers {
|
|
let Ok(header_name) = HeaderName::from_lowercase(name.as_bytes()) else {
|
|
continue;
|
|
};
|
|
let Ok(header_value) = HeaderValue::from_str(&value) else {
|
|
continue;
|
|
};
|
|
response.headers_mut().insert(header_name, header_value);
|
|
}
|
|
response
|
|
}
|
|
}
|