bae705aa97
- 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
265 lines
7.9 KiB
Rust
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
|
|
})))
|
|
}
|