1use 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
21pub struct Application {
23 port: u16,
24 server: Server,
25}
26
27impl Application {
28 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 pub fn port(&self) -> u16 {
66 self.port
67 }
68
69 pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
71 self.server.await
72 }
73}
74
75pub 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}