diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3a42d17 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,143 @@ +use axum::{ + routing::{get, post, put, delete}, + Router, middleware as axum_middleware, +}; +use sea_orm::{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; + +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 app = api_routes.merge(swagger_ui); + + Ok(app) +} + +pub fn server_address() -> SocketAddr { + SocketAddr::from(([0, 0, 0, 0], 8080)) +} diff --git a/src/main.rs b/src/main.rs index d5a768b..ed1cae5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,147 +1,19 @@ -use axum::{ - routing::{get, post, put, delete}, - Router, middleware as axum_middleware, -}; -use sea_orm::{Database, DatabaseConnection, DbErr}; +use family_budget::*; +use sea_orm::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; - -mod models; -mod services; -mod migration; -mod routes; -mod auth; -mod middleware; - -#[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; - -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 -} #[tokio::main] async fn main() -> Result<(), DbErr> { let db = establish_connection().await?; - println!("Successfully connected to database!"); println!("Running migrations..."); - crate::migration::Migrator::up(&db, None).await?; + migration::Migrator::up(&db, None).await?; println!("Migrations completed!"); - let database_url = std::env::var("DATABASE_URL") - .expect("DATABASE_URL must be set in .env file"); + let app = create_app(db).await?; - 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 app = api_routes.merge(swagger_ui); - - let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); + let addr = server_address(); println!("Server running on http://{}", addr); println!("Swagger UI available at http://{}/swagger-ui", addr);