mod auth; mod client; mod cnfg; mod error; mod server; mod state; use std::net::{Ipv6Addr, SocketAddr}; use clap::Parser; use reqwest::header::CONTENT_TYPE; use sellershut_core::auth::{AUTH_FILE_DESCRIPTOR_SET, auth_server::AuthServer}; use stack_up::{Configuration, Services, tracing::Tracing}; use tokio::{signal, task::AbortHandle}; use tonic::service::Routes; use tower::{make::Shared, steer::Steer}; use tracing::{info, trace}; use crate::{error::AppError, server::grpc::interceptor::MyInterceptor, state::AppState}; /// auth-service #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// Path to config file #[arg(short, long)] config_file: Option, } #[tokio::main] async fn main() -> Result<(), AppError> { let args = Args::parse(); let config = include_str!("../auth.toml"); let mut config = config::Config::builder() .add_source(config::File::from_str(config, config::FileFormat::Toml)) .add_source( config::Environment::with_prefix("APP") .separator("__") .convert_case(config::Case::Kebab), ); if let Some(cf) = args.config_file.as_ref().and_then(|v| v.to_str()) { config = config.add_source(config::File::new(cf, config::FileFormat::Toml)); }; let mut config: Configuration = config.build()?.try_deserialize()?; config.application.name = env!("CARGO_CRATE_NAME").into(); config.application.version = env!("CARGO_PKG_VERSION").into(); let _tracing = Tracing::builder().build(&config.monitoring); let services = Services::builder() .postgres(&config.database) .await .inspect_err(|e| tracing::error!("database: {e}"))? .build(); trace!("running migrations"); sqlx::migrate!("./migrations") .run(&services.postgres) .await?; let (state, deletion_task) = AppState::create(services, &config).await?; let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.application.port)); let listener = tokio::net::TcpListener::bind(addr).await?; info!(port = addr.port(), "serving api"); let service = AuthServer::with_interceptor(state.clone(), MyInterceptor); let auth_reflector = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(AUTH_FILE_DESCRIPTOR_SET) .build_v1()?; let grpc_server = Routes::new(service) .add_service(auth_reflector) .into_axum_router(); let service = Steer::new( vec![server::router(state), grpc_server], |req: &axum::extract::Request, _services: &[_]| { if req .headers() .get(CONTENT_TYPE) .map(|content_type| content_type.as_bytes()) .filter(|content_type| content_type.starts_with(b"application/grpc")) .is_some() { // grpc service 1 } else { // http service 0 } }, ); axum::serve(listener, Shared::new(service)) .with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle())) .await?; deletion_task.await??; Ok(()) } async fn shutdown_signal(deletion_task_abort_handle: AbortHandle) { let ctrl_c = async { signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => { deletion_task_abort_handle.abort() }, _ = terminate => { deletion_task_abort_handle.abort() }, } }