diff --git a/services/identity/src/http/handlers.rs b/services/identity/src/http/handlers.rs index 93dcbadaa..f2bd2aa93 100644 --- a/services/identity/src/http/handlers.rs +++ b/services/identity/src/http/handlers.rs @@ -1,50 +1,73 @@ use super::{ - errors::{create_error_response, http400}, - ErrorResponse, HttpRequest, + errors::{create_error_response, http400, http404}, + utils::{RequestExt, ResponseExt}, + ErrorResponse, HttpRequest, HttpResponse, }; use comm_lib::auth::UserIdentity; use hyper::header::AUTHORIZATION; use hyper::StatusCode; use tracing::error; +#[tracing::instrument(skip_all)] +pub async fn inbound_keys_handler( + req: HttpRequest, + db_client: crate::DatabaseClient, +) -> Result { + verify_csat(&req, &db_client).await?; + + // Device ID query string arg is base64-url encoded + let query_args = req.query_string_args(); + let device_id_param = query_args + .get("device_id") + .ok_or_else(|| http400("missing device_id query param"))?; + let device_id = device_id_param.replace('-', "+").replace('_', "/"); + + let device_data = db_client + .find_device_by_id(&device_id) + .await? + .ok_or_else(|| http404("device not found"))?; + + HttpResponse::json(&device_data) +} + #[tracing::instrument(skip_all)] async fn verify_csat( req: &HttpRequest, db_client: &crate::DatabaseClient, ) -> Result<(), ErrorResponse> { let Some(auth_header) = req.headers().get(AUTHORIZATION) else { return Err(create_error_response( StatusCode::UNAUTHORIZED, "missing Authorization header", )); }; let bearer_token = auth_header .to_str() .map_err(|_| http400("malformed Authorization header"))? .strip_prefix("Bearer ") .ok_or_else(|| http400("malformed Authorization header"))?; let UserIdentity { user_id, device_id, access_token, } = bearer_token .parse() .map_err(|_| http400("malformed Authorization header"))?; let result = db_client .verify_access_token(user_id, device_id, access_token) .await; match result { Ok(true) => Ok(()), Ok(false) => Err(create_error_response( StatusCode::FORBIDDEN, "invalid credentials", )), Err(err) => { error!("CSAT verification error: {err:?}"); Err(err.into()) } } } diff --git a/services/identity/src/http/mod.rs b/services/identity/src/http/mod.rs index a85440e0c..9e7254666 100644 --- a/services/identity/src/http/mod.rs +++ b/services/identity/src/http/mod.rs @@ -1,25 +1,35 @@ -use hyper::{Body, Request, Response}; +use hyper::{Body, Method, Request, Response}; mod errors; mod handlers; +mod utils; type HttpRequest = Request; type HttpResponse = Response; type ErrorResponse = Result; /// Main router for HTTP requests #[tracing::instrument(skip_all, name = "http_request", fields(request_id))] pub(super) async fn handle_http_request( req: HttpRequest, - _db_client: crate::DatabaseClient, + db_client: crate::DatabaseClient, ) -> Result { + use utils::IntoServerResponse; + tracing::Span::current() .record("request_id", uuid::Uuid::new_v4().to_string()); let response = match req.uri().path() { "/health" => Response::new(Body::from("OK")), + "/device_inbound_keys" => match req.method() { + &Method::GET => handlers::inbound_keys_handler(req, db_client) + .await + .into_response()?, + _ => errors::http405()?, + }, _ => errors::http404("Not found")?, }; + Ok(response) } diff --git a/services/identity/src/http/utils.rs b/services/identity/src/http/utils.rs new file mode 100644 index 000000000..0dcf4fb75 --- /dev/null +++ b/services/identity/src/http/utils.rs @@ -0,0 +1,64 @@ +use hyper::{header::CONTENT_TYPE, Body, Response}; +use std::collections::HashMap; +use tracing::error; + +use super::{ + errors::{http500, BoxedError}, + ErrorResponse, HttpRequest, HttpResponse, +}; + +pub trait RequestExt { + fn query_string_args(&self) -> HashMap; +} + +pub trait ResponseExt { + fn json( + response: &T, + ) -> Result; +} + +impl RequestExt for HttpRequest { + fn query_string_args(&self) -> HashMap { + let Some(uri_str) = self.uri().query() else { + return HashMap::new(); + }; + let params: HashMap<_, _> = url::form_urlencoded::parse(uri_str.as_bytes()) + .into_owned() + .collect(); + + tracing::trace!("Found query string args: {:?}", params); + params + } +} + +impl ResponseExt for Response { + fn json( + body: &T, + ) -> Result { + let json_string = serde_json::to_string(&body).map_err(|err| { + error!("JSON serialization error: {err:?}"); + http500() + })?; + let response = Response::builder() + .header(CONTENT_TYPE, "application/json") + .body(Body::from(json_string)) + .map_err(|err| ErrorResponse::Err(Box::new(err)))?; + Ok(response) + } +} + +pub trait IntoServerResponse { + /// Convenience helper for converting handler return value + /// into response returned by server future + fn into_response(self) -> Result; +} + +impl IntoServerResponse for Result { + fn into_response(self) -> Result { + let response = match self { + Ok(ok_response) => ok_response, + Err(err_response) => err_response?, + }; + Ok(response) + } +}