headless_lms_server/
config.rs

1//! Functionality for configuring the server
2
3use crate::{
4    OAuthClient,
5    domain::{models_requests::JwtKey, request_span_middleware::RequestSpan},
6};
7use actix_http::{StatusCode, body::MessageBody};
8use actix_web::{
9    HttpResponse,
10    error::InternalError,
11    web::{self, Data, PayloadConfig, ServiceConfig},
12};
13use anyhow::Context;
14use headless_lms_utils::{
15    ApplicationConfiguration, cache::Cache, file_store::FileStore, icu4x::Icu4xBlob,
16    ip_to_country::IpToCountryMapper, tmc::TmcClient,
17};
18use oauth2::{AuthUrl, ClientId, ClientSecret, TokenUrl, basic::BasicClient};
19use sqlx::{PgPool, postgres::PgPoolOptions};
20use std::{env, sync::Arc};
21use url::Url;
22
23pub struct ServerConfigBuilder {
24    pub database_url: String,
25    pub oauth_application_id: String,
26    pub oauth_secret: String,
27    pub auth_url: Url,
28    pub icu4x_postcard_path: String,
29    pub file_store: Arc<dyn FileStore + Send + Sync>,
30    pub app_conf: ApplicationConfiguration,
31    pub redis_url: String,
32    pub jwt_password: String,
33    pub tmc_client: TmcClient,
34}
35
36impl ServerConfigBuilder {
37    pub fn try_from_env() -> anyhow::Result<Self> {
38        Ok(Self {
39            database_url: env::var("DATABASE_URL").context("DATABASE_URL must be defined")?,
40            oauth_application_id: env::var("OAUTH_APPLICATION_ID")
41                .context("OAUTH_APPLICATION_ID must be defined")?,
42            oauth_secret: env::var("OAUTH_SECRET").context("OAUTH_SECRET must be defined")?,
43            auth_url: "https://tmc.mooc.fi/oauth/token"
44                .parse()
45                .context("Failed to parse auth_url")?,
46            icu4x_postcard_path: env::var("ICU4X_POSTCARD_PATH")
47                .context("ICU4X_POSTCARD_PATH must be defined")?,
48            file_store: crate::setup_file_store(),
49            app_conf: ApplicationConfiguration::try_from_env()?,
50            redis_url: env::var("REDIS_URL").context("REDIS_URL must be defined")?,
51            jwt_password: env::var("JWT_PASSWORD").context("JWT_PASSWORD must be defined")?,
52            tmc_client: TmcClient::new_from_env()?,
53        })
54    }
55
56    pub async fn build(self) -> anyhow::Result<ServerConfig> {
57        let json_config = web::JsonConfig::default().limit(2_097_152).error_handler(
58            |err, _req| -> actix_web::Error {
59                info!("Bad request: {}", &err);
60                let body = format!("{{\"title\": \"Bad Request\", \"message\": \"{}\"}}", &err);
61                // create custom error response
62                let response = HttpResponse::with_body(StatusCode::BAD_REQUEST, body.boxed());
63                InternalError::from_response(err, response).into()
64            },
65        );
66        let json_config = Data::new(json_config);
67
68        let payload_config = PayloadConfig::default().limit(2_097_152);
69        let payload_config = Data::new(payload_config);
70
71        let db_pool = PgPoolOptions::new()
72            .max_connections(15)
73            .min_connections(5)
74            .connect(&self.database_url)
75            .await?;
76        let db_pool = Data::new(db_pool);
77
78        let oauth_client: OAuthClient = BasicClient::new(ClientId::new(self.oauth_application_id))
79            .set_client_secret(ClientSecret::new(self.oauth_secret))
80            .set_auth_uri(AuthUrl::from_url(self.auth_url.clone()))
81            .set_token_uri(TokenUrl::from_url(self.auth_url));
82        let oauth_client = Data::new(oauth_client);
83
84        let icu4x_blob = Icu4xBlob::new(&self.icu4x_postcard_path)?;
85        let icu4x_blob = Data::new(icu4x_blob);
86
87        let app_conf = Data::new(self.app_conf);
88
89        let ip_to_country_mapper = IpToCountryMapper::new(&app_conf)?;
90        let ip_to_country_mapper = Data::new(ip_to_country_mapper);
91
92        let cache = Cache::new(&self.redis_url)?;
93        let cache = Data::new(cache);
94
95        let jwt_key = JwtKey::new(&self.jwt_password)?;
96        let jwt_key = Data::new(jwt_key);
97
98        let tmc_client = Data::new(self.tmc_client);
99
100        let config = ServerConfig {
101            json_config,
102            db_pool,
103            oauth_client,
104            icu4x_blob,
105            ip_to_country_mapper,
106            file_store: self.file_store,
107            app_conf,
108            jwt_key,
109            cache,
110            payload_config,
111            tmc_client,
112        };
113        Ok(config)
114    }
115}
116
117#[derive(Clone)]
118pub struct ServerConfig {
119    pub payload_config: Data<PayloadConfig>,
120    pub json_config: Data<web::JsonConfig>,
121    pub db_pool: Data<PgPool>,
122    pub oauth_client: Data<OAuthClient>,
123    pub icu4x_blob: Data<Icu4xBlob>,
124    pub ip_to_country_mapper: Data<IpToCountryMapper>,
125    pub file_store: Arc<dyn FileStore + Send + Sync>,
126    pub app_conf: Data<ApplicationConfiguration>,
127    pub cache: Data<Cache>,
128    pub jwt_key: Data<JwtKey>,
129    pub tmc_client: Data<TmcClient>,
130}
131
132/// Common configuration that is used by both production and testing.
133pub fn configure(config: &mut ServiceConfig, server_config: ServerConfig) {
134    let ServerConfig {
135        json_config,
136        db_pool,
137        oauth_client,
138        icu4x_blob,
139        ip_to_country_mapper,
140        file_store,
141        app_conf,
142        jwt_key,
143        cache,
144        payload_config,
145        tmc_client,
146    } = server_config;
147    // turns file_store from `dyn FileStore + Send + Sync` to `dyn FileStore` to match controllers
148    // Not using Data::new for file_store to avoid double wrapping it in a arc
149    let file_store = Data::from(file_store as Arc<dyn FileStore>);
150    config
151        .app_data(payload_config)
152        .app_data(json_config)
153        .app_data(db_pool)
154        .app_data(oauth_client)
155        .app_data(icu4x_blob)
156        .app_data(ip_to_country_mapper)
157        .app_data(file_store)
158        .app_data(app_conf)
159        .app_data(jwt_key)
160        .app_data(cache)
161        .app_data(tmc_client)
162        .service(
163            web::scope("/api/v0")
164                .wrap(RequestSpan)
165                .configure(crate::controllers::configure_controllers),
166        );
167}