1use crate::{
2 auth_n::{self, certificates::get_certification_provider, middleware::jwt::User},
3 fhir_client::ServerCTX,
4 fhir_http::{HTTPBody, HTTPRequest, http_request_to_fhir_request},
5 mcp,
6 middleware::{
7 errors::{log_operationoutcome_errors, operation_outcome_error_handle},
8 security_headers::SecurityHeaderLayer,
9 },
10 openapi,
11 services::{AppState, ConfigError, create_services, get_pool},
12 static_assets::{create_static_server, root_asset_route},
13};
14use axum::{
15 Extension, Router, ServiceExt,
16 body::Body,
17 extract::{DefaultBodyLimit, OriginalUri, Path, State},
18 http::Request,
19 http::{HeaderName, HeaderValue, Method, Uri},
20 middleware::from_fn,
21 response::{IntoResponse, Response},
22 routing::{any, get, post},
23};
24use axum_client_ip::ClientIpSource;
25use haste_config::get_config;
26use haste_fhir_client::FHIRClient;
27use haste_fhir_model::r4::generated::terminology::IssueType;
28use haste_fhir_operation_error::OperationOutcomeError;
29use haste_fhir_search::SearchEngine;
30use haste_fhir_terminology::FHIRTerminology;
31use haste_jwt::{ProjectId, TenantId};
32use haste_repository::{Repository, types::SupportedFHIRVersions};
33use sentry::integrations::tower::NewSentryLayer;
34use serde::Deserialize;
35use std::{collections::HashMap, sync::Arc};
36use std::{net::SocketAddr, str::FromStr};
37use tower::{Layer, ServiceBuilder};
38use tower_http::normalize_path::NormalizePath;
39use tower_http::{
40 compression::CompressionLayer,
41 cors::{Any, CorsLayer},
42 normalize_path::NormalizePathLayer,
43 set_header::SetResponseHeaderLayer,
44 trace::TraceLayer,
45};
46use tower_sessions::{
47 Expiry, SessionManagerLayer,
48 cookie::{SameSite, time::Duration},
49};
50use tower_sessions_sqlx_store::PostgresStore;
51
52const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
53
54#[derive(Deserialize)]
55struct FHIRHandlerPath {
56 tenant: TenantId,
57 project: ProjectId,
58 fhir_version: SupportedFHIRVersions,
59 fhir_location: Option<String>,
60}
61
62#[derive(Deserialize)]
63struct FHIRRootHandlerPath {
64 tenant: TenantId,
65 project: ProjectId,
66 fhir_version: SupportedFHIRVersions,
67}
68
69async fn fhir_handler<
70 Repo: Repository + Send + Sync + 'static,
71 Search: SearchEngine + Send + Sync + 'static,
72 Terminology: FHIRTerminology + Send + Sync + 'static,
73>(
74 user: Arc<User>,
75 method: Method,
76 uri: Uri,
77 path: FHIRHandlerPath,
78 state: Arc<AppState<Repo, Search, Terminology>>,
79 body: String,
80) -> Result<Response, OperationOutcomeError> {
81 let fhir_location = path.fhir_location.unwrap_or_default();
82
83 async {
84 let http_req = HTTPRequest::new(
85 method,
86 fhir_location,
87 HTTPBody::String(body),
88 uri.query()
89 .map(|q| {
90 url::form_urlencoded::parse(q.as_bytes())
91 .into_owned()
92 .collect()
93 })
94 .unwrap_or_else(HashMap::new),
95 );
96
97 let fhir_request = http_request_to_fhir_request(SupportedFHIRVersions::R4, http_req)?;
98
99 let ctx = Arc::new(ServerCTX::new(
100 path.tenant,
101 path.project,
102 path.fhir_version,
103 user.clone(),
104 state.fhir_client.clone(),
105 state.rate_limit.clone(),
106 ));
107
108 let response = state.fhir_client.request(ctx, fhir_request).await?;
109
110 let http_response = response.into_response();
111 Ok(http_response)
112 }
113 .await
114}
115
116async fn fhir_root_handler<
117 Repo: Repository + Send + Sync + 'static,
118 Search: SearchEngine + Send + Sync + 'static,
119 Terminology: FHIRTerminology + Send + Sync + 'static,
120>(
121 method: Method,
122 Extension(user): Extension<Arc<User>>,
123 OriginalUri(uri): OriginalUri,
124 Path(path): Path<FHIRRootHandlerPath>,
125 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
126 body: String,
127) -> Result<Response, OperationOutcomeError> {
128 fhir_handler(
129 user,
130 method,
131 uri,
132 FHIRHandlerPath {
133 tenant: path.tenant,
134 project: path.project,
135 fhir_version: path.fhir_version,
136 fhir_location: None,
137 },
138 state,
139 body,
140 )
141 .await
142}
143
144async fn fhir_type_handler<
145 Repo: Repository + Send + Sync + 'static,
146 Search: SearchEngine + Send + Sync + 'static,
147 Terminology: FHIRTerminology + Send + Sync + 'static,
148>(
149 method: Method,
150 Extension(user): Extension<Arc<User>>,
151 OriginalUri(uri): OriginalUri,
152 Path(path): Path<FHIRHandlerPath>,
153 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
154 body: String,
155) -> Result<Response, OperationOutcomeError> {
156 fhir_handler(user, method, uri, path, state, body).await
157}
158
159pub async fn server() -> Result<NormalizePath<Router>, OperationOutcomeError> {
160 let config = get_config("environment".into());
161
162 let ip_source = config
164 .get(crate::ServerEnvironmentVariables::IpSource)
165 .and_then(|ip_source| {
166 ClientIpSource::from_str(&ip_source).map_err(|_e| {
167 OperationOutcomeError::fatal(
168 IssueType::Exception(None),
169 format!("Invalid IP_SOURCE value: {}", ip_source),
170 )
171 })
172 })
173 .unwrap_or_else(|_e| ClientIpSource::ConnectInfo);
174
175 get_certification_provider();
177
178 let pool = get_pool(config.as_ref()).await;
179 let session_store = PostgresStore::new(pool.clone());
180 session_store.migrate().await.map_err(ConfigError::from)?;
181
182 let max_body_size = config
183 .get(crate::ServerEnvironmentVariables::MaxRequestBodySize)
184 .ok()
185 .and_then(|s| s.parse::<usize>().ok())
186 .unwrap_or(4 * 1024 * 1024);
187 let shared_state = create_services(config).await?;
188
189 let fhir_router = Router::new()
190 .route("/{fhir_version}", any(fhir_root_handler))
191 .route("/{fhir_version}/{*fhir_location}", any(fhir_type_handler));
192
193 let protected_resources_router = Router::new()
194 .nest("/fhir", fhir_router)
195 .route("/mcp", post(mcp::route::mcp_handler))
196 .layer(
197 ServiceBuilder::new()
198 .layer(axum::middleware::from_fn_with_state(
199 shared_state.clone(),
200 auth_n::middleware::basic_auth::basic_auth_middleware,
201 ))
202 .layer(axum::middleware::from_fn_with_state(
203 shared_state.clone(),
204 auth_n::middleware::jwt::token_verifcation,
205 ))
206 .layer(axum::middleware::from_fn(
207 auth_n::middleware::project_access::project_access,
208 )),
209 );
210
211 let project_router = Router::new().merge(protected_resources_router).nest(
212 "/oidc",
213 auth_n::oidc::routes::create_router(shared_state.clone()),
214 );
215
216 let tenant_router = Router::new()
217 .nest("/auth", auth_n::tenant::routes::create_router())
218 .nest("/{project}/api/v1", project_router)
219 .layer(
220 ServiceBuilder::new()
222 .layer(from_fn(operation_outcome_error_handle))
223 .layer(from_fn(log_operationoutcome_errors)),
224 );
225
226 let discovery_2_0_document_router = Router::new()
227 .route(
228 "/openid-configuration/w/{tenant}/{project}/{*resource}",
229 get(auth_n::oidc::routes::discovery::openid_configuration),
230 )
231 .route(
232 "/openid-configuration/w/{tenant}/{project}",
233 get(auth_n::oidc::routes::discovery::openid_configuration),
234 )
235 .route(
236 "/oauth-protected-resource/w/{tenant}/{project}/{*resource}",
237 get(auth_n::oidc::routes::discovery::oauth_protected_resource),
238 );
239
240 let app = Router::new()
241 .nest("/.well-known", discovery_2_0_document_router)
242 .nest(
243 "/auth",
244 auth_n::global::routes::create_router(shared_state.clone()),
245 )
246 .route("/openapi.json", get(openapi::openapi_document_handler))
247 .nest("/w/{tenant}", tenant_router)
248 .layer(
249 ServiceBuilder::new()
250 .layer(ip_source.into_extension())
251 .layer(NewSentryLayer::<Request<Body>>::new_from_top())
252 .layer(TraceLayer::new_for_http())
253 .layer(DefaultBodyLimit::max(max_body_size))
255 .layer(CompressionLayer::new())
256 .layer(SecurityHeaderLayer::new())
257 .layer(SetResponseHeaderLayer::overriding(
258 HeaderName::from_static("x-api-version"),
259 HeaderValue::from_static(SERVER_VERSION),
260 ))
261 .layer(
262 SessionManagerLayer::new(session_store)
263 .with_secure(true)
264 .with_same_site(SameSite::None)
265 .with_expiry(Expiry::OnInactivity(Duration::days(3))),
266 )
267 .layer(
268 CorsLayer::new()
269 .allow_methods(Any)
271 .allow_origin(Any)
273 .allow_headers(Any),
274 ),
275 )
276 .with_state(shared_state)
277 .nest(root_asset_route().to_str().unwrap(), create_static_server());
278
279 Ok(NormalizePathLayer::trim_trailing_slash().layer(app))
280}
281
282pub async fn serve(port: u16) -> Result<(), OperationOutcomeError> {
283 let server = server().await?;
284
285 let addr = format!("0.0.0.0:{}", port);
286 let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
287
288 tracing::info!("Server started");
289 axum::serve(
290 listener,
291 <tower_http::normalize_path::NormalizePath<Router> as ServiceExt<
292 axum::http::Request<Body>,
293 >>::into_make_service_with_connect_info::<SocketAddr>(server),
294 )
295 .await
296 .unwrap();
297
298 Ok(())
299}