voice_echo/api/
outbound.rs1use axum::extract::State;
2use axum::http::{HeaderMap, StatusCode};
3use axum::response::IntoResponse;
4use axum::Json;
5use serde::{Deserialize, Serialize};
6
7use crate::{AppState, CallMeta};
8
9#[derive(Debug, Deserialize)]
10pub struct CallRequest {
11 pub to: String,
13 #[allow(dead_code)]
15 pub message: Option<String>,
16 pub context: Option<String>,
19 pub reason: Option<String>,
22}
23
24#[derive(Debug, Serialize)]
25pub struct CallResponse {
26 pub call_sid: String,
27 pub status: String,
28}
29
30#[derive(Debug, Serialize)]
31struct ErrorResponse {
32 error: String,
33}
34
35pub async fn handle_call(
47 State(state): State<AppState>,
48 headers: HeaderMap,
49 Json(req): Json<CallRequest>,
50) -> impl IntoResponse {
51 if let Err(resp) = check_auth(&headers, &state.config.api.token) {
53 return resp;
54 }
55
56 tracing::info!(to = %req.to, "Outbound call requested");
57
58 match state.twilio.call(&req.to).await {
59 Ok(call_sid) => {
60 if req.context.is_some() || req.reason.is_some() {
62 state.call_metas.lock().await.insert(
63 call_sid.clone(),
64 CallMeta {
65 context: req.context,
66 reason: req.reason,
67 },
68 );
69 tracing::info!(call_sid = %call_sid, "Stored call metadata");
70 }
71 (
72 StatusCode::OK,
73 Json(CallResponse {
74 call_sid,
75 status: "initiated".to_string(),
76 }),
77 )
78 .into_response()
79 }
80 Err(e) => {
81 tracing::error!("Failed to initiate call: {e}");
82 (
83 StatusCode::INTERNAL_SERVER_ERROR,
84 Json(ErrorResponse {
85 error: e.to_string(),
86 }),
87 )
88 .into_response()
89 }
90 }
91}
92
93#[allow(clippy::result_large_err)]
94pub fn check_auth(
95 headers: &HeaderMap,
96 expected_token: &str,
97) -> Result<(), axum::response::Response> {
98 if expected_token.is_empty() {
99 tracing::warn!("API token not configured — rejecting request");
100 return Err((
101 StatusCode::SERVICE_UNAVAILABLE,
102 Json(ErrorResponse {
103 error: "API token not configured".to_string(),
104 }),
105 )
106 .into_response());
107 }
108
109 let provided = headers
110 .get("authorization")
111 .and_then(|v| v.to_str().ok())
112 .and_then(|v| v.strip_prefix("Bearer "));
113
114 match provided {
115 Some(token) if token == expected_token => Ok(()),
116 _ => {
117 tracing::warn!("Unauthorized API request");
118 Err((
119 StatusCode::UNAUTHORIZED,
120 Json(ErrorResponse {
121 error: "Invalid or missing bearer token".to_string(),
122 }),
123 )
124 .into_response())
125 }
126 }
127}