use async_trait::async_trait; use loco_rs::{ app::{AppContext, Hooks, Initializer}, bgworker::{BackgroundWorker, Queue}, boot::{create_app, BootResult, StartMode}, config::Config, controller::AppRoutes, db::{self, truncate_table}, environment::Environment, storage::{self, Storage}, task::Tasks, Result, }; use migration::Migrator; use std::{path::Path, sync::Arc}; #[allow(unused_imports)] use crate::{ controllers::{ account, admin_categories, admin_customers, admin_dashboard, admin_discount_profiles, admin_form, admin_orders, admin_products, admin_shipping, auth, auth_pages, cart, checkout, home, i18n, media, oauth2, shop, }, initializers, models::_entities::users, tasks, workers::downloader::DownloadWorker, }; pub struct App; #[async_trait] impl Hooks for App { fn app_name() -> &'static str { env!("CARGO_CRATE_NAME") } fn app_version() -> String { format!( "{} ({})", env!("CARGO_PKG_VERSION"), option_env!("BUILD_SHA") .or(option_env!("GITHUB_SHA")) .unwrap_or("dev") ) } async fn boot( mode: StartMode, environment: &Environment, config: Config, ) -> Result { create_app::(mode, environment, config).await } async fn load_config(environment: &Environment) -> Result { dotenvy::dotenv().ok(); environment.load() } /// Attach the Casbin authorization layer on top of all routes. Order /// matters: `inject_subject` is the outermost layer so it runs first and /// stamps the JWT-derived role onto the request before the inner /// `CasbinAxumLayer` enforces the policy. See `shared::rbac`. async fn after_routes(router: axum::Router, ctx: &AppContext) -> Result { let casbin = crate::shared::rbac::layer().await?; Ok(router .layer(casbin) .layer(axum::middleware::from_fn_with_state( ctx.clone(), crate::shared::rbac::inject_subject, )) // CSRF runs outermost so it validates the double-submit token before // any handler sees the request and stamps the cookie on safe ones. .layer(axum::middleware::from_fn_with_state( ctx.clone(), crate::shared::csrf::protect, ))) } async fn initializers(_ctx: &AppContext) -> Result>> { Ok(vec![ Box::new(initializers::view_engine::ViewEngineInitializer), Box::new(initializers::admin_seeder::AdminSeeder), Box::new(initializers::shipping_seeder::ShippingSeeder), Box::new(initializers::oauth2::OAuth2StoreInitializer), Box::new(initializers::oauth2_session::OAuth2SessionInitializer), ]) } fn routes(_ctx: &AppContext) -> AppRoutes { AppRoutes::with_default_routes() // feature routes below // public .add_route(home::routes()) .add_route(shop::routes()) .add_route(cart::routes()) .add_route(checkout::routes()) // cross-cutting .add_route(auth::routes()) .add_route(auth_pages::routes()) .add_route(account::routes()) .add_route(oauth2::routes()) .add_route(i18n::routes()) .add_route(media::routes()) // admin .add_route(admin_dashboard::routes()) .add_route(admin_products::routes()) .add_route(admin_discount_profiles::routes()) .add_route(admin_categories::routes()) .add_route(admin_orders::routes()) .add_route(admin_customers::routes()) .add_route(admin_shipping::routes()) } async fn after_context(ctx: AppContext) -> Result { let upload_root = media::uploads_root(&ctx.config)?; tokio::fs::create_dir_all(upload_root.join(media::IMAGE_STORAGE_DIR)).await?; let driver = storage::drivers::local::new_with_prefix(&upload_root)?; Ok(AppContext { storage: Arc::new(Storage::single(driver)), ..ctx }) } async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { queue.register(DownloadWorker::build(ctx)).await?; Ok(()) } #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { // tasks-inject (do not remove) } async fn truncate(ctx: &AppContext) -> Result<()> { truncate_table(&ctx.db, users::Entity).await?; Ok(()) } async fn seed(ctx: &AppContext, base: &Path) -> Result<()> { db::seed::(&ctx.db, &base.join("users.yaml").display().to_string()) .await?; crate::seed::seed_catalog(ctx).await?; Ok(()) } }