feat(saas): expose redacted operational signals
req: operations/003 req: operations/005 req: operations/007 req: security/008 req: v1_release/003
This commit is contained in:
+87
-10
@@ -11,6 +11,7 @@ use hemx_saas_example::{home_page, live_status, registry, settings_page, ui, App
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::Infallible;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -29,6 +30,9 @@ fn app(ctx: AppContext) -> Router {
|
||||
.route("/", get(home).post(interact))
|
||||
.route("/settings", get(settings))
|
||||
.route("/projects", post(create_project))
|
||||
.route("/health/live", get(health_live))
|
||||
.route("/health/ready", get(health_ready))
|
||||
.route("/metrics", get(metrics))
|
||||
.route("/events", get(events))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.route("/app.css", get(css))
|
||||
@@ -75,6 +79,7 @@ async fn create_project(
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<BTreeMap<String, String>>,
|
||||
) -> Response {
|
||||
let started = Instant::now();
|
||||
let bearer = headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
@@ -85,18 +90,90 @@ async fn create_project(
|
||||
.unwrap_or_default();
|
||||
let name = form.get("name").map(String::as_str).unwrap_or_default();
|
||||
let csrf = form.get("csrf").map(String::as_str).unwrap_or_default();
|
||||
match ctx.create_project_authorized(name, bearer, csrf, origin) {
|
||||
Ok(_) => (StatusCode::SEE_OTHER, [("location", "/")], "").into_response(),
|
||||
Err(
|
||||
error @ (hemx_saas_example::AppError::MissingSession
|
||||
| hemx_saas_example::AppError::CsrfRejected
|
||||
| hemx_saas_example::AppError::OriginRejected),
|
||||
) => (StatusCode::FORBIDDEN, error.to_string()).into_response(),
|
||||
Err(error @ hemx_saas_example::AppError::Validation(_)) => {
|
||||
(StatusCode::BAD_REQUEST, error.to_string()).into_response()
|
||||
if let Some(client_fingerprint) = headers
|
||||
.get("x-hemx-fingerprint")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
let current_fingerprint = ui::BUILD_FINGERPRINT.0.to_string();
|
||||
if client_fingerprint != current_fingerprint {
|
||||
ctx.record_mutation("mismatch", started.elapsed());
|
||||
return Response::builder()
|
||||
.status(StatusCode::CONFLICT)
|
||||
.header("content-type", "application/problem+json")
|
||||
.header("x-hemx-recovery", "reload")
|
||||
.header("x-hemx-fingerprint", current_fingerprint)
|
||||
.body(Body::from("{\"code\":\"deployment-mismatch\"}"))
|
||||
.expect("deployment mismatch response");
|
||||
}
|
||||
Err(error) => (StatusCode::SERVICE_UNAVAILABLE, error.to_string()).into_response(),
|
||||
}
|
||||
let (outcome, response) = match ctx.create_project_authorized(name, bearer, csrf, origin) {
|
||||
Ok(_) => (
|
||||
"succeeded",
|
||||
(StatusCode::SEE_OTHER, [("location", "/")], "").into_response(),
|
||||
),
|
||||
Err(
|
||||
hemx_saas_example::AppError::MissingSession
|
||||
| hemx_saas_example::AppError::CsrfRejected
|
||||
| hemx_saas_example::AppError::OriginRejected,
|
||||
) => (
|
||||
"denied",
|
||||
problem(StatusCode::FORBIDDEN, "authorization-denied"),
|
||||
),
|
||||
Err(hemx_saas_example::AppError::Validation(_)) => (
|
||||
"invalid",
|
||||
problem(StatusCode::BAD_REQUEST, "invalid-project"),
|
||||
),
|
||||
Err(_) => (
|
||||
"failed",
|
||||
problem(StatusCode::SERVICE_UNAVAILABLE, "storage-unavailable"),
|
||||
),
|
||||
};
|
||||
ctx.record_mutation(outcome, started.elapsed());
|
||||
response
|
||||
}
|
||||
|
||||
fn problem(status: StatusCode, code: &'static str) -> Response {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", "application/problem+json")
|
||||
.body(Body::from(format!("{{\"code\":\"{code}\"}}")))
|
||||
.expect("problem response")
|
||||
}
|
||||
|
||||
// req: operations/007
|
||||
async fn health_live() -> Response {
|
||||
json_response(StatusCode::OK, "{\"status\":\"live\"}".to_owned())
|
||||
}
|
||||
|
||||
// req: operations/007
|
||||
async fn health_ready(State(ctx): State<AppContext>) -> Response {
|
||||
if ctx.ready() {
|
||||
json_response(
|
||||
StatusCode::OK,
|
||||
format!(
|
||||
"{{\"status\":\"ready\",\"fingerprint\":\"{}\"}}",
|
||||
ui::BUILD_FINGERPRINT.0
|
||||
),
|
||||
)
|
||||
} else {
|
||||
json_response(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"{\"status\":\"not-ready\",\"code\":\"storage-unavailable\"}".to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// req: operations/005 req: operations/007
|
||||
async fn metrics(State(ctx): State<AppContext>) -> Response {
|
||||
json_response(StatusCode::OK, ctx.metrics_json())
|
||||
}
|
||||
|
||||
fn json_response(status: StatusCode, body: String) -> Response {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body))
|
||||
.expect("JSON response")
|
||||
}
|
||||
|
||||
async fn runtime() -> impl IntoResponse {
|
||||
|
||||
Reference in New Issue
Block a user