Skip to main content

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}