hermod_api/
startup.rs

1//! Contains code neccessary to bootstrap the application and run the server.
2use actix_cors::Cors;
3
4use crate::clients::twilio::TwilioClient;
5use crate::handlers::{
6    delete_qr_code, edit_qr_code, get_form, get_qr_code_data, health_check, list_qr_codes, login,
7    logout, register, store_form, store_qr_code, who_am_i,
8};
9use crate::services::configuration::DatabaseSettings;
10use crate::services::configuration::Settings;
11use crate::services::jwt::JwtClient;
12use actix_web::dev::Server;
13use actix_web::web::Data;
14use actix_web::{web, App, HttpServer};
15use sqlx::postgres::PgPoolOptions;
16use sqlx::{ConnectOptions, PgPool};
17use std::net::TcpListener;
18use tracing::log::LevelFilter;
19use tracing_actix_web::TracingLogger;
20
21/// Represents the server application.
22pub struct Application {
23    port: u16,
24    server: Server,
25}
26
27impl Application {
28    /// Given a configuration, build application dependencies and return a configured application.
29    pub async fn build(configuration: Settings) -> Result<Self, std::io::Error> {
30        let connection_pool = get_connection_pool(&configuration.database)
31            .await
32            .expect("Failed to connect to Postgres.");
33
34        let jwt_client = JwtClient::new(
35            configuration.application.jwt_signing_key,
36            connection_pool.clone(),
37        );
38
39        let twilio_client = TwilioClient::new(
40            configuration.twilio.base_url,
41            std::time::Duration::from_secs(5),
42            configuration.twilio.account_sid,
43            configuration.twilio.auth_token,
44            configuration.twilio.from,
45        );
46
47        sqlx::migrate!("./migrations")
48            .run(&connection_pool)
49            .await
50            .expect("Failed to migrate the database");
51
52        let address = format!(
53            "{}:{}",
54            configuration.application.host, configuration.application.port
55        );
56        let listener = TcpListener::bind(&address)?;
57        let port = listener.local_addr().unwrap().port();
58
59        let server = run(listener, connection_pool, jwt_client, twilio_client)?;
60
61        Ok(Self { port, server })
62    }
63
64    /// The port that the server is listening on.
65    pub fn port(&self) -> u16 {
66        self.port
67    }
68
69    /// Run the HTTP server until interupted.
70    pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
71        self.server.await
72    }
73}
74
75/// Given a configuration, returns a pool of Postgres database connections.
76pub async fn get_connection_pool(configuration: &DatabaseSettings) -> Result<PgPool, sqlx::Error> {
77    let db_connect_options = configuration
78        .with_db()
79        .log_statements(LevelFilter::Trace)
80        .to_owned();
81
82    PgPoolOptions::new()
83        .connect_timeout(std::time::Duration::from_secs(2))
84        .connect_with(db_connect_options)
85        .await
86}
87
88fn run(
89    listener: TcpListener,
90    db_pool: PgPool,
91    jwt_client: JwtClient,
92    twilio_client: TwilioClient,
93) -> Result<Server, std::io::Error> {
94    let db_pool = Data::new(db_pool);
95    let jwt_client = Data::new(jwt_client);
96    let twilio_client = Data::new(twilio_client);
97
98    let server = HttpServer::new(move || {
99        let cors = Cors::permissive();
100
101        App::new()
102            .wrap(cors)
103            .wrap(TracingLogger::default())
104            .route("/login", web::get().to(login))
105            .route("/logout", web::get().to(logout))
106            .route("/whoami", web::get().to(who_am_i))
107            .route("/register", web::post().to(register))
108            .route("/health_check", web::get().to(health_check))
109            .route("/qr_code", web::get().to(get_qr_code_data))
110            .route("/qr_codes", web::get().to(list_qr_codes))
111            .route("/qr_code/store", web::get().to(store_qr_code))
112            .route("/qr_code/edit", web::get().to(edit_qr_code))
113            .route("/qr_code/delete", web::get().to(delete_qr_code))
114            .route("/form", web::get().to(get_form))
115            .route("/form/store", web::post().to(store_form))
116            .app_data(db_pool.clone())
117            .app_data(jwt_client.clone())
118            .app_data(twilio_client.clone())
119    })
120    .listen(listener)?
121    .run();
122    Ok(server)
123}