1use lambda_runtime::{Error, LambdaEvent, run, service_fn};
39use sea_orm::DatabaseConnection;
40use std::{env, sync::LazyLock};
41
42mod db;
43mod handlers;
44mod models;
45
46use db::create_db;
47use handlers::{handle_get_health, handle_get_messages, handle_post_message_new};
48use models::{Request, Response};
49
50static CORS_ORIGIN: LazyLock<String> = LazyLock::new(|| {
51 env::var("CORS_ORIGIN").expect("環境変数 'CORS_ORIGIN' が設定されていません。")
52});
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55enum Route {
56 GetHealth,
57 GetMessages,
58 PostMessageNew,
59 MethodNotAllowed,
60 NotFound,
61}
62
63fn route_request(event: &Request) -> Route {
64 let path = event
65 .raw_path
66 .as_deref()
67 .or(event.request_context.http.path.as_deref())
68 .unwrap_or("");
69
70 let method = event.request_context.http.method.as_str();
71
72 match path {
73 "/health" => {
74 if method == "GET" {
75 Route::GetHealth
76 } else {
77 Route::MethodNotAllowed
78 }
79 }
80 "/messages" => {
81 if method == "GET" {
82 Route::GetMessages
83 } else {
84 Route::MethodNotAllowed
85 }
86 }
87 "/message/new" => {
88 if method == "POST" {
89 Route::PostMessageNew
90 } else {
91 Route::MethodNotAllowed
92 }
93 }
94 _ => Route::NotFound,
95 }
96}
97
98async fn function_handler(db: &DatabaseConnection, event: LambdaEvent<Request>) -> Result<Response, Error> {
139 let (event, _context) = event.into_parts();
140 let route = route_request(&event);
141 let row_log = serde_json::to_string(&event).unwrap_or_else(|_| {
142 tracing::warn!("Failed to serialize API Gateway event for row_log");
143 "{}".to_string()
144 });
145
146 let cors_origin = &*CORS_ORIGIN;
147
148 let result = match route {
149 Route::GetHealth => handle_get_health(db, cors_origin).await,
150 Route::GetMessages | Route::PostMessageNew => {
151 let auth_info = event.request_context.authorizer.as_ref().and_then(|auth| {
152 let email = auth.jwt.claims.email.as_deref()?;
153 if email.is_empty() {
154 return None;
155 }
156 let cognito_sub = auth.jwt.claims.cognito_sub.as_deref()?;
157 if cognito_sub.is_empty() {
158 return None;
159 }
160 match uuid::Uuid::parse_str(cognito_sub) {
161 Ok(cognito_sub) => Some((email, cognito_sub)),
162 Err(err) => {
163 tracing::warn!("Invalid cognito_sub in JWT claims: {}", err);
164 None
165 }
166 }
167 });
168
169 let (email, cognito_sub) = match auth_info {
170 Some(auth_info) => auth_info,
171 None => {
172 return Ok(Response::error(
173 401,
174 "Unauthorized",
175 "Invalid or missing required JWT claims (email and sub)",
176 &cors_origin,
177 ));
178 }
179 };
180
181 match route {
182 Route::GetMessages => {
183 handle_get_messages(db, email, cognito_sub, &cors_origin).await
184 }
185 Route::PostMessageNew => {
186 let body = event.body.as_deref().unwrap_or("");
187 handle_post_message_new(db, cognito_sub, body, &row_log, &cors_origin).await
188 }
189 _ => unreachable!(),
190 }
191 }
192 Route::MethodNotAllowed => Ok(Response::error(
193 405,
194 "Method Not Allowed",
195 "Method not allowed",
196 &cors_origin,
197 )),
198 Route::NotFound => Ok(Response::error(
199 404,
200 "Not Found",
201 "The requested resource was not found",
202 &cors_origin,
203 )),
204 };
205
206 result.or_else(|e| {
207 tracing::error!("Error processing request: {:?}", e);
208 Ok(Response::error(
209 500,
210 "INTERNAL_SERVER_ERROR",
211 "An error occurred while processing your request",
212 &cors_origin,
213 ))
214 })
215}
216
217#[tokio::main]
232async fn main() -> Result<(), Error> {
233 tracing_subscriber::fmt()
234 .with_max_level(tracing::Level::INFO)
235 .with_target(false)
236 .without_time()
237 .init();
238
239 let dsql_endpoint = env::var("DSQL_ENDPOINT").map_err(|_| {
240 tracing::error!("DSQL_ENDPOINT environment variable is not set");
241 anyhow::anyhow!("DSQL_ENDPOINT environment variable is not set")
242 })?;
243
244 let dsql_region = env::var("DSQL_REGION").map_err(|_| {
245 tracing::error!("DSQL_REGION environment variable is not set");
246 anyhow::anyhow!("DSQL_REGION environment variable is not set")
247 })?;
248
249 let db = create_db("lambda", &dsql_endpoint, &dsql_region).await?;
250
251 LazyLock::force(&CORS_ORIGIN);
253
254 run(service_fn(|event| {
255 let db = db.clone();
256 async move { function_handler(&db, event).await }
257 })).await
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::models::{Claims, Http, Jwt, RequestContext};
264
265 #[test]
266 fn test_route_request_health_is_unauthenticated() {
267 let request = Request {
268 request_context: RequestContext {
269 http: Http {
270 method: "GET".to_string(),
271 path: Some("/health".to_string()),
272 },
273 authorizer: None,
274 },
275 raw_path: Some("/health".to_string()),
276 body: None,
277 };
278
279 assert_eq!(route_request(&request), Route::GetHealth);
280 }
281
282 #[test]
283 fn test_route_request_messages_get() {
284 let request = Request {
285 request_context: RequestContext {
286 http: Http {
287 method: "GET".to_string(),
288 path: Some("/messages".to_string()),
289 },
290 authorizer: None,
291 },
292 raw_path: Some("/messages".to_string()),
293 body: None,
294 };
295
296 assert_eq!(route_request(&request), Route::GetMessages);
297 }
298
299 #[test]
300 fn test_route_request_not_found() {
301 let request = Request {
302 request_context: RequestContext {
303 http: Http {
304 method: "GET".to_string(),
305 path: Some("/non-existent".to_string()),
306 },
307 authorizer: None,
308 },
309 raw_path: Some("/non-existent".to_string()),
310 body: None,
311 };
312
313 assert_eq!(route_request(&request), Route::NotFound);
314 }
315
316 #[test]
317 fn test_route_request_method_not_allowed() {
318 let request = Request {
319 request_context: RequestContext {
320 http: Http {
321 method: "POST".to_string(),
322 path: Some("/messages".to_string()),
323 },
324 authorizer: None,
325 },
326 raw_path: Some("/messages".to_string()),
327 body: None,
328 };
329
330 assert_eq!(route_request(&request), Route::MethodNotAllowed);
331 }
332
333 #[test]
334 fn test_route_request_message_new() {
335 let request = Request {
336 request_context: RequestContext {
337 http: Http {
338 method: "POST".to_string(),
339 path: Some("/message/new".to_string()),
340 },
341 authorizer: Some(models::Authorizer {
342 jwt: Jwt {
343 claims: Claims {
344 email: Some("test@example.com".to_string()),
345 cognito_sub: Some(uuid::Uuid::now_v7().to_string()),
346 },
347 },
348 }),
349 },
350 raw_path: Some("/message/new".to_string()),
351 body: Some(r#"{"body":"hello"}"#.to_string()),
352 };
353
354 assert_eq!(route_request(&request), Route::PostMessageNew);
355 }
356}