1use aws_config::{BehaviorVersion, Region};
17use aws_sdk_dsql::auth_token::{AuthTokenGenerator, Config};
18use lambda_runtime::{run, service_fn, Error, LambdaEvent};
19use sea_orm::{
20 ActiveModelTrait, Database, DatabaseConnection, DbBackend, FromQueryResult, Set, Statement,
21};
22use sea_orm_entities::entity::inquiries;
23use serde::{Deserialize, Serialize};
24use serde_json::{json, Value};
25use std::collections::HashMap;
26use std::env;
27
28#[derive(Debug, Deserialize)]
30struct Request {
31 #[serde(rename = "requestContext")]
33 request_context: RequestContext,
34 body: Option<String>,
36}
37
38#[derive(Debug, Deserialize)]
40struct RequestContext {
41 http: Http,
43 authorizer: Option<Authorizer>,
45}
46
47#[derive(Debug, Deserialize)]
49struct Http {
50 method: String,
52}
53
54#[derive(Debug, Deserialize)]
56struct Authorizer {
57 jwt: Jwt,
59}
60
61#[derive(Debug, Deserialize)]
63struct Jwt {
64 claims: Claims,
66}
67
68#[derive(Debug, Deserialize)]
70struct Claims {
71 email: String,
73}
74
75#[derive(Debug, Serialize)]
77struct Response {
78 #[serde(rename = "statusCode")]
80 status_code: u16,
81 headers: HashMap<String, String>,
83 body: String,
85}
86
87#[derive(Debug, Serialize, FromQueryResult)]
89struct Inquiry {
90 id: uuid::Uuid,
92 email: String,
94 subject: String,
96 body: String,
98 created_at: chrono::DateTime<chrono::FixedOffset>,
100}
101
102#[derive(Debug, Serialize)]
104struct InquiryListResponse {
105 email: String,
107 count: u64,
109 inquiries: Vec<Inquiry>,
111}
112
113#[derive(Debug, Deserialize)]
115struct CreateInquiryRequest {
116 subject: String,
118 body: String,
120}
121
122#[derive(Debug, Serialize)]
124struct CreateInquiryResponse {
125 inquiry: Inquiry,
127}
128
129impl From<inquiries::Model> for Inquiry {
130 fn from(model: inquiries::Model) -> Self {
131 Inquiry {
132 id: model.id,
133 email: model.email,
134 subject: model.subject,
135 body: model.body,
136 created_at: model.created_at,
137 }
138 }
139}
140
141impl Response {
142 fn new(status_code: u16, body: Value, cors_origin: &str) -> Self {
149 let mut headers = HashMap::new();
150 headers.insert("Content-Type".to_string(), "application/json".to_string());
151 headers.insert("Access-Control-Allow-Origin".to_string(), cors_origin.to_string());
152
153 Response {
154 status_code,
155 headers,
156 body: body.to_string(),
157 }
158 }
159
160 fn error(status_code: u16, error: &str, message: &str, cors_origin: &str) -> Self {
168 Self::new(
169 status_code,
170 json!({
171 "error": error,
172 "message": message
173 }),
174 cors_origin,
175 )
176 }
177}
178
179async fn generate_token(endpoint: &str, region: &str) -> Result<String, Error> {
192 let sdk_config = aws_config::load_defaults(BehaviorVersion::latest()).await;
193 let signer = AuthTokenGenerator::new(
194 Config::builder()
195 .hostname(endpoint)
196 .region(Region::new(region.to_owned()))
197 .build()
198 .map_err(|e| anyhow::anyhow!("Failed to build DSQL config: {}", e))?,
199 );
200 let token = signer
201 .db_connect_auth_token(&sdk_config) .await
203 .map_err(|e| anyhow::anyhow!("Failed to generate DSQL token: {}", e))?;
204 Ok(token.to_string())
205}
206
207async fn create_db(endpoint: &str, region: &str) -> Result<DatabaseConnection, Error> {
219 tracing::info!("Generating DSQL authentication token...");
220 let token = generate_token(endpoint, region).await?;
221
222 tracing::info!("Creating database connection...");
223 let encoded_token = urlencoding::encode(&token);
224 let db_url = format!(
225 "postgres://selectview:{}@{}:5432/postgres?sslmode=require",
226 encoded_token, endpoint
227 );
228
229 let db = Database::connect(db_url)
230 .await
231 .map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
232
233 Ok(db)
234}
235
236async fn handle_get_inquiries(
250 db: &DatabaseConnection,
251 email: &str,
252 cors_origin: &str,
253) -> Result<Response, Error> {
254 tracing::info!("Querying inquiries for email: {}", email);
255
256 let inquiries: Vec<Inquiry> = Inquiry::find_by_statement(Statement::from_sql_and_values(
257 DbBackend::Postgres,
258 "SELECT id, email, subject, body, created_at FROM get_inquiries_by_email($1) ORDER BY created_at DESC",
259 [email.to_owned().into()],
260 ))
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 response_body = InquiryListResponse {
269 email: email.to_string(),
270 count: inquiries.len() as u64,
271 inquiries,
272 };
273
274 Ok(Response::new(200, serde_json::to_value(response_body)?, cors_origin))
275}
276
277async fn handle_post_inquiry(
292 db: &DatabaseConnection,
293 email: &str,
294 body: &str,
295 cors_origin: &str,
296) -> Result<Response, Error> {
297 tracing::info!("Creating inquiry for email: {}", email);
298
299 let create_request: CreateInquiryRequest = serde_json::from_str(body)
301 .map_err(|e| {
302 tracing::error!("Failed to parse request body: {}", e);
303 anyhow::anyhow!("Invalid request body: {}", e)
304 })?;
305
306 let id = uuid::Uuid::now_v7();
308 let now = chrono::Utc::now().fixed_offset();
309
310 let new_inquiry = inquiries::ActiveModel {
312 id: Set(id),
313 email: Set(email.to_string()),
314 subject: Set(create_request.subject.clone()),
315 body: Set(create_request.body.clone()),
316 created_at: Set(now),
317 };
318
319 new_inquiry.insert(db).await.map_err(|e| {
320 tracing::error!("Failed to insert inquiry: {}", e);
321 anyhow::anyhow!("Failed to insert inquiry: {}", e)
322 })?;
323
324 let inquiry = Inquiry {
326 id,
327 email: email.to_string(),
328 subject: create_request.subject,
329 body: create_request.body,
330 created_at: now,
331 };
332
333 let response_body = CreateInquiryResponse { inquiry };
334
335 Ok(Response::new(201, serde_json::to_value(response_body)?, cors_origin))
336}
337
338async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
354 let (event, _context) = event.into_parts();
355
356 let cors_origin = env::var("CORS_ORIGIN").unwrap_or_else(|_| "http://localhost:11029".to_string());
358
359 let dsql_endpoint = env::var("DSQL_ENDPOINT").map_err(|_| {
360 tracing::error!("DSQL_ENDPOINT environment variable is not set");
361 anyhow::anyhow!("DSQL_ENDPOINT environment variable is not set")
362 })?;
363
364 let dsql_region = env::var("DSQL_REGION").map_err(|_| {
365 tracing::error!("DSQL_REGION environment variable is not set");
366 anyhow::anyhow!("DSQL_REGION environment variable is not set")
367 })?;
368
369 let email = event
371 .request_context
372 .authorizer
373 .as_ref()
374 .and_then(|auth| {
375 let email = auth.jwt.claims.email.as_str();
376 if email.is_empty() {
377 None
378 } else {
379 Some(email)
380 }
381 });
382
383 let email = match email {
384 Some(email) => email,
385 None => {
386 return Ok(Response::error(
387 401,
388 "Unauthorized",
389 "Invalid or missing JWT token",
390 &cors_origin,
391 ));
392 }
393 };
394
395 let db = create_db(&dsql_endpoint, &dsql_region).await?;
397
398 let result = match event.request_context.http.method.as_str() {
399 "GET" => handle_get_inquiries(&db, email, &cors_origin).await,
400 "POST" => {
401 let body = event.body.as_deref().unwrap_or("");
402 handle_post_inquiry(&db, email, body, &cors_origin).await
403 }
404 _ => Ok(Response::error(
405 405,
406 "Method Not Allowed",
407 "Method not allowed",
408 &cors_origin,
409 )),
410 };
411
412 if let Err(err) = db.close().await {
414 tracing::error!("Failed to close database connection: {:?}", err);
415 }
416
417 result.or_else(|e| {
418 tracing::error!("Error processing request: {:?}", e);
419 Ok(Response::error(
420 500,
421 "INTERNAL_SERVER_ERROR",
422 "An error occurred while processing your request",
423 &cors_origin,
424 ))
425 })
426}
427
428#[tokio::main]
432async fn main() -> Result<(), Error> {
433 tracing_subscriber::fmt()
434 .with_max_level(tracing::Level::INFO)
435 .with_target(false)
436 .without_time()
437 .init();
438
439 run(service_fn(function_handler)).await
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
448 fn test_response_new_status_code() {
449 let resp = Response::new(200, serde_json::json!({"ok": true}), "https://example.com");
450 assert_eq!(resp.status_code, 200);
451 }
452
453 #[test]
455 fn test_response_new_cors_header() {
456 let origin = "https://example.com";
457 let resp = Response::new(200, serde_json::json!({}), origin);
458 assert_eq!(
459 resp.headers.get("Access-Control-Allow-Origin").map(String::as_str),
460 Some(origin)
461 );
462 }
463
464 #[test]
466 fn test_response_new_content_type() {
467 let resp = Response::new(200, serde_json::json!({}), "https://example.com");
468 assert_eq!(
469 resp.headers.get("Content-Type").map(String::as_str),
470 Some("application/json")
471 );
472 }
473
474 #[test]
476 fn test_response_error_status_and_body() {
477 let resp = Response::error(401, "Unauthorized", "Invalid token", "https://example.com");
478 assert_eq!(resp.status_code, 401);
479 let body: serde_json::Value = serde_json::from_str(&resp.body).unwrap();
480 assert_eq!(body["error"], "Unauthorized");
481 assert_eq!(body["message"], "Invalid token");
482 }
483
484 #[test]
486 fn test_inquiry_serialization() {
487 let id = uuid::Uuid::now_v7();
488 let now = chrono::Utc::now().fixed_offset();
489 let inquiry = Inquiry {
490 id,
491 email: "test@example.com".to_string(),
492 subject: "Test subject".to_string(),
493 body: "Test body".to_string(),
494 created_at: now,
495 };
496 let json = serde_json::to_value(&inquiry).unwrap();
497 assert_eq!(json["email"], "test@example.com");
498 assert_eq!(json["subject"], "Test subject");
499 assert_eq!(json["body"], "Test body");
500 }
501
502 #[test]
504 fn test_create_inquiry_request_deserialization() {
505 let json = r#"{"subject": "Hello", "body": "World"}"#;
506 let req: CreateInquiryRequest = serde_json::from_str(json).unwrap();
507 assert_eq!(req.subject, "Hello");
508 assert_eq!(req.body, "World");
509 }
510}