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