Files
boc/aamos-ledger-rust/src/routes.rs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

265 lines
7.9 KiB
Rust

use axum::{
extract::{Path, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde_json::json;
use std::sync::Arc;
use uuid::Uuid;
use crate::{
auth::{auth_middleware, create_jwt},
db,
models::*,
};
pub struct AppState {
pub pool: sqlx::PgPool,
pub jwt_secret: String,
}
pub fn create_router(state: Arc<AppState>) -> Router {
let public_routes = Router::new()
.route("/health", get(health_check))
.route("/login", post(login));
let protected_routes = Router::new()
.route("/accounts", get(list_accounts).post(create_account_handler))
.route("/accounts/:id", get(get_account_handler))
.route("/journal-entries", get(list_journal_entries).post(create_journal_entry_handler))
.route("/journal-entries/:id", get(get_journal_entry_handler))
.route("/trial-balance", get(get_trial_balance_legacy))
.route_layer(axum::middleware::from_fn(auth_middleware));
let api_routes = Router::new()
.nest("/api/v1", protected_routes);
// Legacy /api/ledger routes for Ouroboros UI compatibility
let ledger_routes = Router::new()
.route("/journal", get(list_journal_entries_legacy))
.route("/trial-balance", get(get_trial_balance_legacy))
.route_layer(axum::middleware::from_fn(auth_middleware));
Router::new()
.merge(public_routes)
.merge(api_routes)
.nest("/api/ledger", ledger_routes)
.with_state(state)
}
async fn health_check() -> Json<serde_json::Value> {
Json(json!({"status": "ok"}))
}
async fn login(
State(state): State<Arc<AppState>>,
Json(req): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, (StatusCode, Json<serde_json::Value>)> {
// Simplified auth - in production, verify password hash against database
if req.password != "password" {
return Err((
StatusCode::UNAUTHORIZED,
Json(json!({"error": "Invalid credentials"})),
));
}
let token = create_jwt(&state.jwt_secret, &req.username).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Failed to create token: {}", e)})),
)
})?;
Ok(Json(LoginResponse { token }))
}
async fn list_accounts(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<Account>>, (StatusCode, Json<serde_json::Value>)> {
let accounts = db::get_accounts(&state.pool).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
Ok(Json(accounts))
}
async fn create_account_handler(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateAccount>,
) -> Result<(StatusCode, Json<Account>), (StatusCode, Json<serde_json::Value>)> {
let account = db::create_account(&state.pool, &req).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
Ok((StatusCode::CREATED, Json(account)))
}
async fn get_account_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<Json<Account>, (StatusCode, Json<serde_json::Value>)> {
let account = db::get_account_by_id(&state.pool, id).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
match account {
Some(account) => Ok(Json(account)),
None => Err((
StatusCode::NOT_FOUND,
Json(json!({"error": "Account not found"})),
)),
}
}
async fn list_journal_entries(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<JournalEntry>>, (StatusCode, Json<serde_json::Value>)> {
let entries = db::get_journal_entries(&state.pool).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
Ok(Json(entries))
}
async fn create_journal_entry_handler(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateJournalEntry>,
) -> Result<(StatusCode, Json<JournalEntryWithLines>), (StatusCode, Json<serde_json::Value>)> {
// Validate that debits equal credits
let total_debit: f64 = req
.lines
.iter()
.filter_map(|l| l.debit)
.fold(0.0, |acc, val| acc + val);
let total_credit: f64 = req
.lines
.iter()
.filter_map(|l| l.credit)
.fold(0.0, |acc, val| acc + val);
if (total_debit - total_credit).abs() > 0.01 {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({"error": format!("Total debits ({:.2}) must equal total credits ({:.2})", total_debit, total_credit)})),
));
}
let entry = db::create_journal_entry(&state.pool, &req, "system")
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
Ok((StatusCode::CREATED, Json(entry)))
}
async fn get_journal_entry_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> Result<Json<JournalEntryWithLines>, (StatusCode, Json<serde_json::Value>)> {
let entry = db::get_journal_entry_by_id(&state.pool, id).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
match entry {
Some(entry) => Ok(Json(entry)),
None => Err((
StatusCode::NOT_FOUND,
Json(json!({"error": "Journal entry not found"})),
)),
}
}
async fn get_trial_balance_handler(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<TrialBalanceRow>>, (StatusCode, Json<serde_json::Value>)> {
let rows = db::get_trial_balance(&state.pool).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
Ok(Json(rows))
}
// Legacy handler for /api/ledger/journal - returns format compatible with Ouroboros UI
async fn list_journal_entries_legacy(
State(state): State<Arc<AppState>>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let entries = db::get_journal_entries(&state.pool).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
// Format compatible with Ouroboros UI
let formatted_entries: Vec<serde_json::Value> = entries
.into_iter()
.map(|e| {
json!({
"entry_number": e.entry_number,
"entry_date": e.entry_date,
"description": e.description,
"reference": e.entry_number,
"rader": []
})
})
.collect();
Ok(Json(json!({
"ok": true,
"entries": formatted_entries
})))
}
// Legacy handler for /api/ledger/trial-balance - returns format compatible with Ouroboros UI
async fn get_trial_balance_legacy(
State(state): State<Arc<AppState>>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let rows = db::get_trial_balance(&state.pool).await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": format!("Database error: {}", e)})),
)
})?;
// Format compatible with Ouroboros UI
let accounts: Vec<serde_json::Value> = rows
.into_iter()
.map(|r| {
json!({
"account_number": r.account_code,
"account_name": r.account_name,
"balance": r.balance.unwrap_or(0.0)
})
})
.collect();
Ok(Json(json!({
"ok": true,
"accounts": accounts
})))
}