use crate::nip98_auth; use crate::notification_manager::UserNotificationSettings; use crate::relay_connection::RelayConnection; use http_body_util::Full; use hyper::body::Buf; use hyper::body::Bytes; use hyper::body::Incoming; use hyper::{Request, Response, StatusCode}; use http_body_util::BodyExt; use serde_json::from_value; use crate::notification_manager::NotificationManager; use hyper::Method; use serde_json::{json, Value}; use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; pub struct APIHandler { notification_manager: Arc, base_url: String, } impl APIHandler { pub fn new(notification_manager: Arc, base_url: String) -> Self { APIHandler { notification_manager, base_url, } } // MARK: - HTTP handling pub async fn handle_http_request( &self, req: Request, ) -> Result>, hyper::http::Error> { // Check if the request is a websocket upgrade request. if hyper_tungstenite::is_upgrade_request(&req) { return match self.handle_websocket_upgrade(req).await { Ok(response) => Ok(response), Err(err) => { log::error!("Error handling websocket upgrade request: {}", err); Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(http_body_util::Full::new(Bytes::from( "Internal server error", )))?) } }; } // If not, handle the request as a normal API request. let final_api_response: APIResponse = match self.try_to_handle_http_request(req).await { Ok(api_response) => APIResponse { status: api_response.status, body: api_response.body, }, Err(err) => { // Detect if error is a APIError::AuthenticationError and return a 401 status code if let Some(api_error) = err.downcast_ref::() { match api_error { APIError::AuthenticationError(message) => APIResponse { status: StatusCode::UNAUTHORIZED, body: json!({ "error": "Unauthorized", "message": message }), }, } } else { // Otherwise, return a 500 status code let random_case_uuid = uuid::Uuid::new_v4(); log::error!( "Error handling request: {} (Case ID: {})", err, random_case_uuid ); APIResponse { status: StatusCode::INTERNAL_SERVER_ERROR, body: json!({ "error": "Internal server error", "message": format!("Case ID: {}", random_case_uuid) }), } } } }; Response::builder() .header("Content-Type", "application/json") .header("Access-Control-Allow-Origin", "*") .status(final_api_response.status) .body(http_body_util::Full::new(Bytes::from( final_api_response.body.to_string(), ))) } async fn handle_websocket_upgrade( &self, mut req: Request, ) -> Result>, Box> { let (response, websocket) = hyper_tungstenite::upgrade(&mut req, None)?; log::info!("New websocket connection."); let new_notification_manager = self.notification_manager.clone(); tokio::spawn(async move { match RelayConnection::run(websocket, new_notification_manager).await { Ok(_) => {} Err(e) => { log::error!("Error with websocket connection: {:?}", e); } } }); Ok(response) } async fn try_to_handle_http_request( &self, mut req: Request, ) -> Result> { let parsed_request = self.parse_http_request(&mut req).await?; let api_response: APIResponse = self.handle_parsed_http_request(&parsed_request).await?; log::info!( "[{}] {} (Authorized pubkey: {}): {}", req.method(), req.uri(), parsed_request.authorized_pubkey, api_response.status ); Ok(api_response) } async fn parse_http_request( &self, req: &mut Request, ) -> Result> { // 1. Read the request body let body_buffer = req.body_mut().collect().await?.aggregate(); let body_bytes = body_buffer.chunk(); let body_bytes = if body_bytes.is_empty() { None } else { Some(body_bytes) }; // 2. NIP-98 authentication let authorized_pubkey = match self.authenticate(req, body_bytes).await? { Ok(pubkey) => pubkey, Err(auth_error) => { return Err(Box::new(APIError::AuthenticationError(auth_error))); } }; // 3. Parse the request Ok(ParsedRequest { uri: req.uri().path().to_string(), method: req.method().clone(), body_bytes: body_bytes.map(|b| b.to_vec()), authorized_pubkey, }) } // MARK: - Router async fn handle_parsed_http_request( &self, parsed_request: &ParsedRequest, ) -> Result> { if let Some(url_params) = route_match( &Method::PUT, "/user-info/:pubkey/:deviceToken", parsed_request, ) { return self.handle_user_info(parsed_request, &url_params).await; } if let Some(url_params) = route_match( &Method::DELETE, "/user-info/:pubkey/:deviceToken", parsed_request, ) { return self .handle_user_info_remove(parsed_request, &url_params) .await; } if let Some(url_params) = route_match( &Method::GET, "/user-info/:pubkey/:deviceToken/preferences", parsed_request, ) { return self.get_user_settings(parsed_request, &url_params).await; } if let Some(url_params) = route_match( &Method::PUT, "/user-info/:pubkey/:deviceToken/preferences", parsed_request, ) { return self.set_user_settings(parsed_request, &url_params).await; } Ok(APIResponse { status: StatusCode::NOT_FOUND, body: json!({ "error": "Not found" }), }) } // MARK: - Authentication async fn authenticate( &self, req: &Request, body_bytes: Option<&[u8]>, ) -> Result, Box> { let auth_header = match req.headers().get("Authorization") { Some(header) => header, None => return Ok(Err("Authorization header not found".to_string())), }; Ok(nip98_auth::nip98_verify_auth_header( auth_header.to_str()?.to_string(), &format!("{}{}", self.base_url, req.uri().path()), req.method().as_str(), body_bytes, ) .await) } // MARK: - Endpoint handlers async fn handle_user_info( &self, req: &ParsedRequest, url_params: &HashMap<&str, String>, ) -> Result> { // Early return if `deviceToken` is missing let device_token = match url_params.get("deviceToken") { Some(token) => token, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "deviceToken is required on the URL" }), }) } }; // Early return if `pubkey` is missing let pubkey = match url_params.get("pubkey") { Some(key) => key, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "pubkey is required on the URL" }), }) } }; // Validate the `pubkey` and prepare it for use let pubkey = match nostr::PublicKey::from_hex(pubkey) { Ok(key) => key, Err(_) => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "Invalid pubkey" }), }) } }; // Early return if `pubkey` does not match `req.authorized_pubkey` if pubkey != req.authorized_pubkey { return Ok(APIResponse { status: StatusCode::FORBIDDEN, body: json!({ "error": "Forbidden" }), }); } // Proceed with the main logic after passing all checks self.notification_manager .save_user_device_info_if_not_present(pubkey, device_token) .await?; Ok(APIResponse { status: StatusCode::OK, body: json!({ "message": "User info saved successfully" }), }) } async fn handle_user_info_remove( &self, req: &ParsedRequest, url_params: &HashMap<&str, String>, ) -> Result> { // Early return if `deviceToken` is missing let device_token = match url_params.get("deviceToken") { Some(token) => token, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "deviceToken is required on the URL" }), }) } }; // Early return if `pubkey` is missing let pubkey = match url_params.get("pubkey") { Some(key) => key, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "pubkey is required on the URL" }), }) } }; // Validate the `pubkey` and prepare it for use let pubkey = match nostr::PublicKey::from_hex(pubkey) { Ok(key) => key, Err(_) => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "Invalid pubkey" }), }) } }; // Early return if `pubkey` does not match `req.authorized_pubkey` if pubkey != req.authorized_pubkey { return Ok(APIResponse { status: StatusCode::FORBIDDEN, body: json!({ "error": "Forbidden" }), }); } // Proceed with the main logic after passing all checks self.notification_manager .remove_user_device_info(pubkey, device_token) .await?; Ok(APIResponse { status: StatusCode::OK, body: json!({ "message": "User info removed successfully" }), }) } async fn set_user_settings( &self, req: &ParsedRequest, url_params: &HashMap<&str, String>, ) -> Result> { // Early return if `deviceToken` is missing let device_token = match url_params.get("deviceToken") { Some(token) => token, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "deviceToken is required on the URL" }), }) } }; // Early return if `pubkey` is missing let pubkey = match url_params.get("pubkey") { Some(key) => key, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "pubkey is required on the URL" }), }) } }; // Validate the `pubkey` and prepare it for use let pubkey = match nostr::PublicKey::from_hex(pubkey) { Ok(key) => key, Err(_) => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "Invalid pubkey" }), }) } }; // Early return if `pubkey` does not match `req.authorized_pubkey` if pubkey != req.authorized_pubkey { return Ok(APIResponse { status: StatusCode::FORBIDDEN, body: json!({ "error": "Forbidden" }), }); } // Proceed with the main logic after passing all checks let body = req.body_json()?; let settings: UserNotificationSettings = match from_value(body.clone()) { Ok(settings) => settings, Err(_) => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "Invalid settings" }), }); } }; self.notification_manager .save_user_notification_settings( &req.authorized_pubkey, device_token.to_string(), settings, ) .await?; Ok(APIResponse { status: StatusCode::OK, body: json!({ "message": "User settings saved successfully" }), }) } async fn get_user_settings( &self, req: &ParsedRequest, url_params: &HashMap<&str, String>, ) -> Result> { // Early return if `deviceToken` is missing let device_token = match url_params.get("deviceToken") { Some(token) => token, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "deviceToken is required on the URL" }), }) } }; // Early return if `pubkey` is missing let pubkey = match url_params.get("pubkey") { Some(key) => key, None => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "pubkey is required on the URL" }), }) } }; // Validate the `pubkey` and prepare it for use let pubkey = match nostr::PublicKey::from_hex(pubkey) { Ok(key) => key, Err(_) => { return Ok(APIResponse { status: StatusCode::BAD_REQUEST, body: json!({ "error": "Invalid pubkey" }), }) } }; // Early return if `pubkey` does not match `req.authorized_pubkey` if pubkey != req.authorized_pubkey { return Ok(APIResponse { status: StatusCode::FORBIDDEN, body: json!({ "error": "Forbidden" }), }); } // Proceed with the main logic after passing all checks let settings = self .notification_manager .get_user_notification_settings(&req.authorized_pubkey, device_token.to_string()) .await?; Ok(APIResponse { status: StatusCode::OK, body: json!(settings), }) } } // MARK: - Extensions impl Clone for APIHandler { fn clone(&self) -> Self { APIHandler { notification_manager: self.notification_manager.clone(), base_url: self.base_url.clone(), } } } // MARK: - Helper types // Define enum error types including authentication error #[derive(Debug, Error)] enum APIError { #[error("Authentication error: {0}")] AuthenticationError(String), } struct ParsedRequest { uri: String, method: Method, body_bytes: Option>, authorized_pubkey: nostr::PublicKey, } impl ParsedRequest { fn body_json(&self) -> Result> { if let Some(body_bytes) = &self.body_bytes { Ok(serde_json::from_slice(body_bytes)?) } else { Ok(json!({})) } } } struct APIResponse { status: StatusCode, body: Value, } // MARK: - Helper functions /// Matches the request to a specified route, returning a hashmap of the route parameters /// e.g. GET /user/:id/info route against request GET /user/123/info matches to { "id": "123" } fn route_match<'a>( method: &Method, path: &'a str, req: &ParsedRequest, ) -> Option> { if method != req.method { return None; } let mut params = HashMap::new(); let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); let req_segments: Vec<&str> = req.uri.split('/').filter(|s| !s.is_empty()).collect(); if path_segments.len() != req_segments.len() { return None; } for (i, segment) in path_segments.iter().enumerate() { if let Some(key) = segment.strip_prefix(':') { let value = req_segments[i].to_string(); params.insert(key, value); } else if segment != &req_segments[i] { return None; } } Some(params) }