Skip to content

Commit

Permalink
Merge pull request #311 from nekochans/feature/issue310/change-api-route
Browse files Browse the repository at this point in the history
API用のBFFはEdge Runtime利用のRoute Handlersを利用するように変更
  • Loading branch information
keitakn authored Mar 26, 2024
2 parents d775e05 + 0bb2413 commit d118afe
Show file tree
Hide file tree
Showing 43 changed files with 1,040 additions and 160 deletions.
1 change: 1 addition & 0 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ https://vercel.com/docs/cli#commands/dev/when-to-use-this-command
NEXT_PUBLIC_APP_ENV=local
NEXT_PUBLIC_APP_URL=本アプリケーションのURL、ローカルの場合は http://localhost:2222
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=GTM-から始まるGoogle Tag ManagerのIDを指定
NEXT_PUBLIC_LGTMEOW_BFF_URL=https://github.com/nekochans/lgtm-cat-bff が稼働しているURLを指定
EDGE_CONFIG=Vercel Edge ConfigのURL(Vercel上の値を参照)
COGNITO_TOKEN_ENDPOINT=/oauth2/token で終わるCognitoのエンドポイントを指定
COGNITO_CLIENT_ID=CognitoUserPoolのClient IDを指定
COGNITO_CLIENT_SECRET=CognitoUserPoolのClient Secretを指定
LGTMEOW_API_URL=https://github.com/nekochans/lgtm-cat-api が稼働しているAPIサーバーのURLを指定
IMAGE_RECOGNITION_API_URL=ねこ画像判定APIが稼働しているAPIサーバーのURLを指定
UPSTASH_REDIS_REST_URL=upstash Redis REST APIのURLを指定
UPSTASH_REDIS_REST_TOKEN=upstash Redis REST APIのトークンを指定
```

ローカルでSentryやChromaticの動作確認を実施する場合 [direnv](https://github.com/direnv/direnv) を使って `.envrc` に以下の環境変数を設定します。
Expand Down
45 changes: 44 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
},
"dependencies": {
"@sentry/nextjs": "^7.100.1",
"@upstash/ratelimit": "^1.0.1",
"@upstash/redis": "^1.29.0",
"@vercel/edge-config": "^1.0.0",
"clipboard": "^2.0.11",
"lodash.throttle": "^4.1.1",
Expand All @@ -36,7 +38,8 @@
"react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"ress": "^5.0.2",
"valtio": "^1.13.1"
"valtio": "^1.13.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
Expand Down
29 changes: 29 additions & 0 deletions src/api/cognito/IssueClientCredentialsAccessTokenError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type Options = {
statusCode?: number;
statusText?: string;
headers?: Record<string, string>;
responseBody?: unknown;
};

export class IssueClientCredentialsAccessTokenError extends Error {
static {
this.prototype.name = 'IssueClientCredentialsAccessTokenError';
}

private readonly statusCode: number | undefined;

private readonly statusText: string | undefined;

private readonly headers: Record<string, string> | undefined;

private readonly responseBody: unknown;

constructor(message = '', options: Options = {}) {
const { statusCode, statusText, headers, responseBody, ...rest } = options;
super(message, rest);
this.statusCode = statusCode;
this.statusText = statusText;
this.headers = headers;
this.responseBody = responseBody;
}
}
1 change: 1 addition & 0 deletions src/api/cognito/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { issueClientCredentialsAccessToken } from './openIdConnect';
122 changes: 122 additions & 0 deletions src/api/cognito/openIdConnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { upstashRedisRestToken, upstashRedisRestUrl } from '@/constants';
import { validation, type IssueClientCredentialsAccessToken } from '@/features';
import { Redis } from '@upstash/redis';
import { z } from 'zod';
import { IssueClientCredentialsAccessTokenError } from './IssueClientCredentialsAccessTokenError';

type CognitoTokenResponseBody = {
access_token: string;
// eslint-disable-next-line no-magic-numbers
expires_in: 3600;
token_type: 'Bearer';
};

const cognitoTokenResponseBodySchema = z.object({
access_token: z.string(),
expires_in: z.number().min(3600),
token_type: z.literal('Bearer'),
});

const isCognitoTokenResponseBody = (
value: unknown,
): value is CognitoTokenResponseBody => {
return validation(cognitoTokenResponseBodySchema, value).isValidate;
};

const cognitoClientId = (): string => {
if (process.env.COGNITO_CLIENT_ID != null) {
return process.env.COGNITO_CLIENT_ID;
}

throw new IssueClientCredentialsAccessTokenError(
'COGNITO_CLIENT_ID is not defined',
);
};

const cognitoClientSecret = (): string => {
if (process.env.COGNITO_CLIENT_SECRET != null) {
return process.env.COGNITO_CLIENT_SECRET;
}

throw new IssueClientCredentialsAccessTokenError(
'COGNITO_CLIENT_SECRET is not defined',
);
};

const cognitoTokenEndpoint = (): string => {
if (process.env.COGNITO_TOKEN_ENDPOINT != null) {
return process.env.COGNITO_TOKEN_ENDPOINT;
}

throw new IssueClientCredentialsAccessTokenError(
'COGNITO_TOKEN_ENDPOINT is not defined',
);
};

export const issueClientCredentialsAccessToken: IssueClientCredentialsAccessToken =
async () => {
const redis = new Redis({
url: upstashRedisRestUrl(),
token: upstashRedisRestToken(),
});

const cachedAccessToken = await redis.get(cognitoClientId());
if (typeof cachedAccessToken === 'string') {
return cachedAccessToken;
}

const authorization = btoa(`${cognitoClientId()}:${cognitoClientSecret()}`);

const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${authorization}`,
},
body: 'grant_type=client_credentials&scope=api.lgtmeow/all image-recognition-api.lgtmeow/all',
};

const response = await fetch(cognitoTokenEndpoint(), options);
if (!response.ok) {
const responseBody = await response.text();
const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});

throw new IssueClientCredentialsAccessTokenError(
`failed to issueAccessToken: ${response.status} ${response.statusText}`,
{
statusCode: response.status,
statusText: response.statusText,
headers,
responseBody,
},
);
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const responseBody = (await response.json()) as unknown;

if (isCognitoTokenResponseBody(responseBody)) {
await redis.set(cognitoClientId(), responseBody.access_token);
await redis.expire(cognitoClientId(), 3000);

return responseBody.access_token;
}

const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});

throw new IssueClientCredentialsAccessTokenError(
'response body is invalid',
{
statusCode: response.status,
statusText: response.statusText,
headers,
responseBody,
},
);
};
1 change: 0 additions & 1 deletion src/api/fetch/index.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './fetch';
export * from './cognito';
export * from './lgtmeow';
2 changes: 2 additions & 0 deletions src/api/lgtmeow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lgtmImage';
export { isFetchLgtmImagesResponseBody } from './validation';
Loading

0 comments on commit d118afe

Please sign in to comment.