api_handler/main.rs
1//! # バックエンドAPIデータベース Lambda ハンドラー
2//!
3//! このモジュールは、AWS Lambda上で動作するコンタクトフォームAPIのエントリーポイントです。
4//! Amazon API Gateway HTTP API からのリクエストを受け取り、JWTトークンによる認証を行った後、
5//! HTTPメソッドに基づいて適切なハンドラーにルーティングします。
6//!
7//! ## アーキテクチャ概要
8//!
9//! ```text
10//! クライアント
11//! └─▶ Amazon API Gateway HTTP API (JWT Authorizer)
12//! └─▶ AWS Lambda (このモジュール)
13//! └─▶ Amazon Aurora DSQL (SeaORM経由)
14//! ```
15//!
16//! ## 認証フロー
17//!
18//! 1. クライアントは Amazon Cognito からJWT IDトークンを取得する
19//! 2. JWT IDトークンを `Authorization: Bearer <token>` ヘッダーに付与してリクエストを送信する
20//! 3. API Gateway の JWT Authorizer がトークンを検証する
21//! 4. 検証済みのJWTクレーム(`email`、`sub`)が `requestContext.authorizer.jwt.claims` に格納される
22//! 5. このモジュールがクレームを読み取り、ユーザーを識別する
23//!
24//! ## サポートするエンドポイント
25//!
26//! | メソッド | パス | 説明 |
27//! |--------|------|------|
28//! | GET | /inquiries | 認証済みユーザーのお問い合わせ一覧を取得 |
29//! | POST | /inquiries | 新規お問い合わせを作成 |
30//!
31//! ## 環境変数
32//!
33//! | 変数名 | 必須 | 説明 |
34//! |--------|------|------|
35//! | `DSQL_ENDPOINT` | ✓ | Aurora DSQLクラスターのエンドポイント |
36//! | `DSQL_REGION` | ✓ | Aurora DSQLクラスターのAWSリージョン |
37//! | `CORS_ORIGIN` | - | 許可するCORSオリジン(デフォルト: `https://ngicf-testpage.pages.dev`) |
38use lambda_runtime::{run, service_fn, Error, LambdaEvent};
39use std::{env, sync::LazyLock};
40
41mod db;
42mod handlers;
43mod models;
44
45use db::create_db;
46use handlers::{handle_get_inquiries, handle_post_inquiry};
47use models::{Request, Response};
48
49static CORS_ORIGIN: LazyLock<String> = LazyLock::new(|| {
50 env::var("CORS_ORIGIN").unwrap_or_else(|_| "https://ngicf-testpage.pages.dev".to_string())
51});
52
53/// メインのLambda関数ハンドラー
54///
55/// API Gatewayからの受信HTTPリクエストを処理し、JWTで認証してから
56/// HTTPメソッドに基づいて適切なハンドラーにルーティングします。
57///
58/// # 処理フロー
59///
60/// 1. リクエストコンテキストからJWTクレームを抽出する
61/// 2. `email` クレームと `sub` クレームの存在・妥当性を検証する
62/// 3. `DSQL_ENDPOINT` / `DSQL_REGION` 環境変数からデータベース接続を確立する
63/// 4. HTTPメソッドに応じて [`handle_get_inquiries`] または [`handle_post_inquiry`] にディスパッチする
64/// 5. 処理完了後、データベース接続を閉じる
65/// 6. ハンドラーでエラーが発生した場合は HTTP 500 を返す
66///
67/// # Arguments
68///
69/// * `event` - API Gatewayリクエストを含むLambdaイベント。
70/// [`Request`] 構造体にデシリアライズされ、リクエストコンテキスト(JWTクレームを含む)と
71/// オプションのリクエストボディを持ちます。
72///
73/// # Returns
74///
75/// * `Ok(Response)` - API Gatewayに返すHTTPレスポンス。
76/// 正常時はハンドラーが返すレスポンスをそのまま返します。
77/// 内部エラー時は HTTP 500 レスポンスを返します。
78/// * `Err(Error)` - Lambda ランタイムレベルの致命的なエラー(通常は発生しない)
79///
80/// # エラーレスポンス
81///
82/// | ステータスコード | エラー | 発生条件 |
83/// |--------------|--------|---------|
84/// | 401 | Unauthorized | JWTクレームに `email` または `sub` が存在しない、もしくは `sub` がUUID形式でない |
85/// | 405 | Method Not Allowed | GET/POST以外のHTTPメソッドが使用された |
86/// | 500 | INTERNAL_SERVER_ERROR | データベース接続エラー、クエリエラーなどの内部エラー |
87///
88/// # Authentication
89///
90/// すべてのリクエストには、Amazon CognitoからのJWT IDトークンが必要です。
91/// トークンにはユーザーを識別するために使用される`email`クレームと`sub`クレームが含まれている必要があります。
92/// `sub` クレームはCognito ユーザーの一意識別子(UUID v4形式)です。
93async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
94 let (event, _context) = event.into_parts();
95
96 let cors_origin = &*CORS_ORIGIN;
97
98 let dsql_endpoint = env::var("DSQL_ENDPOINT").map_err(|_| {
99 tracing::error!("DSQL_ENDPOINT environment variable is not set");
100 anyhow::anyhow!("DSQL_ENDPOINT environment variable is not set")
101 })?;
102
103 let dsql_region = env::var("DSQL_REGION").map_err(|_| {
104 tracing::error!("DSQL_REGION environment variable is not set");
105 anyhow::anyhow!("DSQL_REGION environment variable is not set")
106 })?;
107
108 // JWTクレームからメールアドレスを抽出する
109 let auth_info = event.request_context.authorizer.as_ref().and_then(|auth| {
110 let email = auth.jwt.claims.email.as_deref()?;
111 if email.is_empty() {
112 return None;
113 }
114 let cognito_sub = auth.jwt.claims.cognito_sub.as_deref()?;
115 if cognito_sub.is_empty() {
116 return None;
117 }
118 match uuid::Uuid::parse_str(cognito_sub) {
119 Ok(cognito_sub) => Some((email, cognito_sub)),
120 Err(err) => {
121 tracing::warn!("Invalid cognito_sub in JWT claims: {}", err);
122 None
123 }
124 }
125 });
126
127 let (email, cognito_sub) = match auth_info {
128 Some(auth_info) => auth_info,
129 None => {
130 return Ok(Response::error(
131 401,
132 "Unauthorized",
133 "Invalid or missing required JWT claims (email and sub)",
134 &cors_origin,
135 ));
136 }
137 };
138
139 // SeaORMデータベース接続を作成する
140 let db = create_db("crudrole", &dsql_endpoint, &dsql_region).await?;
141
142 let result = match event.request_context.http.method.as_str() {
143 "GET" => handle_get_inquiries(&db, email, cognito_sub, &cors_origin).await,
144 "POST" => {
145 let body = event.body.as_deref().unwrap_or("");
146 handle_post_inquiry(&db, email, cognito_sub, body, &cors_origin).await
147 }
148 _ => Ok(Response::error(
149 405,
150 "Method Not Allowed",
151 "Method not allowed",
152 &cors_origin,
153 )),
154 };
155
156 // データベース接続を閉じる
157 if let Err(err) = db.close().await {
158 tracing::error!("Failed to close database connection: {:?}", err);
159 }
160
161 result.or_else(|e| {
162 tracing::error!("Error processing request: {:?}", e);
163 Ok(Response::error(
164 500,
165 "INTERNAL_SERVER_ERROR",
166 "An error occurred while processing your request",
167 &cors_origin,
168 ))
169 })
170}
171
172/// Lambda関数のエントリーポイント
173///
174/// ロギングを初期化してLambdaランタイムを起動します。
175///
176/// ## 初期化処理
177///
178/// 1. `tracing_subscriber` を INFO レベルで初期化します。ログにはターゲット名と時刻は含めません。
179/// 2. Lambda ランタイムを起動し、[`function_handler`] をサービス関数として登録します。
180/// 3. ランタイムはAWS Lambda環境からイベントを受け取り、[`function_handler`] を呼び出します。
181///
182/// # Returns
183///
184/// * `Ok(())` - ランタイムが正常に終了した場合(通常は発生しない)
185/// * `Err(Error)` - ランタイムの初期化または実行中に致命的なエラーが発生した場合
186#[tokio::main]
187async fn main() -> Result<(), Error> {
188 tracing_subscriber::fmt()
189 .with_max_level(tracing::Level::INFO)
190 .with_target(false)
191 .without_time()
192 .init();
193
194 run(service_fn(function_handler)).await
195}