diff --git a/src/api/issueAccessToken.ts b/src/api/issueAccessToken.ts index c1f93a5..c9f5100 100644 --- a/src/api/issueAccessToken.ts +++ b/src/api/issueAccessToken.ts @@ -77,7 +77,8 @@ export const issueAccessToken = async ( if (isCognitoTokenResponseBody(responseBody)) { await dto.cacheClient.put(dto.cognitoClientId, responseBody.access_token, { - expirationTtl: 3600, + // トークンの有効期限が3600秒なのでそれよりも10分早い3000秒をcacheの有効期限とする + expirationTtl: 3000, }); const issueAccessTokenResponse = { diff --git a/src/bindings.d.ts b/src/bindings.d.ts new file mode 100644 index 0000000..562e9b9 --- /dev/null +++ b/src/bindings.d.ts @@ -0,0 +1,9 @@ +export type Bindings = { + APP_ENV: 'staging' | 'production'; + COGNITO_CLIENT_ID: string; + COGNITO_CLIENT_SECRET: string; + COGNITO_TOKEN_ENDPOINT: `https://${string}`; + IMAGE_RECOGNITION_API_URL: `https://${string}`; + LGTMEOW_API_URL: `https://${string}`; + COGNITO_TOKEN: KVNamespace; +}; diff --git a/src/handlers/handleFetchLgtmImagesInRandom.ts b/src/handlers/handleFetchLgtmImagesInRandom.ts new file mode 100644 index 0000000..d218043 --- /dev/null +++ b/src/handlers/handleFetchLgtmImagesInRandom.ts @@ -0,0 +1,93 @@ +import type { CacheClient } from '../api/cacheClient'; +import { fetchLgtmImagesInRandom } from '../api/fetchLgtmImages'; +import { issueAccessToken } from '../api/issueAccessToken'; +import { isValidationErrorResponse } from '../api/validationErrorResponse'; +import { httpStatusCode } from '../httpStatusCode'; +import { isFailureResult } from '../result'; +import { + createErrorResponse, + createSuccessResponse, + createValidationErrorResponse, + ResponseHeader, +} from './handlerResponse'; + +type Dto = { + env: { + cognitoTokenEndpoint: string; + cognitoClientId: string; + cognitoClientSecret: string; + apiBaseUrl: string; + cacheClient: CacheClient; + }; +}; + +export const handleFetchLgtmImagesInRandom = async ( + dto: Dto +): Promise => { + const issueTokenRequest = { + endpoint: dto.env.cognitoTokenEndpoint, + cognitoClientId: dto.env.cognitoClientId, + cognitoClientSecret: dto.env.cognitoClientSecret, + cacheClient: dto.env.cacheClient, + }; + + const issueAccessTokenResult = await issueAccessToken(issueTokenRequest); + if (isFailureResult(issueAccessTokenResult)) { + const problemDetails = { + title: 'failed to issue access token', + type: 'InternalServerError', + status: httpStatusCode.internalServerError, + } as const; + + return createErrorResponse( + problemDetails, + httpStatusCode.internalServerError + ); + } + + const fetchLgtmImagesRequest = { + apiBaseUrl: dto.env.apiBaseUrl, + accessToken: issueAccessTokenResult.value.jwtAccessToken, + }; + + const fetchLgtmImagesResult = await fetchLgtmImagesInRandom( + fetchLgtmImagesRequest + ); + + const headers: ResponseHeader = { + 'Content-Type': 'application/json', + }; + + if (fetchLgtmImagesResult.value.xRequestId != null) { + headers['X-Request-Id'] = fetchLgtmImagesResult.value.xRequestId; + } + + if (fetchLgtmImagesResult.value.xLambdaRequestId != null) { + headers['X-Lambda-Request-Id'] = + fetchLgtmImagesResult.value.xLambdaRequestId; + } + + if (isFailureResult(fetchLgtmImagesResult)) { + if (isValidationErrorResponse(fetchLgtmImagesResult.value)) { + return createValidationErrorResponse( + fetchLgtmImagesResult.value.invalidParams, + headers + ); + } + + const problemDetails = { + title: 'failed to fetch lgtm images in random', + type: 'InternalServerError', + status: httpStatusCode.internalServerError, + } as const; + + return createErrorResponse( + problemDetails, + httpStatusCode.internalServerError + ); + } + + const responseBody = { lgtmImages: fetchLgtmImagesResult.value.lgtmImages }; + + return createSuccessResponse(responseBody, httpStatusCode.ok, headers); +}; diff --git a/src/handlers/handlerResponse.ts b/src/handlers/handlerResponse.ts new file mode 100644 index 0000000..1d09fba --- /dev/null +++ b/src/handlers/handlerResponse.ts @@ -0,0 +1,56 @@ +import { HttpStatusCode, httpStatusCode } from '../httpStatusCode'; +import { InvalidParams } from '../validator'; + +export type ResponseHeader = { + 'Content-Type': 'application/json'; + 'X-Request-Id'?: string; + 'X-Lambda-Request-Id'?: string; +}; + +export const createSuccessResponse = ( + body: unknown, + statusCode: HttpStatusCode = httpStatusCode.ok, + headers: ResponseHeader = { 'Content-Type': 'application/json' } +): Response => { + const jsonBody = JSON.stringify(body); + + return new Response(jsonBody, { headers, status: statusCode }); +}; + +export type ProblemDetails = { + title: string; + type: 'ResourceNotFound' | 'InternalServerError'; + status?: HttpStatusCode; + detail?: string; +}; + +export type ValidationProblemDetails = { + title: 'unprocessable entity'; + type: 'ValidationError'; + status: HttpStatusCode; + invalidParams: InvalidParams; +}; + +export const createErrorResponse = ( + problemDetails: ProblemDetails, + statusCode: HttpStatusCode = httpStatusCode.internalServerError, + headers: ResponseHeader = { 'Content-Type': 'application/json' } +): Response => createSuccessResponse(problemDetails, statusCode, headers); + +export const createValidationErrorResponse = ( + invalidParams: InvalidParams, + headers: ResponseHeader = { 'Content-Type': 'application/json' } +): Response => { + const validationProblemDetails: ValidationProblemDetails = { + title: 'unprocessable entity', + type: 'ValidationError', + status: httpStatusCode.unprocessableEntity, + invalidParams, + } as const; + + return createSuccessResponse( + validationProblemDetails, + httpStatusCode.unprocessableEntity, + headers + ); +}; diff --git a/src/index.ts b/src/index.ts index a47a003..a877b23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,31 @@ import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { Bindings } from './bindings'; +import { handleFetchLgtmImagesInRandom } from './handlers/handleFetchLgtmImagesInRandom'; -const app = new Hono(); +const app = new Hono<{ Bindings: Bindings }>(); app.get('/', (c) => c.text('Hello! Hono!')); +app.use('*', async (c, next) => { + const handler = + c.env.APP_ENV === 'production' + ? cors({ origin: 'https://lgtmeow.com' }) + : cors(); + + await handler(c, next); +}); + +app.get('/lgtm-images', async (c) => { + return await handleFetchLgtmImagesInRandom({ + env: { + cognitoTokenEndpoint: c.env.COGNITO_TOKEN_ENDPOINT, + cognitoClientId: c.env.COGNITO_CLIENT_ID, + cognitoClientSecret: c.env.COGNITO_CLIENT_SECRET, + apiBaseUrl: c.env.LGTMEOW_API_URL, + cacheClient: c.env.COGNITO_TOKEN, + }, + }); +}); + export default app; diff --git a/wrangler.toml b/wrangler.toml index cb10ab0..29a4c90 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,6 +1,12 @@ name = "lgtm-cat-bff" main = "src/index.ts" compatibility_date = "2022-12-15" +kv_namespaces = [ + { binding = "COGNITO_TOKEN", id = "79245f90d047421bb54d2d0cb0d4eeb5", preview_id = "09863c096f884e9d8009368614f9590b" } +] [env.staging] name = "staging-lgtm-cat-bff" +kv_namespaces = [ + { binding = "COGNITO_TOKEN", id = "79245f90d047421bb54d2d0cb0d4eeb5", preview_id = "09863c096f884e9d8009368614f9590b" } +]