use actix_web::{web, Responder, HttpResponse, Result as ActixResult};
use serde::{Deserialize, Serialize};
use jsonwebtoken::{encode, Header, EncodingKey, Algorithm};
use chrono::{Utc, Duration};
use std::env;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::utils::ensure_dotenv_loaded;
#[derive(Clone)]
pub struct TwilioConfig {
pub account_sid: String,
pub api_key_sid: String,
pub api_key_secret: String,
}
impl TwilioConfig {
pub fn load() -> Result<Self, String> {
ensure_dotenv_loaded();
let account_sid = env::var("TWILIO_ACCOUNT_SID")
.map_err(|_| "Missing TWILIO_ACCOUNT_SID env var".to_string())?;
let api_key_sid = env::var("TWILIO_API_KEY_SID")
.map_err(|_| "Missing TWILIO_API_KEY_SID env var".to_string())?;
let api_key_secret = env::var("TWILIO_API_KEY_SECRET")
.map_err(|_| "Missing TWILIO_API_KEY_SECRET env var".to_string())?;
Ok(Self {
account_sid,
api_key_sid,
api_key_secret,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
struct VideoGrant {
room: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Grants {
identity: String,
video: VideoGrant,
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
iss: String,
exp: usize,
jti: String,
aud: String,
grants: Grants,
}
#[derive(Deserialize)]
pub struct TokenRequestQuery {
pub identity: String,
#[serde(rename = "roomName")]
pub room_name: String,
}
#[derive(Serialize)]
pub struct TokenResponse {
pub token: String,
}
pub async fn generate_token(
query: web::Query<TokenRequestQuery>,
config: web::Data<TwilioConfig>,
) -> ActixResult<impl Responder> {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let jti = format!("{}-{}", config.api_key_sid, now_secs);
let expiry_seconds: i64 = env::var("TOKEN_EXPIRY")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(3600);
let expiration = (Utc::now() + Duration::seconds(expiry_seconds)).timestamp() as usize;
let claims = Claims {
sub: config.account_sid.clone(),
iss: config.api_key_sid.clone(),
aud: "https://video.twilio.com".to_string(),
exp: expiration,
jti,
grants: Grants {
identity: query.identity.clone(),
video: VideoGrant {
room: Some(query.room_name.clone()),
},
},
};
println!("DEBUG: Generating token with iss(SK): {}, sub(AC): {}", claims.iss, claims.sub);
let mut header = Header::new(Algorithm::HS256);
header.cty = Some("twilio-fpa;v=1".to_string());
header.typ = Some("JWT".to_string());
match encode(&header, &claims, &EncodingKey::from_secret(config.api_key_secret.as_ref())) {
Ok(token) => Ok(HttpResponse::Ok().json(TokenResponse { token })),
Err(e) => {
eprintln!("Error generating token: {}", e);
Ok(HttpResponse::InternalServerError().body("Failed to generate token"))
}
}
}