headless_lms_server/
config.rs

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