まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:api

IBM Cloud から提供されているユーザーディレクトリ(ログイン機能用サービス)である App ID で、デモ用途などでまとめて一括でユーザーを作成したいことはないでしょうか? 1件や2件程度であればサービスダッシュボードから直接作成してもいいのですが、10件とか100件とかになると面倒ですよね。

というわけで「指定した CSV ファイルに記載された情報を使って App ID のユーザーを API 経由で作成するツール」を作って公開してみました。Node.js 環境があれば実行可能です:
https://github.com/dotnsf/appid_users


上記リポジトリを git clone するかダウンロード&展開すると test.csv という CSV ファイルが見つかります。このフォーマットに従う形で、
 表示名,メールアドレス,パスワード
という順に値が1行ずつ記録された CSV ファイルを用意します(メールアドレス=ログインIDになります)。

次に settings.js ファイルを編集して、利用する App ID のサービス接続情報に書き換えます。

準備の最後に依存ライブラリをインストールします:
$ npm install


そして create_user.js を Node.js で実行すると、この CSV ファイルに記述されたユーザーをまとめて作成します(最後に対象 CSV ファイルを指定します):
$ node create_user test.csv

この作業後に App ID のコンソールからクラウド・ディレクトリーのユーザー一覧を確認すると、CSV ファイルで指定したユーザーが作成されていることを確認できます:
2021071001


作成後にログイン確認用のウェブアプリケーションを実行して、ユーザーが正しく作成されているかどうかを確認することができます。その場合はリダイレクト URL に http://localhost:8080/appid/callback を追加しておいてください:
2021071003


改めてアプリケーション起動後($ node app)に http://localhost:8080/ にアクセスし、CSV ファイルに含まれていた ID とパスワードを指定して、正しくログインできるかどうかを確認してください。ログインできるようになっていれば無事に CSV ファイルからユーザーをインポートすることができた、ことになります:
$ node app


2021071002



こんなのを作ってみました。これ自体がそのまま直接役立つとは思ってませんが、cron などのスケジュールジョブと合わせて使うことで時系列データを簡単に入手できるので、「生の解析用サンプルデータを用意する」のが比較的容易にできちゃうと思っています。

文字通りの「為替取得 REST API」です。エンドポイント URL はこちらです:
http://fx.mybluemix.net/


REST API なので、何らかの(機械的な)HTTP クライアントからアクセスされることを想定しています。とりあえずデータ・フォーマットを確認する目的で、ウェブブラウザでアクセスしてみると、このような JSON テキストが得られるはずです:
2021061501


JSON テキストを整形するとこんな感じの内容です:
{
 "status":true,
 "result":{
  "datetime":"2021-06-16 14:48:27+0",
  "rate":{
   "USDJPY":109.961,
   "EURJPY":133.339,
   "EURUSD":1.21255,
   "AUDJPY":84.707,
   "GBPJPY":155.129,
   "NZDJPY":78.49,
   "CADJPY":90.334,
   "CHFJPY":122.216,
   "HKDJPY":14.158,
   "GBPUSD":1.4107,
   "USDCHF":0.89958,
   "ZARJPY":7.99,
   "AUDUSD":0.77031,
   "NZDUSD":0.71376,
   "EURAUD":1.57405,
   "TRYJPY":12.872,
   "CNHJPY":17.186,
   "NOKJPY":13.116,
   "SEKJPY":13.137,
   "MXNJPY":5.465
  }
 }
}

status が API の実行結果(true/false)で、成功した場合は result がその結果です。result.datetime が取得したタイミングの GMT 日時です( API を実行した瞬間の日時になっているはずです)。そして result.rate 内にそのタイミングでの(リアルタイムの)各通貨ペアの為替情報が格納されています。例えば上の例では result.rate.USDJPY = 109.961 となっていますが、これはこのタイミングで「1米ドル(USD)=109.961日本円(JPY)」だったことを表しています。同様にして result.rate.EURJPY = 133.339 なので「1ユーロ(EUR)=133.339日本円(JPY)」です。他の通貨表記についてはこのあたりを参照してください:
https://www.gaitame.com/gaitame/gaika/gaika_index.html


このように上記 URL に GET リクエストを発行するだけでリアルタイムな為替情報 20 ペア分が取得できるものです。後はこれを1分おきとか1時間おきに取得して、その結果を RDB なり、JSON DB なりに格納する、という処理を1~10日くらい動かしっぱなしにしておけば、それなりにまとまったデータが取得できるはずです。一応数値は本物の為替情報で、深く考えなくてもただ GET リクエストを実行すれば結果が取得できるので、解析元となるデータを集める上では比較的便利かな、と思っています。

これはあくまで一例ですが、Node-RED のインジェクションノードを定期実行する設定にした上でこんな感じのフローを作るだけで定期的な為替情報を集めて DB に格納するまでが(簡単に)できちゃいます:
2021061502


後はここで集めたデータを使って解析学習時のデータにするもよし、予測機能を作るもよし、ご自由にお使いください。

このブログエントリは以下の続きです:
Github API を使う(1)
Github API を使う(2)

過去2つのエントリで Github API の基本的な内容として OAuth による認証を行ってアクセストークンを取得し、そのアクセストークンを使って(ログインしたユーザーの権限で)「自分自身の情報を取得する」、「特定リポジトリの特定ブランチに含まれるファイル一覧を取得する」という2つの Github API の実装内容をサンプルソースコードと合わせて紹介しました。

シリーズ最終回の今回はこれまでの内容に加えて以下の機能を Github API で実装して、このシリーズ当初の目的であった「Github ベースのファイルサーバー」っぽいものを作ってみます:
・ブランチを作成する/マージする
・ブランチにファイルをコミットする


実際に作ってみたものを先に紹介しておきます。ソースコードはこちらから参照してください:
https://github.com/dotnsf/githubapi

2021051901


これまでのサンプルと同様に git clone などでソースコード取得後、 settings.js に OAuth アプリケーションの client_id と client_secret 、コールバック URL、API で操作する対象のリポジトリを指定します(この時に作って登録した OAuth アプリケーションのものを流用しても構いません)。またこれも同様ですが、アプリケーション登録時に使ったユーザーとは別のユーザーでアクセスする場合は collaborators に追加しておく必要があります。

加えて、このアプリケーションでは各ユーザー毎にブランチを(ログイン時に自動的に)作成してもらい、そのブランチで作業した内容を特定の別ブランチにマージできるような機能があり、そのマージ先のブランチを target_branch_name 変数に指定する必要があります(デフォルト状態では "__all__")。この初期設定を最初に済ませておいてください。

ここで早速動作確認、、、する前に対象となるリポジトリがアプリケーションによってどのように変化していくかを確認しておきます。今回 settings.js の repo_name で指定されたリポジトリはこちらで、main ブランチの中に README.md ファイルが1つだけ格納されている状態です:
2021051905


また main 以外にはブランチは存在していません:
2021051906


ではこのリポジトリがどのように変わっていくのかを確認するため、改めてアプリケーションを実行します:
$ npm install

$ node app

起動したらウェブブラウザで http://localhost:8080/ にアクセスして、前回までと同様に画面右上の login ボタンから Gihub ユーザー ID でログインします:
2021051902


OAuth ログインに成功すると以下のような画面になります:
2021051903


画面右上にログインしたユーザーのアバター画像、画面内には files, merge, push という3つのボタンと、ファイル選択フィールドが表示されているはずです。各ボタンやフィールドの説明をする前に、この(ログイン直後の)時点でリポジトリ側に変化が加えられているので説明しておきます。

対象リポジトリをリロードして、改めてブランチを確認します。すると直前までは main 1つだけだったブランチに2つのブランチ("3183150" と "__all__")が追加されていることがわかります:
2021051907


このアプリケーションではユーザーがログインすると同時に Gihub ユーザー ID と同じ名前のブランチと、settings.js の target_branch_name 変数で指定したブランチの2つが指定リポジトリ内に作成されます(作成済みの場合は失敗するだけなので変化ありません)。この時点で指定リポジトリにはもともと存在していた main ブランチに加えて、ログインしたユーザーのユーザー ID 名と同じブランチ(以降、「個人ブランチ」と呼びます)と、(今回は "__all__" が指定されているので)__all__ ブランチが作成されています。つまり "318350" とはログインした私の Github ユーザー ID ということです。ちなみにユーザー ID を確認するにはログイン後にアバター画像の上をマウスでホバーした時に表示されます:
2021051904
(↑この例だと 318350)


別の(collaborators として権限を与えられた)ユーザーがログインすると、そのユーザーの個人ブランチも作成されます。なお新たに作成される個人ブランチは「その時点での main ブランチの内容をコピー」した状態で作成されます。

改めてボタン類の説明をします。このうち files ボタンは前回紹介したものと同様ですが、main ブランチではなく個人ブランチ内のファイル一覧が表示されます。ログイン直後は main ブランチをコピーした状態なので、もとの main ブランチに含まれていたファイルが一覧(この例では README.md)表示されます。ファイルの(個人ブランチからの)削除もここから行うことができます:
2021051908


まずこの状態に変化を加えるため、ファイルを1つ追加してみます。「ファイルを選択」と書かれた部分をクリックしてローカルシステムからファイルを1つ選択し、push ボタンをクリックします:
2021052001


すると選択したファイルがアップロードされ、個人ブランチに追加されます。アップロード後に files ボタンをクリックすると、アップロードしたファイルが追加されて一覧表示されることがわかります:
2021052002


今回のサンプルアプリでは DELETE ボタンでブランチから削除、またファイル名部分のクリックでファイルダウンロードまでが実装されています:
2021052003


またこの時点でリモートリポジトリの該当ブランチを確認すると、アップロードしたファイルが含まれていることを確認できます:
2021052004


ではこの個人ブランチに加えた変更(ファイル追加)を(settings.js で指定した)別ブランチにマージします。マージするには画面内の merge ボタンをクリックするだけです(画面に { "result": true } と表示されれば成功です):
2021052005


改めてリモートリポジトリの __all__ ブランチ(settings.js の target_branch_name 変数で指定したブランチ)を確認すると、このアクションによって個人ブランチでの変更がマージされているはずです。つまりマージ処理も Github API で実装できていることが確認できます:
2021052006


可能であれば(リポジトリに collaborators として招待した)別の Github ユーザーで同じ手順を実行してみてください。まずはログインして、別のファイルを push します:
2021052007


この時点で(正確には最初のログイン直後の時点で)このユーザーの個人ブランチが作られ、同ブランチにファイルが push された状態になっています:
2021052008

2021052009


ここでこのユーザーも merge を行うと、__all__ ブランチには元のユーザーが追加したファイルと、このユーザーが追加したファイル両方が含まれる状態になります(マージ元である個人ブランチには変化はありません):
2021052010


というわけで、ログインユーザー毎にブランチを作ってファイルを追加/削除し、特定ブランチにマージする機能が Github API で実装できました。当初「Github API を使ってファイルサーバーを作る」ことを目的としていたのですが、ここまで実現できたことで技術的に最低限必要な機能は実装できそうだと思っています。


【Github API による実装内容】
次にこのサンプルアプリケーションのソースコードを解説して、このアプリケーションの機能を Github API でどのように実装しているのかを紹介します。改めてソースコードはこちらです:
https://github.com/dotnsf/githubapi

また、以下の説明の大半は api/github.js ファイル内で実装している内容です。実際のソースコードを参照する場合はこのファイルを見て確認してください。

すべてのコードを説明するのはさすがにちと大変そうなので、「ブランチを作成」する処理と「ファイルをブランチに追加」するという2つの処理内容をソースコード含めて紹介します。


「ブランチ作成」
まずは「ブランチを作る」部分(正確には「main ブランチをコピーして新しいブランチを作る」部分)です。サンプルアプリケーションではログイン時に個人ブランチとマージ対象ブランチを作っています(いずれも作成成功するのは初回のみで、2回目以降は "already exists" というエラーになりますが無視します)。

OAuth 処理の一環で GET /api/callback を処理してログインが成功すると、InitMyBranch() 関数を実行します。この InitMyBranch() 関数は次のようになっています:
async function InitMyBranch( access_token ){
  return new Promise( async function( resolve, reject ){
    if( access_token ){
      var option = {
        url: 'https://api.github.com/user',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      request( option, async function( err, res0, body ){
        if( err ){
          console.log( { err } );
          resolve( false );
        }else{
          body = JSON.parse( body );
          //. body = { login: 'dotnsf', id: XXXXXX, avatar_url: 'xxx', name: 'きむらけい', email: 'xxx@xxx', created_at: 'XX', updated_at: 'XX', ... }
          loggedIns[access_token].user = body;

          //. https://qiita.com/nysalor/items/68d2463bcd0bb24cf69b

          //. main ブランチの SHA 取得
          var option1 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/main',
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          console.log( { option1 } );
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              resolve( false );
            }else{
              body1 = JSON.parse( body1 );
              console.log( { body1 } );  //. body1 = { message: 'Git Repository is empty.', documentation_url 'xxx' }  ->  あらかじめリポジトリの main ブランチに README.md などを登録しておくことで回避
              var sha1 = body1.object.sha;
  
              //. 個人ブランチ作成
              var data2 = {
                ref: 'refs/heads/' + body.id,
                sha: sha1
              };
              var option2 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                json: data2,
                method: 'POST'
              };
              request( option2, async function( err2, res2, body2 ){
                if( err2 ){
                  console.log( { err2 } );
                  resolve( false );
                }else{
                  console.log( { body2 } );  //. { message: 'Reference already exists', .. }
                  //body2 = JSON.parse( body2 );
                  //console.log( { body2 } );

                  //. 作成したブランチの SHA 取得(?)
                  var option3 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + body.id,
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    method: 'GET'
                  };
                  console.log( { option3 } );
                  request( option3, async function( err3, res3, body3 ){
                    var obj = false;
                    if( err3 ){
                      console.log( { err3 } );
                      //resolve( false );
                    }else{
                      body3 = JSON.parse( body3 );
                      console.log( { body3 } );  //. 権限がないユーザーだと { message: 'Not Found', documentation_url: '' }
                      if( body3.message ){
                        //resolve( false );
                      }else{
                        var sha3 = body3.object.sha;
                        //req.session.oauth.sha = sha3;
                        body.sha = sha3;

                        //. ターゲットブランチの生成結果に関係なく、この値を返す
                        obj = JSON.parse( JSON.stringify( body ) );
                        //resolve( body );
                      }
                    }

                    //. ターゲットブランチ作成
                    var data4 = {
                      ref: 'refs/heads/' + settings.target_branch_name,
                      sha: sha1
                    };
                    var option4 = {
                      url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                      headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                      json: data4,
                      method: 'POST'
                    };
                    request( option4, async function( err4, res4, body4 ){
                      if( err4 ){
                        console.log( { err4 } );
                      }else{
                        console.log( { body4 } );  //. { message: 'Reference already exists', .. }
                      }
                      resolve( obj );
                    });
                  });
                }
              });
            }
          });
        }
      });
    }else{
      resolve( false );
    }
  });
}

InitMyBranch 関数は OAuth 処理で取得したアクセストークンを引数に渡されて実行します。この関数内ではまず(1)個人ブランチを作成 して、次に(2)マージ対象ブランチを作成 します。処理内容は似ていますが、この順に説明します。

まず(1)個人ブランチの作成です。個人ブランチはユーザー個人の Github ユーザー ID 名のブランチを作ることになるので、まずは実行ユーザーの Github ユーザー ID を取得する必要があります。そのため以前のブログエントリでも説明した個人ユーザー情報を取得する API を実行します:
            :
            :
      var option = {
        url: 'https://api.github.com/user',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      request( option, async function( err, res0, body ){
        if( err ){
          console.log( { err } );
          resolve( false );
        }else{
          body = JSON.parse( body );
            :
            :

これで Github API を取得すれば、作成する個人ブランチの名称が決まったことになります。次にその名称のブランチを作成するのですが、git のブランチは作成元ブランチを指定する必要があります(CLI でも同様ですが、ブランチ作成元になるブランチを指定する必要がある、という意味です)。今回は main ブランチを元に個人ブランチを作るので、まずは main ブランチの SHA 情報を取得しておく必要があります。というわけで、これも以前のブログエントリで説明した方法で main ブランチの情報を取得する API を実行します:
            :
            :
          //. main ブランチの SHA 取得
          var option1 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/main',
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          console.log( { option1 } );
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              resolve( false );
            }else{
              body1 = JSON.parse( body1 );
              console.log( { body1 } );
              var sha1 = body1.object.sha;

            :
            :

これで個人ブランチを作成するために必要な情報が揃いました。あらためて main ブランチの SHA を指定して作成元を明示した上で、新しいブランチを Github ユーザー ID 名で作成します:
            :
            :
              //. 個人ブランチ作成
              var data2 = {
                ref: 'refs/heads/' + body.id,
                sha: sha1
              };
              var option2 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                json: data2,
                method: 'POST'
              };
              request( option2, async function( err2, res2, body2 ){
                if( err2 ){
                  console.log( { err2 } );
                  resolve( false );
                }else{
                  console.log( { body2 } );  //. { message: 'Reference already exists', .. }

            :
            :

余談ですが、既に個人ブランチが作成済みであった場合、この API 実行は失敗するのですが、REST API の HTTP ステータスとしては 200 が、作成メッセージとして "Reference already exists" が返ります(上のコードの赤字部分)。一般的にはこういう場合は 400 番代のエラーステータスコードが返ってくることが多いと思っていますが、この点は注意が必要です(処理的にはエラー扱いしなくてよい、こちらの方が楽ですけど・・)。

これで個人ブランチが作成された状態になりました。続いて同様にマージ対象ブランチを作成します。これも main ブランチを元に新しいブランチを作る形になるため、main ブランチの SHA を指定して、settings.js 内の target_branch_name 変数で定義した新しいブランチを作成します:
            :
            :
                    //. ターゲットブランチ作成
                    var data4 = {
                      ref: 'refs/heads/' + settings.target_branch_name,
                      sha: sha1
                    };
                    var option4 = {
                      url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                      headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                      json: data4,
                      method: 'POST'
                    };
                    request( option4, async function( err4, res4, body4 ){
                      if( err4 ){
                        console.log( { err4 } );
                      }else{
                        console.log( { body4 } );  //. { message: 'Reference already exists', .. }
                      }
                      resolve( obj );
                    });
            :
            :


「ファイルを追加」
次は「ブランチにファイルを追加する」部分です。処理としてはブラウザからファイルをアップロードして、そのアップロードしたファイルを API でブランチ(の tree)に追加する、という流れになります。tree については前回のブログエントリを参照してください。

まずはファイルアップロード処理の部分から見てみます。ブラウザ画面からファイルを選択して push ボタンを押すと、POST /api/file という API が実行されます。その内容がこちらで、アップロードされたファイル(のパス)を取り出し、アクセストークンなどと一緒に PushToMyBranch() 関数を実行しています:
router.post( '/file', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  if( req.session && req.session.oauth && req.session.oauth.token && req.session.oauth.id, req.session.oauth.sha ){
    //. https://qiita.com/ngs/items/34e51186a485c705ffdb
    var filepath = req.file.path;
    var filetype = req.file.mimetype;
    //var filesize = req.file.size;
    var ext = filetype.split( "/" )[1];
    var filename = req.file.filename;
    var originalfilename = req.file.originalname;

    var r = await PushToMyBranch( req.session.oauth.token, req.session.oauth.id, req.session.oauth.sha, filepath, filetype, originalfilename );
    console.log( { r } );
    if( r ){
      //. 追加したブランチの最新 SHA を取得
      r = await InitMyBranch( req.session.oauth.token );
      console.log( { r } );
      if( r ){
        req.session.oauth.sha = r.sha;
      }
    }

    //res.write( JSON.stringify( { result: r }, null, 2 ) );
    //res.end();
    res.redirect( '/' );
  }else{
    //res.status( 400 );
    //res.write( JSON.stringify( { error: 'no access_token' }, null, 2 ) );
    //res.end();
    res.redirect( '/' );
  }
});

PushToMyBranch() 関数内の処理がこちらです。
async function PushToMyBranch( access_token, id, sha, filepath, filetype, originalfilename ){
  return new Promise( async function( resolve, reject ){
    if( access_token && id && sha ){
      var data1 = {};
      if( filetype.startsWith( 'text' ) ){
        //. text
        var text = fs.readFileSync( filepath, 'utf8' );
        data1 = {
          content: text,
          encoding: 'utf-8'
        };
      }else{
        //. binary
        var bin = fs.readFileSync( filepath );
        data1 = {
          content: new Buffer( bin ).toString( 'base64' ),
          encoding: 'base64'
        };
      }
      console.log( { data1 } );
  
      //. BLOB 作成
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/blobs',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        json: data1,
        method: 'POST'
      };
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          fs.unlink( filepath, function( e ){} );
          resolve( false );
        }else{
          //body1 = JSON.parse( body1 );
          //. body1 = { url: 'XXXXX', sha: 'XXXXXX' }
          console.log( { body1 } );
          var sha1 = body1.sha;

          //. ここで Tree を新規に作成するのではなく、既存の最新 Tree を取得して追加する
          //. 最後に InitMyBranch() を実行するなりして、セッション内 sha の更新が必要?
          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha,
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          request( option2, async function( err2, res2, body2 ){
            if( err2 ){
              console.log( { err2 } );
              resolve( false );
            }else{
              body2 = JSON.parse( body2 );  //. body2 = { commit: {}, url: '', author: {}, files: [], .. }
              console.log( { body2 } );

              //. tree
              var option3 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees/' + body2.sha,
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                method: 'GET'
              };
              request( option3, async function( err3, res3, body3 ){
                if( err3 ){
                  console.log( { err3 } );
                  resolve( false );
                }else{
                  body3 = JSON.parse( body3 );
                  console.log( { body3 } ); //. body3.tree = [ { path: "README.md", size: 130, url: "", .. }, .. ]
    
                  //. Tree 追加
                  var data4 = { tree: [] };
                  data4.tree = JSON.parse( JSON.stringify( body3.tree ) );
                  data4.tree.push( { path: originalfilename, mode: '100644', type: 'blob', sha: sha1 } );

                  var option4 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees',
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    json: data4,
                    method: 'POST'
                  };
                  request( option4, async function( err4, res4, body4 ){
                    if( err4 ){
                      console.log( { err4 } );
                      fs.unlink( filepath, function( e ){} );
                      resolve( false );
                    }else{
                      //body4 = JSON.parse( body4 );
                      console.log( { body4 } );
                      var sha4 = body4.sha;

                      //. 現在の Commit の SHA を取得
                      var option5 = {
                        url: 'https://api.github.com/repos/' + settings.repo_name + '/branches/' + id,
                        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                        method: 'GET'
                      };
                      request( option5, async function( err5, res5, body5 ){
                        if( err5 ){
                          console.log( { err5 } );
                          fs.unlink( filepath, function( e ){} );
                          resolve( false );
                        }else{
                          body5 = JSON.parse( body5 );
                          console.log( { body5 } );
                          var sha5 = body5.commit.sha;

                          //. Commit を作成
                          var ts = ( new Date() ).getTime();
                          var data6 = {
                            message: '' + ts,
                            tree: sha4,
                            parents: [ sha5 ]
                          };
                          var option6 = {
                            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/commits',
                            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                            json: data6,
                            method: 'POST'
                          };
                          request( option6, async function( err6, res6, body6 ){
                            if( err6 ){
                              console.log( { err6 } );
                              fs.unlink( filepath, function( e ){} );
                              resolve( false );
                            }else{
                              //body6 = JSON.parse( body6 );
                              console.log( { body6 } );
                              var sha6 = body6.sha;
          
                              //. リファレンスを更新
                              var data7 = {
                                force: false,
                                sha: sha6
                              };
                              var option7 = {
                                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + id,
                                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                                json: data7,
                                method: 'PATCH'
                              };
                              request( option7, async function( err7, res7, body7 ){
                                if( err7 ){
                                  console.log( { err7 } );
                                  fs.unlink( filepath, function( e ){} );
                                  resolve( false );
                                }else{
                                  //body7 = JSON.parse( body7 );
                                  console.log( { body7 } );
                                  fs.unlink( filepath, function( e ){} );

                                  var sha7 = body7.object.sha;

                                  resolve( { sha: sha7 } );
                                }
                              });
                            }
                          });
                        }
                      });
                    }
                  });
                }
              });
            }
          });
        }
      });
    }else{
      if( filepath ){
        fs.unlink( filepath, function( e ){} );
      }
      resolve( false );
    }
  });
}

では PushToMyBranch() 関数内の処理を順に説明します。この関数内ではまず(1)ファイルの blob を作成 した後に(2)ブランチ内の現在のファイルツリーを取得 します。そして(3)ファイルの blob をツリーに追加 してから(4)ファイルをコミット し、最後に(5)リファレンス SHA を更新 する、という一連の流れを処理しています。では上述のコードを少しずつ見ながら、この流れを説明します。

まず(1)ファイルの blob を作成する部分です。ここはアップロードされたファイルのタイプを参照して、テキストファイルだったら UTF-8 エンコードで、それ以外だったら Base64 エンコードして中身を取り出した上で blob オブジェクトを作成し、その SHA を取り出します:
            :
            :
      var data1 = {};
      if( filetype.startsWith( 'text' ) ){
        //. text
        var text = fs.readFileSync( filepath, 'utf8' );
        data1 = {
          content: text,
          encoding: 'utf-8'
        };
      }else{
        //. binary
        var bin = fs.readFileSync( filepath );
        data1 = {
          content: new Buffer( bin ).toString( 'base64' ),
          encoding: 'base64'
        };
      }
      console.log( { data1 } );
  
      //. BLOB 作成
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/blobs',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        json: data1,
        method: 'POST'
      };
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          fs.unlink( filepath, function( e ){} );
          resolve( false );
        }else{
          //body1 = JSON.parse( body1 );
          //. body1 = { url: 'XXXXX', sha: 'XXXXXX' }
          console.log( { body1 } );
          var sha1 = body1.sha;
            :
            :

(2)この blob をブランチに追加するのですが、そのためにブランチの現在のファイルツリーを取得しておきます:
            :
            :
          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha,
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          request( option2, async function( err2, res2, body2 ){
            if( err2 ){
              console.log( { err2 } );
              resolve( false );
            }else{
              body2 = JSON.parse( body2 );  //. body2 = { commit: {}, url: '', author: {}, files: [], .. }
              console.log( { body2 } );

              //. tree
              var option3 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees/' + body2.sha,
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                method: 'GET'
              };
              request( option3, async function( err3, res3, body3 ){
                if( err3 ){
                  console.log( { err3 } );
                  resolve( false );
                }else{
                  body3 = JSON.parse( body3 );
                  console.log( { body3 } ); //. body3.tree = [ { path: "README.md", size: 130, url: "", .. }, .. ]
            :
            :

そして(3)ファイルの blob をツリーに追加します:
            :
            :
                  //. Tree 追加
                  var data4 = { tree: [] };
                  data4.tree = JSON.parse( JSON.stringify( body3.tree ) );
                  data4.tree.push( { path: originalfilename, mode: '100644', type: 'blob', sha: sha1 } );

                  var option4 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees',
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    json: data4,
                    method: 'POST'
                  };
                  request( option4, async function( err4, res4, body4 ){
                    if( err4 ){
                      console.log( { err4 } );
                      fs.unlink( filepath, function( e ){} );
                      resolve( false );
                    }else{
                      //body4 = JSON.parse( body4 );
                      console.log( { body4 } );
                      var sha4 = body4.sha;

            :
            :

そして(4)この状態をコミットします:
            :
            :
                      //. 現在の Commit の SHA を取得
                      var option5 = {
                        url: 'https://api.github.com/repos/' + settings.repo_name + '/branches/' + id,
                        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                        method: 'GET'
                      };
                      request( option5, async function( err5, res5, body5 ){
                        if( err5 ){
                          console.log( { err5 } );
                          fs.unlink( filepath, function( e ){} );
                          resolve( false );
                        }else{
                          body5 = JSON.parse( body5 );
                          console.log( { body5 } );
                          var sha5 = body5.commit.sha;

                          //. Commit を作成
                          var ts = ( new Date() ).getTime();
                          var data6 = {
                            message: '' + ts,
                            tree: sha4,
                            parents: [ sha5 ]
                          };
                          var option6 = {
                            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/commits',
                            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                            json: data6,
                            method: 'POST'
                          };
                          request( option6, async function( err6, res6, body6 ){
                            if( err6 ){
                              console.log( { err6 } );
                              fs.unlink( filepath, function( e ){} );
                              resolve( false );
                            }else{
                              //body6 = JSON.parse( body6 );
                              console.log( { body6 } );
                              var sha6 = body6.sha;

            :
            :

最後にこの後の処理に備えて(5)リファレンス SHA を更新することで一連のアップロード処理は完了します:
            :
            :
                              //. リファレンスを更新
                              var data7 = {
                                force: false,
                                sha: sha6
                              };
                              var option7 = {
                                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + id,
                                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                                json: data7,
                                method: 'PATCH'
                              };
                              request( option7, async function( err7, res7, body7 ){
                                if( err7 ){
                                  console.log( { err7 } );
                                  fs.unlink( filepath, function( e ){} );
                                  resolve( false );
                                }else{
                                  //body7 = JSON.parse( body7 );
                                  console.log( { body7 } );
                                  fs.unlink( filepath, function( e ){} );

                                  var sha7 = body7.object.sha;

                                  resolve( { sha: sha7 } );
                                }
                              });

            :
            :

これ以外にもブランチからファイルを削除するなど、他に実装している内容もありますが、なんとなく API を実行する際の共通の流れ(変更する場合は直前の状態の SHA を取得し、その値を指定して変更の POST を実行)がわかるのではないかと思います。

また上のソースコードの最後の Github API 実行部分をよく見ると、新しい状態の SHA を取得するための REST API のメソッドが珍しい PATCH になっていることがわかります。メソッドの種類として存在は知っていましたが、実際に使ったのは初めてのような気がします。それくらい珍しい REST API でした。



このブログエントリの続きです:
Github API を使う(1)


前回は Github API を使って外部アプリケーションから OAuth 認証を行ってアクセストークンと取得するところまでをサンプルコードと一緒に紹介しました。今回は取得したアクセストークンを使って、実際に API を実行する様子を紹介します。内容としては前回の続きとなるので、まずは前回の内容を理解しておいてください。

また今回もこのサンプルアプリケーションを使って説明します。ソースコードなどはこちらを参照してください:
https://github.com/dotnsf/github_oauth_sample


今回紹介するのは2つの API です:
(1)ログインしたユーザー自身の情報を取り出す
(2)ログインしたユーザーの権限で対象リポジトリ内の main ブランチのファイル一覧を取り出す


まずは(1)の実装を説明します。(1)は機能としては前回説明した内容にも含まれていました。前回の最後にサンプルアプリケーションにログインすると、ログインしたユーザーのアバター画像(URL)や Github ID を取得して表示していたのですが、実はここで既に(1)に相当する API が実行されていました:
2021051602


これはソースコードでは api/github.js 内の(前回紹介した)アクセストークンを取得した直後に GetMyInfo() 関数を実行して取得していたものです。この GetMyInfo() 関数は以下のようになっています:
async function GetMyInfo( access_token ){
  return new Promise( async function( resolve, reject ){
    if( access_token ){
      var option = {
        url: 'https://api.github.com/user',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      request( option, async function( err, res0, body ){
        if( err ){
          console.log( { err } );
          resolve( false );
        }else{
          body = JSON.parse( body );
          //. body = { login: 'dotnsf', id: XXXXXX, avatar_url: 'xxx', name: 'きむらけい', email: 'xxx@xxx', created_at: 'XX', updated_at: 'XX', ... }

          resolve( body );
        }
      });
    }else{
      resolve( false );
    }
  });
}

取得したアクセストークン(access_token)をヘッダに入れて GET https://api.gihub.com/user を実行しています。これで OAuth 認証したユーザーの情報を取得することができ、その結果(body)をそのまま返す、という関数です。

ログイン後のサンプルアプリケーションではこの結果を使ってアバターの URL (avatar_url)から Github アバター画像を表示したり、ホバー時に Github ID を表示するようにしていました:
2021051602


次に(2)です。これはログイン後の画面に表示される files ボタンをクリックすると画面下部に表示される、dotnsf/my_githubapi_test リポジトリ内 main ブランチのファイル一覧を取得する API です:
2021051604


ソースコードでは少し長めの関数(ListFiles())になっています。複数の Github API を連続で実行しているので、1つずつ説明します:
//. 現在のファイル一覧
async function ListFiles( access_token ){
  return new Promise( async function( resolve, reject ){
    if( access_token ){
      //. main ブランチの SHA 取得
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/main',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      console.log( { option1 } );
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          resolve( false );
        }else{
          body1 = JSON.parse( body1 );
          console.log( { body1 } );  //. body1 = { message: 'Git Repository is empty.', documentation_url 'xxx' }  ->  あらかじめリポジトリの main ブランチに README.md などを登録しておくことで回避
          var sha1 = body1.object.sha;

          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha1,
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          request( option2, async function( err2, res2, body2 ){
            if( err2 ){
              console.log( { err2 } );
              resolve( false );
            }else{
              body2 = JSON.parse( body2 );  //. body2 = { commit: {}, url: '', author: {}, files: [], .. }
              console.log( { body2 } );

              //. tree
              var option3 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees/' + body2.sha,
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                method: 'GET'
              };
              request( option3, async function( err3, res3, body3 ){
                if( err3 ){
                  console.log( { err3 } );
                  resolve( false );
                }else{
                  body3 = JSON.parse( body3 );
                  console.log( { body3 } ); //. body3.tree = [ { path: "README.md", size: 130, url: "", .. }, .. ]
    
                  resolve( body3.tree );
                }
              });
            }
          });
        }
      });
    }else{
      resolve( false );
    }
  });
}

この ListFiles() 関数では Github API を3回実行しています。特定ブランチのファイル一覧を取得するだけなんですが、一発で取得するような便利な方法は用意されておらず、ある程度 Git の内部構造を理解する必要もあります。まあ、いい勉強にもなるので、もう少しお付き合いください。

ListFiles() 関数内では以下3つの Github API を順次実行しています:

(2-1)対象リポジトリの、対象ブランチの SHA 値を取得する

Github API では多くのタイミングで SHA(Secure Hash Algorithm) 値を取得したり、取得した値を使って実行したりします。これは1つのリポジトリ内に多くのブランチが存在していたり、ブランチ内にも多くのコミット情報が記録されているので、「リポジトリ内のある瞬間の状態」が複数管理されている状態にあるわけです。そのそれぞれの状態が SHA によるハッシュ値を使って管理されています。今回はファイル一覧の情報を取得することが目的なのですが、「どのファイル一覧」かを指定するための準備段階から実装する必要があります。

ソースコード(api/github.js)内の ListFiles() 関数では、まず(settings.js 内の repo_name 変数に記載された)対象リポジトリの main ブランチを対象としてファイル一覧を取得しています。その第一段階として現在の main ブランチの SHA を取得する必要があり、具体的には GET https://api.github.com/repos/(対象リポジトリ)/(対象ブランチ)/git/refs/heads/main を実行して、その実行結果の object.sha 値を取り出しています。


(2-2)対象ブランチの最新コミットの SHA 値を取得する

取得した main ブランチの SHA を使って、同ブランチの最新コミットの SHA 値を取得(インスペクト)します。ここは具体的には GET https://api.github.com/(対象リポジトリ)/(対象ブランチ)/commits/(SHA値) を実行し、その実行結果の sha 値を取り出しています。


(2-3)対象ブランチの最新コミットのファイルツリーを取得する

ここまでの準備作業を経て、目的のファイル一覧を取得できるようになります。ファイル一覧はツリーと呼ばれる構造に格納されているので、直前に取得したコミットの SHA 値を使って、GET https://api.github.com/(対象リポジトリ)/(対象ブランチ)/git/trees/(SHA 値) を実行し、その実行結果の tree 値を取り出して、関数の実行結果としています。


フロントエンド側ではこの結果を使ってファイル一覧を表示します:
2021051604



とりあえず2回に渡って Github API を使った OAuth ログインおよびアクセストークンの取得と、取得したアクセストークンを使っての(ログインしたユーザー権限での)Github API をいくつか実行する様子をサンプルを交えて紹介しました。基本的にはこの応用でもっといろいろな API も実行できるし、前回の冒頭で紹介した「Github リポジトリを使ったファイルサーバーっぽいもの」の開発もできると思っています。

予定としては次回のエントリで、実際に Github API で作成したファイルサーバーを紹介するつもりです。お楽しみに。


(2021/05/21 追記 続きはこちら

Github API を使って簡易アプリケーションを作ってみました。その成果物を紹介すると同時に、Github API そのものについても少しずつ紹介する内容を書きたいと思ってブログエントリを書き始めています。アプリケーションの紹介までは長くなりそうなのでテーマを分割して、今回はとりあえず準備段階を含めた OAuth ログインを紹介します。


【もともとやりたかったこと】
もともとは「ファイルサーバー的なものを作りたい」と思っていました。ファイルサーバー自体はアプリケーション開発テーマとしては(わざわざ新たに作る必要があるものとも思わず、Box とかを使えばいいので)既に枯れたテーマだと思っていますが、ファイルのバージョン管理機能など細かな使い勝手を意識して実装しようとすると既存のものだけでは難しそうだと判断しました。その技術検討の中で(本来の目的とは違う使い方であることを理解した上で)Git のリポジトリをファイルサーバーとみなすとバージョン管理ははじめからついているし、個人ごとのフォルダも個人ごとにブランチを作ればいけそうだし、管理者は管理者用ブランチにマージすればまとめて見れるし、自分の希望に近いことを実現できるのではないか? と思いついたのでした。

要するにファイルサーバーのバックエンドとして Git を使い、そのバックエンド部分を API で読み書きするようなフロントエンドをアプリケーションとして実装すればよい、ということになります。実現するための Git の API として上述の Github API の存在を知ったことでなんかできそうな目処がたったので、実際に作って検証してみた、という経緯です。


【Github API を使う準備】
これから作るアプリケーションでは Github API を使うため、まずは Github に API を利用するアプリケーションを登録して、OAuth 認証用の各種 ID を取得したり、コールバック URL を設定しておく、という準備段階を済ませておく必要があります。以下、その手順を紹介します。

ウェブブラウザで Github にログインし、右上のアイコンメニューから Settings を選択します:
2021051601


Settings 画面の左で Developer settings を選択します:
2021051602


今回作るアプリケーションは Github に外部ログインするウェブアプリケーションです。この外部ログインを実現するため OAuth App として登録する必要があります。左メニューで OAuth Apps を選択して、"New OAuth App" ボタン(初めて登録する場合は "Register a new application" ボタン)をクリックします:
2021051603


アプリケーション名(適当)とアプリケーション URL (適当、http://localhost:8080/ など)を指定後、コールバック URL を指定します。この値は OAuth 認証後にリダイレクトする先の URL となり、アプリケーション側ではこの URL にアクセスされた際のパラメータを見て(API 実行時に必要な)アクセストークンを取得・保管する処理が必要になります。なので、アプリケーション毎にこの処理を行う URL を定義する必要がありますが、後述のサンプルアプリケーションでは "http://localhost:8080/api/callback" に GET リクエストがあった時にこの処理を行う想定で作っています。そのためアプリケーション URL には http://localhost:8080/api/callback と入力してください。他の値はオプションなので適当に入力し、最後に "Register Application" ボタンをクリックします:
2021051604


※注意点として、Github API ではこのコールバック URL は OAuth アプリケーションごとに1つだけしか登録できないようです。今回は localhost 環境で動かす想定でコールバック URL を http://localhost:8080/api/callback と登録しましたが、実際に公開するサービスとして利用する場合は、その本番サーバーのホスト名やポート番号を使って指定する必要があります。本番利用時にはこの値を書き換えるか、あるいは本番サーバー用の OAuth アプリケーションを新たに登録し、そこで取得した値(後述)を使ってアプリケーションを動かしてください。


アプリケーションの登録が完了した直後の画面に、外部アプリケーションからの認証時に指定する必要のある各種情報が表示されます(client_secret は "Generate a new client secret" ボタンをクリックすることで表示されます)。このうちの client_idclient_secret の値を(後で使うので)メモしておきます。なお、client_id の値は忘れてしまった場合でも改めて OAuth Apps 一覧から選択することで再び参照することができますが、client_secret の値はこの画面を閉じてしまうと2度と参照することができません(client_id ごとリセットして取得し直す必要があります)。間違えないように正しく記録を残しておくように気をつけてください:
2021051605


これで Github API を使うための、OAuth App を登録する手順が完了しました。次はこの OAuth App を実際に作って動かす段階となりますが、その前に Github API を使って操作する Github リポジトリを用意しておきます。


【Github API で操作するリポジトリを用意する】
後述のサンプルアプリケーションでは「指定した特定の Github リポジトリ内の main ブランチに属しているファイルの一覧を取得する」ことが可能な実装をしています。そのための「特定の Github リポジトリ」を用意します。

単にファイル一覧を読み取るだけなので(変更を加えるわけではないので)、既存の Github リポジトリがあればそれを使ってもいいし、新規に Github リポジトリを作成した上で指定しても構いません。今回は dotnsf/my_githubapi_test という動作確認用リポジトリを作り、この中の main ブランチのファイル一覧を取得するようなアプリケーションとして以下の説明を続けます(実際にはみなさんも独自のリポジトリを作って、README.md 他を main ブランチに入れておいてください):
2021051606


ただ一点注意が必要です。上述の client_id / client_secret を取得した時に使った Github ユーザー ID と同じ ID でログインする場合はこのままでいいのですが、別の Github ユーザー ID で使いたい場合や、友人など別の Github ユーザー ID からも同アプリケーションを使わせたい場合など、(client_id / client_secret を取得した時とは)別の Github ユーザーにもこのアプリケーションを使わせたい場合、そのユーザーが対象のリポジトリを読み取るための Collaborator 権限設定が必要になります。 その場合は同リポジトリのオーナーでブラウザログインし、対象リポジトリを開いてから Settings メニューを選択します:
2021051607


そして画面左から Manage Access メニューを選択し(パスワードを聞かれます)、画面右の Manage Access 内の "Invite a collaborator" ボタンをクリックします:
2021051608


そして後述のアプリケーションを使わせたい Github ユーザーを指定して "Add **** to this repository" ボタンをクリックします:
2021051609


すると指定されたユーザーに招待メールが送信され、メール内の "View invitations" リンクから遷移して accept することで該当リポジトリに対する Collaborator 権限が付与され、リポジトリのオーナー以外のユーザーでも操作できるようになります:
2021051601



ここまでの作業で外部アプリケーションからログインするための設定と、操作対象リポジトリの準備ができました。ではサンプルのアプリケーションを使って、実際に Github API が動作する様子を体験してみます。


【Github の OAuth を使って外部アプリケーションから Github にログインする】
Node.js を使って Github API を実際に動かすサンプルアプリケーションを用意しました。Node.js 導入環境を使って、こちらから git clone するかダウンロード&展開してください:
https://github.com/dotnsf/github_oauth_sample


ソースコード内の settings.js ファイルを編集して、上述の準備段階で集めた情報を指定します。git clone 後かダウンロード後、ローカルシステムにある同ファイルをテキストエディタで開き、以下のように値を入力します(青字はコメント):
//. settings.js
exports.client_id = 'xxxxxxx'; OAuth App 作成時に取得した client_id の値
exports.client_secret = 'xxxxxxx'; OAuth App 作成時に取得した client_secret の値
exports.callback_url = 'http://localhost:8080/api/callback'; OAuth App 作成時に指定したコールバック URL の値
exports.repo_name = 'dotnsf/my_githubapi_test';  操作対象リポジトリ名

この状態で npm install を指定して依存ライブラリを導入してから node app でアプリケーションを起動します:
$ npm install

$ node app

起動に成功すると、このサンプルアプリケーションは 8080 番ポートで HTTP リクエストを待ち受けます。ウェブブラウザで http://localhost:8080/ にアクセスします:
2021051601


最初はログイン前なので "login" ボタンが表示されています。このボタンをクリックすると Github API を使った OAuth ログインの処理がスタートします。一度もログインしたことがない場合は以下のような同意画面が表示されるので、ユーザー名を確認後に "Authorize ***" をクリックしてください:
E1ewH_vVkAAwHHE


すると再度 http://localhost:8080/ に転送されますが、今度はログイン後なのでユーザー情報を取得することができ、ログインした Github ユーザーのアバターアイコンやユーザー ID が画面右上に表示されます。このアイコンが自分の Github ユーザーアイコンであることを確認してください。このアイコンをクリックしてログアウトすることもでき、ログアウトすると再度 login ボタンが表示されます:
2021051602


とりあえず、Github API を使ったログイン処理を実装することができました。以下、アプリケーションのログイン部分の仕組みを解説します。


【Github の OAuth ログインの仕組み】
詳しくはサンプルアプリケーションのソースコード内 api/github.js を見ていただきたいのですが、正確には「ログイン認証の仕組み」というよりは「ログイン認証してアクセストークンを取得する仕組み」です。

まずログイン認証の仕組みは OAuth を使っています。アプリケーションで "login" ボタンをクリックすると、https://github.com/login/oauth/authorize にリダイレクトしています。その際に URL パラメータに付与する形で settings.js 内に記載した client_id の値と callback_url の値を指定しています:
router.get( '/login', function( req, res ){
  //. GitHub API V3
  //. https://docs.github.com/en/developers/apps/authorizing-oauth-apps
  res.redirect( 'https://github.com/login/oauth/authorize?client_id=' + settings.client_id + '&redirect_uri=' + settings.callback_url + '&scope=repo' );
});


このリダイレクト先で上述の Github の認証を行います:
E1ewH_vVkAAwHHE


ここで Authorize するとコールバック URL に転送されます。その際に一時的なアクセストークンが code パラメータに指定される形で送付されてくるので、これを取り出した上で client_id や client_secrent などの値と一緒に https://github.com/login/oauth/access_token に POST リクエストを発行します。その実行結果(form encoded形式)から access_token 変数としてアクセストークンを取り出すことができるので、(本サンプルアプリケーションではセッション内に記録する形で)保存して、この後の Github API 実行時に指定できるようにしています。その上で改めて GetMyInfo() 関数を実行してログイン情報を取得し(詳しくは次回に)トップページにリダイレクトしています:
router.get( '/callback', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var code = req.query.code;
  var option = {
    url: 'https://github.com/login/oauth/access_token',
    form: { client_id: settings.client_id, client_secret: settings.client_secret, code: code, redirect_uri: settings.callback_url },
    method: 'POST'
  };
  request( option, async function( err, res0, body ){
    if( err ){
      console.log( { err } );
    }else{
      //. body = 'access_token=XXXXX&scope=YYYY&token_type=ZZZZ';
      var tmp1 = body.split( '&' );
      for( var i = 0; i < tmp1.length; i ++ ){
        var tmp2 = tmp1[i].split( '=' );
        if( tmp2.length == 2 && tmp2[0] == 'access_token' ){
          var access_token = tmp2[1];

          req.session.oauth = {};
          req.session.oauth.token = access_token;

          var r = await GetMyInfo( access_token );
          if( r ){
            req.session.oauth.id = r.id;
            req.session.oauth.login = r.login;
            req.session.oauth.name = r.name;
            req.session.oauth.email = r.email;
            req.session.oauth.avatar_url = r.avatar_url;
          }
        }
      }
    }
    //console.log( 'redirecting...' );
    res.redirect( '/' );
  });
});

トップページでは画面ロード直後に GET /api/isLoggedIn という API が実行されます。この API はセッション内にアクセストークンがあるかどうかを調べ、含まれていた場合はその情報(ログイン時に GetMyInfo() 関数によって取り出したユーザー情報)をレスポンスの値として返します。つまりログインしていない場合は false 値が、ログインしている場合はユーザー情報を返すという関数です。これによって、未ログイン時のトップページでは login ボタンを、ログイン時のトップページでは logout ボタンをそれぞれ表示するようにしています:
router.get( '/isLoggedIn', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var status = false;
  if( req.session && req.session.oauth && req.session.oauth.token ){
    status = JSON.parse( JSON.stringify( ( req.session.oauth ) ) );
  }

  if( !status ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false }, null, 2 ) );
  }else{
    res.write( JSON.stringify( { status: true, user: status }, null, 2 ) );
  }
  res.end();
});

なお、logout ボタンが押された場合はセッションの中身を空にする、という処理が実行されます。これによって取得したアクセストークンは無効になり、(再ログインによって)再びアクセストークンを取得するまで Github API は実行できなくなります:
router.post( '/logout', function( req, res ){
  if( req.session.oauth ){
    req.session.oauth = {};
  }
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

と、Github API を使ったログイン時の処理内容を中心に紹介しました。次回は GetMyInfo() 関数でユーザー情報を取り出す仕組みと、ログイン後に main ブランチ内のファイル一覧を取り出す仕組みを紹介する予定です。


(2021/05/18 追記 続きはこちらこちら

このページのトップヘ