feat: harden runtime lifecycle and application credentials
- enforce deterministic runtime lifecycle state transitions - add live graceful-to-forced shutdown escalation - align HTTP draining and worker shutdown with global deadline - guarantee deterministic shutdown hook ordering - add secure application client IDs and one-time client secrets - hash application secrets with BLAKE3 and constant-time verification - make credential creation and rotation transactionally auditable - enforce strict client_id authentication and redirect URI validation - add SQLite and PostgreSQL credential migrations - add application credential and runtime lifecycle acceptance tests - update Dioxus application management workflows - update security and architecture documentation
This commit is contained in:
1 parent
4c697e9adf
commit
dc5417334b
26 files changed
+2477
-183
No files matched your search
+50
-8
@@ -59,9 +59,25 @@ fn client() -> Client {
|
||||
Client::new()
|
||||
}
|
||||
|
||||
pub trait RequestBuilderExtHelper {
|
||||
fn with_credentials_include(self) -> Self;
|
||||
}
|
||||
|
||||
impl RequestBuilderExtHelper for reqwest::RequestBuilder {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn with_credentials_include(self) -> Self {
|
||||
self.fetch_credentials_include()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn with_credentials_include(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach credentials + optional bearer session token.
|
||||
fn authorize(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
let builder = builder.fetch_credentials_include();
|
||||
let builder = builder.with_credentials_include();
|
||||
if let Some(token) = session::load_access_token() {
|
||||
builder.header("Authorization", format!("Bearer {token}"))
|
||||
} else {
|
||||
@@ -180,7 +196,7 @@ pub async fn login(username: &str, password: &str) -> Result<LoginResponse, ApiE
|
||||
let url = api_url("/auth/login");
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.fetch_credentials_include()
|
||||
.with_credentials_include()
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&body)
|
||||
@@ -369,25 +385,51 @@ pub async fn list_applications() -> Result<Vec<ApplicationView>, ApiError> {
|
||||
Ok(r.applications)
|
||||
}
|
||||
|
||||
pub async fn create_application(name: &str, slug: &str) -> Result<ApplicationView, ApiError> {
|
||||
let body = serde_json::json!({ "name": name, "slug": slug });
|
||||
let r: Value = post_json("/applications", &body).await?;
|
||||
serde_json::from_value(r.get("application").cloned().unwrap_or(Value::Null))
|
||||
.map_err(|e| ApiError::Other(e.to_string()))
|
||||
pub async fn create_application(
|
||||
name: &str,
|
||||
slug: &str,
|
||||
description: Option<&str>,
|
||||
redirect_urls: Option<Vec<String>>,
|
||||
scopes: Option<Vec<String>>,
|
||||
) -> Result<CreateApplicationResponse, ApiError> {
|
||||
let body = serde_json::json!({
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"description": description,
|
||||
"redirect_urls": redirect_urls,
|
||||
"scopes": scopes,
|
||||
});
|
||||
post_json("/applications", &body).await
|
||||
}
|
||||
|
||||
pub async fn update_application(
|
||||
id: &str,
|
||||
name: &str,
|
||||
slug: &str,
|
||||
description: Option<&str>,
|
||||
redirect_urls: Option<Vec<String>>,
|
||||
scopes: Option<Vec<String>>,
|
||||
enabled: bool,
|
||||
) -> Result<ApplicationView, ApiError> {
|
||||
let body = serde_json::json!({ "name": name, "slug": slug, "enabled": enabled });
|
||||
let body = serde_json::json!({
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"description": description,
|
||||
"redirect_urls": redirect_urls,
|
||||
"scopes": scopes,
|
||||
"enabled": enabled,
|
||||
});
|
||||
let r: Value = patch_json(&format!("/applications/{id}"), &body).await?;
|
||||
serde_json::from_value(r.get("application").cloned().unwrap_or(Value::Null))
|
||||
.map_err(|e| ApiError::Other(e.to_string()))
|
||||
}
|
||||
|
||||
pub async fn rotate_application_secret(id: &str) -> Result<String, ApiError> {
|
||||
let r: RotateSecretResponse =
|
||||
post_json(&format!("/applications/{id}/secret"), &serde_json::json!({})).await?;
|
||||
Ok(r.client_secret)
|
||||
}
|
||||
|
||||
pub async fn delete_application(id: &str) -> Result<(), ApiError> {
|
||||
let _: Value = delete_json(&format!("/applications/{id}")).await?;
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user