1use aws_config::{BehaviorVersion, Region};
18use aws_sdk_dsql::auth_token::{AuthTokenGenerator, Config};
19use lambda_runtime::{run, service_fn, Error, LambdaEvent};
20use sea_orm::{
21 ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, EntityTrait, QueryFilter,
22 QueryOrder, Set,
23};
24use sea_orm_entities::entity::inquiries::{self, Column, Entity as Inquiries};
25use serde::{Deserialize, Serialize};
26use serde_json::{json, Value};
27use std::collections::HashMap;
28use std::env;
29
30#[derive(Debug, Deserialize)]
32struct Request {
33 #[serde(rename = "requestContext")]
35 request_context: RequestContext,
36 body: Option<String>,
38}
39
40#[derive(Debug, Deserialize)]
42struct RequestContext {
43 http: Http,
45 authorizer: Option<Authorizer>,
47}
48
49#[derive(Debug, Deserialize)]
51struct Http {
52 method: String,
54}
55
56#[derive(Debug, Deserialize)]
58struct Authorizer {
59 jwt: Jwt,
61}
62
63#[derive(Debug, Deserialize)]
65struct Jwt {
66 claims: Claims,
68}
69
70#[derive(Debug, Deserialize)]
72struct Claims {
73 email: String,
75}
76
77#[derive(Debug, Serialize)]
79struct Response {
80 #[serde(rename = "statusCode")]
82 status_code: u16,
83 headers: HashMap<String, String>,
85 body: String,
87}
88
89#[derive(Debug, Serialize)]
91struct Inquiry {
92 id: uuid::Uuid,
94 email: String,
96 subject: String,
98 body: String,
100 created_at: chrono::DateTime<chrono::FixedOffset>,
102}
103
104#[derive(Debug, Serialize)]
106struct InquiryListResponse {
107 email: String,
109 count: u64,
111 inquiries: Vec<Inquiry>,
113}
114
115#[derive(Debug, Deserialize)]
117struct CreateInquiryRequest {
118 subject: String,
120 body: String,
122}
123
124#[derive(Debug, Serialize)]
126struct CreateInquiryResponse {
127 inquiry: Inquiry,
129}
130
131impl From<inquiries::Model> for Inquiry {
132 fn from(model: inquiries::Model) -> Self {
133 Inquiry {
134 id: model.id,
135 email: model.email,
136 subject: model.subject,
137 body: model.body,
138 created_at: model.created_at,
139 }
140 }
141}
142
143impl Response {
144 fn new(status_code: u16, body: Value, cors_origin: &str) -> Self {
151 let mut headers = HashMap::new();
152 headers.insert("Content-Type".to_string(), "application/json".to_string());
153 headers.insert("Access-Control-Allow-Origin".to_string(), cors_origin.to_string());
154
155 Response {
156 status_code,
157 headers,
158 body: body.to_string(),
159 }
160 }
161
162 fn error(status_code: u16, error: &str, message: &str, cors_origin: &str) -> Self {
170 Self::new(
171 status_code,
172 json!({
173 "error": error,
174 "message": message
175 }),
176 cors_origin,
177 )
178 }
179}
180
181async fn generate_token(endpoint: &str, region: &str) -> Result<String, Error> {
194 let sdk_config = aws_config::load_defaults(BehaviorVersion::latest()).await;
195 let signer = AuthTokenGenerator::new(
196 Config::builder()
197 .hostname(endpoint)
198 .region(Region::new(region.to_owned()))
199 .build()
200 .map_err(|e| anyhow::anyhow!("Failed to build DSQL config: {}", e))?,
201 );
202 let token = signer
203 .db_connect_admin_auth_token(&sdk_config)
204 .await
205 .map_err(|e| anyhow::anyhow!("Failed to generate DSQL token: {}", e))?;
206 Ok(token.to_string())
207}
208
209async fn create_db(endpoint: &str, region: &str) -> Result<DatabaseConnection, Error> {
221 tracing::info!("Generating DSQL authentication token...");
222 let token = generate_token(endpoint, region).await?;
223
224 tracing::info!("Creating database connection...");
225 let encoded_token = urlencoding::encode(&token);
226 let db_url = format!(
227 "postgres://admin:{}@{}:5432/postgres?sslmode=require",
228 encoded_token, endpoint
229 );
230
231 let db = Database::connect(db_url)
232 .await
233 .map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
234
235 Ok(db)
236}
237
238async fn handle_get_inquiries(
252 db: &DatabaseConnection,
253 email: &str,
254 cors_origin: &str,
255) -> Result<Response, Error> {
256 tracing::info!("Querying inquiries for email: {}", email);
257
258 let models = Inquiries::find()
259 .filter(Column::Email.eq(email))
260 .order_by_desc(Column::CreatedAt)
261 .all(db)
262 .await
263 .map_err(|e| {
264 tracing::error!("Database query failed: {}", e);
265 anyhow::anyhow!("Database query failed: {}", e)
266 })?;
267
268 let inquiries: Vec<Inquiry> = models.into_iter().map(Inquiry::from).collect();
269
270 let response_body = InquiryListResponse {
271 email: email.to_string(),
272 count: inquiries.len() as u64,
273 inquiries,
274 };
275
276 Ok(Response::new(200, serde_json::to_value(response_body)?, cors_origin))
277}
278
279async fn handle_post_inquiry(
294 db: &DatabaseConnection,
295 email: &str,
296 body: &str,
297 cors_origin: &str,
298) -> Result<Response, Error> {
299 tracing::info!("Creating inquiry for email: {}", email);
300
301 let create_request: CreateInquiryRequest = serde_json::from_str(body)
303 .map_err(|e| {
304 tracing::error!("Failed to parse request body: {}", e);
305 anyhow::anyhow!("Invalid request body: {}", e)
306 })?;
307
308 let id = uuid::Uuid::now_v7();
310 let now = chrono::Utc::now().fixed_offset();
311
312 let new_inquiry = inquiries::ActiveModel {
314 id: Set(id),
315 email: Set(email.to_string()),
316 subject: Set(create_request.subject.clone()),
317 body: Set(create_request.body.clone()),
318 created_at: Set(now),
319 };
320
321 new_inquiry.insert(db).await.map_err(|e| {
322 tracing::error!("Failed to insert inquiry: {}", e);
323 anyhow::anyhow!("Failed to insert inquiry: {}", e)
324 })?;
325
326 let inquiry = Inquiry {
328 id,
329 email: email.to_string(),
330 subject: create_request.subject,
331 body: create_request.body,
332 created_at: now,
333 };
334
335 let response_body = CreateInquiryResponse { inquiry };
336
337 Ok(Response::new(201, serde_json::to_value(response_body)?, cors_origin))
338}
339
340fn handle_options(cors_origin: &str) -> Response {
350 tracing::info!("handle_options");
351 let mut headers = HashMap::new();
352 headers.insert("Content-Type".to_string(), "application/json".to_string());
353 headers.insert("Access-Control-Allow-Origin".to_string(), cors_origin.to_string());
354 headers.insert("Access-Control-Allow-Methods".to_string(), "GET,POST,OPTIONS".to_string());
355 headers.insert("Access-Control-Allow-Headers".to_string(), "Content-Type,Authorization".to_string());
356 headers.insert("Access-Control-Max-Age".to_string(), "3600".to_string());
357
358 Response {
359 status_code: 200,
360 headers,
361 body: "".to_string(),
362 }
363}
364
365async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
381 let (event, _context) = event.into_parts();
382
383 let cors_origin = env::var("CORS_ORIGIN").unwrap_or_else(|_| "http://localhost:11029".to_string());
385
386 if event.request_context.http.method.as_str() == "OPTIONS" {
388 return Ok(handle_options(&cors_origin));
389 }
390
391 let dsql_endpoint = env::var("DSQL_ENDPOINT").map_err(|_| {
392 tracing::error!("DSQL_ENDPOINT environment variable is not set");
393 anyhow::anyhow!("DSQL_ENDPOINT environment variable is not set")
394 })?;
395
396 let dsql_region = env::var("DSQL_REGION").map_err(|_| {
397 tracing::error!("DSQL_REGION environment variable is not set");
398 anyhow::anyhow!("DSQL_REGION environment variable is not set")
399 })?;
400
401 let email = event
403 .request_context
404 .authorizer
405 .as_ref()
406 .and_then(|auth| {
407 let email = auth.jwt.claims.email.as_str();
408 if email.is_empty() {
409 None
410 } else {
411 Some(email)
412 }
413 });
414
415 let email = match email {
416 Some(email) => email,
417 None => {
418 return Ok(Response::error(
419 401,
420 "Unauthorized",
421 "Invalid or missing JWT token",
422 &cors_origin,
423 ));
424 }
425 };
426
427 let db = create_db(&dsql_endpoint, &dsql_region).await?;
429
430 let result = match event.request_context.http.method.as_str() {
431 "GET" => handle_get_inquiries(&db, email, &cors_origin).await,
432 "POST" => {
433 let body = event.body.as_deref().unwrap_or("");
434 handle_post_inquiry(&db, email, body, &cors_origin).await
435 }
436 _ => Ok(Response::error(
437 405,
438 "Method Not Allowed",
439 "Method not allowed",
440 &cors_origin,
441 )),
442 };
443
444 if let Err(err) = db.close().await {
446 tracing::error!("Failed to close database connection: {:?}", err);
447 }
448
449 result.or_else(|e| {
450 tracing::error!("Error processing request: {:?}", e);
451 Ok(Response::error(
452 500,
453 "INTERNAL_SERVER_ERROR",
454 "An error occurred while processing your request",
455 &cors_origin,
456 ))
457 })
458}
459
460#[tokio::main]
464async fn main() -> Result<(), Error> {
465 tracing_subscriber::fmt()
466 .with_max_level(tracing::Level::INFO)
467 .with_target(false)
468 .without_time()
469 .init();
470
471 run(service_fn(function_handler)).await
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
480 fn test_response_new_status_code() {
481 let resp = Response::new(200, serde_json::json!({"ok": true}), "https://example.com");
482 assert_eq!(resp.status_code, 200);
483 }
484
485 #[test]
487 fn test_response_new_cors_header() {
488 let origin = "https://example.com";
489 let resp = Response::new(200, serde_json::json!({}), origin);
490 assert_eq!(
491 resp.headers.get("Access-Control-Allow-Origin").map(String::as_str),
492 Some(origin)
493 );
494 }
495
496 #[test]
498 fn test_response_new_content_type() {
499 let resp = Response::new(200, serde_json::json!({}), "https://example.com");
500 assert_eq!(
501 resp.headers.get("Content-Type").map(String::as_str),
502 Some("application/json")
503 );
504 }
505
506 #[test]
508 fn test_response_error_status_and_body() {
509 let resp = Response::error(401, "Unauthorized", "Invalid token", "https://example.com");
510 assert_eq!(resp.status_code, 401);
511 let body: serde_json::Value = serde_json::from_str(&resp.body).unwrap();
512 assert_eq!(body["error"], "Unauthorized");
513 assert_eq!(body["message"], "Invalid token");
514 }
515
516 #[test]
518 fn test_handle_options_status_and_headers() {
519 let origin = "https://example.com";
520 let resp = handle_options(origin);
521 assert_eq!(resp.status_code, 200);
522 assert_eq!(
523 resp.headers.get("Access-Control-Allow-Origin").map(String::as_str),
524 Some(origin)
525 );
526 assert!(resp.headers.contains_key("Access-Control-Allow-Methods"));
527 assert!(resp.headers.contains_key("Access-Control-Allow-Headers"));
528 }
529
530 #[test]
532 fn test_inquiry_serialization() {
533 let id = uuid::Uuid::now_v7();
534 let now = chrono::Utc::now().fixed_offset();
535 let inquiry = Inquiry {
536 id,
537 email: "test@example.com".to_string(),
538 subject: "Test subject".to_string(),
539 body: "Test body".to_string(),
540 created_at: now,
541 };
542 let json = serde_json::to_value(&inquiry).unwrap();
543 assert_eq!(json["email"], "test@example.com");
544 assert_eq!(json["subject"], "Test subject");
545 assert_eq!(json["body"], "Test body");
546 }
547
548 #[test]
550 fn test_create_inquiry_request_deserialization() {
551 let json = r#"{"subject": "Hello", "body": "World"}"#;
552 let req: CreateInquiryRequest = serde_json::from_str(json).unwrap();
553 assert_eq!(req.subject, "Hello");
554 assert_eq!(req.body, "World");
555 }
556}