20200803のNode.jsに関する記事は8件です。

Twitter で 自動フォロワー獲得ツール を作った話 ②

まえがき

本記事は、Twitter で 自動フォロワー獲得ツール を作った話 の続編です。
前編をご覧になっていない方は、まずは上記のリンクへどうぞ。
また、本記事に記載されている全体のソースコードは、Github からご覧いただけます。

あらすじ

Twitter のフォロワーを増やすための法則を確立したものの、
GAOGAO エンジニアたるもの、プログラミングで実装してみてはどうか」
との @tejitak のお言葉を受け、Node.js で自動化を試みるのでした。
当時は開発経験がまるでなかったため、初のアプリケーション作成になりました。

おさらい

今回の実装内容
どんなコンテンツでも、あるフォーマットに沿ってツイートすれば、ほぼ確実にライクとフォローが期待できる法則

毎日定時に決まったフォーマットでツイートする
自身のタイムラインの全ツイートを可能な限り早くライクする
自身のフォロワーで、フォロワー数の多いユーザーの、最新のツイートにライクしている全ユーザーをフォローする
③ でフォローしたユーザーの中から、フォローバックされていないユーザーのプロフィールに行き、最新のツイート 3件 にライク + ミュートする
一定期間後、フォローされていない、且つミュートになっているユーザーをリムる

⓪Twtter API の 利用

まず、Twitter Developers から API を登録しました。すると、

  • Consumer Key
  • Consumer Secret
  • Access Token
  • Access Token Secret

が発行されるので、環境変数に保存します。
これらを利用して、実際に Twitter にアクセスします。
以前は審査等なかったのですが、現在は英語での利用申請が必要です。

① 毎日定時に決まったフォーマットでツイートする

こちらは、手動でツイートしていたので、今回は割愛します。

② 自身のタイムラインの全ツイートを可能な限り早くライクする

こちらは、twitterFavメソッドにて実装しました。

masanao.js
async function twitterFav() {

  const tweetsCount = 10
  const favouritesCount = 0
  const myId = process.env.my_id

  try {
    const tweets = await twitterApi.getHtimeline(client, tweetsCount)
    let arr = []

    for(let i = 0; i < tweets.length; i++) {
      if(tweets[i].favorited !== true && tweets[i].user.id_str !== myId) {
        arr.push(tweets[i].id_str)
      }
    }

    let checkedTweets = arr.join(',')
    const favTweets = await twitterApi.getLookup(client, checkedTweets)

    for(var i = 0; i < favTweets.length; i++) {
      if(favTweets[i].favorite_count >= favouritesCount) {
        await twitterApi.postFavoriting(client, favTweets[i].id_str)
      }
    }
  } catch(err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterFav()
  • tweetsCount:タイムラインから取得するツイート数
  • favouritesCount:対象ツイートのライク数
  • process.env.my_id:実行者の TwitterID

上記を設定し、エンドポイントをまとめたtwitterApiファイルから適宜メソッドを呼び出しています。

まず、自身のタイムラインからtweetsCount分のツイートを取得。
繰り返し処理にて、ライク済みのツイートと、自分のツイート以外を配列に格納。
配列内のfavouritesCountで設定したライク数を超えているツイートにライクを実行。
という流れになっています。

favouritesCountがあることで、誰もライクしていないツイートには触れないといった、ややテクニカルなプレーが可能になります。

③ 自身のフォロワーで、フォロワー数の多いユーザーの、最新のツイートにライクしている全ユーザーをフォローする

こちらは、twitterBegメソッドにて実装しました。

hajime.js
const asyncFunc = (targetTweet) => {
  return new Promise((resolve, reject) => {
    request(`https://twitter.com/i/activity/favorited_popup?id=${targetTweet}`, (err, response, body) => {
      resolve(body)
    })
  })
}

async function twitterBeg() {
  const myId = process.env.my_id;
  const influencerId = ''
  tweetCount = 1

  try
  {
    const tweets = await twitterApi.getUtimeline(client, influencerId, tweetCount)
    const arr = []

    for (let i = 0; i < tweets.length; i++) {
      arr.push({
        id: tweets[i].id_str,
        favorite_count: tweets[i].favorite_count
      })
    }

    arr.sort((a, b) => {
      const countA = a.favorite_count
      const countB = b.favorite_count
      if (countA > countB) {
        return -1
      } else if (countA < countB) {
        return 1
      }
      return 0
    })

    const targetTweet = arr[0].id
    const body = await asyncFunc(targetTweet)
    const json = JSON.parse(body)
    const content = json.htmlUsers
    const regex = /data-user-id=\"(\d+)\"/g
    let set = new Set()

    while((m = regex.exec(content)) !== null) {
      set.add(m[1])
    }
    const userIds = Array.from(set)

    for (var i = 0; i < userIds.length; i++) {
      try {
        const friendships = await twitterApi.getRelationship(client, myId, userIds[i])
        if (friendships.relationship.source.following !== true && friendships.relationship.source.muting !== true) {
          await twitterApi.postFollowing(client, userIds[i])
        }
      } catch (err) {
        outLogger.warn(err)
        taskLogger.warn(err)
      }
      await twitterApi.sleep(5000)
    }
  } catch (err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterBeg()
  • myId:実行者の TwitterID
  • influencerId:対象ユーザーの TwitterID
  • tweetCount:ユーザータイムラインから取得するツイート数

まず、influencerIdのユーザータイムラインから、tweetCount分のツイートを取得。
その上でツイートのIDと、ライク数を抽出し、配列に格納。ライク数順にソート。
最もライク数の多いツイートの、https://twitter.com/i/activity/favorited_popup?id=${targetTweet}にアクセスし、ライクしている全ユーザーのIDをスクレイピングで取得。(*現在、Twitter の仕様が変わってしまった様で、上記サンプルコード実行できませんでした。折を見て修正予定です。)
繰り返し処理にて、取得したユーザーの中からフォロー中、ミュート中のユーザーを省き、フォローを実行。
という流れになっています。

ミュート中のユーザーを省く意図については、以前⑤でフォロー解除したユーザーへの再フォローを防止する施策になります。

④ ③ でフォローしたユーザーの中から、フォローバックされていないユーザーのプロフィールに行き、最新のツイート 3件 にライク + ミュートする

こちらは、twitterMuteメソッドにて実装しました。

tejitak.js
async function twitterMute() {

  const userId = process.env.my_id
  const friendsCount = 1
  const tweetsCount = 20
  const favoritedlimit = 3

  try {
    const friends = await twitterApi.getFriendsIds(client, userId, friendsCount)
    const targetIds = friends.ids

    for(var i = 0; i < targetIds.length; i++) {
      try {
        const friendships = await twitterApi.getRelationship(client, userId, targetIds[i])

        if(friendships.relationship.target.following !== true && friendships.relationship.source.muting !== true) {
          const friendsTweets = await twitterApi.getFriendstimeline(client, targetIds[i], tweetsCount)
          let favoritedCount = 0

          for(var j = 0; j < friendsTweets.length; j++) {
            if(friendsTweets[j].favorited !== true && favoritedCount < favoritedlimit) {
              await twitterApi.postFavoriting(client, friendsTweets[j].id_str)
              await twitterApi.sleep(1000)
              favoritedCount++
            }
          }
          await twitterApi.sleep(1000)
          await twitterApi.postMute(client, targetIds[i])
        }
      } catch(err) {
        outLogger.warn(err)
        taskLogger.warn(err)
      }
      await twitterApi.sleep(2000)
    }
  } catch(err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterMute()
  • userId:実行者の TwitterID
  • friendsCount:取得するフォロー中のユーザー数
  • tweetsCount:ユーザータイムラインから取得するツイート数
  • favoritedlimit:ライクするツイート数

まず、実行者のフォロー中のユーザーをfriendsCount分取得。
繰り返し処理にて、フォローされておらず、未だミュートしていないユーザーを抽出。
該当ユーザーのユーザータイムラインからtweetsCount分のツイートを取得。
favoritedlimitに達するまで、未だライクしていないツイートにライクを実行。
リクエスト制限対策としてsleep関数にて実行を停止した後に、フラグ用のミュートを実行。
という流れになっています。

我ながら、ミュート大好き過ぎますね。。

⑤ 一定期間後、フォローされていない、且つミュートになっているユーザーをリムる

最後に、twitterUnfollowメソッドの実装になります。

hideyoshi.js
async function twitterUnfollow() {

  const userId = process.env.my_id
  const friendsCount = 25

  try {
    const friends = await twitterApi.getFriendsIds(client, userId, friendsCount)
    const targetIds = friends.ids

    for(var i = 0; i < targetIds.length; i++) {
      try {
        const friendships = await twitterApi.getRelationship(client, userId, targetIds[i])

        if(friendships.relationship.target.following !== true && friendships.relationship.source.muting === true) {
          try {
            await twitterApi.postUnfollowing(client, targetIds[i])
          } catch(err) {
            outLogger.warn(err)
            taskLogger.warn(err)
          }
        }
      } catch(err) {
        outLogger.warn(err)
        taskLogger.warn(err)
      }
      await twitterApi.sleep(3000)
    }
  } catch(err) {
    outLogger.warn(err)
    taskLogger.warn(err)
  }

}

twitterUnfollow()
  • userId:実行者の TwitterID
  • friendsCount:取得するフォロー中のユーザー数

まず、実行者のフォロー中のユーザーをfriendsCount分取得。
繰り返し処理にて、フォローされておらず、既にミュート済みのユーザーを抽出。
フォロー解除を実行。
という流れになっています。

以上 ① - ⑤ をターミナルから定期実行していました。

まとめ

じきに @masumi_sugaeくんと一緒にプロダクト化してみようという話にもなったのですが、程なくして Twitter API 規制があり、頓挫してしまいました。
機会があれば調整して実用化したいとも思うので、ご興味のある方はぜひ。

また無知により、リーダブルコード信仰の代わりに、アニミズム信仰を持ち出して、ファイル名に自身や、身近な人の名前をつけていました。(ex. masanao.js)
今となっては完全に謎です。

今回、Twitter のフォロワーを増やす方法と題して、二週にわたって執筆して参りましたが、
この方法で獲得したフォロワーは、数字上の意味しか持たないことをご留意ください。
よって、FF比率をカモフラージュするために徒らなフォロー解除を繰り返すと、ほぼ100%相手もリムってきます。

結論:フォロワー数を増やしたいのであれば、インフルエンサーに絡もう

-次回 (8/10)
「Webサイト案件における初学者のジレンマ」
*次回は Note での記載を予定しております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twilioを使って、ブラウザで電話を受け取れるようにする

やること

Twilioのライブラリを利用して、電話の着信機能を作成します。
Twilioへの登録と、電話番号の取得が必要です。
以下がTwilio側とブラウザ側の処理の流れです。

:telephone_receiver:Twilio側の流れ

  1. Twilioが通話を受け取る
  2. Twilioが自動応答のシナリオ(TwiML)を返すAPIを呼び出す
  3. TwilioがTwiMLを読み取って、クライアント(ブラウザの着信者)を呼び出す

:desktop:ブラウザ側の流れ

  1. twilio-clientライブラリを使って、着信時の挙動を設定する
  2. クライアントのトークンを作成するAPIを呼び出す
  3. トークンを使って、着信を受け取れるようにする

APIのコードをかく:muscle:

node.jsで書きます。前もってtwilioパッケージをインストールしときましょう
npm i twilio

TwiMLを返すAPIを用意する

expressを使っています。ざっくり書きます。

import { twiml } from "twilio";
import express = require("express"); // expressを利用

const app = express();
app.route("/incoming").post((req, res) => {
  const voice = new twiml.VoiceResponse(); // ここに設定していく
  voice.say("hello"); // 音声を再生
  voice.say({ voice: "Polly.Mizuki", language: "ja-JP" }, "日本語でおk"); // 日本語で再生(voiceなどは設定可能)
  voice.pause({ length: 1 }); // 1秒待つ

  // クライアント名を指定して電話をかける
  voice
    .dial({ callerId: req.body.From || "", ringTone: "jp" }) // オプション
    .client("[クライアント名]");

  const response = voice.toString(); // TwiML(XML)を作成

  res.type("text/xml");
  res.send(response); // レスポンスを返す
});

クライアントのトークンを作って返すAPIを用意する

ざっくり書きます。

import { jwt } from "twilio";

// capabilityに能力を足していく
const capability = new jwt.ClientCapability({
  accountSid: "TwilioアカウントのSID",
  authToken: "Twilioアカウントのトークン",
  ttl: 600, // トークンの有効期限。最大24H(86400)らしい
});
capability.addScope(
  new jwt.ClientCapability.IncomingClientScope("[クライアント名]"),
);
// 今は使わないが、外にかけるときに使う
// capability.addScope(
//   new jwt.ClientCapability.OutgoingClientScope({
//     applicationSid: "TwilioAppのSID",
//   })
);
const token = capability.toJwt(); // これを返す

TwilioコンソールでAPIを登録する:two_men_holding_hands:

↓コンソールはこれです
https://jp.twilio.com/console/

電話番号 / 番号の管理 / アクティブな電話番号 /の「A CALL COMES IN」にTwiMLを返すAPIを指定します。

クライアント側のコードをかく:muscle:

js(typescript)で書きます。JSフレームワークは好きなの選択。

前もってtwilio-clientをインストールしときましょう
(間違ってnpm i twilioをしないように気をつけてください。こっちはサーバ向けです)

デバイスのセットアップ方法を、ざっくり書きます。

import { Device, Connection } from "twilio-client";

const device = new Device(); // ここに処理をのっける

device.on("incoming", (connection: Connection) => {
  const onAccept = () => {
    connection.accept(); // 受け取る
  };
  const onReject = () => {
    connection.reject(); // 拒否る
  };

  openIncomingCallModal(onAccept, onReject); // Viewの部分はお任せ
});

その後、前に作った「クライアントのトークンを作って返すAPI」を呼び出して、トークンを取得します(コード略)。
トークンを受け取ったら、それを使ってデバイスをセットアップ。

device.setup(jwt);

これでOK:ok_hand:

ちなみに、こちらから通話を切るときはdisconnectAll()を呼びます。

device.disconnectAll();

まだ機能が少ないので、ドキュメントをみて、諸々の機能を足していくといいかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsよりODBCでOracle, Salesforceなどに1ドライバで接続

Connect to Oracle, Salesforce and others using ODBC drivers from Node.js

はじめに

Node.jsアプリケーションからODBCでSQL Server、Oracle、DB2、Postgres、MongoDBなどのDBや、Salesforce、Eloqua、Oracle Sales Cloud、Oracle Service CloudなどのSaaSアプリケーションに接続する必要性があるならば、DataDirect ODBCドライバを利用すると楽勝です。

この記事では、DataDirect ODBC ドライバを使用して Node.js から Oracle データベースに接続してみます。

事前準備

Node.js

DataDirect Oracle ODBC ドライバのダウンロード&インストール

1,ここからOracleデータベース用の64ビットODBCドライバをダウンロードします。

2,パッケージを展開し、インストーラーを実行してODBCドライバをインストールします。

3,ODBC Administrator(64ビット)を開き、新しいデータソースを追加するには、[追加]をクリックします。

image.png

4,ドライバに「DataDirect 8.0 Oracle Wire Protocol」を選択します。

image.png

5,ここで、ホスト名、ポート、SID/サービス名などの接続情報を入力し、Test Connectをクリックして接続の詳細を確認します。

image.png

Node.js ODBC パッケージのインストール

1,Node.jsでODBCドライバを使用するために、node-odbcパッケージをインストールします。

2,以下を実行し、パッケージをインストールします。

npm i odbc

Node.jsよりOralceに接続

1,以下はDataDirect ODBC ドライバを使用してOracle に接続し、テーブルにクエリを実行するサンプル コードです。

var db = require('odbc')()
    , cn = "Dsn=Oracle;UID=saikrishnabobba;PWD=<password>;"
//open the connection
db.open(cn, function (err) {
    if (err) {
      return console.log(err);
    }
// Run a sample query
     db.query("select * from COUNTRIES", function (err, rows, moreResultSets) {

      if (err) {
        return console.log(err);
      }

      console.log(rows);
     });
  });

2,DSN、ユーザー名、パスワード、クエリを要件に応じて変更してください。上記のコードを実行すると、テーブルのデータをコンソールに出力します。

簡単ですね!
他のRBD、SaaS、NoSQLも手順は同様です。適切なデータソース用の DataDirect ODBC ドライバで、お好みのデータソースに接続おできます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Bitbucketからコンテナサービスにアプリをデプロイする方法

今回のチュートリアルでは、BitbucketからAlibaba Cloudコンテナサービスにアプリケーションをデプロイし、Let's Encrypt SSL/TLS 2019でセキュリティを確保します。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

Docker

Dockerは、コンテナを使ってアプリケーションを作成、デプロイ、実行するために設計されたオープンソース・ソフトウェアです。コンテナを利用することで、開発者はアプリケーションに必要なライブラリやその他の依存関係などをすべてパッケージ化し、1つのパッケージとして出荷することができます。そうすることで、コンテナのおかげで、開発者は、コードを書いたりテストしたりするために使用するマシンとは異なるカスタマイズされた設定があっても、他の Linux マシン上でアプリケーションを実行することができるので、安心してください。

コンテナは仮想マシンに少し似ています。しかし、仮想マシンとは異なり、仮想オペレーティングシステム全体を作成するのではなく、Dockerではアプリケーションは実行中のシステムと同じLinuxカーネルを使用することができ、アプリケーションはホストコンピュータ上で実行されていないものと一緒に出荷される必要があるだけです。これにより、パフォーマンスが大幅に向上し、アプリケーションのサイズを小さくすることができます。

このチュートリアルでは、Dockerを使ってアプリケーションをコンテナ化し、Alibaba Cloud Container Service上でデプロイして実行できるようにします。

Bitbucket

Bitbucketは、プロのチーム向けに設計されたGitバージョン管理リポジトリ管理ソリューションです。gitリポジトリの管理、ソースコードのコラボレーション、開発フローのガイドなどを一元的に行うことができます。最大5人の協力者と無制限に無料のプライベートリポジトリを作成することができます。これは、プロプライエタリなコードをホストするのに最適な場所になります。

主な機能は以下の通りです。

1、ソースコードへのアクセスを制限するアクセスコントロール。
2、プロジェクトやチームのワークフローを強制するワークフローコントロール。
3、コードレビューでの共同作業のためのインラインコメント付きのプルリクエスト。
4、開発の完全なトレーサビリティのための Jira 統合
5、マーケットプレイスから利用可能な機能がない場合、ワークフローに合わせたカスタム機能を構築するためのFull Rest API
このチュートリアルでは、Bitbucketを使用してアプリケーションのコードのリポジトリをホストし、CI/CDパイプラインを作成してAlibaba Cloud Container Serviceにアプリケーションをデプロイします。

Alibaba Cloud Container Registry

Alibaba Cloud Container Registry (ACR)は、コンテナ化されたイメージのライフサイクル管理を提供する安全なイメージホスティングプラットフォームです。ACRを使用すると、保存されたイメージを完全にコントロールすることができます。ACRには、GitHubやBitbucketなどとの統合を含む多くの機能があります。また、ソースコードからアプリケーションへのコンパイルやテスト後に自動的に新しいイメージを構築することもできます。画像レジストリの作成やメンテナンスを簡素化し、複数のリージョンでの画像管理をサポートします。Container Serviceなどの他のクラウドサービスと組み合わせることで、Container RegistryはクラウドでDockerを利用するための最適なソリューションを提供します。

このチュートリアルでは、DockerイメージのホスティングにACRを使用して、後からAlibaba Cloud Container Service上にアプリケーションをデプロイするために使用します。

Alibaba Cloud Container Service

Alibaba Cloud Container Service(ACS)は、DockerとKubernetesを使用してコンテナ化されたアプリケーションのライフサイクルを管理できる、高性能で拡張性の高いコンテナアプリケーション管理サービスです。

Container Serviceは、さまざまなアプリケーションの公開方法や継続的なデリバリー機能を提供し、マイクロサービスアーキテクチャをサポートしています。

Container Serviceはコンテナ管理クラスタの構築を簡素化し、アリババクラウドの仮想化、ストレージ、ネットワーク、セキュリティ機能を統合して、クラウド上に最適なコンテナ実行環境を構築します。

このチュートリアルでは、Alibaba Cloud Container Serviceを使用して、マイクロサービスをホストしてデプロイする方法を説明します。

Node.js

Node.jsは非同期イベント駆動型のJavaScriptランタイム環境です。これはスケーラブルなネットワークアプリケーションを構築するために設計されています。Node ランタイム環境には、JavaScript で書かれたプログラムをスタンドアロンアプリケーションとしてマシン上で実行するために必要なものがすべて含まれています。Node.js は V8 JavaScript ランタイムエンジン上で動作します。このエンジンはJavaScriptのコードを受け取り、より高速なマシンコードに変換します。マシンコードとは、コンピュータが最初に解釈しなくても実行できる低レベルのコードのことです。

このチュートリアルでは、Node.js を使ってシンプルな "Hello World" アプリケーションを作成し、ACS をデプロイしてみます。

SSL/TLSを暗号化してみよう

Let's Encryptは世界的な認証局(CA)です。彼らは、世界中の人々や組織が SSL/TLS 証明書を取得、更新、管理できるようにしています。これらの証明書は、ウェブサイトが安全な HTTPS 接続を可能にするために使用することができます。Let's Encrypt は、ドメイン検証(DV)証明書のみを提供しています。これらの証明書に手数料はかかりません。Let's Encrypt は、HTTPS の普及を促進することで、より安全でプライバシーを尊重した Web を構築することを使命とする非営利団体である。彼らのサービスは無料で簡単に利用できるので、どんなウェブサイトでも HTTPS を導入することができます。

このチュートリアルでは、Let's Encrypt SSL/TLSを使用してアプリケーションのセキュリティを確保します。

前提条件

まず最初に、アプリケーションのコードのプライベートリポジトリをBitbucket上に作成します。アカウントを持っていない場合は、アカウントにサインアップすることができます。

次に、Alibaba Cloud Servicesを使ってDockerイメージを作成し、コンテナ化されたアプリケーションをデプロイします。そのため、Alibaba Cloudのアカウントも設定しておく必要があります。アカウントを持っていない場合は、このリンクから無料のアカウントにサインアップすることができます。

パートⅠ:Bitbucket Gitリポジトリにアプリを追加する

このセクションでは、Bitbucket Gitリポジトリを作成し、そこにアプリケーションとDockerの設定を追加していきます。

ステップ1: Bitbucketリポジトリの作成

まず、Bitbucketアカウントにログインし、左メニューバーにある「+」ボタンをクリックし、オプションから「リポジトリ」を選択し、「新しいリポジトリの作成」フォームで、リポジトリの名前を入力します。そして、「リポジトリの作成」ボタンを押します。

image.png

ステップ2: アプリケーションファイルとDockerファイルをリポジトリに追加する

リポジトリの作成に成功したので、アプリケーションとDockerのファイルをリポジトリに追加していきます。

まずはローカルディレクトリに git を初期化します。まず、ローカルマシン上に空のディレクトリを作成し、"sample-app-for-alibaba-cloud "と名付け、"Git Bash "またはお好みのCLIで開きます。そして、その中でgit initcommandを実行します。

では、そのディレクトリにファイルを追加していきます。まず、以下のような内容のアプリケーションファイル "app.js "を追加します。

const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

これで、ポート3000のNode.jsが組み込まれたサーバ上で動作するシンプルな「Hello World」アプリが作成されます。

ここでは、Dockerの設定ファイル「Dockerfile」を追加し、以下の内容で作成します。

# Use Node version 10.15.1
FROM node:10.15.1
# Create a directory where our app will be placed
RUN mkdir -p /usr/src/app
# Change directory so that our commands run inside this new directory
WORKDIR /usr/src/app
# Get all the code needed to run the app
COPY . /usr/src/app
# Expose the port the app runs in
EXPOSE 3000
# Serve the app
CMD node app.js

ファイル内のコメントは、各行が何をするのかを説明する必要があります。このファイルで設定した内容をもとに、アプリケーションのコンテナイメージをどのように作成するかを決定します。

これで、これらのファイルを先ほど作成した Bitbucket リポジトリにプッシュする準備が整いました。

git add .
git commit -m "app.js and dockerfile added"
git remote add origin https://your-username@bitbucket.org/your-username/the-repo-name.git
git push -u origin master

パートII: BitbucketとACRの統合

最初に行う必要があるのは、Alibaba Cloud Container Registryを有効化することです。これは、Alibaba Cloud Consoleの「Elastic Computing」セクションの下にあります。

Container Registry Consoleに移動し、サービスを構成してデプロイすることができます。その際に、dockerクライアントのパスワードの入力を求められることがあるので、「dockerにログインする際には、先ほど設定したユーザー名とパスワードをAlibaba Cloudアカウントで使用する」と覚えておきましょう。

では、新しい ACR Namespace を作成します。 通常はコンテナを使ってマイクロサービスを作成します。 今回のアプリケーションでは単一のサービスのみを使用していますが、現実世界では通常そうはいきません。 そこで、ACR Namespacesの出番です。名前空間はレポジトリーのコレクションであり、リポジトリはイメージのコレクションです。 各アプリケーションに1つの名前空間を作成し、各サービスイメージに1つのリポジトリを作成することをお勧めします。

新しいネームスペースを作成するには、ACRコンソールのNamespacesタブに移動し、右上の "Create Namespace "ボタンをクリックします。 その後、名前空間の名前を入力し、"Confirm "ボタンをクリックします。

image.png

ここで、BitbucketアカウントをACRにバインドします。これを行うには、メインACRコンソールの "Code Source "タブに移動し、そこからBitbucketの横にある "Bind Account "ボタンをクリックしてください。

image.png

これでBitbucketに移動し、Alibaba Cloudへのアクセスを許可するように要求されます。そこで「アクセスを許可する」ボタンをクリックして次に進みます。

image.png

これで、ACR リポジトリを作成する準備ができました。リポジトリの詳細を入力するフォームが表示されますので、地域、名前空間、リポジトリ名、概要などを入力してください。

image.png

必要事項を記入した後、「次へ」ボタンをクリックしてください。次のページでは、「コードソース」オプションを選択するフォームが表示されます。このページでは、Bitbucket リポジトリを ACR リポジトリと統合します。Code Source "タブから "Bitbucket "を選択し、先ほど作成したBitbucket Repositoryをクリックします。

継続的インテグレーションの設定を支援するために、「イメージを自動的にビルドする」にチェックを入れてください。これを選択すると、コードを送信した後にイメージが自動的にビルドされ、手動でビルドを開始する必要はありません。最後に「リポジトリの作成」ボタンをクリックして、ACRリポジトリを作成し、Bitbucketリポジトリとの統合を行います。

image.png

パート III: アプリケーションのデプロイ

まず、VPC を作成する必要があります。そのためには、ネットワークの下の仮想プライベートクラウドに移動するだけです。VPC コンソールに移動し、"Create VPC" ボタンをクリックして VPC と VSwitch を作成します。ボタンをクリックすると、新しいVPCのオプションを選択するための小さなフォームが表示されますが、ここではデフォルトの設定を使用して名前を入力し、「OK」ボタンをクリックして続行します。

image.png

Alibaba Cloud Cloud Serviceを使い始めるには、まず、コンテナサービスのダッシュボードに移動します。このダッシュボードは、Alibaba Cloud ConsoleのElastic Computingセクションの下にあります。

最初に試してみると、「コンテナサービスはデフォルトのロールを作成する必要があります」という警告が表示されることがあります。これは、クラスタをリクエストすると、Alibaba Cloudがクラスタの実行に必要なリソースのプロビジョニングを行うためです。これには、ロードバランサー、仮想マシン、ルーターなどが含まれます。基本的には、自分のDockerクラスタを動かすために必要なものはすべて含まれています。そして、ロードバランサは動作する前にRAM(Resource Access Management)サービスを設定する必要があります...。これに対処するには、"Go to RAM console "ボタンをクリックし、リダイレクトされたら "Confirm Authorization Policy "ボタンをクリックします。

Container Service Overviewのページが表示されますので、左上の "Container Serv-Swarm "を選択し、"Create Cluster "ボタンをクリックします。

image.png

これでクラスタ作成ウィザードに移動します。名前、地域、ゾーン、VPCなど、クラスタを構成するために必要なすべてのオプションが表示されます。

image.png

大部分はデフォルト値を使用しますが、このデモの目的のために、"Instance Configuration "の "Instance Type "を "1 Core(s) 1 G ( ecs.n1.tiny ) "に変更します。また、このクラスタのために作成されるECSインスタンスのログインを設定する必要があります。これを設定した後、"Create "ボタンをクリックして続行します。その後、作成されたクラスタのすべての詳細を示すダイアログが表示されますので、すべてが正しいことを確認し、"OK "ボタンをクリックして続行します。

image.png

ここで、Bitbucket上にソースコード用のTag(release)を作成します。これを行うには、まず、先ほど作成したBitbucketリポジトリのコミットページに移動し、そこにプッシュした最新のコミットを選択します。そのページの右側に "タグ "オプションがあり、その横に "+"ボタンがありますので、それをクリックすると、新しいタグを作成するためのダイアログが開きます。

image.png

次に、ACR コンソールのリポジトリに戻り、先ほど作成した ACR リポジトリの横にある「管理」ボタンをクリックします。

image.png

リポジトリの詳細ページに移動し、リポジトリの名前、地域、タイプ、コードリポジトリリンク、インターネットアドレス、VPCアドレスなどの詳細が表示されます。そこから「ビルド」タブに移動し、「ビルドログ」の下に先ほど作成したタグのビルドが表示されます。ビルドのステータスが "Successful "になったら、右上の "Deploy Application "ボタンをクリックします。Container Cluster "オプションで "Swarm "を選択し、"Deploy "ボタンをクリックします。

image.png

これにより、ACSアプリケーション作成ウィザードに移動します。そこでは、「基本情報」タブで、名前、バージョン、クラスタ、更新、説明などのオプションが用意されています。名前を入力し、先ほど作成したクラスタを選択し、「Pull Docker Image」にチェックを入れて「Next」ボタンをクリックして次に進みます。

image.png

これで作成ウィザードの "Configuration "タブに移動します。ここではDockerコンテナの「Image Name」と「Image Version」を選択し、「Port Mapping」の「host Port」に「3000」を使用します。次に、「Web routing」の下にある「Port」と「Domain」を追加します。このページには他にも多くの設定オプションがありますが、ここではデフォルト値を使用します。ここで「作成」ボタンをクリックして、いよいよアプリケーションを作成します。

image.png

完了ページで "View Application List "リンクをクリックしてアプリケーションの一覧ページに進みます。次に「ルート」タブをクリックして、アプリケーションのアクセスエンドポイントを取得します。

image.png

これで、コンテナ化されたアプリケーションのデプロイに成功しました。さて、最後のパートに入りましょう。

パートIV: SSL/TLSを暗号化してアプリケーションを保護しよう

これを始めるには、まず、John HanleyによるAlibaba Cloud上でのLet's Encrypt ACMEの構成に関するこのガイドに従うべきである。さて、すべての設定を行い、証明書ファイルを作成したら、前述の手順に進みます。

サーバーのロードバランサー層ではHTTPSに対応しているので、HTTPSに対応しているかどうかを確認します。HTTPS をサポートするには、Server Load Balancer 証明書を作成する必要があります。これを行うには、ネットワーキングの下のサーバーロードバランサーコンソールに移動し、左パネルの「Certificates」リンクをクリックして「Certificates」ページに進みます。証明書の作成」ボタンをクリックし、「サードパーティ証明書のアップロード」オプションを選択して「次へ」ボタンをクリックします。

image.png

これで「Upload Third-Party Certificate」に移行し、必要事項を記入して「次へ」ボタンをクリックします。

image.png

証明書が正常に作成されたら、サーバーロードバランサーのページに移動し、クラスタ作成時に割り当てられたサーバーロードバランサーのインスタンスを探してクリックします。インスタンスに移動したら、「リスナー」タブの下にある「リスナーの追加」ボタンをクリックします。

image.png

リスナーを追加するためのウィザードが開きます。そこから、このガイドに従って HTTP を HTTPS に変更してウィザードを記入します。これで、Let's Encrypt SSL/TLSで保護されたコンテナ化アプリケーションが完成しました。

出典:コンテナ化されたアプリケーションのデプロイ方法の基本的なガイド BitbucketからAlibaba Cloud Container Serviceにアプリケーションをデプロイし、Let's Encrypt SSL/TLSでセキュリティを確保する方法の基本ガイド2019

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsでsleep処理をasync awaitを使って書く

sleep処理を書くときはsetTimeoutを使うのが一般的だと思いますが、callbackを書きたくないので今風にasync awaitで書きます。

配列にある会社idの情報を1秒ごとに順番にリクエストする処理を書くと以下のようになります。

index.js
async function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function request(id) {
    // 割愛
}

async function exec() {
  const companyIdList = [1, 2, 3, 4, 5];

  for (const id of companyIdList) {
    await sleep(1000);
    await request(id);
  }
}

exec();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Lambda(Node.js)においてmiddyを使ってAmazon RDSに接続する方法

AWS Lambda(Node.js)において、middyを使ってAmazon RDSに接続する方法を紹介します。

はじめに

データベースエンジン

この記事ではデータベースエンジンとしてAurora MySQLを使用している場合の例を示しますが、他のデータベースエンジンの場合も基本的な実装は変わりません。

プログラミング言語

この記事ではソースコードをTypeScriptで書いているので型定義をインストールしたり型アノテーションを記述したりしていますが、JavaScriptで書く場合は不要です。

ミドルウェア

middyでRDSに接続するために、@middy/db-managerを使います。
@middy/db-managerでは、データベースクライアントとしてKnex.jsが使われます。

接続情報をAWS Secrets Managerで管理していない場合

接続情報をAWS Secrets Managerで管理していない場合、ミドルウェアとして@middy/db-managerのみを使います。

パッケージをインストール

middyを使うために、@middy/coreパッケージをインストールします。

$ npm i -d @middy/core

middyでRDSに接続するために、@middy/db-managerパッケージをインストールします。

$ npm i -d @middy/db-manager

また、データベースエンジンに応じたパッケージをインストールします。
今回はAurora MySQLなので、mysqlパッケージをインストールします。

$ npm i -d mysql

Lambdaの型定義として、@types/aws-lambdaパッケージをインストールします。

$ npm i -D @types/aws-lambda

ハンドラーを作成

middyを用いて、ハンドラーをつくります。

src/index.ts
import middy from '@middy/core'
import dbManager from '@middy/db-manager'
import lambda from 'aws-lambda'
import Knex from 'knex'

interface Context extends lambda.Context {
  db: Knex
}

const handler = middy(async (
  event: any,
  context: Context,
  callback: lambda.Callback<any>
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})

handler.use(dbManager({
  config: {
    client: 'mysql',
    connection: {
      host: '127.0.0.1',
      port: 3306,
      user: 'your_database_user',
      password: 'your_database_password',
      database: 'myapp_test'
    }
  }
}))

export { handler }

詳しく解説していきます。

interface Context extends lambda.Context {
  db: Knex
}

@middy/db-managerを使うとコンテキストのdbプロパティにKnexインスタンスが割り当てられるので、dbプロパティを持つContextを定義しています。

const handler = middy(async (
  event: any,
  context: Context,
  callback: lambda.Callback<any>
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})

ハンドラーをmiddy関数でラップすることで、middyfy(middy化)しています。
ハンドラー内では、context.dbからKnex.jsのAPIを用いてデータベース操作が行なえます。

handler.use(dbManager({
  config: {
    client: 'mysql',
    connection: {
      host: '127.0.0.1',
      port: 3306,
      user: 'your_database_user',
      password: 'your_database_password',
      database: 'myapp_test'
    }
  }
}))

ハンドラーで、ミドルウェアとしてdbManagerを使っています。
configには、Knex.jsの設定オブジェクトを渡します。
ここでは例示のために接続情報をべた書きしていますが、実際には環境変数やAWS Secrets Managerで管理すべきでしょう。

接続情報をAWS Secrets Managerで管理している場合

接続情報をAWS Secrets Managerで管理している場合、ミドルウェアとして@middy/db-manager@middy/secrets-managerを使います。

パッケージをインストール

middyを使うために、@middy/coreパッケージをインストールします。

$ npm i -d @middy/core

middyでRDSに接続するために、@middy/db-managerパッケージをインストールします。

$ npm i -d @middy/db-manager

また、データベースエンジンに応じたパッケージをインストールします。
今回はAurora MySQLなので、mysqlパッケージをインストールします。

$ npm i -d mysql

middyでSecrets Managerに接続するために、@middy/secrets-managerパッケージをインストールします。

$ npm i -d @middy/secrets-manager

Lambdaの型定義として、@types/aws-lambdaパッケージをインストールします。

$ npm i -D @types/aws-lambda

ハンドラーを作成

middyを用いて、ハンドラーをつくります。

src/index.ts
import middy from '@middy/core'
import dbManager from '@middy/db-manager'
import secretsManager from '@middy/secrets-manager'
import lambda from 'aws-lambda'
import Knex from 'knex'

interface Context extends lambda.Context {
  db: Knex
}

const MIDDY_RDS_SECRET_KEY = 'MIDDY_RDS_SECRET'

const handler = middy(async (
  event: any,
  context: Context,
  callback: lambda.Callback<any>
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})

handler.use(secretsManager({
  secrets: {
    [MIDDY_RDS_SECRET_KEY]: 'secret_name'
  },
  cache: true,
  throwOnFailedCall: true
}))

handler.use({
  before: (handler, next) => {
    interface Secret {
      host: string
      port: number
      username: string
      password: string
      dbname: string
    }
    interface Connection {
      host: string
      port: number
      user: string
      password: string
      database: string
    }
    interface Context extends lambda.Context {
      [MIDDY_RDS_SECRET_KEY]: Secret | Connection
    }
    const context = handler.context as Context
    const secret = context[MIDDY_RDS_SECRET_KEY] as Secret
    context[MIDDY_RDS_SECRET_KEY] = {
      host: secret.host,
      port: secret.port,
      user: secret.username,
      password: secret.password,
      database: secret.dbname
    }
    return next()
  }
})

handler.use(dbManager({
  config: {
    client: 'mysql'
  },
  secretsPath: MIDDY_RDS_SECRET_KEY,
  removeSecrets: true
}))

export { handler }

詳しく解説していきます。

interface Context extends lambda.Context {
  db: Knex
}

@middy/db-managerを使うとコンテキストのdbプロパティにKnexインスタンスが割り当てられるので、dbプロパティを持つContextを定義しています。

const handler = middy(async (
  event: any,
  context: Context,
  callback: lambda.Callback<any>
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})

ハンドラーをmiddy関数でラップすることで、middyfy(middy化)しています。
ハンドラー内では、context.dbからKnex.jsのAPIを用いてデータベース操作が行なえます。

handler.use(secretsManager({
  secrets: {
    [MIDDY_RDS_SECRET_KEY]: 'secret_name'
  },
  cache: true,
  throwOnFailedCall: true
}))

ハンドラーで、ミドルウェアとしてsecretsManagerを使っています。
secretsでは、コンテキストのどのプロパティに対してSecrets Managerのどのシークレットを割り当てるかを指定します。
cachetrueにすることで、キャッシュを有効化しています。
throwOnFailedCalltrueにすることで、シークレットの取得に失敗した場合にエラーをスローするようにしています。

handler.use({
  before: (handler, next) => {
    interface Secret {
      host: string
      port: number
      username: string
      password: string
      dbname: string
    }
    interface Connection {
      host: string
      port: number
      user: string
      password: string
      database: string
    }
    interface Context extends lambda.Context {
      [MIDDY_RDS_SECRET_KEY]: Secret | Connection
    }
    const context = handler.context as Context
    const secret = context[MIDDY_RDS_SECRET_KEY] as Secret
    context[MIDDY_RDS_SECRET_KEY] = {
      host: secret.host,
      port: secret.port,
      user: secret.username,
      password: secret.password,
      database: secret.dbname
    }
    return next()
  }
})

Secrets Managerに保存されているシークレットを、Knex.jsに渡す形式に変換しています。
シークレットの形式は、データベースエンジンによって異なります。1

handler.use(dbManager({
  config: {
    client: 'mysql'
  },
  secretsPath: MIDDY_RDS_SECRET_KEY,
  removeSecrets: true
}))

ハンドラーで、ミドルウェアとしてdbManagerを使っています。
secretsPathには、Secrets Managerのシークレットが割り当てられているコンテキストのプロパティを指定します。
removeSecretstrueにすることで、データベースに接続後にコンテキストからシークレットを削除しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Amplify での Cognito アクセスは React Context.Provider を使って認証処理を Hooks 化しよう

Amplify × React Hooks.png

AWS Cognito は認証・認可を提供している AWS のサービスです。Amplify と統合することで、超高速に構築できます。Cognito を使用することで、API Gateway や S3 など他の AWS サービスとの統合がより簡単にできるようになります。

本記事では、Cognito を使用した React アプリケーションの実装例を紹介します。Cognito へのアクセスには amplify-js というライブラリを使用します。さらに React の Context.Provider という機能を使うことで認証に関連する処理をカスタムフックに集約する方法を考察します。

本記事で実装されたアプリケーションは以下のような動作をします。ログイン、ログアウト、サインアップ、確認メールなど。

完成するアプリケーション

本アプリケーションは Vercel にデプロイされています。
https://task-app.geeawa.vercel.app/login

また、以下の GitHub リポジトリにホストしています。
https://github.com/daisuke-awaji/task-app

amplify-js でも React Hooks を使いたい

先週は React アプリに Auth0 でシュッと認証を組み込んで Vercel に爆速デプロイする という記事を書きました。Auth0 のクライアントライブラリは非常に使い勝手がよく、<Auth0Provider> という Provider で包むだけで useAuth0 フックを使用できるようになります。

index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Auth0Provider } from "@auth0/auth0-react";
import "bootstrap/dist/css/bootstrap.min.css";
import { App } from "./App";

ReactDOM.render(
  <Auth0Provider
    domain={process.env.REACT_APP_AUTH0_DOMAIN!}
    clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!}
    redirectUri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.querySelector("#root")
);

一方で amplify-js にはこのような機能はありません。認証系処理のメソッドは Auth モジュールから取り出して使う必要があります。以下はサインアップするメソッドです。参考: 公式 Sign up, Sign in & Sign out

import { Auth } from "aws-amplify";

async function signUp() {
  try {
    const user = await Auth.signUp({
      username,
      password,
      attributes: {
        email,
        phone_number,
      },
    });
    console.log({ user });
  } catch (error) {
    console.log("error signing up:", error);
  }
}

メソッドしか用意されておらず、ログインユーザの情報などを React アプリでグローバルに保持する仕組みは自分で用意する必要があります。amplify-js でも Auth0 のような使いやすい DX(開発者体験)にしたい! ということが本記事のモチベーションです。つまり、以下のように使用したいわけです。

index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import "./index.css";
import CognitoAuthProvider from "./cognito/CognitoAuthProvider";

ReactDOM.render(
  <CognitoAuthProvider>
    <App />
  </CognitoAuthProvider>,
  document.getElementById("root")
);

<App/> コンポーネントを <CognitoAuthProvider> でラップするだけで、認証系の処理やログインユーザのステートを取り出す useAuth フックが使えるようにしていきます。

LogoutButton.tsx
import React from "react";
import { useAuth } from "../../cognito/CognitoAuthProvider";

export default function LogoutButton(props: any) {
  const { isAuthenticated, signOut } = useAuth();

  if (!isAuthenticated) return null;

  return <Button onClick={() => signOut()} {...props} />;
}

React.Context とは

React の Context は配下の子コンポーネントにデータを渡すための便利な方法です。従来は props を使用することで、子コンポーネントにデータを渡していましたが、コンポーネントのネストが深くなると非常に面倒で複雑になります。 Context を使用することで 認証UI テーマ など多くのコンポーネントが使用する情報を共有して保持・取得できます。

context.provider.png

React.createContext

Context オブジェクトを作成します。React がこの Context オブジェクトが登録されているコンポーネントをレンダーする場合、ツリー内の最も近い上位の一致する Provider から現在の Context の値を読み取ります。

const MyContext = React.createContext(defaultValue);

Context.Provider

全ての Context オジェクトには Context.Provider コンポーネントが付属しています。これにより Context.Consumer コンポーネントは Context の変更を購読できます。実際のユースケースでは Consumer ではなく、useContext フックを使用することが多いでしょう。

<MyContext.Provider value={/* 何らかの値 */}>

useContext

Context オブジェクトを受け取り、その Context の value を返します。<MyContext.Provider/> が更新されると、このフックは MyContext.Provider に渡された value を使用してコンポーネントを再レンダーします。

const value = useContext(MyContext);

認証情報を Context に集約する

さて、認証情報として以下のようなメソッドとステートを保持する Context を作っていきます。これらの値があればログイン、ログアウト、サインアップ、確認コード入力の一連の流れが実装できます。

項目 概要
isAuthenticated ログインしているか
isLoading ローディング中か(画面制御で使用)
user ログインしているユーザの情報
error ログイン処理、サインアップ処理などでエラーがあれば詰める
signIn サインインする。
signUp サインアップする。
confirmSignUp サインアップ確認コードを入力する
signOut サインアウトする。

State

Context が保持するステートの定義(インタフェース)を作成します。

import { CognitoUser } from "amazon-cognito-identity-js";
export interface AuthState {
  isAuthenticated: boolean;
  isLoading: boolean;
  user?: CognitoUser;
  error?: any;
}
const initialState: AuthState = {
  isAuthenticated: false,
  isLoading: false,
};
const stub = (): never => {
  throw new Error(
    "You forgot to wrap your component in <CognitoAuthProvider>."
  );
};
export const initialContext = {
  ...initialState,
  signIn: stub,
  signUp: stub,
  confirmSignUp: stub,
  signOut: stub,
};

Context

Context オブジェクトを作成します。各コンポーネントから取り出すためのカスタムフック useAuth() を合わせて作成しておきます。

import React, { useContext } from "react";
import { SignUpParams } from "@aws-amplify/auth/lib-esm/types";
import { CognitoUser } from "amazon-cognito-identity-js";
import { AuthState, initialContext } from "./AuthState";
import { LoginOption } from "./CognitoAuthProvider";
interface IAuthContext extends AuthState {
  signIn: (signInOption: LoginOption) => Promise<void>;
  signUp: (params: SignUpParams) => Promise<CognitoUser | undefined>;
  confirmSignUp: (params: any) => Promise<void>;
  signOut: () => void;
}
export const AuthContext = React.createContext<IAuthContext>(initialContext);
export const useAuth = () => useContext(AuthContext);

Provider

最後に Provider には Cognito とやりとりする処理と、認証情報を保持する処理を実装します。

import React from "react";

import { useState, useEffect } from "react";
import { SignUpParams } from "@aws-amplify/auth/lib-esm/types";
import { CognitoUser } from "amazon-cognito-identity-js";

import { Auth } from "aws-amplify";
import Amplify from "aws-amplify";
import { AuthContext } from "./AuthContext";

export type LoginOption = {
  username: string;
  password: string;
};
interface ICognitoAuthProviderParams {
  amplifyConfig: {
    aws_project_region: string;
    aws_cognito_identity_pool_id: string;
    aws_cognito_region: string;
    aws_user_pools_id: string;
    aws_user_pools_web_client_id: string;
    oauth: {
      domain: string;
      scope: string[];
      redirectSignIn: string;
      redirectSignOut: string;
      responseType: string;
    };
    federationTarget: string;
  };
  children: any;
}

export default function CognitoAuthProvider(props: ICognitoAuthProviderParams) {
  Amplify.configure(props.amplifyConfig);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [user, setUser] = useState<CognitoUser>();

  useEffect(() => {
    checkAuthenticated();
    currentAuthenticatedUser();
  }, []);

  const checkAuthenticated = () => {
    setIsLoading(true);
    Auth.currentSession()
      .then((data) => {
        if (data) setIsAuthenticated(true);
      })
      .catch((err) => console.log("current session error", err))
      .finally(() => {
        setIsLoading(false);
      });
  };

  const currentAuthenticatedUser = async (): Promise<void> => {
    const user: CognitoUser = await Auth.currentAuthenticatedUser();

    setUser(user);
  };

  const signIn = async ({ username, password }: LoginOption): Promise<void> => {
    setIsLoading(true);
    try {
      await Auth.signIn(username, password);
      setIsAuthenticated(true);
    } catch (error) {
      console.log("error signing in", error);
      setError(error);
      setIsAuthenticated(false);
    }
    setIsLoading(false);
  };

  const signUp = async (
    param: SignUpParams
  ): Promise<CognitoUser | undefined> => {
    setIsLoading(true);
    let result;
    try {
      result = await Auth.signUp(param);
      setUser(result.user);
    } catch (error) {
      console.log("error signing up", error);
      setError(error);
    }
    setIsLoading(false);
    return result?.user;
  };

  const confirmSignUp = async ({ username, code }: any): Promise<void> => {
    setIsLoading(true);
    try {
      await Auth.confirmSignUp(username, code);
      setIsAuthenticated(true);
    } catch (error) {
      console.log("error confirming sign up", error);
      setError(error);
    }
    setIsLoading(false);
  };

  const signOut = () => {
    setIsLoading(true);
    Auth.signOut()
      .then(() => {
        setIsAuthenticated(false);
      })
      .catch((err) => console.log("error signing out: ", err))
      .finally(() => {
        setIsLoading(false);
      });
  };

  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        isLoading,
        signIn,
        signUp,
        confirmSignUp,
        signOut,
        user,
        error,
      }}
    >
      {props.children}
    </AuthContext.Provider>
  );
}

使用方法

ここまで準備ができれば使用する側はこの CognitoAuthProvider でコンポーネントをラップすることで useAuth() フック経由で各種ステートの値またはメソッドを使用できます。

amplifyConfig として設定値は外部ファイルで保持しています。

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
import CognitoAuthProvider from "./cognito/CognitoAuthProvider";
import awsconfig from "./aws-exports";

ReactDOM.render(
  <CognitoAuthProvider amplifyConfig={awsconfig}>
    <App />
  </CognitoAuthProvider>,
  document.getElementById("root")
);

amplifyConfig は以下のようなファイルになります。

const amplifyConfig = {
  aws_project_region: "ap-northeast-1",
  aws_cognito_identity_pool_id: "ap-northeast-1:12345678909876543234567890",
  aws_cognito_region: "ap-northeast-1",
  aws_user_pools_id: "ap-northeast-1_xxxxxxxx",
  aws_user_pools_web_client_id: "xxxxxxxxxxxxxxx",
  oauth: {
    domain: "mydomain.auth.ap-northeast-1.amazoncognito.com",
    scope: [
      "phone",
      "email",
      "openid",
      "profile",
      "aws.cognito.signin.user.admin",
    ],
    redirectSignIn: "http://localhost:3000/",
    redirectSignOut: "http://localhost:3000/logout/",
    responseType: "code",
  },
  federationTarget: "COGNITO_USER_POOLS",
};

export default amplifyConfig;

ログアウトボタンのコンポーネントです。コードベースをシンプルにできました。

LogoutButton.tsx
import React from "react";
import { useAuth } from "../../cognito/CognitoAuthProvider";

export default function LogoutButton(props: any) {
  const { isAuthenticated, signOut } = useAuth();

  if (!isAuthenticated) return null;

  return <Button onClick={() => signOut()} {...props} />;
}

さいごに

React の Context を使用することで、認証情報などのグローバルな値を一元的に管理できるようになります。
ただ、 Context は多くのコンポーネントからアクセスされる場合に使用することとしましょう。
Context はコンポーネントの再利用をより難しくする為、慎重に利用してください。

本記事で紹介した React.Context を使用したカスタムフックを使用するという発想はそのうち amplify-js に PullRequest しようと思います。Cognito ユーザ(または Amplify ユーザ)が個別にこのような実装をしなくとも、ライブラリとして提供し、すぐに簡単なインタフェースで認証処理を実現できるようにしていきたいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] セミコロンをつければ絶対安心できるのか?;

はじめに

こちらの記事 を読んで、改めてJavaScriotの ; について気になったので調べてみました。

結論

私が思ったより安心じゃなかった。

セミコロンありのスタイルでも、下記のように予期せぬセミコロンの挿入が起こってしまうパターンがあります。

function f() {
    return
        {
          foo: 'foo'
        };
}

f(); // undefined

セミコロンに関することは参考にさせていただいたセミコロンに関する ESLint ルールという記事に全部書いてありました :pray::pray::pray:

以下の内容はリンク先の記事に(個人的な感想以外)書いてあることなので、ぜひそちらを参考にしてください!

ASI(Auto Semicolon Insertion)

JavaScript の改行箇所で構文エラーがあった際にセミコロンにを自動的に補い再解釈する言語機能のことですが、結局のところこいつが曲者で、初心者や他言語からJavaScriptを触るようになった方が理解し難く感じる要因でしょう。

ECMAScript の言語仕様に ASI 利用に関する警告が ES2019 から追加 されています。

ASIの振る舞いで気をつけるべきこと

予期せぬセミコロン挿入の欠損

console.log(4) // > 4
['foo', 'bar'].forEach(el => console.log(el)) // > Uncaught TypeError: Cannot read property 'bar' of undefined

console.log(4)['foo', 'bar'].forEach が構文上正しいと解釈され、セミコロンの自動挿入が起こりません。, がカンマ演算子として解釈されるため、 bar が未定義というエラーが出ます。

このパターンは実行してみてエラーで気づくことができそうです。また、TypeScriptなら型チェックで気づけます。

予期せぬセミコロン挿入

function f1()
{
    return
        2020;
}

function f2()
{
    return
        {
          foo: 'foo'
        };
}

function f3()
{
    return
        ({
          foo: 'foo',
          bar: 'bar'
        });
}

f1(); // undefined
f2(); // undefined
f3(); // undefined

こちらが先に上げたパターンと同じですが、 return のあとにセミコロンが自動挿入され、改行後の値が返されません。

また、f2の関数には Object そのものが存在していません。
{ foo: 'foo' } の部分は Object ではなく、ブロックとして解釈されてしまっています。 foo:fooラベル構文です。意味のないブロックですが、文法そのものに影響しないので無視されています。

C#とか Allman brace style が推奨の言語から来た方は引っかかる?(多くの場合はそんなことないでしょうが)
Lint などがない場合は間違って改行していた場合気づかないかもしれませんね。

ちなみに、ラベル構文の後にカンマをつけることはできないため以下は構文エラーになります。

function f() {
    return
        {
          foo: 'foo',
          bar: 'bar
        };
}

// > Uncaught SyntaxError: Unexpected token :

まとめ

今回調べてみて結局のところ、セミコロンありなのかなしなのかは好みの問題という域を出ないように感じました。
それよりも、ASIの独特の動きを理解して(といってもそんなに多くの動作は無いように思います。)、Lint や Formatter などのツールをしっかり活用することで、こういった問題を起こさない様にすることの方が重要だと思います。

特に、ESLintやPrettierを使う際にはJavaScript Standard Styleのようなプリセットを利用し、極力自力で設定しないことが、このような罠にハマらないために大切です。(JavaScript Standard Styleにはセミコロンあり版が存在します)

また、可読性についても一長一短で、個人によるところが大きいと思います。(私はセミコロンレスの方が見やすく感じます)

参考

セミコロンをつけ忘れただけなのに...【JavaScript】

セミコロンに関する ESLint ルール

To Semicolon, Or Not To Semicolon;

Using Semicolons? Never Use Them!

JavaScript Standard Style

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む