Amplifyを使って招待機能を実装してみた

#はじめに
こんにちは！この記事は「 [AWS Amplify Advent Calendar 2021] (https://qiita.com/advent-calendar/2021/amplify) 」7日目の記事です。
私は大学を1年間休学してtoB向けアプリ開発をしているスタートアップで長期エンジニアインターンをしております。

Amplifyを用いる中で、ユーザーの招待機能を実装したので、その方法について共有させていただきたいと思います！
一つの実装例として、どなたかのご参考になればと思います

[注意] 今回はフロントエンド部分の実装には触れず、バックエンドの実装のご紹介のみとさせていただきます。



#要件
実装に当たって下記要件を満たす必要がありました。

* 招待したいユーザーのメールアドレス宛に招待メールが送信できること
* 一度招待したユーザーのメールアドレス宛に招待メールが再送できること
* 招待時に送信するメールと通常のユーザーサインアップ時に送信するメールを動的に使い分ける
* 招待するユーザーのcognitoのユーザー名は一意であること



#実際に作ってみる
ユーザーの招待(作成）にはcognitoのsdkの[AdminCreateUserCommand](https://docs.aws.amazon.com/ja_jp/AWSJavaScriptSDK/v3/latest/clients/client-cognito-identity-provider/classes/admincreateusercommand.html)を用いていきます。






##1.cognitoのsdkを実行するLambdaを作成する

今回は、cognitoのsdkの[AdminCreateUserCommand](https://docs.aws.amazon.com/ja_jp/AWSJavaScriptSDK/v3/latest/clients/client-cognito-identity-provider/classes/admincreateusercommand.html)をLambdaで実行していきたいと思います。

[amplify CLI](https://docs.amplify.aws/cli/function/)でLambdaを作成します。

```
amplify add function
```
要件にも記述した通り、「1度招待（作成）したユーザーのメールアドレス宛にメールを再送できること」が条件だったため、そちらを考慮した作りにしました。

また、「招待するユーザーのcognitoのユーザー名は一意であること」という要件を達成するために、また、招待時に作成するユーザー名は[uuidv4](https://www.npmjs.com/package/uuidv4)を用いてランダムな文字列を作成することで対応しました。

「ユーザー名をランダムな文字列に設定してサインインの際に困らないの？」と思われるかもしれませんが、cognitoのユーザープール作成時に[属性](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-aliases-settings)にてエンドユーザーが**Email**と**任意のユーザー名**のどちらのサインインにも対応できるようにしてあるので問題ありません。

```typescript:作成したLambdaの一部抜粋
/**
 * 既にユーザーが存在している場合メールを再送
 */
const resendEmail = async (
  client: CognitoIdentityProviderClient,
  userPoolID: string,
  username: string,
): Promise<void> => {
  const resendEmailParams = {
    UserPoolId: userPoolID,
    DesiredDeliveryMediums: ["EMAIL"],
    Username: username,
    //メールの再送を設定
    MessageAction: "RESEND",
  };
  await client.send(new AdminCreateUserCommand(resendEmailParams));

};
//中略


/**
 * ユーザーを作成する
 *
 * 同じ username が存在する場合にはメールを再送する
 */
export const createUser = async (
  client: CognitoIdentityProviderClient,
  userPoolID: string,
  params: HandlerArgument["arguments"]
): Promise<void> => {
  try {
    //一度ユーザーが作成されている前提でメールの再送を試みる
    await resendEmail(
      cognito,
      userPoolID,
      username,
      firstName,
      lastName
    );
  } catch (e) {
    // 同一ユーザーが存在しないため、エラーを握りつぶしてユーザーを新規作成
  }

  const createUserParams = {
    UserPoolId: userPoolID,
    DesiredDeliveryMediums: ["EMAIL"],
    Username: username,
    UserAttributes: [
      {
        Name: "email",
        Value: email,
      },
      {
        Name: "email_verified",
        Value: "true",
      },
    ],
  };

await client.send(new AdminCreateUserCommand(createUserParams));
```

##2.作成したLambdaをクライアントから呼び出す
GraphQL API経由で先ほど作成したLambda関数をクライアントから呼び出します。
schemaの ```type Mutation```にて下記のように記述します。

```:schema.graphql
type Mutation
{
 inviteUser(
    username: String!
    email: String!
    group: String!
  ):inviteUserResponse
  @function(name: "inviteUser-${env}")
  @auth(rules: [
    {allow: private, provider: userPools},
  ])
　　　　　　//省略
  ])
}

```
次に適切な権限設定を行った[AppSyncClient](https://www.npmjs.com/package/appsync-client)を用いて下記でLambdaを呼び出します。

```javascript:
const client = new AppsyncClient({
//適切な権限設定を行う
    url: "",
    region:""
    //etc...
});

await client.mutate({
      mutation: gql(inviteUser),
      variables: variable,
    })
```
## 3.Cognitoから送信するメールを動的にカスタマイズ
さて、残すところは「招待時に送信するメールと通常のユーザーサインアップ時に送信するメールを動的に使い分ける」という要件のみです。こちらは、[カスタムメッセージのLambdaトリガー](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-lambda-custom-message.html)を使用して対応しました。

こちらのLambdaも[Amplify CLI](https://docs.amplify.aws/cli/usage/lambda-triggers/)から追加することができます。

```
amplify update auth
```

Lambdaトリガーの中身はこちらになります。
eventとしてCognitoからLambdaへ受け取ったtriggerSource値に応じて動的に送信する内容を変えます。
triggerSourceが```CustomMessage_AdminCreateUser```の際に招待ユーザー用のサインアップ画面のURL添付することで、招待を受けたユーザーがそのリンクからサインアップを完了させます。

```javascript:カスタムメッセージのLambdaトリガー

    switch (event.triggerSource) {
      case "CustomMessage_SignUp":
        event.response.emailMessage = //サインアップ時のメッセージ
        break;

      case "CustomMessage_AdminCreateUser":
        event.response.emailMessage = //招待時のメッセージ&招待ユーザー用のサインアップ画面のURLを貼る
        break;

      case "CustomMessage_ForgotPassword":
        event.response.emailMessage = //パスワード忘れに対応したメッセージ
        break;
       //省略
    }
   return event;
```
これですべての要件を満たした招待機能が実装できました。

#おわりに
今回はフロントエンドについては触れず、バックエンド部分の実装方法についてご紹介させていただきました！

このやり方以外にも、招待機能を実装する方法はいろいろあると思います。
Amplify含めたAWSのサービスについて、まだまだ勉強中の身なので「ここはこうした方が良い」といったアドバイスいただけるととてもありがたいです！