From c7139472e2bfa0e177ed8e2bc6a55d0abce0f107 Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 8 Dec 2024 01:33:44 +0000 Subject: [PATCH 01/44] =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=82=92=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E3=81=99=E3=82=8B?= =?UTF-8?q?Lambda=E9=96=A2=E6=95=B0=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/backend/lambda/upload_profile_image/upload_profile_image.py diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py new file mode 100644 index 00000000..4efe69fd --- /dev/null +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -0,0 +1,53 @@ +import base64 +import logging +import os + +import boto3 + +# S3 バケット名 +S3_BUCKET = os.environ["USER_SETTINGS_BUCKET"] + +# S3 クライアントの初期化 +s3_client = boto3.client("s3") + +# ログ設定 +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +def lambda_handler(event, context): + try: + # Cognito Authorizer から user_id を取得 + user_id = event["requestContext"]["authorizer"]["claims"]["sub"] + + # リクエストの body から画像データを取得 + body = event["body"] + is_base64_encoded = event.get("isBase64Encoded", False) + + if is_base64_encoded: + image_data = base64.b64decode(body) + else: + image_data = body.encode("utf-8") + + # S3 パスを設定 + s3_key = f"profile/image/{user_id}.png" + + # S3 にアップロード + s3_client.put_object( + Bucket=S3_BUCKET, + Key=s3_key, + Body=image_data, + ContentType="image/png", + ) + + logger.info(f"Image uploaded successfully to {S3_BUCKET}/{s3_key}") + + # レスポンスを返す + return { + "statusCode": 200, + "body": f"Profile image uploaded successfully for user_id: {user_id}", + } + + except Exception as e: + logger.error(f"Error uploading image: {str(e)}") + return {"statusCode": 500, "body": f"Failed to upload image: {str(e)}"} From 5bb4182158ae183d257700864bc98d119f4a7067 Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 8 Dec 2024 01:43:52 +0000 Subject: [PATCH 02/44] =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E7=94=BB=E5=83=8F=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E7=94=A8CDK=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/constructs/settings.ts | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/backend/lib/constructs/settings.ts diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts new file mode 100644 index 00000000..6a036f74 --- /dev/null +++ b/src/backend/lib/constructs/settings.ts @@ -0,0 +1,53 @@ +import * as cdk from 'aws-cdk-lib' +import * as apigateway from 'aws-cdk-lib/aws-apigateway' +import type * as cognito from 'aws-cdk-lib/aws-cognito' +import * as lambda from 'aws-cdk-lib/aws-lambda' +import * as s3 from 'aws-cdk-lib/aws-s3' +import { Construct } from 'constructs' + +export interface BouquetProps { + userPool: cognito.UserPool + api: apigateway.RestApi + cognitoAuthorizer: apigateway.CognitoUserPoolsAuthorizer +} + +export class Bouquet extends Construct { + constructor(scope: Construct, id: string, props: BouquetProps) { + super(scope, id) + + // 花束画像保存用のS3バケットの作成 + const userSettingsBucket = new s3.Bucket(this, 'userSettingsBucket', { + enforceSSL: true, + serverAccessLogsPrefix: 'log/', + }) + + //プロフィール画像アップロード用Lambda関数の定義 + const uploadProfileImageFunction = new lambda.Function(this, 'uploadProfileImageFunction', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'upload_profile_image_function.lambda_handler', + timeout: cdk.Duration.seconds(15), + code: lambda.Code.fromAsset('lambda/upload_profile_image_function', { + bundling: { + image: lambda.Runtime.PYTHON_3_11.bundlingImage, + }, + }), + environment: { + USER_SETTINGS_BUCKET: userSettingsBucket.bucketName, + }, + }) + + // /APIの設定 + const settingsApi = props.api.root.addResource('settings') + + settingsApi.addMethod('POST', new apigateway.LambdaIntegration(uploadProfileImageFunction), { + requestParameters: { + 'method.request.querystring.date': true, + }, + requestValidatorOptions: { + requestValidatorName: 'ValidateQueryString', + validateRequestParameters: true, + }, + authorizer: props.cognitoAuthorizer, + }) + } +} From b8ff5b3a5b58dfbd9683edb11a1fd1f460b3bf9b Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 8 Dec 2024 10:52:28 +0900 Subject: [PATCH 03/44] =?UTF-8?q?stack=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=ABsettings=E3=81=AE=E3=82=B3=E3=83=B3=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=83=A9=E3=82=AF=E3=83=88=E3=82=92=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/backend-stack.ts | 8 +++++++- src/backend/lib/constructs/index.ts | 1 + src/backend/lib/constructs/settings.ts | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/backend/lib/backend-stack.ts b/src/backend/lib/backend-stack.ts index 9866f123..88f8e161 100644 --- a/src/backend/lib/backend-stack.ts +++ b/src/backend/lib/backend-stack.ts @@ -2,7 +2,7 @@ import * as cdk from 'aws-cdk-lib' import * as acm from 'aws-cdk-lib/aws-certificatemanager' import * as s3 from 'aws-cdk-lib/aws-s3' import type { Construct } from 'constructs' -import { Api, Auth, Bouquet, Flower, Identity, Web } from './constructs' +import { Api, Auth, Bouquet, Flower, Identity, Settings, Web } from './constructs' import { Diary } from './constructs/diary' interface BackendStackProps extends cdk.StackProps {} @@ -71,6 +71,12 @@ export class BackendStack extends cdk.Stack { flowerImageBucket: flower.flowerImageBucket, }) + const settings = new Settings(this, 'Settings', { + userPool: auth.userPool, + api: api.api, + cognitoAuthorizer: api.cognitoAuthorizer, + }) + const web = new Web(this, 'Web', { userPool: auth.userPool, userPoolClient: auth.userPoolClient, diff --git a/src/backend/lib/constructs/index.ts b/src/backend/lib/constructs/index.ts index 77af4513..319d3995 100644 --- a/src/backend/lib/constructs/index.ts +++ b/src/backend/lib/constructs/index.ts @@ -3,4 +3,5 @@ export * from './auth' export * from './bouquet' export * from './flower' export * from './identity' +export * from './settings' export * from './web' diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index 6a036f74..1be21cb8 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -5,14 +5,14 @@ import * as lambda from 'aws-cdk-lib/aws-lambda' import * as s3 from 'aws-cdk-lib/aws-s3' import { Construct } from 'constructs' -export interface BouquetProps { +export interface SettingsProps { userPool: cognito.UserPool api: apigateway.RestApi cognitoAuthorizer: apigateway.CognitoUserPoolsAuthorizer } -export class Bouquet extends Construct { - constructor(scope: Construct, id: string, props: BouquetProps) { +export class Settings extends Construct { + constructor(scope: Construct, id: string, props: SettingsProps) { super(scope, id) // 花束画像保存用のS3バケットの作成 From 9f2b2961bfa4dceb9b229bff835f4c018322bde3 Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 8 Dec 2024 10:54:46 +0900 Subject: [PATCH 04/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/constructs/settings.ts | 11 +- .../test/__snapshots__/snapshot.test.ts.snap | 268 +++++++++++++++++- 2 files changed, 268 insertions(+), 11 deletions(-) diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index 1be21cb8..7382bcf2 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -24,9 +24,9 @@ export class Settings extends Construct { //プロフィール画像アップロード用Lambda関数の定義 const uploadProfileImageFunction = new lambda.Function(this, 'uploadProfileImageFunction', { runtime: lambda.Runtime.PYTHON_3_11, - handler: 'upload_profile_image_function.lambda_handler', + handler: 'upload_profile_image.lambda_handler', timeout: cdk.Duration.seconds(15), - code: lambda.Code.fromAsset('lambda/upload_profile_image_function', { + code: lambda.Code.fromAsset('lambda/upload_profile_image', { bundling: { image: lambda.Runtime.PYTHON_3_11.bundlingImage, }, @@ -40,13 +40,6 @@ export class Settings extends Construct { const settingsApi = props.api.root.addResource('settings') settingsApi.addMethod('POST', new apigateway.LambdaIntegration(uploadProfileImageFunction), { - requestParameters: { - 'method.request.querystring.date': true, - }, - requestValidatorOptions: { - requestValidatorName: 'ValidateQueryString', - validateRequestParameters: true, - }, authorizer: props.cognitoAuthorizer, }) } diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index 8e82b551..f6faf262 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -336,7 +336,7 @@ exports[`Snapshot test 1`] = ` "Type": "AWS::IAM::Role", "UpdateReplacePolicy": "Delete", }, - "ApiDiaryApiDeploymentCA0DCBF532677587787cfd0f4a098a1bce3727a4": { + "ApiDiaryApiDeploymentCA0DCBF5b04204490493f7bae0d0fbb51bda4809": { "DependsOn": [ "ApiDiaryApibouquetGET7AC93032", "ApiDiaryApibouquetPOST2DEB7999", @@ -350,6 +350,8 @@ exports[`Snapshot test 1`] = ` "ApiDiaryApiflowerGETEAC01B09", "ApiDiaryApiflowerOPTIONS2A81112B", "ApiDiaryApiflower7C61878D", + "ApiDiaryApisettingsPOST5E845A8B", + "ApiDiaryApisettings70D21EB5", "ApiDiaryApititleGET40FCE039", "ApiDiaryApititle5FEB75D0", "ApiDiaryApiRequestValidator4DFB42AA", @@ -378,7 +380,7 @@ exports[`Snapshot test 1`] = ` "Format": "{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","user":"$context.identity.user","caller":"$context.identity.caller","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}", }, "DeploymentId": { - "Ref": "ApiDiaryApiDeploymentCA0DCBF532677587787cfd0f4a098a1bce3727a4", + "Ref": "ApiDiaryApiDeploymentCA0DCBF5b04204490493f7bae0d0fbb51bda4809", }, "MethodSettings": [ { @@ -1383,6 +1385,142 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::ApiGateway::Method", }, + "ApiDiaryApisettings70D21EB5": { + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "ApiDiaryApi1E03348A", + "RootResourceId", + ], + }, + "PathPart": "settings", + "RestApiId": { + "Ref": "ApiDiaryApi1E03348A", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "ApiDiaryApisettingsPOST5E845A8B": { + "Properties": { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "ApiCognitoAuthorizer23B91BA2", + }, + "HttpMethod": "POST", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":apigateway:", + { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SettingsuploadProfileImageFunction5C7EC09C", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": { + "Ref": "ApiDiaryApisettings70D21EB5", + }, + "RestApiId": { + "Ref": "ApiDiaryApi1E03348A", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "ApiDiaryApisettingsPOSTApiPermissionCdkSampleStackApiDiaryApi4AB3EF3APOSTsettings2B68743D": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SettingsuploadProfileImageFunction5C7EC09C", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiDiaryApi1E03348A", + }, + "/", + { + "Ref": "ApiDiaryApiDeploymentStageprodD9912276", + }, + "/POST/settings", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ApiDiaryApisettingsPOSTApiPermissionTestCdkSampleStackApiDiaryApi4AB3EF3APOSTsettingsAFB703A3": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SettingsuploadProfileImageFunction5C7EC09C", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiDiaryApi1E03348A", + }, + "/test-invoke-stage/POST/settings", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, "ApiDiaryApititle5FEB75D0": { "Properties": { "ParentId": { @@ -3912,6 +4050,132 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Policy", }, + "SettingsuploadProfileImageFunction5C7EC09C": { + "DependsOn": [ + "SettingsuploadProfileImageFunctionServiceRoleC037996C", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "HASH_REPLACED.zip", + }, + "Environment": { + "Variables": { + "USER_SETTINGS_BUCKET": { + "Ref": "SettingsuserSettingsBucket1D2B648F", + }, + }, + }, + "Handler": "upload_profile_image.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "SettingsuploadProfileImageFunctionServiceRoleC037996C", + "Arn", + ], + }, + "Runtime": "python3.11", + "Timeout": 15, + }, + "Type": "AWS::Lambda::Function", + }, + "SettingsuploadProfileImageFunctionServiceRoleC037996C": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "SettingsuserSettingsBucket1D2B648F": { + "DeletionPolicy": "Retain", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "LoggingConfiguration": { + "LogFilePrefix": "log/", + }, + "OwnershipControls": { + "Rules": [ + { + "ObjectOwnership": "ObjectWriter", + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "SettingsuserSettingsBucketPolicyACEFAD72": { + "Properties": { + "Bucket": { + "Ref": "SettingsuserSettingsBucket1D2B648F", + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": { + "AWS": "*", + }, + "Resource": [ + { + "Fn::GetAtt": [ + "SettingsuserSettingsBucket1D2B648F", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SettingsuserSettingsBucket1D2B648F", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "WebCloudFrontDistributionAD0EA8F1": { "Metadata": { "cfn_nag": { From deaf5c7d78298322927840dbb9b7e2e8a83dc976 Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 8 Dec 2024 11:03:37 +0900 Subject: [PATCH 05/44] =?UTF-8?q?Lambda=E4=BD=9C=E6=88=90=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=AE=E4=B8=8D=E8=A6=81=E3=81=AA=E7=AE=87?= =?UTF-8?q?=E6=89=80=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/constructs/settings.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index 7382bcf2..d937ea17 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -26,11 +26,7 @@ export class Settings extends Construct { runtime: lambda.Runtime.PYTHON_3_11, handler: 'upload_profile_image.lambda_handler', timeout: cdk.Duration.seconds(15), - code: lambda.Code.fromAsset('lambda/upload_profile_image', { - bundling: { - image: lambda.Runtime.PYTHON_3_11.bundlingImage, - }, - }), + code: lambda.Code.fromAsset('lambda/upload_profile_image'), environment: { USER_SETTINGS_BUCKET: userSettingsBucket.bucketName, }, From 0ead980474ecbbc75649b7012cfc8f179c83b85a Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 21:04:44 +0900 Subject: [PATCH 06/44] =?UTF-8?q?=E3=83=87=E3=83=90=E3=83=83=E3=82=B0?= =?UTF-8?q?=E7=94=A8=20info=20=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/upload_profile_image/upload_profile_image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 4efe69fd..ac519b20 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -16,19 +16,22 @@ def lambda_handler(event, context): + logger.info("upload profile image function") try: # Cognito Authorizer から user_id を取得 user_id = event["requestContext"]["authorizer"]["claims"]["sub"] # リクエストの body から画像データを取得 body = event["body"] + logger.info(f"body: {body}") is_base64_encoded = event.get("isBase64Encoded", False) - + logger.info(f"is base64: {is_base64_encoded}") if is_base64_encoded: image_data = base64.b64decode(body) else: image_data = body.encode("utf-8") + logger.info(f"image data: {image_data}") # S3 パスを設定 s3_key = f"profile/image/{user_id}.png" From 35df919597b87b2eaa89ea35098fd82db26ac16a Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 21:14:05 +0900 Subject: [PATCH 07/44] =?UTF-8?q?S3=E3=81=B8=E3=81=AEPut=E6=A8=A9=E9=99=90?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image.py | 4 +- src/backend/lib/constructs/settings.ts | 3 +- .../test/__snapshots__/snapshot.test.ts.snap | 42 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index ac519b20..56862384 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -23,15 +23,13 @@ def lambda_handler(event, context): # リクエストの body から画像データを取得 body = event["body"] - logger.info(f"body: {body}") is_base64_encoded = event.get("isBase64Encoded", False) - logger.info(f"is base64: {is_base64_encoded}") + if is_base64_encoded: image_data = base64.b64decode(body) else: image_data = body.encode("utf-8") - logger.info(f"image data: {image_data}") # S3 パスを設定 s3_key = f"profile/image/{user_id}.png" diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index d937ea17..663462e6 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -15,7 +15,7 @@ export class Settings extends Construct { constructor(scope: Construct, id: string, props: SettingsProps) { super(scope, id) - // 花束画像保存用のS3バケットの作成 + // プロフィール画像保存用のS3バケットの作成 const userSettingsBucket = new s3.Bucket(this, 'userSettingsBucket', { enforceSSL: true, serverAccessLogsPrefix: 'log/', @@ -31,6 +31,7 @@ export class Settings extends Construct { USER_SETTINGS_BUCKET: userSettingsBucket.bucketName, }, }) + userSettingsBucket.grantPut(uploadProfileImageFunction) // /APIの設定 const settingsApi = props.api.root.addResource('settings') diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index f6faf262..115e2446 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -4052,6 +4052,7 @@ exports[`Snapshot test 1`] = ` }, "SettingsuploadProfileImageFunction5C7EC09C": { "DependsOn": [ + "SettingsuploadProfileImageFunctionServiceRoleDefaultPolicy8B4E4832", "SettingsuploadProfileImageFunctionServiceRoleC037996C", ], "Properties": { @@ -4111,6 +4112,47 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Role", }, + "SettingsuploadProfileImageFunctionServiceRoleDefaultPolicy8B4E4832": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SettingsuserSettingsBucket1D2B648F", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "SettingsuploadProfileImageFunctionServiceRoleDefaultPolicy8B4E4832", + "Roles": [ + { + "Ref": "SettingsuploadProfileImageFunctionServiceRoleC037996C", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "SettingsuserSettingsBucket1D2B648F": { "DeletionPolicy": "Retain", "Properties": { From e2c8158711c5c80269c586001b68a947d9ac22c7 Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 12:21:49 +0000 Subject: [PATCH 08/44] =?UTF-8?q?white=20space=20=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/upload_profile_image/upload_profile_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 56862384..7773a435 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -24,7 +24,7 @@ def lambda_handler(event, context): # リクエストの body から画像データを取得 body = event["body"] is_base64_encoded = event.get("isBase64Encoded", False) - + if is_base64_encoded: image_data = base64.b64decode(body) else: From 45f8cc022f98d618911dc28a3cb16c88fd1767fd Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 12:40:51 +0000 Subject: [PATCH 09/44] =?UTF-8?q?docstring=20=E3=82=92=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image.py | 102 ++++++++++++++---- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 7773a435..9d83c7e4 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -15,40 +15,104 @@ logger.setLevel(logging.INFO) +def get_user_id(event): + """ + Cognito Authorizer から user_id を取得 + + Args: + event (dict): API Gateway からのリクエストイベント。Cognito 認証情報を含む。 + + Returns: + string: ユーザーの user_id。 + + Raises: + ValueError: `event` に `user_id` を取得できない場合。 + """ + try: + return event["requestContext"]["authorizer"]["claims"]["sub"] + except KeyError as e: + raise ValueError("Failed to retrieve user_id from request context") from e + + +def decode_image_data(encoded_body): + """ + Base64 エンコードされた画像データをデコード + + Args: + encoded_body (string): Base64 エンコードされた画像データ。 + + Returns: + bytes: デコードされた画像データ。 + + Raises: + ValueError: `encoded_body` が無効な Base64 データの場合。 + """ + try: + return base64.b64decode(encoded_body) + except Exception as e: + raise ValueError("Invalid base64-encoded image data") from e + + +def upload_to_s3(bucket, key, data, content_type="image/png"): + """ + S3 に画像データをアップロード + + Args: + bucket (string): アップロード先の S3 バケット名。 + key (string): S3 バケット内のファイルのキー(パス)。 + data (bytes): アップロードする画像データ。 + content_type (string): 画像のコンテンツタイプ(デフォルトは "image/png")。 + + Raises: + RuntimeError: S3 アップロード中にエラーが発生した場合。 + """ + try: + s3_client.put_object( + Bucket=bucket, Key=key, Body=data, ContentType=content_type + ) + logger.info(f"Image uploaded successfully to {bucket}/{key}") + except Exception as e: + raise RuntimeError(f"Failed to upload image to S3: {str(e)}") from e + + def lambda_handler(event, context): + """ + Lambda 関数のエントリーポイント。API Gateway からのリクエストを処理し、ユーザーのプロフィール画像を S3 にアップロード。 + + Args: + event (dict): API Gateway からのリクエストイベント。 + context (object): Lambda 実行環境の情報。 + + Returns: + dict: レスポンスコードとメッセージを含む辞書。 + """ logger.info("upload profile image function") try: - # Cognito Authorizer から user_id を取得 - user_id = event["requestContext"]["authorizer"]["claims"]["sub"] + # ユーザーIDを取得 + user_id = get_user_id(event) # リクエストの body から画像データを取得 - body = event["body"] - is_base64_encoded = event.get("isBase64Encoded", False) - - if is_base64_encoded: - image_data = base64.b64decode(body) - else: - image_data = body.encode("utf-8") + body = event.get("body", "") + image_data = decode_image_data(body) # S3 パスを設定 s3_key = f"profile/image/{user_id}.png" # S3 にアップロード - s3_client.put_object( - Bucket=S3_BUCKET, - Key=s3_key, - Body=image_data, - ContentType="image/png", - ) - - logger.info(f"Image uploaded successfully to {S3_BUCKET}/{s3_key}") + upload_to_s3(S3_BUCKET, s3_key, image_data) - # レスポンスを返す + # 成功レスポンスを返す return { "statusCode": 200, "body": f"Profile image uploaded successfully for user_id: {user_id}", } + except ValueError as ve: + logger.error(f"Validation error: {str(ve)}") + return {"statusCode": 400, "body": f"Invalid input: {str(ve)}"} + except RuntimeError as re: + logger.error(f"Runtime error: {str(re)}") + return {"statusCode": 500, "body": f"Server error: {str(re)}"} except Exception as e: - logger.error(f"Error uploading image: {str(e)}") + logger.error(f"Unexpected error: {str(e)}") return {"statusCode": 500, "body": f"Failed to upload image: {str(e)}"} From a6ec41b786fbc3f68d38b1a84c7aad3c3633b499 Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 13:25:14 +0000 Subject: [PATCH 10/44] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image.py | 38 +++---------- .../test/pytest/test_upload_profile_image.py | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 src/backend/test/pytest/test_upload_profile_image.py diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 9d83c7e4..485ef7cb 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -4,36 +4,11 @@ import boto3 -# S3 バケット名 -S3_BUCKET = os.environ["USER_SETTINGS_BUCKET"] - -# S3 クライアントの初期化 -s3_client = boto3.client("s3") - # ログ設定 logger = logging.getLogger() logger.setLevel(logging.INFO) -def get_user_id(event): - """ - Cognito Authorizer から user_id を取得 - - Args: - event (dict): API Gateway からのリクエストイベント。Cognito 認証情報を含む。 - - Returns: - string: ユーザーの user_id。 - - Raises: - ValueError: `event` に `user_id` を取得できない場合。 - """ - try: - return event["requestContext"]["authorizer"]["claims"]["sub"] - except KeyError as e: - raise ValueError("Failed to retrieve user_id from request context") from e - - def decode_image_data(encoded_body): """ Base64 エンコードされた画像データをデコード @@ -53,12 +28,11 @@ def decode_image_data(encoded_body): raise ValueError("Invalid base64-encoded image data") from e -def upload_to_s3(bucket, key, data, content_type="image/png"): +def upload_to_s3(key, data, content_type="image/png"): """ S3 に画像データをアップロード Args: - bucket (string): アップロード先の S3 バケット名。 key (string): S3 バケット内のファイルのキー(パス)。 data (bytes): アップロードする画像データ。 content_type (string): 画像のコンテンツタイプ(デフォルトは "image/png")。 @@ -66,6 +40,10 @@ def upload_to_s3(bucket, key, data, content_type="image/png"): Raises: RuntimeError: S3 アップロード中にエラーが発生した場合。 """ + # S3 クライアントの初期化 + s3_client = boto3.client("s3") + # S3 バケット名 + bucket = os.environ["USER_SETTINGS_BUCKET"] try: s3_client.put_object( Bucket=bucket, Key=key, Body=data, ContentType=content_type @@ -88,8 +66,8 @@ def lambda_handler(event, context): """ logger.info("upload profile image function") try: - # ユーザーIDを取得 - user_id = get_user_id(event) + # Cognito Authorizer から user_id を取得 + user_id = event["requestContext"]["authorizer"]["claims"]["sub"] # リクエストの body から画像データを取得 body = event.get("body", "") @@ -99,7 +77,7 @@ def lambda_handler(event, context): s3_key = f"profile/image/{user_id}.png" # S3 にアップロード - upload_to_s3(S3_BUCKET, s3_key, image_data) + upload_to_s3(s3_key, image_data) # 成功レスポンスを返す return { diff --git a/src/backend/test/pytest/test_upload_profile_image.py b/src/backend/test/pytest/test_upload_profile_image.py new file mode 100644 index 00000000..3a1008e6 --- /dev/null +++ b/src/backend/test/pytest/test_upload_profile_image.py @@ -0,0 +1,53 @@ +import base64 +import os +from unittest.mock import MagicMock, patch + +import pytest + +os.environ["USER_SETTINGS_BUCKET"] = "test-bucket" + +from upload_profile_image.upload_profile_image import decode_image_data, upload_to_s3 + + +# decode_image_data 関数のテスト +def test_decode_image_data(): + """decode_image_data 関数のテスト""" + encoded_image = base64.b64encode(b"image data").decode("utf-8") + decoded_image = decode_image_data(encoded_image) + + # デコード後のデータが bytes 型であることを確認 + assert isinstance(decoded_image, bytes) + assert decoded_image == b"image data" + + +# Base64 エンコードされた無効なデータが渡された場合 +def test_decode_image_data_invalid(): + """無効な Base64 データが渡された場合のテスト""" + invalid_base64 = "invalid_base64_string" + with pytest.raises(ValueError): + decode_image_data(invalid_base64) + + +# upload_to_s3 関数のテスト +@patch("upload_profile_image.upload_profile_image.boto3.client") +def test_upload_to_s3(mock_boto_client): + """upload_to_s3 関数のテスト""" + # モックされた S3 クライアントの put_object メソッド + mock_s3 = MagicMock() + mock_boto_client.return_value = mock_s3 + + # テスト用データ + user_id = "test-user-id" + image_data = b"image data" + s3_key = f"profile/image/{user_id}.png" + + # upload_to_s3 を呼び出し + upload_to_s3(s3_key, image_data) + + # put_object メソッドが正しい引数で呼ばれたことを確認 + mock_s3.put_object.assert_called_once_with( + Bucket="test-bucket", + Key=s3_key, + Body=image_data, + ContentType="image/png", + ) From 868e5a0d829bc0856d46e326759fa584765b40ec Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 22:27:26 +0000 Subject: [PATCH 11/44] =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=81=AE=E6=A4=9C=E8=A8=BC=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image.py | 45 ++++++++++++++++--- src/backend/lib/constructs/settings.ts | 7 +++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 485ef7cb..64332ccb 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -3,15 +3,20 @@ import os import boto3 +import magic # MIME タイプを確認するためのライブラリ (python-magic) # ログ設定 logger = logging.getLogger() logger.setLevel(logging.INFO) +# サポートされる MIME タイプ +SUPPORTED_MIME_TYPES = ["image/png", "image/jpeg", "image/gif"] +MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5MB + def decode_image_data(encoded_body): """ - Base64 エンコードされた画像データをデコード + Base64 エンコードされた画像データをデコードし、サイズを確認 Args: encoded_body (string): Base64 エンコードされた画像データ。 @@ -20,22 +25,47 @@ def decode_image_data(encoded_body): bytes: デコードされた画像データ。 Raises: - ValueError: `encoded_body` が無効な Base64 データの場合。 + ValueError: `encoded_body` が無効な Base64 データの場合、またはサイズが大きすぎる場合。 """ try: - return base64.b64decode(encoded_body) + decoded_data = base64.b64decode(encoded_body) + if len(decoded_data) > MAX_FILE_SIZE_BYTES: + raise ValueError( + f"Image data exceeds maximum size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)} MB" + ) + return decoded_data except Exception as e: raise ValueError("Invalid base64-encoded image data") from e -def upload_to_s3(key, data, content_type="image/png"): +def validate_image(data): + """ + 画像データの MIME タイプを確認 + + Args: + data (bytes): デコードされた画像データ。 + + Returns: + string: 検出された MIME タイプ。 + + Raises: + ValueError: MIME タイプがサポートされていない場合。 + """ + mime = magic.Magic(mime=True) + detected_mime_type = mime.from_buffer(data) + if detected_mime_type not in SUPPORTED_MIME_TYPES: + raise ValueError(f"Unsupported image format: {detected_mime_type}") + return detected_mime_type + + +def upload_to_s3(key, data, content_type): """ S3 に画像データをアップロード Args: key (string): S3 バケット内のファイルのキー(パス)。 data (bytes): アップロードする画像データ。 - content_type (string): 画像のコンテンツタイプ(デフォルトは "image/png")。 + content_type (string): 画像のコンテンツタイプ。 Raises: RuntimeError: S3 アップロード中にエラーが発生した場合。 @@ -73,11 +103,14 @@ def lambda_handler(event, context): body = event.get("body", "") image_data = decode_image_data(body) + # 画像データの形式を検証 + mime_type = validate_image(image_data) + # S3 パスを設定 s3_key = f"profile/image/{user_id}.png" # S3 にアップロード - upload_to_s3(s3_key, image_data) + upload_to_s3(s3_key, image_data, mime_type) # 成功レスポンスを返す return { diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index 663462e6..a9827cb2 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -19,6 +19,13 @@ export class Settings extends Construct { const userSettingsBucket = new s3.Bucket(this, 'userSettingsBucket', { enforceSSL: true, serverAccessLogsPrefix: 'log/', + cors: [ + { + allowedHeaders: ['*'], + allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.POST], + allowedOrigins: ['*'], + }, + ], }) //プロフィール画像アップロード用Lambda関数の定義 From 6d0f5a3e542d5d4c9b8e66ff1084945ebcfb0ef4 Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 22:31:01 +0000 Subject: [PATCH 12/44] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image.py | 12 ++++--- .../test/pytest/test_upload_profile_image.py | 36 +++++++++++++++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 64332ccb..c5f2adc3 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -29,14 +29,16 @@ def decode_image_data(encoded_body): """ try: decoded_data = base64.b64decode(encoded_body) - if len(decoded_data) > MAX_FILE_SIZE_BYTES: - raise ValueError( - f"Image data exceeds maximum size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)} MB" - ) - return decoded_data except Exception as e: raise ValueError("Invalid base64-encoded image data") from e + if len(decoded_data) > MAX_FILE_SIZE_BYTES: + raise ValueError( + f"Image data exceeds maximum size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)} MB" + ) + + return decoded_data + def validate_image(data): """ diff --git a/src/backend/test/pytest/test_upload_profile_image.py b/src/backend/test/pytest/test_upload_profile_image.py index 3a1008e6..44feae50 100644 --- a/src/backend/test/pytest/test_upload_profile_image.py +++ b/src/backend/test/pytest/test_upload_profile_image.py @@ -6,7 +6,12 @@ os.environ["USER_SETTINGS_BUCKET"] = "test-bucket" -from upload_profile_image.upload_profile_image import decode_image_data, upload_to_s3 +from upload_profile_image.upload_profile_image import ( + MAX_FILE_SIZE_BYTES, + decode_image_data, + upload_to_s3, + validate_image, +) # decode_image_data 関数のテスト @@ -20,6 +25,15 @@ def test_decode_image_data(): assert decoded_image == b"image data" +def test_decode_image_data_exceeds_max_size(): + """画像データが最大サイズを超える場合のテスト""" + large_image_data = base64.b64encode(b"A" * (MAX_FILE_SIZE_BYTES + 1)).decode( + "utf-8" + ) + with pytest.raises(ValueError, match="exceeds maximum size"): + decode_image_data(large_image_data) + + # Base64 エンコードされた無効なデータが渡された場合 def test_decode_image_data_invalid(): """無効な Base64 データが渡された場合のテスト""" @@ -28,6 +42,21 @@ def test_decode_image_data_invalid(): decode_image_data(invalid_base64) +# validate_image 関数のテスト +def test_validate_image_valid_png(): + """有効な PNG 画像の MIME タイプを検証""" + data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" + mime = validate_image(data) + assert mime == "image/png" + + +def test_validate_image_invalid_format(): + """無効な画像フォーマットの場合のテスト""" + data = b"not an image" + with pytest.raises(ValueError, match="Unsupported image format"): + validate_image(data) + + # upload_to_s3 関数のテスト @patch("upload_profile_image.upload_profile_image.boto3.client") def test_upload_to_s3(mock_boto_client): @@ -40,14 +69,15 @@ def test_upload_to_s3(mock_boto_client): user_id = "test-user-id" image_data = b"image data" s3_key = f"profile/image/{user_id}.png" + content_type = "image/png" # upload_to_s3 を呼び出し - upload_to_s3(s3_key, image_data) + upload_to_s3(s3_key, image_data, content_type) # put_object メソッドが正しい引数で呼ばれたことを確認 mock_s3.put_object.assert_called_once_with( Bucket="test-bucket", Key=s3_key, Body=image_data, - ContentType="image/png", + ContentType=content_type, ) From cb25d80201f886c8819bf046ba083d69a97c0c85 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 18 Dec 2024 07:34:50 +0900 Subject: [PATCH 13/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/__snapshots__/snapshot.test.ts.snap | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index 115e2446..a242bd9b 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -4157,6 +4157,22 @@ exports[`Snapshot test 1`] = ` "DeletionPolicy": "Retain", "Properties": { "AccessControl": "LogDeliveryWrite", + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedHeaders": [ + "*", + ], + "AllowedMethods": [ + "GET", + "POST", + ], + "AllowedOrigins": [ + "*", + ], + }, + ], + }, "LoggingConfiguration": { "LogFilePrefix": "log/", }, From 9bd25533089551fd22540e538cbe3ccc5b8eb2be Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 18 Dec 2024 07:40:01 +0900 Subject: [PATCH 14/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/upload_profile_image/requirements.txt | 2 ++ src/backend/lib/constructs/settings.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/backend/lambda/upload_profile_image/requirements.txt diff --git a/src/backend/lambda/upload_profile_image/requirements.txt b/src/backend/lambda/upload_profile_image/requirements.txt new file mode 100644 index 00000000..7e69e8c1 --- /dev/null +++ b/src/backend/lambda/upload_profile_image/requirements.txt @@ -0,0 +1,2 @@ +# requirements.txt +python-magic \ No newline at end of file diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index a9827cb2..8b43ad23 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -33,7 +33,12 @@ export class Settings extends Construct { runtime: lambda.Runtime.PYTHON_3_11, handler: 'upload_profile_image.lambda_handler', timeout: cdk.Duration.seconds(15), - code: lambda.Code.fromAsset('lambda/upload_profile_image'), + code: lambda.Code.fromAsset('lambda/upload_profile_image', { + bundling: { + image: lambda.Runtime.PYTHON_3_11.bundlingImage, + command: ['bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output'], + }, + }), environment: { USER_SETTINGS_BUCKET: userSettingsBucket.bucketName, }, From ff278708a5b4a0837381c82d07f63b7b2baba6d6 Mon Sep 17 00:00:00 2001 From: shoji Date: Tue, 17 Dec 2024 22:56:42 +0000 Subject: [PATCH 15/44] =?UTF-8?q?mimetypes=20=E3=82=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../upload_profile_image/requirements.txt | 2 -- .../upload_profile_image.py | 17 +++++++++-------- .../test/pytest/test_upload_profile_image.py | 18 ++++++++++++++---- 3 files changed, 23 insertions(+), 14 deletions(-) delete mode 100644 src/backend/lambda/upload_profile_image/requirements.txt diff --git a/src/backend/lambda/upload_profile_image/requirements.txt b/src/backend/lambda/upload_profile_image/requirements.txt deleted file mode 100644 index 7e69e8c1..00000000 --- a/src/backend/lambda/upload_profile_image/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# requirements.txt -python-magic \ No newline at end of file diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index c5f2adc3..4cb8febd 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -1,9 +1,9 @@ import base64 import logging +import mimetypes import os import boto3 -import magic # MIME タイプを確認するためのライブラリ (python-magic) # ログ設定 logger = logging.getLogger() @@ -40,12 +40,13 @@ def decode_image_data(encoded_body): return decoded_data -def validate_image(data): +def validate_image(data, file_name): """ - 画像データの MIME タイプを確認 + 画像データの MIME タイプを確認 (拡張子ベース) Args: data (bytes): デコードされた画像データ。 + file_name (string): ファイル名またはキー名。 Returns: string: 検出された MIME タイプ。 @@ -53,8 +54,8 @@ def validate_image(data): Raises: ValueError: MIME タイプがサポートされていない場合。 """ - mime = magic.Magic(mime=True) - detected_mime_type = mime.from_buffer(data) + # mimetypes による MIME タイプ判定 + detected_mime_type = mimetypes.guess_type(file_name)[0] if detected_mime_type not in SUPPORTED_MIME_TYPES: raise ValueError(f"Unsupported image format: {detected_mime_type}") return detected_mime_type @@ -105,12 +106,12 @@ def lambda_handler(event, context): body = event.get("body", "") image_data = decode_image_data(body) - # 画像データの形式を検証 - mime_type = validate_image(image_data) - # S3 パスを設定 s3_key = f"profile/image/{user_id}.png" + # 画像データの形式を検証(拡張子ベース) + mime_type = validate_image(image_data, s3_key) + # S3 にアップロード upload_to_s3(s3_key, image_data, mime_type) diff --git a/src/backend/test/pytest/test_upload_profile_image.py b/src/backend/test/pytest/test_upload_profile_image.py index 44feae50..666f92ab 100644 --- a/src/backend/test/pytest/test_upload_profile_image.py +++ b/src/backend/test/pytest/test_upload_profile_image.py @@ -45,16 +45,26 @@ def test_decode_image_data_invalid(): # validate_image 関数のテスト def test_validate_image_valid_png(): """有効な PNG 画像の MIME タイプを検証""" - data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" - mime = validate_image(data) + file_name = "test.png" + data = b"dummy image data" + mime = validate_image(data, file_name) assert mime == "image/png" +def test_validate_image_valid_jpeg(): + """有効な JPEG 画像の MIME タイプを検証""" + file_name = "test.jpeg" + data = b"dummy image data" + mime = validate_image(data, file_name) + assert mime == "image/jpeg" + + def test_validate_image_invalid_format(): - """無効な画像フォーマットの場合のテスト""" + """無効な画像フォーマット(不正な拡張子)の場合のテスト""" + file_name = "test.txt" # サポートされていない拡張子 data = b"not an image" with pytest.raises(ValueError, match="Unsupported image format"): - validate_image(data) + validate_image(data, file_name) # upload_to_s3 関数のテスト From efa4d73969bc52c41b17725f885203bdd5feefea Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 18 Dec 2024 07:57:36 +0900 Subject: [PATCH 16/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/constructs/settings.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index 8b43ad23..a9827cb2 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -33,12 +33,7 @@ export class Settings extends Construct { runtime: lambda.Runtime.PYTHON_3_11, handler: 'upload_profile_image.lambda_handler', timeout: cdk.Duration.seconds(15), - code: lambda.Code.fromAsset('lambda/upload_profile_image', { - bundling: { - image: lambda.Runtime.PYTHON_3_11.bundlingImage, - command: ['bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output'], - }, - }), + code: lambda.Code.fromAsset('lambda/upload_profile_image'), environment: { USER_SETTINGS_BUCKET: userSettingsBucket.bucketName, }, From 3c0bd22b6ddbf8380c2585c3c5426199dfb93331 Mon Sep 17 00:00:00 2001 From: shoji Date: Thu, 26 Dec 2024 07:10:08 +0900 Subject: [PATCH 17/44] =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E5=88=B6?= =?UTF-8?q?=E9=99=90=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/upload_profile_image/upload_profile_image.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index 4cb8febd..a68e374d 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -32,11 +32,6 @@ def decode_image_data(encoded_body): except Exception as e: raise ValueError("Invalid base64-encoded image data") from e - if len(decoded_data) > MAX_FILE_SIZE_BYTES: - raise ValueError( - f"Image data exceeds maximum size of {MAX_FILE_SIZE_BYTES / (1024 * 1024)} MB" - ) - return decoded_data From 290b1074e07152ec425b8d0bb2c6e05e98b6987f Mon Sep 17 00:00:00 2001 From: shoji Date: Thu, 26 Dec 2024 07:10:53 +0900 Subject: [PATCH 18/44] =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E5=88=B6?= =?UTF-8?q?=E9=99=90=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/test/pytest/test_upload_profile_image.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/backend/test/pytest/test_upload_profile_image.py b/src/backend/test/pytest/test_upload_profile_image.py index 666f92ab..b5adeee0 100644 --- a/src/backend/test/pytest/test_upload_profile_image.py +++ b/src/backend/test/pytest/test_upload_profile_image.py @@ -7,7 +7,6 @@ os.environ["USER_SETTINGS_BUCKET"] = "test-bucket" from upload_profile_image.upload_profile_image import ( - MAX_FILE_SIZE_BYTES, decode_image_data, upload_to_s3, validate_image, @@ -24,16 +23,6 @@ def test_decode_image_data(): assert isinstance(decoded_image, bytes) assert decoded_image == b"image data" - -def test_decode_image_data_exceeds_max_size(): - """画像データが最大サイズを超える場合のテスト""" - large_image_data = base64.b64encode(b"A" * (MAX_FILE_SIZE_BYTES + 1)).decode( - "utf-8" - ) - with pytest.raises(ValueError, match="exceeds maximum size"): - decode_image_data(large_image_data) - - # Base64 エンコードされた無効なデータが渡された場合 def test_decode_image_data_invalid(): """無効な Base64 データが渡された場合のテスト""" From 265804938694d184e2f1b403cef11ddc214ee13b Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 25 Dec 2024 22:19:14 +0000 Subject: [PATCH 19/44] =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E5=88=B6?= =?UTF-8?q?=E9=99=90=E3=81=AE=E5=AE=9A=E7=BE=A9=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/upload_profile_image/upload_profile_image.py | 1 - src/backend/test/pytest/test_upload_profile_image.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/lambda/upload_profile_image/upload_profile_image.py b/src/backend/lambda/upload_profile_image/upload_profile_image.py index a68e374d..54848051 100644 --- a/src/backend/lambda/upload_profile_image/upload_profile_image.py +++ b/src/backend/lambda/upload_profile_image/upload_profile_image.py @@ -11,7 +11,6 @@ # サポートされる MIME タイプ SUPPORTED_MIME_TYPES = ["image/png", "image/jpeg", "image/gif"] -MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 # 5MB def decode_image_data(encoded_body): diff --git a/src/backend/test/pytest/test_upload_profile_image.py b/src/backend/test/pytest/test_upload_profile_image.py index b5adeee0..dfab3f3a 100644 --- a/src/backend/test/pytest/test_upload_profile_image.py +++ b/src/backend/test/pytest/test_upload_profile_image.py @@ -23,6 +23,7 @@ def test_decode_image_data(): assert isinstance(decoded_image, bytes) assert decoded_image == b"image data" + # Base64 エンコードされた無効なデータが渡された場合 def test_decode_image_data_invalid(): """無効な Base64 データが渡された場合のテスト""" From 8a2f23d23e19d0fc82f5a25bf3f3a344402e8e5e Mon Sep 17 00:00:00 2001 From: shoji-san <85208872+mt-osho-san@users.noreply.github.com> Date: Sat, 28 Dec 2024 17:29:56 +0900 Subject: [PATCH 20/44] =?UTF-8?q?feat:=20=E3=83=97=E3=83=AD=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E7=94=BB=E5=83=8F=E5=8F=96=E5=BE=97?= =?UTF-8?q?API=E3=81=AE=E5=AE=9F=E8=A3=85=20=20(#257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### **User description** ## 概要 プロファイル画像を取得する API を作成 ## 変更点 - Lambda, API GW の CDK コードを追加 - Lambda 関数の中身の Python コードを追加 ## 影響範囲 既存の機能には影響なし ## テスト - pytest によりテストを実行 ## 関連 Issue - 関連 Issue: #244 ___ ### **PR Type** Enhancement ___ ### **Description** - プロファイル画像を取得するAPIを新たに実装しました。 - Lambda関数を追加し、S3から画像を取得してBase64エンコードした結果を返します。 - API GatewayにGETメソッドを追加し、ユーザーのプロフィール画像を取得できるようにしました。 - 画像取得関数に対するユニットテストを実装し、エラーハンドリングも確認しました。 ___ ### **Changes walkthrough** 📝
Relevant files
Enhancement
settings.ts
プロファイル画像取得用LambdaとAPIの追加                                                                 

src/backend/lib/constructs/settings.ts - プロファイル画像取得用のLambda関数を追加 - API GatewayにGETメソッドを追加
+15/-0   
get_profile_image.py
S3から画像を取得するLambda関数の実装                                                                     

src/backend/lambda/get_profile_image/get_profile_image.py - S3から画像を取得する関数を実装 - 画像をBase64エンコードして返す処理を追加
+88/-0   
Tests
test_get_profile_image.py
プロファイル画像取得関数のテスト追加                                                                             

src/backend/test/pytest/test_get_profile_image.py - 画像取得関数のユニットテストを追加 - エラーハンドリングのテストを実装
+65/-0   
___ > 💡 **PR-Agent usage**: Comment `/help "your question"` on any pull request to receive relevant information --- .../get_profile_image/get_profile_image.py | 88 +++++++ src/backend/lib/constructs/settings.ts | 15 ++ .../test/__snapshots__/snapshot.test.ts.snap | 234 +++++++++++++++++- .../test/pytest/test_get_profile_image.py | 65 +++++ 4 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 src/backend/lambda/get_profile_image/get_profile_image.py create mode 100644 src/backend/test/pytest/test_get_profile_image.py diff --git a/src/backend/lambda/get_profile_image/get_profile_image.py b/src/backend/lambda/get_profile_image/get_profile_image.py new file mode 100644 index 00000000..0f8d2776 --- /dev/null +++ b/src/backend/lambda/get_profile_image/get_profile_image.py @@ -0,0 +1,88 @@ +import base64 +import logging +import os + +import boto3 +from botocore.exceptions import ClientError + +# ログ設定 +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +def get_image_from_s3(key): + """ + S3 から画像データを取得 + + Args: + key (string): S3 バケット内のファイルのキー(パス)。 + + Returns: + bytes: 取得した画像データ。 + string: 画像のコンテンツタイプ。 + + Raises: + RuntimeError: S3 からデータを取得中にエラーが発生した場合。 + """ + # S3 クライアントの初期化 + s3_client = boto3.client("s3") + bucket = os.environ["USER_SETTINGS_BUCKET"] + + try: + response = s3_client.get_object(Bucket=bucket, Key=key) + data = response["Body"].read() + content_type = response["ContentType"] + return data, content_type + except ClientError as e: + if e.response["Error"]["Code"] == "NoSuchKey": + raise FileNotFoundError( + f"The key {key} does not exist in bucket {bucket}" + ) from e + raise RuntimeError(f"Failed to retrieve image from S3: {str(e)}") from e + + +def lambda_handler(event, context): + """ + Lambda 関数のエントリーポイント。API Gateway からのリクエストを処理し、ユーザーのプロフィール画像を S3 から取得。 + + Args: + event (dict): API Gateway からのリクエストイベント。 + context (object): Lambda 実行環境の情報。 + + Returns: + dict: レスポンスコードとメッセージを含む辞書。 + """ + logger.info("get profile image function") + try: + # Cognito Authorizer から user_id を取得 + user_id = event["requestContext"]["authorizer"]["claims"]["sub"] + + # S3 パスを設定 + s3_key = f"profile/image/{user_id}.png" + + # S3 から画像を取得 + image_data, content_type = get_image_from_s3(s3_key) + + # Base64 エンコード + encoded_image = base64.b64encode(image_data).decode("utf-8") + + # 成功レスポンスを返す + return { + "statusCode": 200, + "headers": { + "Content-Type": content_type, + "Content-Encoding": "base64", + }, + "body": encoded_image, + "isBase64Encoded": True, + } + + except FileNotFoundError as fe: + logger.error(f"File not found: {str(fe)}") + return {"statusCode": 404, "body": f"Image not found: {str(fe)}"} + except RuntimeError as re: + logger.error(f"Runtime error: {str(re)}") + return {"statusCode": 500, "body": f"Server error: {str(re)}"} + except Exception as e: + logger.error(f"Unexpected error: {str(e)}") + return {"statusCode": 500, "body": f"Failed to retrieve image: {str(e)}"} diff --git a/src/backend/lib/constructs/settings.ts b/src/backend/lib/constructs/settings.ts index a9827cb2..7b87af14 100644 --- a/src/backend/lib/constructs/settings.ts +++ b/src/backend/lib/constructs/settings.ts @@ -40,11 +40,26 @@ export class Settings extends Construct { }) userSettingsBucket.grantPut(uploadProfileImageFunction) + //プロフィール画像取得用Lambda関数の定義 + const getProfileImageFunction = new lambda.Function(this, 'getProfileImageFunction', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'get_profile_image.lambda_handler', + timeout: cdk.Duration.seconds(15), + code: lambda.Code.fromAsset('lambda/get_profile_image'), + environment: { + USER_SETTINGS_BUCKET: userSettingsBucket.bucketName, + }, + }) + userSettingsBucket.grantRead(getProfileImageFunction) // /APIの設定 const settingsApi = props.api.root.addResource('settings') settingsApi.addMethod('POST', new apigateway.LambdaIntegration(uploadProfileImageFunction), { authorizer: props.cognitoAuthorizer, }) + + settingsApi.addMethod('GET', new apigateway.LambdaIntegration(getProfileImageFunction), { + authorizer: props.cognitoAuthorizer, + }) } } diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index a242bd9b..d9ef9e57 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -336,7 +336,7 @@ exports[`Snapshot test 1`] = ` "Type": "AWS::IAM::Role", "UpdateReplacePolicy": "Delete", }, - "ApiDiaryApiDeploymentCA0DCBF5b04204490493f7bae0d0fbb51bda4809": { + "ApiDiaryApiDeploymentCA0DCBF5ddc2bf13dbc3d7fbb845a336b665119f": { "DependsOn": [ "ApiDiaryApibouquetGET7AC93032", "ApiDiaryApibouquetPOST2DEB7999", @@ -350,6 +350,7 @@ exports[`Snapshot test 1`] = ` "ApiDiaryApiflowerGETEAC01B09", "ApiDiaryApiflowerOPTIONS2A81112B", "ApiDiaryApiflower7C61878D", + "ApiDiaryApisettingsGET4E223DBA", "ApiDiaryApisettingsPOST5E845A8B", "ApiDiaryApisettings70D21EB5", "ApiDiaryApititleGET40FCE039", @@ -380,7 +381,7 @@ exports[`Snapshot test 1`] = ` "Format": "{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","user":"$context.identity.user","caller":"$context.identity.caller","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}", }, "DeploymentId": { - "Ref": "ApiDiaryApiDeploymentCA0DCBF5b04204490493f7bae0d0fbb51bda4809", + "Ref": "ApiDiaryApiDeploymentCA0DCBF5ddc2bf13dbc3d7fbb845a336b665119f", }, "MethodSettings": [ { @@ -1400,6 +1401,127 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::ApiGateway::Resource", }, + "ApiDiaryApisettingsGET4E223DBA": { + "Properties": { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "ApiCognitoAuthorizer23B91BA2", + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":apigateway:", + { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "SettingsgetProfileImageFunctionDBA40214", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": { + "Ref": "ApiDiaryApisettings70D21EB5", + }, + "RestApiId": { + "Ref": "ApiDiaryApi1E03348A", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "ApiDiaryApisettingsGETApiPermissionCdkSampleStackApiDiaryApi4AB3EF3AGETsettings7E6B2258": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SettingsgetProfileImageFunctionDBA40214", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiDiaryApi1E03348A", + }, + "/", + { + "Ref": "ApiDiaryApiDeploymentStageprodD9912276", + }, + "/GET/settings", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ApiDiaryApisettingsGETApiPermissionTestCdkSampleStackApiDiaryApi4AB3EF3AGETsettings86A23BEE": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SettingsgetProfileImageFunctionDBA40214", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiDiaryApi1E03348A", + }, + "/test-invoke-stage/GET/settings", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, "ApiDiaryApisettingsPOST5E845A8B": { "Properties": { "AuthorizationType": "COGNITO_USER_POOLS", @@ -4050,6 +4172,114 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Policy", }, + "SettingsgetProfileImageFunctionDBA40214": { + "DependsOn": [ + "SettingsgetProfileImageFunctionServiceRoleDefaultPolicyBC909A15", + "SettingsgetProfileImageFunctionServiceRole36952281", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "HASH_REPLACED.zip", + }, + "Environment": { + "Variables": { + "USER_SETTINGS_BUCKET": { + "Ref": "SettingsuserSettingsBucket1D2B648F", + }, + }, + }, + "Handler": "get_profile_image.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "SettingsgetProfileImageFunctionServiceRole36952281", + "Arn", + ], + }, + "Runtime": "python3.11", + "Timeout": 15, + }, + "Type": "AWS::Lambda::Function", + }, + "SettingsgetProfileImageFunctionServiceRole36952281": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "SettingsgetProfileImageFunctionServiceRoleDefaultPolicyBC909A15": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "SettingsuserSettingsBucket1D2B648F", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "SettingsuserSettingsBucket1D2B648F", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "SettingsgetProfileImageFunctionServiceRoleDefaultPolicyBC909A15", + "Roles": [ + { + "Ref": "SettingsgetProfileImageFunctionServiceRole36952281", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "SettingsuploadProfileImageFunction5C7EC09C": { "DependsOn": [ "SettingsuploadProfileImageFunctionServiceRoleDefaultPolicy8B4E4832", diff --git a/src/backend/test/pytest/test_get_profile_image.py b/src/backend/test/pytest/test_get_profile_image.py new file mode 100644 index 00000000..ecb046f4 --- /dev/null +++ b/src/backend/test/pytest/test_get_profile_image.py @@ -0,0 +1,65 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest +from botocore.exceptions import ClientError +from get_profile_image.get_profile_image import get_image_from_s3 + +# 環境変数の設定 +os.environ["USER_SETTINGS_BUCKET"] = "test-bucket" + + +# fixtureでS3からの画像レスポンスを定義 +@pytest.fixture +def s3_image_response(): + return b"test_image_data" + + +# get_image_from_s3のテスト +@patch("get_profile_image.get_profile_image.boto3.client") +def test_get_image_from_s3(mock_boto3_client, s3_image_response): + """S3から画像を取得する関数のテスト""" + + # S3クライアントのモック + mock_s3_client = MagicMock() + mock_boto3_client.return_value = mock_s3_client + + # S3レスポンスをモック + mock_s3_client.get_object.return_value = { + "Body": MagicMock(read=MagicMock(return_value=s3_image_response)), + "ContentType": "image/png", + } + + s3_key = "profile/image/test-user-id.png" + + # 関数を呼び出して結果を検証 + result_data, result_content_type = get_image_from_s3(s3_key) + assert result_data == s3_image_response + assert result_content_type == "image/png" + + # S3の呼び出しを確認 + mock_s3_client.get_object.assert_called_once_with( + Bucket="test-bucket", + Key=s3_key, + ) + + +def test_get_image_from_s3_no_such_key_error(): + """S3からのキーが存在しない場合のエラーテスト""" + with patch("get_profile_image.get_profile_image.boto3.client") as mock_boto3_client: + mock_s3_client = MagicMock() + mock_boto3_client.return_value = mock_s3_client + + # NoSuchKey エラーをシミュレート + error_response = { + "Error": { + "Code": "NoSuchKey", + "Message": "The specified key does not exist.", + } + } + mock_s3_client.get_object.side_effect = ClientError(error_response, "GetObject") + + s3_key = "profile/image/non-existent-user-id.png" + + with pytest.raises(FileNotFoundError): + get_image_from_s3(s3_key) From 74feee2c8d15b12e7dd00f513902658d7684f11b Mon Sep 17 00:00:00 2001 From: shoji Date: Sat, 28 Dec 2024 09:19:53 +0000 Subject: [PATCH 21/44] =?UTF-8?q?=E6=97=A5=E8=A8=98=E3=81=AE=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=92=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B?= =?UTF-8?q?Python=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/get_diary_data/get_diary_data.py | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/backend/lambda/get_diary_data/get_diary_data.py diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py new file mode 100644 index 00000000..07bc5a0a --- /dev/null +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -0,0 +1,209 @@ +import base64 +import json +import logging +import os +from typing import Any, Dict, Optional + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) +formatter = logging.Formatter( + "[%(asctime)s - %(levelname)s - %(filename)s(func:%(funcName)s, line:%(lineno)d)] %(message)s" +) +handler = logging.StreamHandler() +handler.setFormatter(formatter) +logger.addHandler(handler) +logger.setLevel(logging.INFO) + +dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1") + + +def create_response(status_code: int, body: dict) -> dict: + """ + HTTPレスポンスを生成する + + Args: + status_code (int): HTTPステータスコード + body (dict): レスポンスボディ + + Returns: + dict: フォーマット済みのHTTPレスポンス + """ + return { + "statusCode": status_code, + "body": json.dumps(body), + "headers": { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + + +def validate_date(date: str) -> bool: + """ + 日付形式を検証する + + Args: + date (str): 検証する日付文字列 + + Returns: + bool: 日付が有効な場合True + """ + import re + + return bool(re.match(r"^\d{4}-\d{2}-\d{2}$", date)) + + +def get_flower_id(user_id: str, date: str) -> Optional[str]: + """ + DynamoDBからflower_idを取得する + + Args: + user_id (str): ユーザーID + date (str): 日付 + + Returns: + Optional[str]: flower_idまたはNone + """ + table_name = os.getenv("GENERATIVE_AI_TABLE_NAME") + if not table_name: + logger.error("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") + raise ValueError("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") + + table = dynamodb.Table(table_name) + + try: + response = table.get_item(Key={"user_id": user_id, "date": date}) + return response.get("Item", {}).get("flower_id") + except ClientError as e: + logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") + raise + + +def get_image(flower_id: str) -> Optional[str]: + """ + S3から画像を取得する + + Args: + flower_id (str): Flower ID + + Returns: + Optional[str]: Base64エンコードされた画像データまたはNone + """ + s3 = boto3.client("s3") + bucket_name = os.getenv("BUCKET_NAME") + if not bucket_name: + logger.error("BUCKET_NAME 環境変数が設定されていません") + raise ValueError("BUCKET_NAME 環境変数が設定されていません") + + s3_key = f"flowers/{flower_id}.png" + + try: + response = s3.get_object(Bucket=bucket_name, Key=s3_key) + body = response["Body"].read() + return base64.b64encode(body).decode("utf-8") + except ClientError as e: + if e.response["Error"].get("Code") == "NoSuchKey": + logger.info(f"画像が見つかりません: {s3_key}") + return None + logger.error(f"S3エラー: {e.response['Error']['Message']}") + raise + + +def get_title(user_id: str, date: str) -> Optional[str]: + """ + DynamoDBからタイトルを取得する + + Args: + user_id (str): ユーザーID + date (str): 日付 + + Returns: + Optional[str]: タイトルまたはNone + """ + table_name = os.getenv("TABLE_NAME") + if not table_name: + logger.error("TABLE_NAME 環境変数が設定されていません") + raise ValueError("TABLE_NAME 環境変数が設定されていません") + + table = dynamodb.Table(table_name) + + try: + response = table.get_item(Key={"user_id": user_id, "date": date}) + return response.get("Item", {}).get("title") + except ClientError as e: + logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") + raise + + +def get_body(user_id: str, date: str) -> Optional[str]: + """ + DynamoDBから本文を取得する + + Args: + user_id (str): ユーザーID + date (str): 日付 + + Returns: + Optional[str]: 本文またはNone + """ + table_name = os.getenv("TABLE_NAME") + if not table_name: + logger.error("TABLE_NAME 環境変数が設定されていません") + raise ValueError("TABLE_NAME 環境変数が設定されていません") + + table = dynamodb.Table(table_name) + + try: + response = table.get_item(Key={"user_id": user_id, "date": date}) + return response.get("Item", {}).get("body") + except ClientError as e: + logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") + raise + + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """ + DynamoDBから画像、タイトル、本文を取得してレスポンスを返す + + Args: + event (Dict[str, Any]): イベントデータ + context (Any): Lambdaコンテキスト + + Returns: + Dict[str, Any]: API Gateway互換レスポンス + """ + try: + query_params = event.get("queryStringParameters", {}) + if not query_params or "date" not in query_params: + return create_response(400, {"error": "Missing required parameter: date"}) + + user_id = event["requestContext"]["authorizer"]["claims"]["sub"] + date = query_params["date"] + + if not validate_date(date): + return create_response(400, {"error": "Invalid date format"}) + + flower_id = get_flower_id(user_id, date) + image = get_image(flower_id) if flower_id else None + title = get_title(user_id, date) + body = get_body(user_id, date) + + response_data = { + "image": image, + "title": title or "", + "body": body or "", + } + + return create_response(200, response_data) + + except Exception as e: + logger.error(f"予期しないエラー: {str(e)}") + return create_response( + 500, + { + "error": "Internal server error", + "details": str(e) if os.getenv("DEBUG") else "Please contact support", + }, + ) From 5419ae5e5edf6f92436631b94bc50ebe97fea2ef Mon Sep 17 00:00:00 2001 From: shoji Date: Sat, 28 Dec 2024 09:29:31 +0000 Subject: [PATCH 22/44] =?UTF-8?q?=E6=97=A5=E8=A8=98=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=AECDK=E3=82=92=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/get_diary_data/get_diary_data.py | 26 +++++++++---------- src/backend/lib/constructs/diary.ts | 14 ++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 07bc5a0a..9399b7eb 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -92,10 +92,10 @@ def get_image(flower_id: str) -> Optional[str]: Optional[str]: Base64エンコードされた画像データまたはNone """ s3 = boto3.client("s3") - bucket_name = os.getenv("BUCKET_NAME") + bucket_name = os.getenv("FLOWER_IMAGE_BUCKET_NAME") if not bucket_name: - logger.error("BUCKET_NAME 環境変数が設定されていません") - raise ValueError("BUCKET_NAME 環境変数が設定されていません") + logger.error("FLOWER_IMAGE_BUCKET_NAME 環境変数が設定されていません") + raise ValueError("FLOWER_IMAGE_BUCKET_NAME 環境変数が設定されていません") s3_key = f"flowers/{flower_id}.png" @@ -122,15 +122,15 @@ def get_title(user_id: str, date: str) -> Optional[str]: Returns: Optional[str]: タイトルまたはNone """ - table_name = os.getenv("TABLE_NAME") - if not table_name: - logger.error("TABLE_NAME 環境変数が設定されていません") - raise ValueError("TABLE_NAME 環境変数が設定されていません") + generative_ai_table_name = os.getenv("GENERATIVE_AI_TABLE_NAME") + if not generative_ai_table_name: + logger.error("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") + raise ValueError("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") - table = dynamodb.Table(table_name) + generative_ai_table = dynamodb.Table(generative_ai_table_name) try: - response = table.get_item(Key={"user_id": user_id, "date": date}) + response = generative_ai_table.get_item(Key={"user_id": user_id, "date": date}) return response.get("Item", {}).get("title") except ClientError as e: logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") @@ -148,15 +148,15 @@ def get_body(user_id: str, date: str) -> Optional[str]: Returns: Optional[str]: 本文またはNone """ - table_name = os.getenv("TABLE_NAME") - if not table_name: + diary_table_name = os.getenv("TABLE_NAME") + if not diary_table_name: logger.error("TABLE_NAME 環境変数が設定されていません") raise ValueError("TABLE_NAME 環境変数が設定されていません") - table = dynamodb.Table(table_name) + diary_table = dynamodb.Table(diary_table_name) try: - response = table.get_item(Key={"user_id": user_id, "date": date}) + response = diary_table.get_item(Key={"user_id": user_id, "date": date}) return response.get("Item", {}).get("body") except ClientError as e: logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index df56daeb..bb9797c7 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -174,5 +174,19 @@ export class Diary extends Construct { titleApi.addMethod('GET', new apigateway.LambdaIntegration(titleGetFunction), { authorizer: props.cognitoAuthorizer, }) + + const getDiaryDataFunction = new lambda.Function(this, 'getDiaryDataFunction', { + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'get_diary_data.lambda_handler', + code: lambda.Code.fromAsset('lambda/get_diary_data'), + environment: { + GENERATIVE_TABLE_NAME: props.generativeAiTable.tableName, + DIARY_TABLE_NAME: props.table.tableName, + FLOWER_IMAGE_BUCKET_NAME: props.flowerImageBucket.bucketName, + }, + }) + props.generativeAiTable.grantReadData(getDiaryDataFunction) + props.flowerImageBucket.grantRead(getDiaryDataFunction) + props.table.grantReadData(getDiaryDataFunction) } } From 5636a0750d16971eeb6e32a0b8e3d23a2feab930 Mon Sep 17 00:00:00 2001 From: shoji Date: Sat, 28 Dec 2024 18:30:02 +0900 Subject: [PATCH 23/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/__snapshots__/snapshot.test.ts.snap | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index d9ef9e57..3a4f0eb3 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -3237,6 +3237,167 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Policy", }, + "DiarygetDiaryDataFunctionAAA49ECF": { + "DependsOn": [ + "DiarygetDiaryDataFunctionServiceRoleDefaultPolicy5A0D2EBD", + "DiarygetDiaryDataFunctionServiceRole505DDB87", + ], + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "HASH_REPLACED.zip", + }, + "Environment": { + "Variables": { + "DIARY_TABLE_NAME": { + "Ref": "FlowerdiaryContentsTableCA7C6940", + }, + "FLOWER_IMAGE_BUCKET_NAME": { + "Ref": "FlowerflowerImageBucket46E60C76", + }, + "GENERATIVE_TABLE_NAME": { + "Ref": "FlowergenerativeAiTable021268D8", + }, + }, + }, + "Handler": "get_diary_data.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "DiarygetDiaryDataFunctionServiceRole505DDB87", + "Arn", + ], + }, + "Runtime": "python3.11", + }, + "Type": "AWS::Lambda::Function", + }, + "DiarygetDiaryDataFunctionServiceRole505DDB87": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "DiarygetDiaryDataFunctionServiceRoleDefaultPolicy5A0D2EBD": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "FlowergenerativeAiTable021268D8", + "Arn", + ], + }, + { + "Ref": "AWS::NoValue", + }, + ], + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "FlowerflowerImageBucket46E60C76", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "FlowerflowerImageBucket46E60C76", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "FlowerdiaryContentsTableCA7C6940", + "Arn", + ], + }, + { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "DiarygetDiaryDataFunctionServiceRoleDefaultPolicy5A0D2EBD", + "Roles": [ + { + "Ref": "DiarygetDiaryDataFunctionServiceRole505DDB87", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "DiarytitleGetFunction90C2A326": { "DependsOn": [ "DiarytitleGetFunctionServiceRoleDefaultPolicyF8576449", From 2a10d4e74350b5c16fa05d5486f48c01b1f25df3 Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 29 Dec 2024 11:21:31 +0900 Subject: [PATCH 24/44] =?UTF-8?q?flowerImageBucket=20=E3=82=92Original=20b?= =?UTF-8?q?ukcet=20=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/diary_create/diary_create.py | 2 +- src/backend/lib/backend-stack.ts | 4 +-- src/backend/lib/constructs/bouquet.ts | 6 ++-- src/backend/lib/constructs/diary.ts | 6 ++-- src/backend/lib/constructs/flower.ts | 14 ++++---- .../test/__snapshots__/snapshot.test.ts.snap | 34 +++++++++---------- src/backend/test/pytest/test_diary_create.py | 4 +-- 7 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/backend/lambda/diary_create/diary_create.py b/src/backend/lambda/diary_create/diary_create.py index 73a77dce..ae89c525 100644 --- a/src/backend/lambda/diary_create/diary_create.py +++ b/src/backend/lambda/diary_create/diary_create.py @@ -47,7 +47,7 @@ def get_img_from_s3(flower_id): Exception: S3 操作が失敗した場合に発生。 """ s3 = boto3.client("s3") - bucket_name = os.environ["FLOWER_IMAGE_BUCKET_NAME"] + bucket_name = os.environ["ORIGINAL_IMAGE_BUCKET_NAME"] s3_key = f"flowers/{flower_id}.png" try: diff --git a/src/backend/lib/backend-stack.ts b/src/backend/lib/backend-stack.ts index 88f8e161..bc82e763 100644 --- a/src/backend/lib/backend-stack.ts +++ b/src/backend/lib/backend-stack.ts @@ -59,7 +59,7 @@ export class BackendStack extends cdk.Stack { table: flower.table, generativeAiTable: flower.generativeAiTable, flowerSelectFunction: flower.flowerSelectFunction, - flowerImageBucket: flower.flowerImageBucket, + originalImageBucket: flower.originalImageBucket, }) const bouquet = new Bouquet(this, 'Bouquet', { @@ -68,7 +68,7 @@ export class BackendStack extends cdk.Stack { api: api.api, generativeAiTable: flower.generativeAiTable, cognitoAuthorizer: api.cognitoAuthorizer, - flowerImageBucket: flower.flowerImageBucket, + originalImageBucket: flower.originalImageBucket, }) const settings = new Settings(this, 'Settings', { diff --git a/src/backend/lib/constructs/bouquet.ts b/src/backend/lib/constructs/bouquet.ts index 7414c752..a4bbcb08 100644 --- a/src/backend/lib/constructs/bouquet.ts +++ b/src/backend/lib/constructs/bouquet.ts @@ -12,7 +12,7 @@ export interface BouquetProps { api: apigateway.RestApi generativeAiTable: dynamodb.Table cognitoAuthorizer: apigateway.CognitoUserPoolsAuthorizer - flowerImageBucket: s3.Bucket + originalImageBucket: s3.Bucket } export class Bouquet extends Construct { @@ -66,13 +66,13 @@ export class Bouquet extends Construct { environment: { GENERATIVE_AI_TABLE_NAME: props.generativeAiTable.tableName, BOUQUET_TABLE_NAME: bouquetTable.tableName, - FLOWER_BUCKET_NAME: props.flowerImageBucket.bucketName, + FLOWER_BUCKET_NAME: props.originalImageBucket.bucketName, BOUQUET_BUCKET_NAME: bouquetBucket.bucketName, }, }) props.generativeAiTable.grantReadData(BouquetCreate) bouquetTable.grantWriteData(BouquetCreate) - props.flowerImageBucket.grantRead(BouquetCreate) + props.originalImageBucket.grantRead(BouquetCreate) bouquetBucket.grantPut(BouquetCreate) // /bouquet APIの設定 diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index df56daeb..6f22d40e 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -14,7 +14,7 @@ export interface DiaryProps { table: dynamodb.Table generativeAiTable: dynamodb.Table flowerSelectFunction: lambda.Function - flowerImageBucket: s3.Bucket + originalImageBucket: s3.Bucket } export class Diary extends Construct { @@ -36,14 +36,14 @@ export class Diary extends Construct { logRetention: 14, environment: { TABLE_NAME: props.table.tableName, - FLOWER_IMAGE_BUCKET_NAME: props.flowerImageBucket.bucketName, + ORIGINAL_IMAGE_BUCKET_NAME: props.originalImageBucket.bucketName, FLOWER_SELECT_FUNCTION_NAME: props.flowerSelectFunction.functionName, }, timeout: cdk.Duration.seconds(30), }) props.table.grantWriteData(diaryCreateFunction) props.flowerSelectFunction.grantInvoke(diaryCreateFunction) - props.flowerImageBucket.grantRead(diaryCreateFunction) + props.originalImageBucket.grantRead(diaryCreateFunction) // 日記編集用Lambda関数の定義 const diaryEditFunction = new lambda.Function(this, 'diaryEditLambda', { diff --git a/src/backend/lib/constructs/flower.ts b/src/backend/lib/constructs/flower.ts index a264f7b3..bf7beec7 100644 --- a/src/backend/lib/constructs/flower.ts +++ b/src/backend/lib/constructs/flower.ts @@ -15,7 +15,7 @@ export interface FlowerProps { } export class Flower extends Construct { - public readonly flowerImageBucket: s3.Bucket + public readonly originalImageBucket: s3.Bucket public readonly table: dynamodb.Table public readonly generativeAiTable: dynamodb.Table public readonly flowerSelectFunction: lambda.Function @@ -52,7 +52,7 @@ export class Flower extends Construct { }) // 花の画像保存用S3バケットの作成 - const flowerImageBucket = new s3.Bucket(this, 'flowerImageBucket', { + const originalImageBucket = new s3.Bucket(this, 'originalImageBucket', { enforceSSL: true, serverAccessLogsPrefix: 'log/', }) @@ -70,13 +70,13 @@ export class Flower extends Construct { environment: { DIARY_TABLE_NAME: table.tableName, GENERATIVE_AI_TABLE_NAME: generativeAiTable.tableName, - FLOWER_BUCKET_NAME: flowerImageBucket.bucketName, + FLOWER_BUCKET_NAME: originalImageBucket.bucketName, }, timeout: cdk.Duration.seconds(60), }) generativeAiTable.grantWriteData(flowerSelectFunction) table.grantStreamRead(flowerSelectFunction) - flowerImageBucket.grantPut(flowerSelectFunction) + originalImageBucket.grantPut(flowerSelectFunction) const difyApiKey = ssm.StringParameter.fromStringParameterAttributes(this, 'DifyApiKey', { parameterName: 'DIFY_API_KEY', }) @@ -93,12 +93,12 @@ export class Flower extends Construct { handler: 'flower_get.lambda_handler', code: lambda.Code.fromAsset('lambda/flower_get'), environment: { - BUCKET_NAME: flowerImageBucket.bucketName, + BUCKET_NAME: originalImageBucket.bucketName, GENERATIVE_AI_TABLE_NAME: generativeAiTable.tableName, }, timeout: cdk.Duration.seconds(10), }) - flowerImageBucket.grantRead(flowerGetFunction) + originalImageBucket.grantRead(flowerGetFunction) generativeAiTable.grantReadData(flowerGetFunction) // flower API の設定 const flowerApi = props.api.root.addResource('flower') @@ -138,7 +138,7 @@ export class Flower extends Construct { authorizer: props.cognitoAuthorizer, }) - this.flowerImageBucket = flowerImageBucket + this.originalImageBucket = originalImageBucket this.table = table this.generativeAiTable = generativeAiTable this.flowerSelectFunction = flowerSelectFunction diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index d9ef9e57..9f4761c9 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -1941,7 +1941,7 @@ exports[`Snapshot test 1`] = ` "Ref": "BouquetBouquetTableE43AF35D", }, "FLOWER_BUCKET_NAME": { - "Ref": "FlowerflowerImageBucket46E60C76", + "Ref": "FloweroriginalImageBucket46E60C76", }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", @@ -2050,7 +2050,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -2060,7 +2060,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -2611,8 +2611,8 @@ exports[`Snapshot test 1`] = ` }, "Environment": { "Variables": { - "FLOWER_IMAGE_BUCKET_NAME": { - "Ref": "FlowerflowerImageBucket46E60C76", + "ORIGINAL_IMAGE_BUCKET_NAME": { + "Ref": "FloweroriginalImageBucket46E60C76", }, "FLOWER_SELECT_FUNCTION_NAME": { "Ref": "FlowerflowerSelectFunctionD7EEBADA", @@ -2749,7 +2749,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -2759,7 +2759,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -3390,7 +3390,7 @@ exports[`Snapshot test 1`] = ` "Environment": { "Variables": { "BUCKET_NAME": { - "Ref": "FlowerflowerImageBucket46E60C76", + "Ref": "FloweroriginalImageBucket46E60C76", }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", @@ -3454,7 +3454,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -3464,7 +3464,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -3510,7 +3510,7 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Policy", }, - "FlowerflowerImageBucket46E60C76": { + "FloweroriginalImageBucket46E60C76": { "DeletionPolicy": "Retain", "Properties": { "AccessControl": "LogDeliveryWrite", @@ -3528,10 +3528,10 @@ exports[`Snapshot test 1`] = ` "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, - "FlowerflowerImageBucketPolicyD55EE863": { + "FloweroriginalImageBucketPolicyD55EE863": { "Properties": { "Bucket": { - "Ref": "FlowerflowerImageBucket46E60C76", + "Ref": "FloweroriginalImageBucket46E60C76", }, "PolicyDocument": { "Statement": [ @@ -3549,7 +3549,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -3559,7 +3559,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, @@ -3593,7 +3593,7 @@ exports[`Snapshot test 1`] = ` "Ref": "FlowerdiaryContentsTableCA7C6940", }, "FLOWER_BUCKET_NAME": { - "Ref": "FlowerflowerImageBucket46E60C76", + "Ref": "FloweroriginalImageBucket46E60C76", }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", @@ -3703,7 +3703,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FloweroriginalImageBucket46E60C76", "Arn", ], }, diff --git a/src/backend/test/pytest/test_diary_create.py b/src/backend/test/pytest/test_diary_create.py index f72dbe00..b0ec9da5 100644 --- a/src/backend/test/pytest/test_diary_create.py +++ b/src/backend/test/pytest/test_diary_create.py @@ -38,14 +38,14 @@ def test_get_img_from_s3(mock_boto_client): "Body": MagicMock(read=lambda: b"fake_image_data") } - os.environ["FLOWER_IMAGE_BUCKET_NAME"] = "TEST_BUCKET" + os.environ["ORIGINAL_IMAGE_BUCKET_NAME"] = "TEST_BUCKET" # テスト実行 flower_id = "test_flower_id" image_data = get_img_from_s3(flower_id) # アサーション s3_client_mock.get_object.assert_called_once_with( - Bucket=os.environ["FLOWER_IMAGE_BUCKET_NAME"], Key=f"flowers/{flower_id}.png" + Bucket=os.environ["ORIGINAL_IMAGE_BUCKET_NAME"], Key=f"flowers/{flower_id}.png" ) assert image_data == base64.b64encode(b"fake_image_data").decode("utf-8") From 3a8ce3c121f6988163354040c759103d317dc2ce Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 29 Dec 2024 11:31:52 +0900 Subject: [PATCH 25/44] =?UTF-8?q?flower=5Fbukcket=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/bouquet_create/bouquet_create.py | 4 ++-- src/backend/lambda/flower_get/flower_get.py | 2 +- src/backend/lib/backend-stack.ts | 1 + src/backend/lib/constructs/bouquet.ts | 2 +- src/backend/lib/constructs/diary.ts | 2 ++ src/backend/lib/constructs/flower.ts | 17 ++++++++++++++--- .../test/__snapshots__/snapshot.test.ts.snap | 4 ++-- 7 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/backend/lambda/bouquet_create/bouquet_create.py b/src/backend/lambda/bouquet_create/bouquet_create.py index 8a3ff670..5c4c225e 100644 --- a/src/backend/lambda/bouquet_create/bouquet_create.py +++ b/src/backend/lambda/bouquet_create/bouquet_create.py @@ -9,7 +9,7 @@ dynamodb = boto3.resource("dynamodb") s3 = boto3.client("s3") -FLOWER_BUCKET_NAME = os.environ["FLOWER_BUCKET_NAME"] +ORIGINAL_IMAGE_BUCKET_NAME = os.environ["ORIGINAL_IMAGE_BUCKET_NAME"] BOUQUET_BUCKET_NAME = os.environ["BOUQUET_BUCKET_NAME"] GENERATIVE_AI_TABLE_NAME = os.environ["GENERATIVE_AI_TABLE_NAME"] BOUQUET_TABLE_NAME = os.environ["BOUQUET_TABLE_NAME"] @@ -847,7 +847,7 @@ def load_image(self, key): Returns: Image: ロードされた画像 """ - obj = s3.get_object(Bucket=FLOWER_BUCKET_NAME, Key=key) + obj = s3.get_object(Bucket=ORIGINAL_IMAGE_BUCKET_NAME, Key=key) img = Image.open(obj["Body"]).convert("RGBA") return img diff --git a/src/backend/lambda/flower_get/flower_get.py b/src/backend/lambda/flower_get/flower_get.py index 9fd11428..0499770d 100644 --- a/src/backend/lambda/flower_get/flower_get.py +++ b/src/backend/lambda/flower_get/flower_get.py @@ -60,7 +60,7 @@ def get_img_from_s3(flower_id: str) -> str: str: 画像のバイナリデータ(Base64エンコード済み) """ s3 = boto3.client("s3") - bucket_name = os.environ["BUCKET_NAME"] + bucket_name = os.environ["FLOWER_BUCKET_NAME"] s3_key = f"flowers/{flower_id}.png" try: diff --git a/src/backend/lib/backend-stack.ts b/src/backend/lib/backend-stack.ts index bc82e763..84e7d6a3 100644 --- a/src/backend/lib/backend-stack.ts +++ b/src/backend/lib/backend-stack.ts @@ -53,6 +53,7 @@ export class BackendStack extends cdk.Stack { }) // Diary機能コンストラクトのスタック化 const diary = new Diary(this, 'Diary', { + flowerBucket: flower.flowerBucket, userPool: auth.userPool, api: api.api, cognitoAuthorizer: api.cognitoAuthorizer, diff --git a/src/backend/lib/constructs/bouquet.ts b/src/backend/lib/constructs/bouquet.ts index a4bbcb08..253c0d68 100644 --- a/src/backend/lib/constructs/bouquet.ts +++ b/src/backend/lib/constructs/bouquet.ts @@ -66,7 +66,7 @@ export class Bouquet extends Construct { environment: { GENERATIVE_AI_TABLE_NAME: props.generativeAiTable.tableName, BOUQUET_TABLE_NAME: bouquetTable.tableName, - FLOWER_BUCKET_NAME: props.originalImageBucket.bucketName, + ORIGINAL_IMAGE_BUCKET_NAME: props.originalImageBucket.bucketName, BOUQUET_BUCKET_NAME: bouquetBucket.bucketName, }, }) diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index 6f22d40e..09c5b406 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -15,6 +15,7 @@ export interface DiaryProps { generativeAiTable: dynamodb.Table flowerSelectFunction: lambda.Function originalImageBucket: s3.Bucket + flowerBucket: s3.Bucket } export class Diary extends Construct { @@ -38,6 +39,7 @@ export class Diary extends Construct { TABLE_NAME: props.table.tableName, ORIGINAL_IMAGE_BUCKET_NAME: props.originalImageBucket.bucketName, FLOWER_SELECT_FUNCTION_NAME: props.flowerSelectFunction.functionName, + FLOWER_BUKCET_NAME: props.flowerBucket.bucketName, }, timeout: cdk.Duration.seconds(30), }) diff --git a/src/backend/lib/constructs/flower.ts b/src/backend/lib/constructs/flower.ts index bf7beec7..d6f0a759 100644 --- a/src/backend/lib/constructs/flower.ts +++ b/src/backend/lib/constructs/flower.ts @@ -19,6 +19,7 @@ export class Flower extends Construct { public readonly table: dynamodb.Table public readonly generativeAiTable: dynamodb.Table public readonly flowerSelectFunction: lambda.Function + public readonly flowerBucket: s3.Bucket constructor(scope: Construct, id: string, props: FlowerProps) { super(scope, id) @@ -51,12 +52,18 @@ export class Flower extends Construct { pointInTimeRecovery: true, }) - // 花の画像保存用S3バケットの作成 + // 元画像保存用S3バケットの作成 const originalImageBucket = new s3.Bucket(this, 'originalImageBucket', { enforceSSL: true, serverAccessLogsPrefix: 'log/', }) + // ユーザーごとの花画像保存用S3バケットの作成 + const flowerBucket = new s3.Bucket(this, 'flowerBucket', { + enforceSSL: true, + serverAccessLogsPrefix: 'log/', + }) + // 花の画像生成用Lambda関数の定義 const flowerSelectFunction = new lambda.Function(this, 'flowerSelectFunction', { runtime: lambda.Runtime.PYTHON_3_11, @@ -70,11 +77,13 @@ export class Flower extends Construct { environment: { DIARY_TABLE_NAME: table.tableName, GENERATIVE_AI_TABLE_NAME: generativeAiTable.tableName, - FLOWER_BUCKET_NAME: originalImageBucket.bucketName, + ORIGINAL_IMAGE_BUCKET_NAME: originalImageBucket.bucketName, + FLOWER_BUCKET_NAME: flowerBucket.bucketName, }, timeout: cdk.Duration.seconds(60), }) generativeAiTable.grantWriteData(flowerSelectFunction) + flowerBucket.grantPut(flowerSelectFunction) table.grantStreamRead(flowerSelectFunction) originalImageBucket.grantPut(flowerSelectFunction) const difyApiKey = ssm.StringParameter.fromStringParameterAttributes(this, 'DifyApiKey', { @@ -93,8 +102,9 @@ export class Flower extends Construct { handler: 'flower_get.lambda_handler', code: lambda.Code.fromAsset('lambda/flower_get'), environment: { - BUCKET_NAME: originalImageBucket.bucketName, + ORIGINAL_BUCKET_NAME: originalImageBucket.bucketName, GENERATIVE_AI_TABLE_NAME: generativeAiTable.tableName, + FLOWER_BUCKET_NAME: flowerBucket.bucketName, }, timeout: cdk.Duration.seconds(10), }) @@ -142,5 +152,6 @@ export class Flower extends Construct { this.table = table this.generativeAiTable = generativeAiTable this.flowerSelectFunction = flowerSelectFunction + this.flowerBucket = flowerBucket } } diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index 9f4761c9..aacdf0b1 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -1940,7 +1940,7 @@ exports[`Snapshot test 1`] = ` "BOUQUET_TABLE_NAME": { "Ref": "BouquetBouquetTableE43AF35D", }, - "FLOWER_BUCKET_NAME": { + "ORIGINAL_IMAGE_BUCKET_NAME": { "Ref": "FloweroriginalImageBucket46E60C76", }, "GENERATIVE_AI_TABLE_NAME": { @@ -3592,7 +3592,7 @@ exports[`Snapshot test 1`] = ` "DIARY_TABLE_NAME": { "Ref": "FlowerdiaryContentsTableCA7C6940", }, - "FLOWER_BUCKET_NAME": { + "ORIGINAL_IMAGE_BUCKET_NAME": { "Ref": "FloweroriginalImageBucket46E60C76", }, "GENERATIVE_AI_TABLE_NAME": { From cd899c574d9d63ce165e206abc8c789d52940b38 Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 29 Dec 2024 11:33:00 +0900 Subject: [PATCH 26/44] =?UTF-8?q?=E6=97=A5=E8=A8=98=E4=BD=9C=E6=88=90Lambd?= =?UTF-8?q?a=E9=96=A2=E6=95=B0=E3=81=ABFlowerImage=E3=81=AERead=E6=A8=A9?= =?UTF-8?q?=E9=99=90=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/constructs/diary.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index 09c5b406..852a7f2f 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -46,6 +46,7 @@ export class Diary extends Construct { props.table.grantWriteData(diaryCreateFunction) props.flowerSelectFunction.grantInvoke(diaryCreateFunction) props.originalImageBucket.grantRead(diaryCreateFunction) + props.flowerBucket.grantPut(diaryCreateFunction) // 日記編集用Lambda関数の定義 const diaryEditFunction = new lambda.Function(this, 'diaryEditLambda', { From ff91e1f9dc37d75f21279a010823805c6241102e Mon Sep 17 00:00:00 2001 From: shoji Date: Sun, 29 Dec 2024 02:34:45 +0000 Subject: [PATCH 27/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/__snapshots__/snapshot.test.ts.snap | 286 +++++++++++++----- src/backend/test/pytest/test_flower_get.py | 2 +- 2 files changed, 206 insertions(+), 82 deletions(-) diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index aacdf0b1..fdda086c 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -1940,12 +1940,12 @@ exports[`Snapshot test 1`] = ` "BOUQUET_TABLE_NAME": { "Ref": "BouquetBouquetTableE43AF35D", }, - "ORIGINAL_IMAGE_BUCKET_NAME": { - "Ref": "FloweroriginalImageBucket46E60C76", - }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", }, + "ORIGINAL_IMAGE_BUCKET_NAME": { + "Ref": "FloweroriginalImageBucket5E40682A", + }, }, }, "Handler": "bouquet_create.lambda_handler", @@ -2050,7 +2050,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -2060,7 +2060,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -2611,12 +2611,15 @@ exports[`Snapshot test 1`] = ` }, "Environment": { "Variables": { - "ORIGINAL_IMAGE_BUCKET_NAME": { - "Ref": "FloweroriginalImageBucket46E60C76", + "FLOWER_BUKCET_NAME": { + "Ref": "FlowerflowerBucket9981D467", }, "FLOWER_SELECT_FUNCTION_NAME": { "Ref": "FlowerflowerSelectFunctionD7EEBADA", }, + "ORIGINAL_IMAGE_BUCKET_NAME": { + "Ref": "FloweroriginalImageBucket5E40682A", + }, "TABLE_NAME": { "Ref": "FlowerdiaryContentsTableCA7C6940", }, @@ -2749,7 +2752,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -2759,7 +2762,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -2769,6 +2772,31 @@ exports[`Snapshot test 1`] = ` }, ], }, + { + "Action": [ + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "FlowerflowerBucket9981D467", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, ], "Version": "2012-10-17", }, @@ -3375,6 +3403,71 @@ exports[`Snapshot test 1`] = ` "Type": "AWS::DynamoDB::Table", "UpdateReplacePolicy": "Delete", }, + "FlowerflowerBucket9981D467": { + "DeletionPolicy": "Retain", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "LoggingConfiguration": { + "LogFilePrefix": "log/", + }, + "OwnershipControls": { + "Rules": [ + { + "ObjectOwnership": "ObjectWriter", + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "FlowerflowerBucketPolicy784D0728": { + "Properties": { + "Bucket": { + "Ref": "FlowerflowerBucket9981D467", + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": { + "AWS": "*", + }, + "Resource": [ + { + "Fn::GetAtt": [ + "FlowerflowerBucket9981D467", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "FlowerflowerBucket9981D467", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "FlowerflowerGetFunctionA4C511EA": { "DependsOn": [ "FlowerflowerGetFunctionServiceRoleDefaultPolicy62A02DD7", @@ -3389,12 +3482,15 @@ exports[`Snapshot test 1`] = ` }, "Environment": { "Variables": { - "BUCKET_NAME": { - "Ref": "FloweroriginalImageBucket46E60C76", + "FLOWER_BUCKET_NAME": { + "Ref": "FlowerflowerBucket9981D467", }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", }, + "ORIGINAL_BUCKET_NAME": { + "Ref": "FloweroriginalImageBucket5E40682A", + }, }, }, "Handler": "flower_get.lambda_handler", @@ -3454,7 +3550,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -3464,7 +3560,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -3510,71 +3606,6 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Policy", }, - "FloweroriginalImageBucket46E60C76": { - "DeletionPolicy": "Retain", - "Properties": { - "AccessControl": "LogDeliveryWrite", - "LoggingConfiguration": { - "LogFilePrefix": "log/", - }, - "OwnershipControls": { - "Rules": [ - { - "ObjectOwnership": "ObjectWriter", - }, - ], - }, - }, - "Type": "AWS::S3::Bucket", - "UpdateReplacePolicy": "Retain", - }, - "FloweroriginalImageBucketPolicyD55EE863": { - "Properties": { - "Bucket": { - "Ref": "FloweroriginalImageBucket46E60C76", - }, - "PolicyDocument": { - "Statement": [ - { - "Action": "s3:*", - "Condition": { - "Bool": { - "aws:SecureTransport": "false", - }, - }, - "Effect": "Deny", - "Principal": { - "AWS": "*", - }, - "Resource": [ - { - "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", - "Arn", - ], - }, - { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", - "Arn", - ], - }, - "/*", - ], - ], - }, - ], - }, - ], - "Version": "2012-10-17", - }, - }, - "Type": "AWS::S3::BucketPolicy", - }, "FlowerflowerSelectFunctionD7EEBADA": { "DependsOn": [ "FlowerflowerSelectFunctionServiceRoleDefaultPolicy7F8366A3", @@ -3592,12 +3623,15 @@ exports[`Snapshot test 1`] = ` "DIARY_TABLE_NAME": { "Ref": "FlowerdiaryContentsTableCA7C6940", }, - "ORIGINAL_IMAGE_BUCKET_NAME": { - "Ref": "FloweroriginalImageBucket46E60C76", + "FLOWER_BUCKET_NAME": { + "Ref": "FlowerflowerBucket9981D467", }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", }, + "ORIGINAL_IMAGE_BUCKET_NAME": { + "Ref": "FloweroriginalImageBucket5E40682A", + }, }, }, "Handler": "flower_select.lambda_handler", @@ -3668,6 +3702,31 @@ exports[`Snapshot test 1`] = ` }, ], }, + { + "Action": [ + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "FlowerflowerBucket9981D467", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, { "Action": "dynamodb:ListStreams", "Effect": "Allow", @@ -3703,7 +3762,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FloweroriginalImageBucket46E60C76", + "FloweroriginalImageBucket5E40682A", "Arn", ], }, @@ -3782,6 +3841,71 @@ exports[`Snapshot test 1`] = ` "Type": "AWS::DynamoDB::Table", "UpdateReplacePolicy": "Delete", }, + "FloweroriginalImageBucket5E40682A": { + "DeletionPolicy": "Retain", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "LoggingConfiguration": { + "LogFilePrefix": "log/", + }, + "OwnershipControls": { + "Rules": [ + { + "ObjectOwnership": "ObjectWriter", + }, + ], + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "FloweroriginalImageBucketPolicyFA65F301": { + "Properties": { + "Bucket": { + "Ref": "FloweroriginalImageBucket5E40682A", + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": { + "AWS": "*", + }, + "Resource": [ + { + "Fn::GetAtt": [ + "FloweroriginalImageBucket5E40682A", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "FloweroriginalImageBucket5E40682A", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "IdentityIdentityPool01DA4AB2": { "DependsOn": [ "AuthDiaryUserPoolClientC42D6E78", diff --git a/src/backend/test/pytest/test_flower_get.py b/src/backend/test/pytest/test_flower_get.py index fa9c0ea7..e57726e9 100644 --- a/src/backend/test/pytest/test_flower_get.py +++ b/src/backend/test/pytest/test_flower_get.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch os.environ["AWS_DEFAULT_REGION"] = "us-east-1" -os.environ["BUCKET_NAME"] = "bucket" +os.environ["FLOWER_BUCKET_NAME"] = "bucket" import pytest from flower_get.flower_get import ( From 75369d310394eee8ebf26c439e8b4aabf654b9b0 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 11:55:26 +0900 Subject: [PATCH 28/44] =?UTF-8?q?=E3=83=90=E3=82=B1=E3=83=83=E3=83=88?= =?UTF-8?q?=E5=90=8D=E5=A4=89=E6=9B=B4=E3=82=92=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 6 +++--- src/backend/lib/constructs/diary.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 9399b7eb..fa5f89d9 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -92,10 +92,10 @@ def get_image(flower_id: str) -> Optional[str]: Optional[str]: Base64エンコードされた画像データまたはNone """ s3 = boto3.client("s3") - bucket_name = os.getenv("FLOWER_IMAGE_BUCKET_NAME") + bucket_name = os.getenv("FLOWER_BUCKET_NAME") if not bucket_name: - logger.error("FLOWER_IMAGE_BUCKET_NAME 環境変数が設定されていません") - raise ValueError("FLOWER_IMAGE_BUCKET_NAME 環境変数が設定されていません") + logger.error("FLOWER_BUCKET_NAME 環境変数が設定されていません") + raise ValueError("FLOWER_BUCKET_NAME 環境変数が設定されていません") s3_key = f"flowers/{flower_id}.png" diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index a9d26fa0..e9e7b313 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -185,11 +185,11 @@ export class Diary extends Construct { environment: { GENERATIVE_TABLE_NAME: props.generativeAiTable.tableName, DIARY_TABLE_NAME: props.table.tableName, - FLOWER_IMAGE_BUCKET_NAME: props.flowerImageBucket.bucketName, + FLOWER_BUCKET_NAME: props.flowerBucket.bucketName, }, }) props.generativeAiTable.grantReadData(getDiaryDataFunction) - props.flowerImageBucket.grantRead(getDiaryDataFunction) + props.flowerBucket.grantRead(getDiaryDataFunction) props.table.grantReadData(getDiaryDataFunction) } } From f73592a7e5d893d78c2274e46c1ded26d3ee8535 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 11:56:38 +0900 Subject: [PATCH 29/44] =?UTF-8?q?=E3=82=B9=E3=83=8A=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=83=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/test/__snapshots__/snapshot.test.ts.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index 0bfa927b..de147304 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -3282,8 +3282,8 @@ exports[`Snapshot test 1`] = ` "DIARY_TABLE_NAME": { "Ref": "FlowerdiaryContentsTableCA7C6940", }, - "FLOWER_IMAGE_BUCKET_NAME": { - "Ref": "FlowerflowerImageBucket46E60C76", + "FLOWER_BUCKET_NAME": { + "Ref": "FlowerflowerBucket9981D467", }, "GENERATIVE_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", @@ -3370,7 +3370,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FlowerflowerBucket9981D467", "Arn", ], }, @@ -3380,7 +3380,7 @@ exports[`Snapshot test 1`] = ` [ { "Fn::GetAtt": [ - "FlowerflowerImageBucket46E60C76", + "FlowerflowerBucket9981D467", "Arn", ], }, From 75cd33bbf40a8b38e67b73baa4d0017ace4be84d Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 12:16:19 +0900 Subject: [PATCH 30/44] =?UTF-8?q?=E8=8A=B1=E6=9D=9F=E4=BD=9C=E6=88=90?= =?UTF-8?q?=E5=8F=AF=E5=90=A6=E3=81=AE=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/get_diary_data/get_diary_data.py | 148 ++++++++++++------ 1 file changed, 102 insertions(+), 46 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index fa5f89d9..9b868647 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -3,6 +3,7 @@ import logging import os from typing import Any, Dict, Optional +from datetime import datetime, timedelta, timezone import boto3 from botocore.exceptions import ClientError @@ -18,17 +19,16 @@ dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1") - def create_response(status_code: int, body: dict) -> dict: """ - HTTPレスポンスを生成する + HTTPレスポンスを生成します。 Args: - status_code (int): HTTPステータスコード - body (dict): レスポンスボディ + status_code (int): HTTPステータスコード。 + body (dict): レスポンスボディ。 Returns: - dict: フォーマット済みのHTTPレスポンス + dict: フォーマット済みのHTTPレスポンス。 """ return { "statusCode": status_code, @@ -39,37 +39,35 @@ def create_response(status_code: int, body: dict) -> dict: }, } - def validate_date(date: str) -> bool: """ - 日付形式を検証する + 日付文字列の形式を検証します。 Args: - date (str): 検証する日付文字列 + date (str): 検証する日付文字列。 Returns: - bool: 日付が有効な場合True + bool: 日付が有効な場合はTrue。 """ import re return bool(re.match(r"^\d{4}-\d{2}-\d{2}$", date)) - def get_flower_id(user_id: str, date: str) -> Optional[str]: """ - DynamoDBからflower_idを取得する + DynamoDBからflower_idを取得します。 Args: - user_id (str): ユーザーID - date (str): 日付 + user_id (str): ユーザーID。 + date (str): 日付文字列。 Returns: - Optional[str]: flower_idまたはNone + Optional[str]: flower_idまたはNone。 """ table_name = os.getenv("GENERATIVE_AI_TABLE_NAME") if not table_name: - logger.error("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") - raise ValueError("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") + logger.error("GENERATIVE_AI_TABLE_NAME is not defined") + raise ValueError("GENERATIVE_AI_TABLE_NAME is not defined") table = dynamodb.Table(table_name) @@ -77,25 +75,24 @@ def get_flower_id(user_id: str, date: str) -> Optional[str]: response = table.get_item(Key={"user_id": user_id, "date": date}) return response.get("Item", {}).get("flower_id") except ClientError as e: - logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") + logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") raise - def get_image(flower_id: str) -> Optional[str]: """ - S3から画像を取得する + S3から画像を取得します。 Args: - flower_id (str): Flower ID + flower_id (str): Flower ID。 Returns: - Optional[str]: Base64エンコードされた画像データまたはNone + Optional[str]: Base64エンコードされた画像データまたはNone。 """ s3 = boto3.client("s3") bucket_name = os.getenv("FLOWER_BUCKET_NAME") if not bucket_name: - logger.error("FLOWER_BUCKET_NAME 環境変数が設定されていません") - raise ValueError("FLOWER_BUCKET_NAME 環境変数が設定されていません") + logger.error("FLOWER_BUCKET_NAME環境変数が設定されていません。") + raise ValueError("FLOWER_BUCKET_NAME環境変数が設定されていません。") s3_key = f"flowers/{flower_id}.png" @@ -105,27 +102,26 @@ def get_image(flower_id: str) -> Optional[str]: return base64.b64encode(body).decode("utf-8") except ClientError as e: if e.response["Error"].get("Code") == "NoSuchKey": - logger.info(f"画像が見つかりません: {s3_key}") + logger.info(f"image not found: {s3_key}") return None logger.error(f"S3エラー: {e.response['Error']['Message']}") raise - def get_title(user_id: str, date: str) -> Optional[str]: """ - DynamoDBからタイトルを取得する + DynamoDBからタイトルを取得します。 Args: - user_id (str): ユーザーID - date (str): 日付 + user_id (str): ユーザーID。 + date (str): 日付文字列。 Returns: - Optional[str]: タイトルまたはNone + Optional[str]: タイトルまたはNone。 """ generative_ai_table_name = os.getenv("GENERATIVE_AI_TABLE_NAME") if not generative_ai_table_name: - logger.error("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") - raise ValueError("GENERATIVE_AI_TABLE_NAME 環境変数が設定されていません") + logger.error("GENERATIVE_AI_TABLE_NAME環境変数が設定されていません。") + raise ValueError("GENERATIVE_AI_TABLE_NAME環境変数が設定されていません。") generative_ai_table = dynamodb.Table(generative_ai_table_name) @@ -133,25 +129,24 @@ def get_title(user_id: str, date: str) -> Optional[str]: response = generative_ai_table.get_item(Key={"user_id": user_id, "date": date}) return response.get("Item", {}).get("title") except ClientError as e: - logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") + logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") raise - def get_body(user_id: str, date: str) -> Optional[str]: """ - DynamoDBから本文を取得する + DynamoDBから本文を取得します。 Args: - user_id (str): ユーザーID - date (str): 日付 + user_id (str): ユーザーID。 + date (str): 日付文字列。 Returns: - Optional[str]: 本文またはNone + Optional[str]: 本文またはNone。 """ diary_table_name = os.getenv("TABLE_NAME") if not diary_table_name: - logger.error("TABLE_NAME 環境変数が設定されていません") - raise ValueError("TABLE_NAME 環境変数が設定されていません") + logger.error("TABLE_NAME環境変数が設定されていません。") + raise ValueError("TABLE_NAME環境変数が設定されていません。") diary_table = dynamodb.Table(diary_table_name) @@ -159,25 +154,78 @@ def get_body(user_id: str, date: str) -> Optional[str]: response = diary_table.get_item(Key={"user_id": user_id, "date": date}) return response.get("Item", {}).get("body") except ClientError as e: - logger.error(f"DynamoDB クライアントエラー: {e.response['Error']['Message']}") + logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") raise +def get_current_week() -> (int, int): + """ + 現在の年と週番号を取得します。 + + Returns: + tuple: 現在の年とISO週番号。 + """ + current_date = datetime.now(timezone.utc) + current_year, current_week, _ = current_date.isocalendar() + return current_year, current_week + +def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> bool: + """ + 現在の週にブーケが作成されたか確認します。 + + Args: + user_id (str): ユーザーID。 + current_year (int): 現在の年。 + current_week (int): 現在のISO週番号。 + + Returns: + bool: ブーケが作成されている場合はTrue、それ以外はFalse。 + """ + bouquet_table_name = os.getenv("BOUQUET_TABLE_NAME") + bouquet_table = dynamodb.Table(bouquet_table_name) + bouquet_response = bouquet_table.get_item( + Key={"user_id": user_id, "year_week": f"{current_year}-{current_week}"} + ) + return "Item" in bouquet_response + +def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> int: + """ + 現在の週にS3にある花の数をカウントします。 + + Args: + user_id (str): ユーザーID。 + current_year (int): 現在の年。 + current_week (int): 現在のISO週番号。 + + Returns: + int: 花の数。 + """ + s3 = boto3.client("s3") + bucket_name = os.getenv("FLOWER_BUCKET_NAME") + prefix = f"{user_id}/{current_year}-{current_week}" + + try: + response = s3.list_objects_v2(Bucket=bucket_name, Prefix=prefix) + return len(response.get("Contents", [])) + except ClientError as e: + logger.error(f"S3 error: {e.response['Error']['Message']}") + raise def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """ - DynamoDBから画像、タイトル、本文を取得してレスポンスを返す + Lambda関数: 画像、タイトル、本文をDynamoDBから取得し、 + ブーケ作成可能かどうかをチェックします。 Args: - event (Dict[str, Any]): イベントデータ - context (Any): Lambdaコンテキスト + event (Dict[str, Any]): Lambdaイベントデータ。 + context (Any): Lambdaコンテキストオブジェクト。 Returns: - Dict[str, Any]: API Gateway互換レスポンス + Dict[str, Any]: API Gateway互換のレスポンス。 """ try: query_params = event.get("queryStringParameters", {}) if not query_params or "date" not in query_params: - return create_response(400, {"error": "Missing required parameter: date"}) + return create_response(400, {"error": "Required parameter is missing: date"}) user_id = event["requestContext"]["authorizer"]["claims"]["sub"] date = query_params["date"] @@ -190,16 +238,24 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: title = get_title(user_id, date) body = get_body(user_id, date) + current_year, current_week = get_current_week() + bouquet_created = check_bouquet_created(user_id, current_year, current_week) + flower_count = count_flowers_in_week(user_id, current_year, current_week) + can_create_bouquet = not bouquet_created and flower_count >= 5 + response_data = { "image": image, "title": title or "", "body": body or "", + "has_title": bool(title), + "has_body": bool(body), + "can_create_bouquet": can_create_bouquet, } return create_response(200, response_data) except Exception as e: - logger.error(f"予期しないエラー: {str(e)}") + logger.error(f"Unexpected error: {str(e)}") return create_response( 500, { From 8650c3deb900b6dce6ff846d0fea1408c9072804 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 03:22:23 +0000 Subject: [PATCH 31/44] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAimport=20?= =?UTF-8?q?=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/get_diary_data/get_diary_data.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 9b868647..28cbdb4d 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -2,8 +2,8 @@ import json import logging import os +from datetime import datetime, timezone from typing import Any, Dict, Optional -from datetime import datetime, timedelta, timezone import boto3 from botocore.exceptions import ClientError @@ -19,6 +19,7 @@ dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1") + def create_response(status_code: int, body: dict) -> dict: """ HTTPレスポンスを生成します。 @@ -39,6 +40,7 @@ def create_response(status_code: int, body: dict) -> dict: }, } + def validate_date(date: str) -> bool: """ 日付文字列の形式を検証します。 @@ -53,6 +55,7 @@ def validate_date(date: str) -> bool: return bool(re.match(r"^\d{4}-\d{2}-\d{2}$", date)) + def get_flower_id(user_id: str, date: str) -> Optional[str]: """ DynamoDBからflower_idを取得します。 @@ -78,6 +81,7 @@ def get_flower_id(user_id: str, date: str) -> Optional[str]: logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") raise + def get_image(flower_id: str) -> Optional[str]: """ S3から画像を取得します。 @@ -107,6 +111,7 @@ def get_image(flower_id: str) -> Optional[str]: logger.error(f"S3エラー: {e.response['Error']['Message']}") raise + def get_title(user_id: str, date: str) -> Optional[str]: """ DynamoDBからタイトルを取得します。 @@ -132,6 +137,7 @@ def get_title(user_id: str, date: str) -> Optional[str]: logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") raise + def get_body(user_id: str, date: str) -> Optional[str]: """ DynamoDBから本文を取得します。 @@ -157,6 +163,7 @@ def get_body(user_id: str, date: str) -> Optional[str]: logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") raise + def get_current_week() -> (int, int): """ 現在の年と週番号を取得します。 @@ -168,6 +175,7 @@ def get_current_week() -> (int, int): current_year, current_week, _ = current_date.isocalendar() return current_year, current_week + def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> bool: """ 現在の週にブーケが作成されたか確認します。 @@ -187,6 +195,7 @@ def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> ) return "Item" in bouquet_response + def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> int: """ 現在の週にS3にある花の数をカウントします。 @@ -210,6 +219,7 @@ def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> logger.error(f"S3 error: {e.response['Error']['Message']}") raise + def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """ Lambda関数: 画像、タイトル、本文をDynamoDBから取得し、 @@ -225,7 +235,9 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: try: query_params = event.get("queryStringParameters", {}) if not query_params or "date" not in query_params: - return create_response(400, {"error": "Required parameter is missing: date"}) + return create_response( + 400, {"error": "Required parameter is missing: date"} + ) user_id = event["requestContext"]["authorizer"]["claims"]["sub"] date = query_params["date"] From b9da2993653e0d507f3c0233278d2531cf62c596 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 12:46:12 +0900 Subject: [PATCH 32/44] =?UTF-8?q?=E7=92=B0=E5=A2=83=E5=A4=89=E6=95=B0?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 9 ++++----- src/backend/lib/constructs/diary.ts | 2 +- src/backend/test/__snapshots__/snapshot.test.ts.snap | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 28cbdb4d..dedfc79e 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -95,8 +95,8 @@ def get_image(flower_id: str) -> Optional[str]: s3 = boto3.client("s3") bucket_name = os.getenv("FLOWER_BUCKET_NAME") if not bucket_name: - logger.error("FLOWER_BUCKET_NAME環境変数が設定されていません。") - raise ValueError("FLOWER_BUCKET_NAME環境変数が設定されていません。") + logger.error("FLOWER_BUCKET_NAME is not defined") + raise ValueError("FLOWER_BUCKET_NAME is not defined") s3_key = f"flowers/{flower_id}.png" @@ -125,8 +125,8 @@ def get_title(user_id: str, date: str) -> Optional[str]: """ generative_ai_table_name = os.getenv("GENERATIVE_AI_TABLE_NAME") if not generative_ai_table_name: - logger.error("GENERATIVE_AI_TABLE_NAME環境変数が設定されていません。") - raise ValueError("GENERATIVE_AI_TABLE_NAME環境変数が設定されていません。") + logger.error("GENERATIVE_AI_TABLE_NAME is not defined") + raise ValueError("GENERATIVE_AI_TABLE_NAME is not defined") generative_ai_table = dynamodb.Table(generative_ai_table_name) @@ -241,7 +241,6 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: user_id = event["requestContext"]["authorizer"]["claims"]["sub"] date = query_params["date"] - if not validate_date(date): return create_response(400, {"error": "Invalid date format"}) diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index e9e7b313..1e64aa79 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -183,7 +183,7 @@ export class Diary extends Construct { handler: 'get_diary_data.lambda_handler', code: lambda.Code.fromAsset('lambda/get_diary_data'), environment: { - GENERATIVE_TABLE_NAME: props.generativeAiTable.tableName, + GENERATIVE_AI_TABLE_NAME: props.generativeAiTable.tableName, DIARY_TABLE_NAME: props.table.tableName, FLOWER_BUCKET_NAME: props.flowerBucket.bucketName, }, diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index de147304..3716270f 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -3285,7 +3285,7 @@ exports[`Snapshot test 1`] = ` "FLOWER_BUCKET_NAME": { "Ref": "FlowerflowerBucket9981D467", }, - "GENERATIVE_TABLE_NAME": { + "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", }, }, From 491e3c2142acc6eeebb5aa3ca885087777c7357a Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 12:47:33 +0900 Subject: [PATCH 33/44] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=A0=E3=82=A2?= =?UTF-8?q?=E3=82=A6=E3=83=88=E3=82=9230s=20=E3=81=AB=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/constructs/diary.ts | 1 + src/backend/test/__snapshots__/snapshot.test.ts.snap | 1 + 2 files changed, 2 insertions(+) diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index 1e64aa79..6198d1ff 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -187,6 +187,7 @@ export class Diary extends Construct { DIARY_TABLE_NAME: props.table.tableName, FLOWER_BUCKET_NAME: props.flowerBucket.bucketName, }, + timeout: cdk.Duration.seconds(30), }) props.generativeAiTable.grantReadData(getDiaryDataFunction) props.flowerBucket.grantRead(getDiaryDataFunction) diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index 3716270f..0ec805ad 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -3298,6 +3298,7 @@ exports[`Snapshot test 1`] = ` ], }, "Runtime": "python3.11", + "Timeout": 30, }, "Type": "AWS::Lambda::Function", }, From 84dff2d0a537a4478eb6a88053a2442780a8aa67 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 12:59:25 +0900 Subject: [PATCH 34/44] =?UTF-8?q?=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E7=92=B0=E5=A2=83=E5=A4=89=E6=95=B0=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/get_diary_data/get_diary_data.py | 10 +- src/backend/lib/constructs/diary.ts | 6 + .../test/__snapshots__/snapshot.test.ts.snap | 142 +++++++++++++++++- 3 files changed, 150 insertions(+), 8 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index dedfc79e..c432a4d5 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -107,9 +107,7 @@ def get_image(flower_id: str) -> Optional[str]: except ClientError as e: if e.response["Error"].get("Code") == "NoSuchKey": logger.info(f"image not found: {s3_key}") - return None - logger.error(f"S3エラー: {e.response['Error']['Message']}") - raise + return "" def get_title(user_id: str, date: str) -> Optional[str]: @@ -149,10 +147,10 @@ def get_body(user_id: str, date: str) -> Optional[str]: Returns: Optional[str]: 本文またはNone。 """ - diary_table_name = os.getenv("TABLE_NAME") + diary_table_name = os.getenv("GENERATIVE_AI_TABLE_NAME") if not diary_table_name: - logger.error("TABLE_NAME環境変数が設定されていません。") - raise ValueError("TABLE_NAME環境変数が設定されていません。") + logger.error("TABLE_NAME is not defined") + raise ValueError("TABLE_NAME is not defined") diary_table = dynamodb.Table(diary_table_name) diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index 6198d1ff..ad12f0ab 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -192,5 +192,11 @@ export class Diary extends Construct { props.generativeAiTable.grantReadData(getDiaryDataFunction) props.flowerBucket.grantRead(getDiaryDataFunction) props.table.grantReadData(getDiaryDataFunction) + + const diaryDataApi = props.api.root.addResource('data') + + diaryDataApi.addMethod('GET', new apigateway.LambdaIntegration(getDiaryDataFunction), { + authorizer: props.cognitoAuthorizer, + }) } } diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index 0ec805ad..f6d87510 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -336,11 +336,13 @@ exports[`Snapshot test 1`] = ` "Type": "AWS::IAM::Role", "UpdateReplacePolicy": "Delete", }, - "ApiDiaryApiDeploymentCA0DCBF5ddc2bf13dbc3d7fbb845a336b665119f": { + "ApiDiaryApiDeploymentCA0DCBF5b35b07922434a278bec4b7d5379f45fe": { "DependsOn": [ "ApiDiaryApibouquetGET7AC93032", "ApiDiaryApibouquetPOST2DEB7999", "ApiDiaryApibouquet8BAEBF58", + "ApiDiaryApidataGET0A09C29A", + "ApiDiaryApidataA4E0EFBE", "ApiDiaryApidiaryDELETE81EA9C37", "ApiDiaryApidiaryGET9A212B54", "ApiDiaryApidiaryOPTIONS118FD5D0", @@ -381,7 +383,7 @@ exports[`Snapshot test 1`] = ` "Format": "{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","user":"$context.identity.user","caller":"$context.identity.caller","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}", }, "DeploymentId": { - "Ref": "ApiDiaryApiDeploymentCA0DCBF5ddc2bf13dbc3d7fbb845a336b665119f", + "Ref": "ApiDiaryApiDeploymentCA0DCBF5b35b07922434a278bec4b7d5379f45fe", }, "MethodSettings": [ { @@ -671,6 +673,142 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::Lambda::Permission", }, + "ApiDiaryApidataA4E0EFBE": { + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "ApiDiaryApi1E03348A", + "RootResourceId", + ], + }, + "PathPart": "data", + "RestApiId": { + "Ref": "ApiDiaryApi1E03348A", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "ApiDiaryApidataGET0A09C29A": { + "Properties": { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "ApiCognitoAuthorizer23B91BA2", + }, + "HttpMethod": "GET", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":apigateway:", + { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "DiarygetDiaryDataFunctionAAA49ECF", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": { + "Ref": "ApiDiaryApidataA4E0EFBE", + }, + "RestApiId": { + "Ref": "ApiDiaryApi1E03348A", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "ApiDiaryApidataGETApiPermissionCdkSampleStackApiDiaryApi4AB3EF3AGETdata3D3C2AA4": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DiarygetDiaryDataFunctionAAA49ECF", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiDiaryApi1E03348A", + }, + "/", + { + "Ref": "ApiDiaryApiDeploymentStageprodD9912276", + }, + "/GET/data", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ApiDiaryApidataGETApiPermissionTestCdkSampleStackApiDiaryApi4AB3EF3AGETdata96E49F5D": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DiarygetDiaryDataFunctionAAA49ECF", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiDiaryApi1E03348A", + }, + "/test-invoke-stage/GET/data", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, "ApiDiaryApidiary4B91FACA": { "Properties": { "ParentId": { From 58f9baacc0ed598b0465cf5b5002b8b539bf254d Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 13:02:18 +0900 Subject: [PATCH 35/44] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=88=E3=83=AB?= =?UTF-8?q?=E3=81=8C=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=81=AF=E7=A9=BA?= =?UTF-8?q?=E3=81=AEString=E3=82=92=E8=BF=94=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index c432a4d5..849b7e66 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -132,8 +132,8 @@ def get_title(user_id: str, date: str) -> Optional[str]: response = generative_ai_table.get_item(Key={"user_id": user_id, "date": date}) return response.get("Item", {}).get("title") except ClientError as e: - logger.error(f"DynamoDB client error: {e.response['Error']['Message']}") - raise + logger.info(f"DynamoDB client error: {e.response['Error']['Message']}") + return "" def get_body(user_id: str, date: str) -> Optional[str]: From a2b9cdaa39972846b1dff80fdb1d0c20b8aa75ad Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 13:31:41 +0900 Subject: [PATCH 36/44] =?UTF-8?q?=E3=83=96=E3=83=BC=E3=82=B1=E3=81=AE?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E6=9C=89=E7=84=A1=E3=81=AE=E8=BF=94=E3=82=8A?= =?UTF-8?q?=E5=80=A4=E3=82=92bool=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 849b7e66..01bf8d9a 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -188,11 +188,13 @@ def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> """ bouquet_table_name = os.getenv("BOUQUET_TABLE_NAME") bouquet_table = dynamodb.Table(bouquet_table_name) - bouquet_response = bouquet_table.get_item( - Key={"user_id": user_id, "year_week": f"{current_year}-{current_week}"} - ) - return "Item" in bouquet_response - + try: + bouquet_response = bouquet_table.get_item( + Key={"user_id": user_id, "year_week": f"{current_year}-{current_week}"} + ) + return True + except ClientError as e: + return False def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> int: """ From e20999612b34f2610442247ca82b02612edf1bec Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 13:33:48 +0900 Subject: [PATCH 37/44] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E5=A4=89?= =?UTF-8?q?=E6=95=B0=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 01bf8d9a..6a062e3a 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -189,11 +189,11 @@ def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> bouquet_table_name = os.getenv("BOUQUET_TABLE_NAME") bouquet_table = dynamodb.Table(bouquet_table_name) try: - bouquet_response = bouquet_table.get_item( + bouquet_table.get_item( Key={"user_id": user_id, "year_week": f"{current_year}-{current_week}"} ) return True - except ClientError as e: + except ClientError: return False def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> int: From ba10f4d944255acf4e7496b8be7d674e84d6d5df Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 13:39:10 +0900 Subject: [PATCH 38/44] =?UTF-8?q?=E3=83=87=E3=83=90=E3=83=83=E3=82=B0?= =?UTF-8?q?=E7=94=A8=E3=82=B3=E3=83=BC=E3=83=89=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 6a062e3a..bf39e3dc 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -251,8 +251,11 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: current_year, current_week = get_current_week() bouquet_created = check_bouquet_created(user_id, current_year, current_week) + logger.info(f"bouquet create : {bouquet_created}") flower_count = count_flowers_in_week(user_id, current_year, current_week) + logger.info(f"flower count: {flower_count}") can_create_bouquet = not bouquet_created and flower_count >= 5 + logger.info(f"can : {can_create_bouquet}") response_data = { "image": image, From 5c6c264628619532d7c0b81e36d64e1e4a6db379 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 13:48:27 +0900 Subject: [PATCH 39/44] =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E?= =?UTF-8?q?=E3=83=83=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 8 ++++---- requirements-dev.lock | 2 +- requirements.lock | 2 +- src/backend/lambda/get_diary_data/get_diary_data.py | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c57256e..7bbca581 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,10 +3,10 @@ name = "backend" version = "0.1.0" description = "Add your description here" dependencies = [ - "pytest>=8.3.3", - "boto3>=1.35.57", - "moto>=5.0.20", - "ruff>=0.7.3", + "pytest>=8.3.3", + "boto3>=1.35.57", + "moto>=5.0.20", + "ruff>=0.8.4", ] readme = "README.md" requires-python = ">= 3.8" diff --git a/requirements-dev.lock b/requirements-dev.lock index 9128731d..24be7ccd 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -59,7 +59,7 @@ requests==2.32.3 # via responses responses==0.25.3 # via moto -ruff==0.7.3 +ruff==0.8.4 # via backend s3transfer==0.10.3 # via boto3 diff --git a/requirements.lock b/requirements.lock index 9128731d..24be7ccd 100644 --- a/requirements.lock +++ b/requirements.lock @@ -59,7 +59,7 @@ requests==2.32.3 # via responses responses==0.25.3 # via moto -ruff==0.7.3 +ruff==0.8.4 # via backend s3transfer==0.10.3 # via boto3 diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index bf39e3dc..524a2fd0 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -196,6 +196,7 @@ def check_bouquet_created(user_id: str, current_year: int, current_week: int) -> except ClientError: return False + def count_flowers_in_week(user_id: str, current_year: int, current_week: int) -> int: """ 現在の週にS3にある花の数をカウントします。 From 8e91a7bb5c7ab667c68a52579a91187fbd8d0faf Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 15:41:46 +0900 Subject: [PATCH 40/44] =?UTF-8?q?bouquet=20table=E3=82=92=20diary=20?= =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=82=B9=E3=83=88=E3=83=A9=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=81=AB=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lib/backend-stack.ts | 1 + src/backend/lib/constructs/bouquet.ts | 22 +---- src/backend/lib/constructs/diary.ts | 21 ++++- .../test/__snapshots__/snapshot.test.ts.snap | 81 ++++++++++--------- 4 files changed, 67 insertions(+), 58 deletions(-) diff --git a/src/backend/lib/backend-stack.ts b/src/backend/lib/backend-stack.ts index 84e7d6a3..0d52de5d 100644 --- a/src/backend/lib/backend-stack.ts +++ b/src/backend/lib/backend-stack.ts @@ -66,6 +66,7 @@ export class BackendStack extends cdk.Stack { const bouquet = new Bouquet(this, 'Bouquet', { userPool: auth.userPool, table: flower.table, + bouquetTable: diary.bouquetTable, api: api.api, generativeAiTable: flower.generativeAiTable, cognitoAuthorizer: api.cognitoAuthorizer, diff --git a/src/backend/lib/constructs/bouquet.ts b/src/backend/lib/constructs/bouquet.ts index 253c0d68..3c494d44 100644 --- a/src/backend/lib/constructs/bouquet.ts +++ b/src/backend/lib/constructs/bouquet.ts @@ -1,7 +1,7 @@ import * as cdk from 'aws-cdk-lib' import * as apigateway from 'aws-cdk-lib/aws-apigateway' import type * as cognito from 'aws-cdk-lib/aws-cognito' -import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' +import type * as dynamodb from 'aws-cdk-lib/aws-dynamodb' import * as lambda from 'aws-cdk-lib/aws-lambda' import * as s3 from 'aws-cdk-lib/aws-s3' import { Construct } from 'constructs' @@ -9,6 +9,7 @@ import { Construct } from 'constructs' export interface BouquetProps { userPool: cognito.UserPool table: dynamodb.Table + bouquetTable: dynamodb.Table api: apigateway.RestApi generativeAiTable: dynamodb.Table cognitoAuthorizer: apigateway.CognitoUserPoolsAuthorizer @@ -37,21 +38,6 @@ export class Bouquet extends Construct { }) bouquetBucket.grantRead(bouquetGetFunction) - // 花束の作成情報を保存するDynamoDBテーブルの作成 - const bouquetTable = new dynamodb.Table(this, 'BouquetTable', { - partitionKey: { - name: 'user_id', - type: dynamodb.AttributeType.STRING, - }, - sortKey: { - name: 'year_week', - type: dynamodb.AttributeType.STRING, - }, - removalPolicy: cdk.RemovalPolicy.DESTROY, - pointInTimeRecovery: true, - stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, - }) - //花束作成用Lambda関数の定義 const BouquetCreate = new lambda.Function(this, 'BouquetCreate', { runtime: lambda.Runtime.PYTHON_3_11, @@ -65,13 +51,13 @@ export class Bouquet extends Construct { }), environment: { GENERATIVE_AI_TABLE_NAME: props.generativeAiTable.tableName, - BOUQUET_TABLE_NAME: bouquetTable.tableName, + BOUQUET_TABLE_NAME: props.bouquetTable.tableName, ORIGINAL_IMAGE_BUCKET_NAME: props.originalImageBucket.bucketName, BOUQUET_BUCKET_NAME: bouquetBucket.bucketName, }, }) props.generativeAiTable.grantReadData(BouquetCreate) - bouquetTable.grantWriteData(BouquetCreate) + props.bouquetTable.grantWriteData(BouquetCreate) props.originalImageBucket.grantRead(BouquetCreate) bouquetBucket.grantPut(BouquetCreate) diff --git a/src/backend/lib/constructs/diary.ts b/src/backend/lib/constructs/diary.ts index ad12f0ab..fffd6a89 100644 --- a/src/backend/lib/constructs/diary.ts +++ b/src/backend/lib/constructs/diary.ts @@ -1,7 +1,7 @@ import * as cdk from 'aws-cdk-lib' import * as apigateway from 'aws-cdk-lib/aws-apigateway' import type * as cognito from 'aws-cdk-lib/aws-cognito' -import type * as dynamodb from 'aws-cdk-lib/aws-dynamodb' +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' import * as lambda from 'aws-cdk-lib/aws-lambda' import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources' import type * as s3 from 'aws-cdk-lib/aws-s3' @@ -20,6 +20,7 @@ export interface DiaryProps { export class Diary extends Construct { public readonly diaryTableEventSource: DynamoEventSource + public readonly bouquetTable: dynamodb.Table constructor(scope: Construct, id: string, props: DiaryProps) { super(scope, id) @@ -178,6 +179,21 @@ export class Diary extends Construct { authorizer: props.cognitoAuthorizer, }) + // 花束の作成情報を保存するDynamoDBテーブルの作成 + const bouquetTable = new dynamodb.Table(this, 'BouquetTable', { + partitionKey: { + name: 'user_id', + type: dynamodb.AttributeType.STRING, + }, + sortKey: { + name: 'year_week', + type: dynamodb.AttributeType.STRING, + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + pointInTimeRecovery: true, + stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES, + }) + const getDiaryDataFunction = new lambda.Function(this, 'getDiaryDataFunction', { runtime: lambda.Runtime.PYTHON_3_11, handler: 'get_diary_data.lambda_handler', @@ -186,6 +202,7 @@ export class Diary extends Construct { GENERATIVE_AI_TABLE_NAME: props.generativeAiTable.tableName, DIARY_TABLE_NAME: props.table.tableName, FLOWER_BUCKET_NAME: props.flowerBucket.bucketName, + BOUQUET_TABLE_NAME: bouquetTable.tableName, }, timeout: cdk.Duration.seconds(30), }) @@ -198,5 +215,7 @@ export class Diary extends Construct { diaryDataApi.addMethod('GET', new apigateway.LambdaIntegration(getDiaryDataFunction), { authorizer: props.cognitoAuthorizer, }) + + this.bouquetTable = bouquetTable } } diff --git a/src/backend/test/__snapshots__/snapshot.test.ts.snap b/src/backend/test/__snapshots__/snapshot.test.ts.snap index f6d87510..098308b4 100644 --- a/src/backend/test/__snapshots__/snapshot.test.ts.snap +++ b/src/backend/test/__snapshots__/snapshot.test.ts.snap @@ -2076,7 +2076,7 @@ exports[`Snapshot test 1`] = ` "Ref": "BouquetbouquetBucket080B429F", }, "BOUQUET_TABLE_NAME": { - "Ref": "BouquetBouquetTableE43AF35D", + "Ref": "DiaryBouquetTableF76EF0C5", }, "GENERATIVE_AI_TABLE_NAME": { "Ref": "FlowergenerativeAiTable021268D8", @@ -2169,7 +2169,7 @@ exports[`Snapshot test 1`] = ` "Resource": [ { "Fn::GetAtt": [ - "BouquetBouquetTableE43AF35D", + "DiaryBouquetTableF76EF0C5", "Arn", ], }, @@ -2245,43 +2245,6 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Policy", }, - "BouquetBouquetTableE43AF35D": { - "DeletionPolicy": "Delete", - "Properties": { - "AttributeDefinitions": [ - { - "AttributeName": "user_id", - "AttributeType": "S", - }, - { - "AttributeName": "year_week", - "AttributeType": "S", - }, - ], - "KeySchema": [ - { - "AttributeName": "user_id", - "KeyType": "HASH", - }, - { - "AttributeName": "year_week", - "KeyType": "RANGE", - }, - ], - "PointInTimeRecoverySpecification": { - "PointInTimeRecoveryEnabled": true, - }, - "ProvisionedThroughput": { - "ReadCapacityUnits": 5, - "WriteCapacityUnits": 5, - }, - "StreamSpecification": { - "StreamViewType": "NEW_AND_OLD_IMAGES", - }, - }, - "Type": "AWS::DynamoDB::Table", - "UpdateReplacePolicy": "Delete", - }, "BouquetbouquetBucket080B429F": { "DeletionPolicy": "Retain", "Properties": { @@ -2688,6 +2651,43 @@ exports[`Snapshot test 1`] = ` }, "Type": "AWS::IAM::Role", }, + "DiaryBouquetTableF76EF0C5": { + "DeletionPolicy": "Delete", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "user_id", + "AttributeType": "S", + }, + { + "AttributeName": "year_week", + "AttributeType": "S", + }, + ], + "KeySchema": [ + { + "AttributeName": "user_id", + "KeyType": "HASH", + }, + { + "AttributeName": "year_week", + "KeyType": "RANGE", + }, + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true, + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Delete", + }, "DiaryTitleGenerateLambda7612098A": { "DependsOn": [ "DiarygenerativeAiLambdaRoleDefaultPolicy5FC5F15D", @@ -3417,6 +3417,9 @@ exports[`Snapshot test 1`] = ` }, "Environment": { "Variables": { + "BOUQUET_TABLE_NAME": { + "Ref": "DiaryBouquetTableF76EF0C5", + }, "DIARY_TABLE_NAME": { "Ref": "FlowerdiaryContentsTableCA7C6940", }, From e6629c1b4e133982a77c9c558735680cd93fa320 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 15:49:08 +0900 Subject: [PATCH 41/44] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E8=BF=94?= =?UTF-8?q?=E3=82=8A=E5=80=A4=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 524a2fd0..1b792944 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -262,8 +262,6 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: "image": image, "title": title or "", "body": body or "", - "has_title": bool(title), - "has_body": bool(body), "can_create_bouquet": can_create_bouquet, } From 8168a96fa944d9a925179d45be795fe32cc51518 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 15:55:51 +0900 Subject: [PATCH 42/44] =?UTF-8?q?=E3=83=87=E3=83=90=E3=83=83=E3=82=B0?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/lambda/get_diary_data/get_diary_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/lambda/get_diary_data/get_diary_data.py b/src/backend/lambda/get_diary_data/get_diary_data.py index 1b792944..9ac96795 100644 --- a/src/backend/lambda/get_diary_data/get_diary_data.py +++ b/src/backend/lambda/get_diary_data/get_diary_data.py @@ -256,7 +256,7 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: flower_count = count_flowers_in_week(user_id, current_year, current_week) logger.info(f"flower count: {flower_count}") can_create_bouquet = not bouquet_created and flower_count >= 5 - logger.info(f"can : {can_create_bouquet}") + logger.info(f"can create bouquet: {can_create_bouquet}") response_data = { "image": image, From cb9e39c5cb743603a209353b01805a50ea4ee641 Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 07:13:39 +0000 Subject: [PATCH 43/44] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/pytest/test_get_diary_data.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/backend/test/pytest/test_get_diary_data.py diff --git a/src/backend/test/pytest/test_get_diary_data.py b/src/backend/test/pytest/test_get_diary_data.py new file mode 100644 index 00000000..8368b680 --- /dev/null +++ b/src/backend/test/pytest/test_get_diary_data.py @@ -0,0 +1,128 @@ +import base64 +import os +from unittest.mock import MagicMock, patch + +from get_diary_data.get_diary_data import ( + get_body, + get_flower_id, + get_image, + get_title, + validate_date, +) + +# 環境変数を設定 +os.environ["AWS_DEFAULT_REGION"] = "ap-northeast-1" +os.environ["GENERATIVE_AI_TABLE_NAME"] = "test_generative_ai_table" + + +# validate_dateのテスト +def test_validate_date(): + """日付形式のバリデーションが正しく動作するかをテスト""" + assert validate_date("2024-03-15") is True + assert ( + validate_date("2024-03-32") is True + ) # フォーマットは正しいが無効な日付は後で検証 + assert validate_date("20240315") is False + assert validate_date("invalid-date") is False + + +# get_flower_idのテスト +@patch("get_diary_data.get_diary_data.dynamodb.Table") +def test_get_flower_id(mock_dynamodb_table): + """DynamoDBからflower_idを取得する関数のテスト""" + mock_table = MagicMock() + mock_table.get_item.return_value = {"Item": {"flower_id": "flower-123"}} + mock_dynamodb_table.return_value = mock_table + + result = get_flower_id("test-user-id", "2024-03-15") + assert result == "flower-123" + + mock_table.get_item.assert_called_once_with( + Key={"user_id": "test-user-id", "date": "2024-03-15"} + ) + + +@patch("get_diary_data.get_diary_data.dynamodb.Table") +def test_get_flower_id_no_item(mock_dynamodb_table): + """DynamoDBにアイテムが存在しない場合のテスト""" + mock_table = MagicMock() + mock_table.get_item.return_value = {} + mock_dynamodb_table.return_value = mock_table + + result = get_flower_id("test-user-id", "2024-03-15") + assert result is None + + +# get_imageのテスト +@patch("get_diary_data.get_diary_data.boto3.client") +def test_get_image(mock_boto3_client): + """S3から画像を取得する関数のテスト""" + os.environ["FLOWER_BUCKET_NAME"] = "test_flower_bucket" + mock_s3_client = MagicMock() + mock_boto3_client.return_value = mock_s3_client + test_image_data = b"test_image_data" + mock_s3_client.get_object.return_value = { + "Body": MagicMock(read=MagicMock(return_value=test_image_data)) + } + + result = get_image("flower-123") + assert result == base64.b64encode(test_image_data).decode("utf-8") + + mock_s3_client.get_object.assert_called_once_with( + Bucket="test_flower_bucket", + Key="flowers/flower-123.png", + ) + + +# get_titleのテスト +@patch("get_diary_data.get_diary_data.dynamodb.Table") +def test_get_title(mock_dynamodb_table): + """DynamoDBからタイトルを取得する関数のテスト""" + mock_table = MagicMock() + mock_table.get_item.return_value = {"Item": {"title": "Test Title"}} + mock_dynamodb_table.return_value = mock_table + + result = get_title("test-user-id", "2024-03-15") + assert result == "Test Title" + + mock_table.get_item.assert_called_once_with( + Key={"user_id": "test-user-id", "date": "2024-03-15"} + ) + + +@patch("get_diary_data.get_diary_data.dynamodb.Table") +def test_get_title_no_item(mock_dynamodb_table): + """DynamoDBにタイトルが存在しない場合のテスト""" + mock_table = MagicMock() + mock_table.get_item.return_value = {} + mock_dynamodb_table.return_value = mock_table + + result = get_title("test-user-id", "2024-03-15") + assert result == None + + +# get_bodyのテスト +@patch("get_diary_data.get_diary_data.dynamodb.Table") +def test_get_body(mock_dynamodb_table): + """DynamoDBから本文を取得する関数のテスト""" + mock_table = MagicMock() + mock_table.get_item.return_value = {"Item": {"body": "Test Body"}} + mock_dynamodb_table.return_value = mock_table + + result = get_body("test-user-id", "2024-03-15") + assert result == "Test Body" + + mock_table.get_item.assert_called_once_with( + Key={"user_id": "test-user-id", "date": "2024-03-15"} + ) + + +@patch("get_diary_data.get_diary_data.dynamodb.Table") +def test_get_body_no_item(mock_dynamodb_table): + """DynamoDBに本文が存在しない場合のテスト""" + mock_table = MagicMock() + mock_table.get_item.return_value = {} + mock_dynamodb_table.return_value = mock_table + + result = get_body("test-user-id", "2024-03-15") + assert result is None From 5d96fc8267860ebd9cda9a918beb0204aa18963f Mon Sep 17 00:00:00 2001 From: shoji Date: Wed, 1 Jan 2025 16:15:58 +0900 Subject: [PATCH 44/44] =?UTF-8?q?=3D=3D=20=E3=82=92=20is=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/test/pytest/test_get_diary_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/test/pytest/test_get_diary_data.py b/src/backend/test/pytest/test_get_diary_data.py index 8368b680..3a6b106b 100644 --- a/src/backend/test/pytest/test_get_diary_data.py +++ b/src/backend/test/pytest/test_get_diary_data.py @@ -98,7 +98,7 @@ def test_get_title_no_item(mock_dynamodb_table): mock_dynamodb_table.return_value = mock_table result = get_title("test-user-id", "2024-03-15") - assert result == None + assert result is None # get_bodyのテスト