Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS plan_loan_lifecycle;
17 changes: 17 additions & 0 deletions backend/migrations/20260824000000_add_plan_loan_lifecycle.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Issue #1035: persist freeze / recall / liquidation progress for
-- yield-bearing vaults so the loan-lifecycle endpoints and trigger-info
-- dashboard have a source of truth besides the chain.

CREATE TABLE IF NOT EXISTS plan_loan_lifecycle (
plan_id UUID PRIMARY KEY REFERENCES plans (id) ON DELETE CASCADE,
freeze_status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (freeze_status IN ('PENDING', 'PROCESSING', 'FROZEN')),
recall_progress INTEGER NOT NULL DEFAULT 0
CHECK (recall_progress >= 0 AND recall_progress <= 100),
settlement_status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (settlement_status IN ('PENDING', 'PROCESSING', 'LIQUIDATED', 'SETTLED')),
outstanding_loaned BIGINT NOT NULL DEFAULT 0
CHECK (outstanding_loaned >= 0),
last_tx_hash TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
28 changes: 27 additions & 1 deletion backend/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ use tower_http::cors::CorsLayer;
use tracing::{error, warn};
use uuid::Uuid;

use crate::auth::{jwt_auth_middleware, signature_auth_middleware, Claims};
use crate::auth::{
jwt_auth_middleware, jwt_or_signature_auth_middleware, signature_auth_middleware, Claims,
};
use crate::cache::PlanCache;
use crate::kyc_webhook::kyc_webhook_handler;
#[cfg(feature = "metrics")]
Expand Down Expand Up @@ -67,6 +69,7 @@ pub struct AppState {
pub plan_cache: PlanCache,
pub apy_cache: dashmap::DashMap<String, u32>,
pub kyc_tx: tokio::sync::broadcast::Sender<crate::ws::KycUpdateEvent>,
pub status_tx: tokio::sync::broadcast::Sender<crate::ws::PlanStatusEvent>,
pub stellar_submit: StellarSubmitClient,
}

Expand Down Expand Up @@ -263,6 +266,8 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
HeaderName::from_static("x-public-key"),
HeaderName::from_static("x-signature"),
])
.max_age(std::time::Duration::from_secs(3600));

Expand All @@ -285,12 +290,32 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/api/plans/{id}/report", get(get_plan_report))
.route_layer(from_fn(jwt_auth_middleware));

// Loan lifecycle: admin JWT or wallet signature.
let loan_lifecycle_routes = Router::new()
.route(
"/api/plans/{id}/freeze-loans",
post(crate::loan_lifecycle::freeze_loans),
)
.route(
"/api/plans/{id}/recall-loans",
post(crate::loan_lifecycle::recall_loans),
)
.route(
"/api/plans/{id}/liquidate-settle",
post(crate::loan_lifecycle::liquidate_and_settle),
)
.route_layer(from_fn(jwt_or_signature_auth_middleware));

// Public or admin routes
let public_routes = Router::new()
.route("/api/plans", get(get_plans))
.route("/api/plans/due-for-claim", get(get_plans_due_for_claim))
.route("/api/plans/due-for-claim/{id}", get(get_plan_due_for_claim))
.route("/api/plans/{id}", get(get_plan_by_id))
.route(
"/api/plans/{id}/trigger-info",
get(crate::loan_lifecycle::get_trigger_info),
)
.route("/api/anchor/payout-status", get(get_anchor_payouts))
.route("/api/lending/current-rate", get(get_current_lending_rate))
.route("/api/kyc/webhook", post(kyc_webhook_handler))
Expand All @@ -307,6 +332,7 @@ pub fn create_router(state: Arc<AppState>) -> Router {
let router = Router::new()
.merge(user_routes)
.merge(admin_routes)
.merge(loan_lifecycle_routes)
.merge(public_routes)
.layer(axum::middleware::from_fn(move |req, next| {
rate_limit_middleware(req, next, store.clone(), config.clone())
Expand Down
21 changes: 21 additions & 0 deletions backend/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@ pub async fn jwt_auth_middleware(
Ok(next.run(req).await)
}

/// Accepts either an admin JWT (`Authorization: Bearer`) or an ed25519
/// request signature (`X-Public-Key` + `X-Signature`). JWT takes precedence
/// when both are present so browser clients that attach a token keep working
/// even if signature headers are incomplete (empty POST bodies are not signed).
pub async fn jwt_or_signature_auth_middleware(
req: Request<Body>,
next: Next,
) -> Result<Response, AuthError> {
let has_bearer = req
.headers()
.get("Authorization")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("Bearer "));

if has_bearer {
jwt_auth_middleware(req, next).await
} else {
signature_auth_middleware(req, next).await
}
}

pub async fn signature_auth_middleware(
req: Request<Body>,
next: Next,
Expand Down
1 change: 1 addition & 0 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod db;
pub mod inactivity_watchdog;

pub mod kyc_webhook;
pub mod loan_lifecycle;
#[cfg(feature = "metrics")]
pub mod metrics;
pub mod middleware;
Expand Down
Loading
Loading