use axum::{ routing::{get, post, put, delete}, Router, middleware as axum_middleware, }; use sea_orm::{sqlx, Database, DatabaseConnection, DbErr}; use sea_orm_migration::prelude::*; use std::net::SocketAddr; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use tower_sessions::{Expiry, SessionManagerLayer}; use tower_sessions_sqlx_store::PostgresStore; use axum_login::AuthManagerLayerBuilder; use time::Duration; use tower_http::cors::CorsLayer; use axum::http::{Method, HeaderValue}; pub mod models; pub mod services; pub mod migration; pub mod routes; pub mod auth; pub mod middleware; pub use auth::AuthBackend; pub use middleware::require_admin; #[derive(OpenApi)] #[openapi( paths( routes::auth::login, routes::auth::logout, routes::family::create_family, routes::family::get_family, routes::family::get_all_families, routes::family::update_family, routes::family::delete_family, routes::category::create_category, routes::category::get_category, routes::category::get_categories_by_family, routes::category::update_category, routes::category::delete_category, routes::expense::create_expense, routes::expense::get_expense, routes::expense::get_expenses_by_category, routes::expense::update_expense, routes::expense::delete_expense, routes::expense::get_remaining_limit, ), components( schemas( models::family::Model, models::category::Model, models::expense::Model, routes::auth::LoginRequest, routes::auth::LoginResponse, routes::family::CreateFamilyRequest, routes::family::UpdateFamilyRequest, routes::category::CreateCategoryRequest, routes::category::UpdateCategoryRequest, routes::expense::CreateExpenseRequest, routes::expense::UpdateExpenseRequest, routes::expense::RemainingLimitResponse, ) ), tags( (name = "auth", description = "Authentication endpoints"), (name = "families", description = "Family management endpoints"), (name = "categories", description = "Category management endpoints"), (name = "expenses", description = "Expense management endpoints") ), info( title = "Family Budget API", version = "0.1.0", description = "REST API" ) )] struct ApiDoc; pub async fn establish_connection() -> Result { dotenvy::dotenv().ok(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set in .env file"); Database::connect(&database_url).await } pub async fn create_app(db: DatabaseConnection) -> Result { let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set in .env file"); let session_store = PostgresStore::new( sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database for sessions"), ); session_store .migrate() .await .expect("Failed to run session store migrations"); let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) .with_expiry(Expiry::OnInactivity(Duration::days(1))); let backend = auth::AuthBackend { db: db.clone() }; let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build(); let protected_routes = Router::new() .route("/families", post(routes::family::create_family)) .route_layer(axum_middleware::from_fn(middleware::require_admin)); let api_routes = Router::new() .route("/login", post(routes::auth::login)) .route("/logout", post(routes::auth::logout)) .merge(protected_routes) .route("/families", get(routes::family::get_all_families)) .route("/families/{id}", get(routes::family::get_family)) .route("/families/{id}", put(routes::family::update_family)) .route("/families/{id}", delete(routes::family::delete_family)) .route("/families/{family_id}/categories", post(routes::category::create_category)) .route("/families/{family_id}/categories", get(routes::category::get_categories_by_family)) .route("/families/{family_id}/categories/{category_id}", get(routes::category::get_category)) .route("/families/{family_id}/categories/{category_id}", put(routes::category::update_category)) .route("/families/{family_id}/categories/{category_id}", delete(routes::category::delete_category)) .route("/families/{family_id}/categories/{category_id}/expenses", post(routes::expense::create_expense)) .route("/families/{family_id}/categories/{category_id}/expenses", get(routes::expense::get_expenses_by_category)) .route("/families/{family_id}/categories/{category_id}/expenses/{expense_id}", get(routes::expense::get_expense)) .route("/families/{family_id}/categories/{category_id}/expenses/{expense_id}", put(routes::expense::update_expense)) .route("/families/{family_id}/categories/{category_id}/expenses/{expense_id}", delete(routes::expense::delete_expense)) .route("/families/{family_id}/categories/{category_id}/remaining", get(routes::expense::get_remaining_limit)) .layer(auth_layer) .with_state(db); let swagger_ui = SwaggerUi::new("/swagger-ui") .url("/api-docs/openapi.json", ApiDoc::openapi()); let cors = CorsLayer::new() .allow_origin([ "http://localhost:3000".parse::().unwrap(), "http://localhost:8080".parse::().unwrap(), ]) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS]) .allow_headers([ axum::http::header::CONTENT_TYPE, axum::http::header::AUTHORIZATION, axum::http::header::ACCEPT, ]) .allow_credentials(true); let app = api_routes .layer(cors) .merge(swagger_ui); Ok(app) } pub fn server_address() -> SocketAddr { SocketAddr::from(([0, 0, 0, 0], 8080)) }