20190127のNode.jsに関する記事は10件です。

初心者のためのNode.jsでServerless Frameworkするやつ。

概要

Serverless Framework を試すためにすることをまとめました。

はじめに

Serverless Framework を試すのに 3 点候補あげておきますが、この記事では「AWS Lambda」を扱います。

  • Amazon Web Services
    • AWS Lambda
  • Google Cloud Platform
    • Google Cloud Functions
  • Microsoft Azure
    • Azure Functions

前提条件

  • AWS アカウント作成済み
  • AWS マネジメントコンソールに入れること
  • IAM でアクセスキーを作成済み
  • IAM で作成したアクセスキーを確認できる状態にすること
  • Homebrew をインストール済み
  • Node.js をインストール済み
  • npm -v で問題なくバージョンが表示されること

やることリスト

  • awscli のインストール
  • AWS CLI の設定
  • Serverless Framework のインストール

awscli のインストール

AWS の機能をターミナル上から使えるようにするためのもの。

brew install awscli

AWS CLI の設定

  • accountName は任意の名前
  • --profile はどのアカウントを設定するのかを明示するもの
    • (複数の AWS アカウントを扱えるようにきちんと切り分ける)
aws configure --profile accountName

解説

IAM で作成したアクセスキーを見ながら入力する。

$ aws configure --profile accountName
AWS Access Key ID [None]: ここには Access key ID
AWS Secret Access Key [None]: ここには Secret access key
Default region name [None]: ap-northeast-1
Default output format [None]: json

Serverless Framework のインストール

npm install -g serverless

Serverless Framework で雛形を作成

  • --template は他にも指定できるものがあり、 serverless create --help で確認できます
  • --path で指定した名前のディレクトリが作成される
serverless create --template aws-nodejs --path quick-start

実行結果

"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.36.3
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

作業ディレクトリに移動

cd quick-start

早速 AWS に デプロイする

  • --verbose は情報を詳細に出してくれる
  • --aws-profileaws configure --profile した時の名前を入れる
  • --region は東京(ap-northeast-1)を指定
serverless deploy --verbose --aws-profile accountName --region ap-northeast-1

実行結果

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...

~~
~~

Serverless: Stack update finished...

Service Information
service: quick-start
stage: dev
region: ap-northeast-1
stack: quick-start-dev
api keys:
  None
endpoints:
  None
functions:
  hello: quick-start-dev-hello
layers:
  None

Stack Outputs
HelloLambdaFunctionQualifiedArn: ...
ServerlessDeploymentBucketName: ...

デプロイしたやつを AWS 上で確認する

AWS コンソールにログイン後

  • 東京リージョンになっていることを確認
  • サービス検索から「Lambda」を検索する

2019-01-27_22-58-51.png

Lambda の画面で

  • 左側に見える「関数」をクリック
  • 今回デプロイしたもの「quick-start-dev-hello」をクリック

2019-01-27_23-03-34.png

quick-start-dev-hello の画面

この画面を活用する方法は別記事で説明します。
デプロイしたものは、この様に確認できます。

2019-01-27_23-08-00.png

デプロイしたやつを実行する

  • --function は実行したい関数の名前
  • --log は実行した時のメモリ使用量や実⾏時間、課⾦対象時間などを表示
  • --aws-profile--region は前回の通り
serverless invoke --function hello --log --aws-profile accountName --region ap-northeast-1

実行結果

{
    "statusCode": 200,
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":{}}"
}

最後に

今回試しに動作させたものを消す

不要だったら捨てる。
必要ならまた serverless deploy しましょう。

serverless remove --aws-profile accountName --region ap-northeast-1

次回

Serverless Framework で API サーバーを構築するやつをやっていきましょう。

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

【Windows】pdfmakeでカスタムフォントを使用したPDFファイル生成

やりたいこと

pdfmakeでカスタムフォントを使用したPDF生成を行う

pdfmakeとは

Client/server side PDF printing in pure JavaScript

JavaScriptでクライアント側、サーバ側でのPDF生成ができるモジュールと理解しています。

利用する際に必要なツール

  1. Git
    • v2.19.0 を利用しています
  2. nodist
    • v0.8.8 を利用しています
    • nodist はWindows環境で Node.js を利用するためにインストールしています

クライアント側でPDF作成できるかチェック

pdfmakeモジュールを取得

  1. 作成した作業ディレクトリにエクスプローラでアクセス
    • 今回は D:\pdfmakeTest を作業フォルダとします
  2. パス表示部分をクリックし cmd と入力
    • 現在のディレクトリをカレントにしたコマンドプロンプトが立ち上がります
  3. git clone https://github.com/bpampuch/pdfmake.git でモジュールを取得
    • モジュール取得が完了すると、作業フォルダ直下に pdfmake というフォルダが生成されます

pdfmakeモジュールの動作確認

作業フォルダにテスト表示用のJSファイル、HTMLファイルを作成してPDFが出力されるか確認します。
(クライアントのJavaScriptだけでPDF作成(2)を参考にさせていただき、確認環境を作成しました)
作業フォルダの構造は以下です。

pdfmakeTest
 L pdfmake            <- 上記でダウンロードした pdfmake のモジュールディレクトリ
 L pdfmakeTest_01.js  <- pdfmake のメソッド呼び出し用JSファイル
 L check_01.html      <- pdfmake 確認用HTMLファイル

ソースコード

pdfmakeTest_01.js

// 連想配列で content にPDF出力用のデータを設定
var pdfContent = {content: "This is pdfmake test. これはpdfmakeのテストです。"};

// pdfmakeで content を設定した変数を引数としてPDF生成
pdfMake.createPdf(pdfContent).open();

check_01.html

<!DOCTYPE html>
<html lang="ja">

  <!-- ヘッダー -->
  <head>

    <title>pdfmakeTest</title>

    <!-- ↓↓↓ pdfmake用JSファイル呼び出し ↓↓↓ -->

    <!-- pdfmakeを利用するJSファイル(モジュールのBuildフォルダ内のJSファイル) -->
    <script type="text/javascript" src="./pdfmake/build/pdfmake.min.js"></script>
    <!-- pdf生成に利用するフォントJSファイル(モジュールのBuildフォルダ内のJSファイル) -->
    <script type="text/javascript" src="./pdfmake/build/vfs_fonts.js"></script>

    <!-- ↑↑↑ pdfmake用JSファイル呼び出し ↑↑↑ -->

    <!-- pdfmake 呼び出し用JSファイル -->
    <script type="text/javascript" src="./pdfmakeTest_01.js"></script>

  </head>

  <body>
    <!-- 何も書かない -->
  </body>
</html>

PDF生成確認

ブラウザに GoogleChrome を利用して動作を確認します。
※ 生成結果はポップアップで表示されますので、ポップアップブロックを許可してください

↓ PDFが生成されることは確認しましたが、日本語はフォントを変えないと表示されないようです。
キャプチャ.PNG

pdfmakeで利用するフォントの変更

pdfmakeで日本語が表示できるようにフォントのJSファイル(vfs_fonts.js)を更新します。

フォントの入手

環境を問わず、日本語を表示できるIPAフォントを利用させていただきます。

IPAフォントは、誰でも無償で利用できる、高品位を目指した日本語フォントです。OSI認定の“IPAフォントライセンス”と、フォントのデファクトスタンダードであるOpenTypeフォントフォーマットを採用することで、プラットフォームの種類を問わず、多様な情報機器で共通に利用することができ、どの環境の下でも同じ形状の高品質な文字の表示・印刷を可能にします。本プロジェクトでは、IPAフォントの品質向上を図るとともに、各プラットフォームでの日本語文字表示に関する情報交換の場となることを目指しています。

  1. 公式サイトのIPA Fonts/IPAex Fonts 4書体パック_IPAフォント(Ver.003.03)からフォントファイルをダウンロード
  2. zipファイルを展開
    • ipagp.tff(等幅ゴシック), ipam.tff(等幅明朝) のファイルを利用します

フォント情報の更新

CUSTOM FONTS (CLIENT-SIDE)を参考にフォント情報を更新します。

フォントファイルの入れ替え

pdfmakeTest\pdfmake\examples\fontsディレクトリ内のファイル(Roboto-xxx.ttfの4ファイル、sampleimage.jpg)を削除し、IPAフォントファイル(ipagp.ttf, ipamp.ttf)を格納します。

vfs_fonts.js の更新

以下の手順でフォント情報を更新します。

  1. pdfmakeディレクトリに移動し、Node.jsモジュールを更新
    • npm install (ズラズラっとメッセージが出て更新が行われます)
  2. 上記でインストールしたpdfmake\node_modulesgulpを利用したvfs_fonts.jsの更新
    • pdfmakeTest\pdfmake\node_modules\.bin\gulp.cmd buildFonts を入力しvfs_fonts.jsを更新します
  3. フォント情報をpdfmakeに反映
    • pdfmakeのフォルダでnpm run buildを行い、モジュールをビルドしなおします

vfs_fonts.js の確認

テキストエディタでファイルを確認し、ipagp.ttfという記述があるかを確認します。
キャプチャ2.PNG

カスタムフォントが正常に利用できるかチェック

利用フォントを設定したJSファイルの作成

作業フォルダのテスト表示用のJSファイル、HTMLファイルを更新し、日本語のPDFが出力されるか確認します。
(クライアントのJavaScriptだけでPDF作成(6-日本語フォントテスト)を参考にさせていただき、確認環境を作成しました)
作業フォルダの構造は以下です。

pdfmakeTest
 L pdfmake            <- 上記でダウンロードした pdfmake のモジュールディレクトリ
 L pdfmakeTest_02.js  <- pdfmake のメソッド呼び出し用JSファイル
 L check_02.html      <- pdfmake 確認用HTMLファイル

ソースコード

pdfmakeTest_02.js

先ほど更新したフォントを利用フォントとして設定し、表示する文字列に対して利用フォントを設定しています。

// フォント情報として、ゴシックと明朝を定義
pdfMake.fonts = {
    IPAgothic: {
        normal:      'ipagp.ttf',
        bold:        'ipagp.ttf',
        italics:     'ipagp.ttf',
        bolditalics: 'ipagp.ttf'
   },
    IPAmincho: {
        normal:      'ipamp.ttf',
        bold:        'ipamp.ttf',
        italics:     'ipamp.ttf',
        bolditalics: 'ipamp.ttf'
   }
}

// JSON形式で content にPDF出力用のデータを設定
var pdfContent = {
    content: [
        { text: "This is pdfmake test."},
        { text: "これは等幅ゴシック体(ipagp.tff)のテストです。", font: "IPAmincho"},
        { text: "これは等幅明朝体(ipamp.tff)のテストです。", font: "IPAgothic"}
   ],
   // デフォルトフォントを等幅ゴシック体に変更
   defaultStyle: {
       font: "IPAgothic"
   }
}

// pdfmakeで content を設定した変数を引数としてPDF生成
pdfMake.createPdf(pdfContent).open();

check_02.html

呼び出すJSファイルを変更しただけです。

<!DOCTYPE html>
<html lang="ja">

  <!-- ヘッダー -->
  <head>

    <title>pdfmakeTest</title>

    <!-- ↓↓↓ pdfmake用JSファイル呼び出し ↓↓↓ -->

    <!-- pdfmakeを利用するJSファイル(モジュールのBuildフォルダ内のJSファイル) -->
    <script type="text/javascript" src="./pdfmake/build/pdfmake.min.js"></script>
    <!-- pdf生成に利用するフォントJSファイル(モジュールのBuildフォルダ内のJSファイル) -->
    <script type="text/javascript" src="./pdfmake/build/vfs_fonts.js"></script>

    <!-- ↑↑↑ pdfmake用JSファイル呼び出し ↑↑↑ -->

    <!-- pdfmake 呼び出し用JSファイル -->
    <script type="text/javascript" src="./pdfmakeTest_02.js"></script>

  </head>

  <body>
    <!-- 何も書かない -->
  </body>
</html>

日本語が表示されているか確認

できました!
3.PNG

ハマったところ

カスタムフォント定義ファイルの更新

問題

grunt を利用してカスタムフォント更新する方法がわかりませんでした。

解決

pdfmakeのイシュー(Grunt #794)を確認し、gulpを利用することに気づきました!
(というよりも、公式のドキュメントにはgulpが記載されているので、最初にそれを見れば解決していました……。)

vfs_fonts.js ファイルの内容を pdfmake に反映させる

問題

vfs_fonts.jsを更新しただけでは、Uncaught File 'ipagp.ttf' not found in virtual file system というエラーが発生し、フォント情報が pdfmake.min.js から読めないようでした。

解決

npm run build で再度ビルドを行うことで、モジュール設定の更新が行われ、正常に表示することができました!

参考サイト

pdfmake

bpampuch/pdfmake
Grunt #794
PDFMAKE

カスタムフォント

クライアントのJavaScriptだけでPDF作成(2)
クライアントのJavaScriptだけでPDF作成(5-フォント作成)
クライアントのJavaScriptだけでPDF作成(6-日本語フォントテスト)

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

GitBook でドキュメントを書く準備を整える

ちょっと書いてみたくなったのでメモ

環境

ミドルウェア バージョン
Node.js 10.15.0
GitBook cli 3.2.3

手順

  1. Node.js をあらかじめインストールしておく
  2. GitBook CLI をグローバルにインストールする

    $ npm install gitbook-cli -g
    
  3. GitBook コマンドで初期化する

    $ gitbook init
    

    新規でディレクトリを作成したい場合は次のように入力する

    $ gitbook init ./<作成したいディレクトリ名>
    
  4. 現在のディレクトリ(もしくは新規作成したディレクトリ)に新しいファイルが作成されていることを確認する

    $ ls
    README.md   SUMMARY.md
    
  5. サーバを立ち上げる

    $ gitbook serve
    Live reload server started on port: 35729
    Press CTRL+C to quit ...
    
    info: 7 plugins are installed 
    info: loading plugin "livereload"... OK 
    info: loading plugin "highlight"... OK 
    info: loading plugin "search"... OK 
    info: loading plugin "lunr"... OK 
    info: loading plugin "sharing"... OK 
    info: loading plugin "fontsettings"... OK 
    info: loading plugin "theme-default"... OK 
    info: found 1 pages 
    info: found 0 asset files 
    info: >> generation finished with success in 0.4s ! 
    
    Starting server ...
    Serving book on http://localhost:4000
    

確認

ブラウザから次のURLにアクセスして、画面が表示されればOK

http://localhost:4000

注意事項

  • GitBook CLI は今後アクティブな開発は行われない(アナウンスがあった)

参考

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

NodejsでSlackにメッセージ投稿(2019年1月版)

いつの間にか仕様が変わってる…
本家の説明 => https://api.slack.com/methods/chat.postMessage

postMsgToSlack.js
  const SLACK_TOKEN = 'ここにトークンを入力'; 
  const CHANNEL_ID = '投稿したいチャンネルのIDを入力';
  const USER_NAME = '投稿する際のユーザーネームを入力';
  const msg = '投稿したいメッセージ';

  const request_promise = require("request-promise");

  const res = await request_promise({
    uri : 'https://slack.com/api/chat.postMessage',
    method : "POST",
    headers : {
        'content-type' : 'application/x-www-form-urlencoded',
        'charset' : 'utf-8' 
    },
    form : {
      token: SLACK_TOKEN,
      channel: CHANNEL_ID ,
      username: USER_NAME ,
      text: msg
    },
    json : true
  });

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

PayPal のREST APIで、ログインやEC決済を試してみる

PayPalは、EC向けにいろんなツール類を用意してくれているのですが、ドキュメントが散在してわかりにくかったり、古いものから新しいものまでごちゃまぜになっている印象があります。

そんな中で、Node.jsの勉強のためもあり、REST APIを使ってみたいと思います。
割と今風で、マルチプラットフォームに対応できそうなので、これを選びました。
(これに慣れたら、最新のBraintreeに移行したいと思っています。)

(情報源)
PayPal Developer
 https://developer.paypal.com/

PayPal API Reference
 https://developer.paypal.com/docs/api/overview/

node.js SDK for PayPal RESTful APIs
 https://github.com/paypal/PayPal-node-SDK

Connect With PayPal
 https://developer.paypal.com/docs/integration/direct/identity/

他の決済サーバと同じように、自由にいじれるSandboxがあるので、気兼ねなく試してみましょう。

毎度のことですが、Swaggerを使ったRESTfulサーバを立ち上げます。
以下ご参考まで。

SwaggerでRESTful環境を構築する

PayPal Developerアカウントの作成

まずは、以下のページからアカウントを作成しましょう。

PayPal Developer
 https://developer.paypal.com/

image.png

まず見ていただきたいのが、Sandbox Accountです。
左側のナビゲータから選択します。

image.png

あらかじめ2つのアカウントができているかと思います。

 XXXXX-facilitator@YYYYY.yyy BUSINESS
 XXXXX-buyer@YYYYY.yyy PERSONAL

これらアカウントは、PayPal Sandboxの中だけのアカウントですので、メールアドレスは実在しませんし、本番PayPalには使えません。
XXXXX-buyer@YYYYY.yyy は、あとでログインに使うので、パスワードを変えておきましょう。

REST APIを使うための準備

いくつかの種類がありますが、今回使うのは、REST API appsです。
「Create App」ボタンを押下します。

App Nameには、適当な名前を付けます。例えば、「TestApp」とします。
Sandbox developer account には、BUSINESSのアカウントを指定するのですが、今回は、あらかじめ作られていた XXXXX-facilitator@YYYYY.yyy を選択します。

image.png

そうすると、Client IDやSecretが表示されます。
後で使うので、覚えておきます。

image.png

また、右上のところで、SandboxとLiveを選択できるのですが、当然ながら試作中なので、Sandboxにしておきます。

RESTfulサーバを立ち上げる

では早速、サーバを立ち上げます。

2つのエンドポイントを使います。
Swagger定義を示します。

swagger.yaml
  /paypal-create:
    get:
      x-swagger-router-controller: routing
      operationId: paypal-create
      responses:
        200:
          description: Success
          schema:
            type: object

  /paypal-redirect:
    get:
      x-swagger-router-controller: routing
      operationId: paypal-redirect
      responses:
        200:
          description: Success
          schema:
            type: object

/paypal-create
 決済を開始します。品目や金額を指定します。例として、100円のお買い物です。
 その後、PayPalがユーザからApproveをもらうために、画面を表示してくれます。

/paypal-redirect
 PayPalの画面でユーザが承認してくれると、このエンドポイントが呼び出されます。
 Approveをもらったので、これで決済を完了させます。
 完了後、ユーザにPaymentIdなど、もろもろ返してあげています。

利用するnpmモジュールは以下の通りです。

  • paypal-rest-sdk
  • node-fetch
  • uuid

PayPalから、paypal-rest-sdkというモジュールを提供していただいているので、だいぶ楽になりました。

(エンドポイント「/paypal-token」もありますが、これは後程説明します。)

index.js
var paypal = require('paypal-rest-sdk');

const base_url = 【RESTfulサーバを立ち上げているURL】;

var Response = require('../../helpers/response');
var Redirect = require('../../helpers/redirect');
var fetch = require('node-fetch');
const { URLSearchParams } = require('url');
const uuidv4 = require('uuid/v4');

const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || 【REST API appsのClient ID】;
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET || 【REST API appsのSecret】;

paypal.configure({
    'mode': 'sandbox', //sandbox or live
    'client_id': PAYPAL_CLIENT_ID,
    'client_secret': PAYPAL_CLIENT_SECRET,
    'headers' : {
        'custom': 'header'
    }
});

exports.handler = (event, context, callback) => {
    if( event.path == '/paypal-token'){
        var body = JSON.parse(event.body);
        var code = body.code;

        var url = 'https://api.sandbox.paypal.com/v1/oauth2/token';
        var params = {
            grant_type: 'authorization_code',
            code: code
        };

        return do_post_form_basic(url, params, PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET )
        .then(json =>{
            console.log(json);
            if( json.error )
                throw json.error;
            return do_get_token('https://api.sandbox.paypal.com/v1/identity/oauth2/userinfo', { schema: 'paypalv1.1'}, json.access_token);
        })
        .then(json =>{
            console.log(json);
            if( json.error )
                throw json.error;
            return new Response( json );
        })
        .catch(error =>{
            return new Response().set_error(error);
        })
    }else if( event.path == '/paypal-create'){
        var orderid = uuidv4();

        var create_payment_json = {
            "intent": "authorize",
            "payer": {
                "payment_method": "paypal"
            },
            "redirect_urls": {
                "return_url": base_url + "/paypal-redirect",
                "cancel_url": base_url + "/paypal/index.html"
            },
            "transactions": [{
                "custom": JSON.stringify({ orderid: orderid, amount: 100 } ),
                "item_list": {
                    "items": [{
                        "name": "item",
                        "price": "100",
                        "currency": "JPY",
                        "quantity": 1
                    }]
                },
                "amount": {
                    "currency": "JPY",
                    "total": "100"
                },
                "description": "This is the payment description."
            }]
        };

        return new Promise((resolve, reject) =>{
            paypal.payment.create(create_payment_json, (error, payment) => {
                if (error) {
                    console.log(error.response);
                    return reject(error);
                }
                console.log(payment);

                for (var index = 0; index < payment.links.length; index++) {
                    if (payment.links[index].rel === 'approval_url')
                        return resolve( new Redirect(payment.links[index].href) );
                }

                return reject('approval_url not found');
            });
        });
    }else if( event.path == '/paypal-redirect'){
        var qs = event.queryStringParameters;
        console.log(qs);

        var payerid = event.queryStringParameters.PayerID;
        var paymentid = event.queryStringParameters.paymentId;

        var execute_payment_json = {
            "payer_id": payerid,
            "transactions": [{
                "amount": {
                    "currency": "JPY",
                    "total": "100"
                }
            }]
        };

        return new Promise((resolve, reject) =>{
            paypal.payment.execute(paymentid, execute_payment_json, (error, payment) => {
                if (error) {
                    console.log(error.response);
                    return reject(error);
                }
                console.log(payment);

                for( var i = 0 ; i < payment.transactions.length ; i++ ){
                    for( var j = 0 ; j < payment.transactions[i].related_resources.length ; j++ ){
                        if( payment.transactions[i].related_resources[j].authorization ){
                            var authorization_id = payment.transactions[i].related_resources[j].authorization.id;
                            console.log('authorization_id=' + authorization_id);
                        }
                    }
                }

                var params = new URLSearchParams();
                for( var key in qs )
                    params.set(key, qs[key] );

                return resolve( new Redirect(base_url + '/paypal/index.html?' + params.toString() ));
            });
        });
    }
};

function do_get_token(url, qs, token){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    return fetch(url + '?' + params.toString(), {
        method : 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }
    })
    .then((response) => {
        return response.json();
    });
}

function do_post_form_basic(url, qs, client_id, client_secret){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    const headers = { 
        "Content-Type" : "application/x-www-form-urlencoded",
        "Authorization" : "Basic " + new Buffer(client_id + ':' + client_secret).toString('base64')
    };

    return fetch(url, {
        method : 'POST',
        body : params,
        headers: headers
    })
    .then((response) => {
        return response.json();
    });
}

ユーティリティも示しておきます。

redirect.js
class Redirect{
    constructor(url){
        this.statusCode = 303;
        this.headers = {'Location' : url};
        this.body = null;
    }
}
response.js
module.exports = Redirect;

class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
        return this;
    }

    set_body(content){
        this.body = JSON.stringify(content);        
        return this;
    }

    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;

環境に合わせて、以下を変更してください。

【REST API appsのClient ID】
【REST API appsのSecret】
【RESTfulサーバを立ち上げているURL】

また、

 "return_url": base_url + "/paypal-redirect"

の部分で、ユーザによる同意の結果のリダイレクト先を指定しています。
また、以下の部分で、決済完了後のリダイレクト先を指定しています。

 return resolve( new Redirect(base_url + '/paypal/index.html?' + params.toString() ));

クライアント側のHTMLページも示します。

/paypal/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>PayPal 連携 テスト</title>

  <script src="js/vue_utils.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>PayPal 連携 テスト</h1>
        <br>
        <a class="btn btn-primary" href="【RESTfulサーバを立ち上げたURL】/paypal-create">Payment</a>
        <br>
    </div>

    <script src="js/start.js"></script>
</body>
start.js
'use strict';

const base_url = 【RESTfulサーバを立ち上げたURL】;
const PAYPAL_CLIENT_ID = 【REST API appsのClient ID】;

var vue_options = {
    el: "#top",
    data: {
    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        if( searchs.paymentId ){
            history.replaceState(null, null, '.');
            alert('決済が完了しました。');
        }
    }
};
var vue = new Vue( vue_options );
vue_utils.js
var hashs = {};
var searchs = {};

function proc_load() {
  hashs = parse_url_vars(location.hash);
  searchs = parse_url_vars(location.search);
}

function parse_url_vars(param){
  if( param.length < 1 )
      return {};

  var hash = param;
  if( hash.slice(0, 1) == '#' || hash.slice(0, 1) == '?' )
      hash = hash.slice(1);
  var hashs  = hash.split('&');
  var vars = {};
  for( var i = 0 ; i < hashs.length ; i++ ){
      var array = hashs[i].split('=');
      vars[array[0]] = array[1];
  }

  return vars;
}

function vue_add_methods(options, funcs){
    for(var func in funcs){
        options.methods[func] = funcs[func];
    }
}
function vue_add_computed(options, funcs){
    for(var func in funcs){
        options.computed[func] = funcs[func];
    }
}

以下に示すように、決済が完了して元のページに戻ってくると、paymentIdが指定されてくるので、それを受けてダイアログ表示するようにしています。

    if( searchs.paymentId ){
        history.replaceState(null, null, '.');
        alert('決済が完了しました。');
    }

それではRESTfulサーバを立ち上げてみましょう。

image.png

Paymentボタンを押下します。
そうすると、ログイン画面が表示されます。
Personalアカウントでログインします。自動で作られるXXXXX-buyer@YYYYY.yyy がありました。

image.png

今度は、品目と価格の確認画面に変わります。
内容を確認したら「同意して続行」ボタンを押下します。

image.png

めでたく最初のページに戻ってきて、ダイアログが表示できました。

image.png

PayPalアカウントでのログイン機能の追加

ついでに、PayPalアカウントでのログイン機能を追加してみます。
クライアント側に少しコードを追加します。

/paypal/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>PayPal 連携 テスト</title>

  <script src="js/vue_utils.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>PayPal 連携 テスト</h1>
        <br>
        <span id='cwppButton'></span>
        <br>
        <br>
        <a class="btn btn-primary" href="【RESTfulサーバを立ち上げたURL】paypal-create">Payment</a>
        <br>
    </div>

    <script src="js/start.js"></script>
    <script src='https://www.paypalobjects.com/js/external/connect/api.js'></script>
    <script>
        paypal.use( ['login'], function (login) {
            login.render ({
            "appid": PAYPAL_CLIENT_ID,
            "authend":"sandbox",
            "scopes":"openid profile",
            "containerid":"cwppButton",
            "locale":"ja-jp",
            "buttonType":"CWP",
            "buttonSize":"lg",
            "returnurl": base_url + "/paypal/"
            });
        });
    </script>
</body>

以下の部分が、PayPalアカウントでのログインボタンになります。

<span id='cwppButton'></span>

各種設定は、以下の部分で行います。

 paypal.use( ['login'], function (login)

containeridの指定の部分で指定したIDのspanの部分に、ボタンを描画してくれます。
ログインのために、PayPalが提供する画面に遷移したのち、ログイン完了後にまた戻ってきます。戻ってくる先は、「returnurl」の部分に指定します。

スクリプトの本体は、以下です。

 <script src='https://www.paypalobjects.com/js/external/connect/api.js'>

PayPalからログイン完了後に戻ってくると、codeが指定されています。
これは認可コードであって、これをアクセストークンに変換する必要があります。
そのために、いったんRESTfulサーバに処理を移す必要があり、それが、以下の部分です。

    if( searchs.code ){

エンドポイント「/paypal-token」を呼んでいます。
呼び出しが成功すると、PayPalアカウント名が書かれたダイアログを表示するようにしています。

start.js
'use strict';

const base_url = 【RESTfulサーバを立ち上げたURL】;
const PAYPAL_CLIENT_ID = 【REST API appsのClient ID】;

var vue_options = {
    el: "#top",
    data: {
    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        if( searchs.paymentId ){
            history.replaceState(null, null, '.');
            alert('決済が完了しました。');
        }
        if( searchs.code ){
            history.replaceState(null, null, '.');

            do_post(base_url + '/paypal-token', { code: searchs.code} )
            .then(json =>{
                console.log(json);
                alert(json.name + 'さん、こんにちは');
            });
        }
    }
};
var vue = new Vue( vue_options );

function do_post(url, body){
    console.log('do_post: ' + url);

    const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } );

    return fetch(url, {
        method : 'POST',
        body : JSON.stringify(body),
        headers: headers
    })
    .then((response) => {
        return response.json();
    });
}

エンドポイント、/paypal-tokenのサーバ部分は、すでに実装を示してました。
世の中で一般的なOAuth2やOpenID Connectとほぼ同じなので、なじみがあるかと思います。

swagger.yaml
  /paypal-token:
    post:
      x-swagger-router-controller: routing
      operationId: paypal-token
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object

それでは、もう一度、RESTfulサーバを再起動して、ブラウザからアクセスしてみます。

image.png

PayPalのボタンが増えているのがわかると思います。
PERSONALのアカウントでログインします。

image.png

PayPalのログインアカウント名が書かれたダイアログが表示されたかと思います。成功です。

image.png

感想

PayPalのドキュメント類は、すごく見にくかったのですが、REST APIに絞って熟読することで、なんとなく動作させることができました。
今後は、これを足掛かりに、他の機能も見ていこうと思います。

以上

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

PayPal のREST APIでログインやEC決済を試してみる

PayPalは、EC向けにいろんなツール類を用意してくれているのですが、ドキュメントが散在してわかりにくかったり、古いものから新しいものまでごちゃまぜになっている印象があります。

そんな中で、Node.jsの勉強のためもあり、REST APIを使ってみたいと思います。
割と今風で、マルチプラットフォームに対応できそうなので、これを選びました。
(これに慣れたら、最新のBraintreeに移行したいと思っています。)

(情報源)
PayPal Developer
 https://developer.paypal.com/

PayPal API Reference
 https://developer.paypal.com/docs/api/overview/

node.js SDK for PayPal RESTful APIs
 https://github.com/paypal/PayPal-node-SDK

Connect With PayPal
 https://developer.paypal.com/docs/integration/direct/identity/

他の決済サーバと同じように、自由にいじれるSandboxがあるので、気兼ねなく試してみましょう。

毎度のことですが、Swaggerを使ったRESTfulサーバを立ち上げます。
以下ご参考まで。

SwaggerでRESTful環境を構築する

PayPal Developerアカウントの作成

まずは、以下のページからアカウントを作成しましょう。

PayPal Developer
 https://developer.paypal.com/

image.png

まず見ていただきたいのが、Sandbox Accountです。
左側のナビゲータから選択します。

image.png

あらかじめ2つのアカウントができているかと思います。

 XXXXX-facilitator@YYYYY.yyy BUSINESS
 XXXXX-buyer@YYYYY.yyy PERSONAL

これらアカウントは、PayPal Sandboxの中だけのアカウントですので、メールアドレスは実在しませんし、本番PayPalには使えません。
XXXXX-buyer@YYYYY.yyy は、あとでログインに使うので、パスワードを変えておきましょう。

REST APIを使うための準備

いくつかの種類がありますが、今回使うのは、REST API appsです。
「Create App」ボタンを押下します。

App Nameには、適当な名前を付けます。例えば、「TestApp」とします。
Sandbox developer account には、BUSINESSのアカウントを指定するのですが、今回は、あらかじめ作られていた XXXXX-facilitator@YYYYY.yyy を選択します。

image.png

そうすると、Client IDやSecretが表示されます。
後で使うので、覚えておきます。

image.png

また、右上のところで、SandboxとLiveを選択できるのですが、当然ながら試作中なので、Sandboxにしておきます。

RESTfulサーバを立ち上げる

では早速、サーバを立ち上げます。

2つのエンドポイントを使います。
Swagger定義を示します。

swagger.yaml
  /paypal-create:
    get:
      x-swagger-router-controller: routing
      operationId: paypal-create
      responses:
        200:
          description: Success
          schema:
            type: object

  /paypal-redirect:
    get:
      x-swagger-router-controller: routing
      operationId: paypal-redirect
      responses:
        200:
          description: Success
          schema:
            type: object

/paypal-create
 決済を開始します。品目や金額を指定します。例として、100円のお買い物です。
 その後、PayPalがユーザからApproveをもらうために、画面を表示してくれます。

/paypal-redirect
 PayPalの画面でユーザが承認してくれると、このエンドポイントが呼び出されます。
 Approveをもらったので、これで決済を完了させます。
 完了後、ユーザにPaymentIdなど、もろもろ返してあげています。

利用するnpmモジュールは以下の通りです。

  • paypal-rest-sdk
  • node-fetch
  • uuid

PayPalから、paypal-rest-sdkというモジュールを提供していただいているので、だいぶ楽になりました。

(エンドポイント「/paypal-token」もありますが、これは後程説明します。)

index.js
var paypal = require('paypal-rest-sdk');

const base_url = 【RESTfulサーバを立ち上げているURL】;

var Response = require('../../helpers/response');
var Redirect = require('../../helpers/redirect');
var fetch = require('node-fetch');
const { URLSearchParams } = require('url');
const uuidv4 = require('uuid/v4');

const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || 【REST API appsのClient ID】;
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET || 【REST API appsのSecret】;

paypal.configure({
    'mode': 'sandbox', //sandbox or live
    'client_id': PAYPAL_CLIENT_ID,
    'client_secret': PAYPAL_CLIENT_SECRET,
    'headers' : {
        'custom': 'header'
    }
});

exports.handler = (event, context, callback) => {
    if( event.path == '/paypal-token'){
        var body = JSON.parse(event.body);
        var code = body.code;

        var url = 'https://api.sandbox.paypal.com/v1/oauth2/token';
        var params = {
            grant_type: 'authorization_code',
            code: code
        };

        return do_post_form_basic(url, params, PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET )
        .then(json =>{
            console.log(json);
            if( json.error )
                throw json.error;
            return do_get_token('https://api.sandbox.paypal.com/v1/identity/oauth2/userinfo', { schema: 'paypalv1.1'}, json.access_token);
        })
        .then(json =>{
            console.log(json);
            if( json.error )
                throw json.error;
            return new Response( json );
        })
        .catch(error =>{
            return new Response().set_error(error);
        })
    }else if( event.path == '/paypal-create'){
        var orderid = uuidv4();

        var create_payment_json = {
            "intent": "authorize",
            "payer": {
                "payment_method": "paypal"
            },
            "redirect_urls": {
                "return_url": base_url + "/paypal-redirect",
                "cancel_url": base_url + "/paypal/index.html"
            },
            "transactions": [{
                "custom": JSON.stringify({ orderid: orderid, amount: 100 } ),
                "item_list": {
                    "items": [{
                        "name": "item",
                        "price": "100",
                        "currency": "JPY",
                        "quantity": 1
                    }]
                },
                "amount": {
                    "currency": "JPY",
                    "total": "100"
                },
                "description": "This is the payment description."
            }]
        };

        return new Promise((resolve, reject) =>{
            paypal.payment.create(create_payment_json, (error, payment) => {
                if (error) {
                    console.log(error.response);
                    return reject(error);
                }
                console.log(payment);

                for (var index = 0; index < payment.links.length; index++) {
                    if (payment.links[index].rel === 'approval_url')
                        return resolve( new Redirect(payment.links[index].href) );
                }

                return reject('approval_url not found');
            });
        });
    }else if( event.path == '/paypal-redirect'){
        var qs = event.queryStringParameters;
        console.log(qs);

        var payerid = event.queryStringParameters.PayerID;
        var paymentid = event.queryStringParameters.paymentId;

        var execute_payment_json = {
            "payer_id": payerid,
            "transactions": [{
                "amount": {
                    "currency": "JPY",
                    "total": "100"
                }
            }]
        };

        return new Promise((resolve, reject) =>{
            paypal.payment.execute(paymentid, execute_payment_json, (error, payment) => {
                if (error) {
                    console.log(error.response);
                    return reject(error);
                }
                console.log(payment);

                for( var i = 0 ; i < payment.transactions.length ; i++ ){
                    for( var j = 0 ; j < payment.transactions[i].related_resources.length ; j++ ){
                        if( payment.transactions[i].related_resources[j].authorization ){
                            var authorization_id = payment.transactions[i].related_resources[j].authorization.id;
                            console.log('authorization_id=' + authorization_id);
                        }
                    }
                }

                var params = new URLSearchParams();
                for( var key in qs )
                    params.set(key, qs[key] );

                return resolve( new Redirect(base_url + '/paypal/index.html?' + params.toString() ));
            });
        });
    }
};

function do_get_token(url, qs, token){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    return fetch(url + '?' + params.toString(), {
        method : 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }
    })
    .then((response) => {
        return response.json();
    });
}

function do_post_form_basic(url, qs, client_id, client_secret){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    const headers = { 
        "Content-Type" : "application/x-www-form-urlencoded",
        "Authorization" : "Basic " + new Buffer(client_id + ':' + client_secret).toString('base64')
    };

    return fetch(url, {
        method : 'POST',
        body : params,
        headers: headers
    })
    .then((response) => {
        return response.json();
    });
}

ユーティリティも示しておきます。

redirect.js
class Redirect{
    constructor(url){
        this.statusCode = 303;
        this.headers = {'Location' : url};
        this.body = null;
    }
}
response.js
module.exports = Redirect;

class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
        return this;
    }

    set_body(content){
        this.body = JSON.stringify(content);        
        return this;
    }

    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;

環境に合わせて、以下を変更してください。

【REST API appsのClient ID】
【REST API appsのSecret】
【RESTfulサーバを立ち上げているURL】

また、

 "return_url": base_url + "/paypal-redirect"

の部分で、ユーザによる同意の結果のリダイレクト先を指定しています。
また、以下の部分で、決済完了後のリダイレクト先を指定しています。

 return resolve( new Redirect(base_url + '/paypal/index.html?' + params.toString() ));

クライアント側のHTMLページも示します。

/paypal/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>PayPal 連携 テスト</title>

  <script src="js/vue_utils.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>PayPal 連携 テスト</h1>
        <br>
        <a class="btn btn-primary" href="【RESTfulサーバを立ち上げたURL】/paypal-create">Payment</a>
        <br>
    </div>

    <script src="js/start.js"></script>
</body>
start.js
'use strict';

const base_url = 【RESTfulサーバを立ち上げたURL】;
const PAYPAL_CLIENT_ID = 【REST API appsのClient ID】;

var vue_options = {
    el: "#top",
    data: {
    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        if( searchs.paymentId ){
            history.replaceState(null, null, '.');
            alert('決済が完了しました。');
        }
    }
};
var vue = new Vue( vue_options );
vue_utils.js
var hashs = {};
var searchs = {};

function proc_load() {
  hashs = parse_url_vars(location.hash);
  searchs = parse_url_vars(location.search);
}

function parse_url_vars(param){
  if( param.length < 1 )
      return {};

  var hash = param;
  if( hash.slice(0, 1) == '#' || hash.slice(0, 1) == '?' )
      hash = hash.slice(1);
  var hashs  = hash.split('&');
  var vars = {};
  for( var i = 0 ; i < hashs.length ; i++ ){
      var array = hashs[i].split('=');
      vars[array[0]] = array[1];
  }

  return vars;
}

function vue_add_methods(options, funcs){
    for(var func in funcs){
        options.methods[func] = funcs[func];
    }
}
function vue_add_computed(options, funcs){
    for(var func in funcs){
        options.computed[func] = funcs[func];
    }
}

以下に示すように、決済が完了して元のページに戻ってくると、paymentIdが指定されてくるので、それを受けてダイアログ表示するようにしています。

    if( searchs.paymentId ){
        history.replaceState(null, null, '.');
        alert('決済が完了しました。');
    }

それではRESTfulサーバを立ち上げてみましょう。

image.png

Paymentボタンを押下します。
そうすると、ログイン画面が表示されます。
Personalアカウントでログインします。自動で作られるXXXXX-buyer@YYYYY.yyy がありました。

image.png

今度は、品目と価格の確認画面に変わります。
内容を確認したら「同意して続行」ボタンを押下します。

image.png

めでたく最初のページに戻ってきて、ダイアログが表示できました。

image.png

PayPalアカウントでのログイン機能の追加

ついでに、PayPalアカウントでのログイン機能を追加してみます。
クライアント側に少しコードを追加します。

/paypal/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>PayPal 連携 テスト</title>

  <script src="js/vue_utils.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>PayPal 連携 テスト</h1>
        <br>
        <span id='cwppButton'></span>
        <br>
        <br>
        <a class="btn btn-primary" href="【RESTfulサーバを立ち上げたURL】paypal-create">Payment</a>
        <br>
    </div>

    <script src="js/start.js"></script>
    <script src='https://www.paypalobjects.com/js/external/connect/api.js'></script>
    <script>
        paypal.use( ['login'], function (login) {
            login.render ({
            "appid": PAYPAL_CLIENT_ID,
            "authend":"sandbox",
            "scopes":"openid profile",
            "containerid":"cwppButton",
            "locale":"ja-jp",
            "buttonType":"CWP",
            "buttonSize":"lg",
            "returnurl": base_url + "/paypal/"
            });
        });
    </script>
</body>

以下の部分が、PayPalアカウントでのログインボタンになります。

<span id='cwppButton'></span>

各種設定は、以下の部分で行います。

 paypal.use( ['login'], function (login)

containeridの指定の部分で指定したIDのspanの部分に、ボタンを描画してくれます。
ログインのために、PayPalが提供する画面に遷移したのち、ログイン完了後にまた戻ってきます。戻ってくる先は、「returnurl」の部分に指定します。
scopesには、取得したいユーザ情報の種類に応じて指定します。(後述)

スクリプトの本体は、以下です。

 <script src='https://www.paypalobjects.com/js/external/connect/api.js'>

PayPalからログイン完了後に戻ってくると、codeが指定されています。
これは認可コードであって、これをアクセストークンに変換する必要があります。
そのために、いったんRESTfulサーバに処理を移す必要があり、それが、以下の部分です。

    if( searchs.code ){

エンドポイント「/paypal-token」を呼んでいます。
呼び出しが成功すると、PayPalアカウント名が書かれたダイアログを表示するようにしています。

start.js
'use strict';

const base_url = 【RESTfulサーバを立ち上げたURL】;
const PAYPAL_CLIENT_ID = 【REST API appsのClient ID】;

var vue_options = {
    el: "#top",
    data: {
    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        if( searchs.paymentId ){
            history.replaceState(null, null, '.');
            alert('決済が完了しました。');
        }
        if( searchs.code ){
            history.replaceState(null, null, '.');

            do_post(base_url + '/paypal-token', { code: searchs.code} )
            .then(json =>{
                console.log(json);
                alert(json.name + 'さん、こんにちは');
            });
        }
    }
};
var vue = new Vue( vue_options );

function do_post(url, body){
    console.log('do_post: ' + url);

    const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } );

    return fetch(url, {
        method : 'POST',
        body : JSON.stringify(body),
        headers: headers
    })
    .then((response) => {
        return response.json();
    });
}

エンドポイント、/paypal-tokenのサーバ部分は、すでに実装を示してました。
世の中で一般的なOAuth2やOpenID Connectとほぼ同じなので、なじみがあるかと思います。

swagger.yaml
  /paypal-token:
    post:
      x-swagger-router-controller: routing
      operationId: paypal-token
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object

それでは、もう一度、RESTfulサーバを再起動して、ブラウザからアクセスしてみます。

image.png

PayPalのボタンが増えているのがわかると思います。
PERSONALのアカウントでログインします。

image.png

PayPalのログインアカウント名が書かれたダイアログが表示されたかと思います。成功です。

image.png

以下の呼び出しにより、アクセストークン等を取得しています。

 https://api.sandbox.paypal.com/v1/oauth2/token

{
  "token_type": "Bearer",
  "expires_in": "28800",
  "nonce": Nonce-Value,
  "scope": "openid profile",
  "refresh_token": Refresh-Token-Value,
  "access_token": Access-Token-Value
}

取得したトークンを使った例として、以下を呼び出してユーザ情報を取得しています。

 https://api.sandbox.paypal.com/v1/identity/oauth2/userinfo

{
 "user_id": "https://www.paypal.com/webapps/auth/identity/user/XXXXXXXX",
 "name": "buyer test"
}

scopesとして、openidとprofileしか指定しなかったため、上記情報だけですが、他も指定すると取得できる情報が増えます。
以下が参考になります。

 https://developer.paypal.com/docs/integration/direct/identity/attributes/

感想

PayPalのドキュメント類は、すごく見にくかったのですが、REST APIに絞って熟読することで、なんとなく動作させることができました。
今後は、これを足掛かりに、他の機能も見ていこうと思います。

以上

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

Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)

Vue.js の主要な機能をざっくりとつかってみたときのメモ」や「Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)」のつづきです。。

前回までで、FirebaseのFirestoreにTodoリストをCRUDしました。今回はFirebaseのOAuthの認証・認可機能を使ってみます。
OAuthってのは、いわゆる「Googleアカウントでログイン」などのリンクを押すと、Googleのサイトのポップアップが出てきて、このアカウントをつかってログインしていい?って聞かれるヤツです。

image.png

本来は、認可サーバであるGoogleが「Firebaseをつかった(WEB)アプリに、あなたのGoogleアカウント情報を読ませてもいいかな?」って、Googleアカウントのオーナ(ユーザ)に許可つまり認可させるという「認可のための」仕組みなのですが、その際にポップアップでユーザを認証するため、結果として「Firebaseをつかった(WEB)アプリにGoogleアカウントでログインする(ユーザ情報が連携される)という処理シーケンスとなります。

参考:Authleteを使った認可サーバの構築と、OAuthクライアント(Webアプリケーション)からの疎通

そのまえに bootstrap-vue いれさせて

そのまえに、前回から、今回の記事作成までの最中にbootstrap-vue を導入しているので、そのインストールについて。

インストールとコードへの反映

$ npm install bootstrap-vue --save 

インストールはこれだけであとは main.js に以下を追加します。

main.js
import BootstrapVue from "bootstrap-vue"
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap-vue/dist/bootstrap-vue.css"
Vue.use(BootstrapVue)

これでvue.jsでbootstrap-vueが利用可能になりました。。Header.vueなどにログインリンクなどをつけたり適度に装飾してますが、コードはあとで出てくるので、ココでは割愛します :-)

Firebaseの認証をやってみる

Google認証を有効にする

Firebaseのコンソールから、自分が作成したプロジェクトを選択し、左のメニュー部の「Authentication」を選択。

image.png

「ログイン方法」を選択すると、下記のようにログイン方法を選択する画面になります。今回はGoogleをつかうので、プロバイダのGoogleを「有効」にしておきましょう。

image.png

OAuthの認可サーバを使用するときって、認可サーバ側からWEBアプリに対して「client_id/client_secret」を払い出してもらい、それらをパラメタに載せることでWEBアプリ自体の認証を行うのですが、Google もFirebaseもGoogle だからですかね、この辺はあらかじめ設定済みになってるようです。

承認済みドメインを追加する

この認証機能を利用出来るドメインを指定します。いまはlocalhostで起動しているので設定不要ですが、外部に公開するサーバのURLが http://client.example.com:8080 などだった場合、 client.example.com を設定追加しておきましょう。

image.png

ライブラリのインストール

Firebaseのライブラリは前回インストール済みなのですが、ログインしているユーザ情報やログイン状態をjsやvue間で共有するために、オブジェクトの状態を管理するフレームワークVuexを導入します。

Vuexは、 Vuex.Store というオブジェクトに対して、

store.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {},
    loginStatus: false
  },
  mutations: {
    user (state, user) {
      state.user = user
    },
    loginStatus (state, loginStatus) {
      state.loginStatus = loginStatus
    }
  },
  plugins: [createPersistedState({storage: window.sessionStorage, key: 'vuex-todo-examples'})]
})

というインスタンスを保持する領域 user,loginStatusを作成しておき、jsやvueからは

Header.vue(のscript部)
<script>
import firebase from 'firebase'

export default {
  name: 'Header',
  computed: {
    loginStatus () {
      return this.$store.state.loginStatus  // vuexのインスタンスを参照
    },
    user () {
      return this.$store.state.user // vuexのインスタンスを参照
    }
  },
  ...
}
</script>

とやることで参照ができます。

保存しているインスタンスの更新については、直接プロパティを操作するのではなく

Login.vue(のscript部。あとで出てますが新規で作ったログイン画面)
<script>
import firebase from 'firebase'

export default {
  name: 'Login',
  ... 割愛
  methods: {
    loginWithGoogle(){
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase
        .auth()
        .signInWithPopup(provider).then((result)=> {

          this.$store.commit('user', result.user) // インスタンスの更新
          this.$store.commit('loginStatus', true) // インスタンスの更新
          this.$router.push(
            this.$route.query.redirect ? this.$route.query.redirect : '/'
          )
        })
        .catch(function (error) {
          const errorCode = error.code
          const errorMessage = error.message
          alert(errorMessage)
        })
    }
  }
}
</script>

このように$store.commit('メソッド名','値') という仕様でVuex.Store にmutationsで定義していたメソッドを呼びだすことで、データを更新します。

このVuexをつかえばアプリケーション全体で状態が管理出来るので、Googleログインが成功した際やユーザ情報が更新されたとき、そのインスタンスを保存しておき、各画面ではそのインスタンスのあるなしで、ログイン済みかどうかを判定することができそうです。

ということでVuexのインストール

npm install --save vuex  vuex-persistedstate

vuexはライブラリそのもの、vuex-persistedstate がそのデータをLocalStorageやSessionStorageに保存するためのライブラリです。

ソース追加・修正内容

さてソースについて。多いので一覧をつけました。

順番に見ていきましょう。

src/main.js で、Firebaseのユーザ情報の更新を検知

ユーザがログイン中かの判定は、Googleアカウントログイン後や、そのユーザ情報が更新された際、そのユーザ情報をvuexに保存しておいて、そのあるなしをチェックすればよいという話でした。

なのでmain.jsで先ほどのstore.jsをimportし「Firebaseの認証機能を経由してユーザ情報が更新されたときに呼ばれるコールバック」で、vuexのユーザ情報とログイン状態を更新することにしました。

src/main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'

import router from './router'
import store from '@/store' // 追加
import firebase from 'firebase'
import firebaseConfig from '@/firebaseConfig'

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(BootstrapVue)

firebase.initializeApp(firebaseConfig)
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION) // 追加

Vue.config.productionTip = false

// 追加
firebase.auth().onAuthStateChanged(function (user) { // ユーザ情報が変更されたら呼ばれる
  if (user) {
    // User is signed in.
    store.commit('user', user)
    store.commit('loginStatus', true)
  } else {
    store.commit('user', {})
    store.commit('loginStatus', false)
  }
})
// ここまで

// router.beforeEach((to, from, next) => {
// あとで追加する

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

src/store.js: Vuexの追加

ユーザ情報とログイン状態を保持するためのvuexを作成しました。先のソースとおなじものです。

src/store.js(新規)
// さっき載せたのとおなじ
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {},
    loginStatus: false
  },
  mutations: {
    user (state, user) {
      state.user = user
    },
    loginStatus (state, loginStatus) {
      state.loginStatus = loginStatus
    }
  },
  plugins: [createPersistedState({storage: window.sessionStorage, key: 'vuex-todo-examples'})]
})

components/Header.vue: ヘッダにログアウトリンクを追加

つづいてHeader.vueです。共通ヘッダにしていた箇所に(まだログインも実装してないんですがorz)ログアウトのリンクを追加します。
<template> に色々書いてますが、bootstrapのGUIの記述がほとんどです。ログアウトリンクの表示可否をvuexのloginStatus で制御しています。

また、ログアウトリンクをクリックしたときに呼ばれる logout() メソッドも追加しています。こちらもfirebase.auth().signOut() を呼び出している程度で、とてもシンプルです。

components/Header.vue
<template>
  <b-navbar toggleable="md" type="dark" variant="info">
  <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
  <b-navbar-brand href="#">ToDo管理</b-navbar-brand>
  <b-collapse is-nav id="nav_collapse">
    <!-- Right aligned nav items -->
    <b-navbar-nav class="ml-auto" v-if='loginStatus' >
      <b-nav-item-dropdown right>
        <!-- Using button-content slot -->
        <template slot="button-content">
          <em><span>{{user.displayName}}</span></em>
        </template>
        <b-dropdown-item @click="logout()" >{{user.displayName}}さんを Signout</b-dropdown-item>
      </b-nav-item-dropdown>
    </b-navbar-nav>
  </b-collapse>
  </b-navbar>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'Header',
  computed: {
    loginStatus () {
      return this.$store.state.loginStatus
    },
    user () {
      return this.$store.state.user
    }
  },
  methods: {
    logout () {
      firebase.auth().signOut()
      // のちに画面遷移処理を追加する。
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>

router/index.js にログイン画面のルーティング追加

Routerについては、/login でログイン画面を表示させるためのルーティングを追加しています。

router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    // ↓追加
    {
      path: '/login',
      component: Login
    }
  ]
})

components/Login.vue: /loginで呼ばれる画面を追加

さて /login で呼び出される実際のログイン画面です。

http://localhost:8080/#/login で表示されるログイン画面となります。

components/Login.vue(新規)
<template>
  <b-container>
    <div class="form-signin">
      <button type="button" class="google-button" @click='loginWithGoogle'>
        <span class="google-button__icon">
          <svg viewBox="0 0 366 372" xmlns="http://www.w3.org/2000/svg"><path d="M125.9 10.2c40.2-13.9 85.3-13.6 125.3 1.1 22.2 8.2 42.5 21 59.9 37.1-5.8 6.3-12.1 12.2-18.1 18.3l-34.2 34.2c-11.3-10.8-25.1-19-40.1-23.6-17.6-5.3-36.6-6.1-54.6-2.2-21 4.5-40.5 15.5-55.6 30.9-12.2 12.3-21.4 27.5-27 43.9-20.3-15.8-40.6-31.5-61-47.3 21.5-43 60.1-76.9 105.4-92.4z" id="Shape" fill="#EA4335"/><path d="M20.6 102.4c20.3 15.8 40.6 31.5 61 47.3-8 23.3-8 49.2 0 72.4-20.3 15.8-40.6 31.6-60.9 47.3C1.9 232.7-3.8 189.6 4.4 149.2c3.3-16.2 8.7-32 16.2-46.8z" id="Shape" fill="#FBBC05"/><path d="M361.7 151.1c5.8 32.7 4.5 66.8-4.7 98.8-8.5 29.3-24.6 56.5-47.1 77.2l-59.1-45.9c19.5-13.1 33.3-34.3 37.2-57.5H186.6c.1-24.2.1-48.4.1-72.6h175z" id="Shape" fill="#4285F4"/><path d="M81.4 222.2c7.8 22.9 22.8 43.2 42.6 57.1 12.4 8.7 26.6 14.9 41.4 17.9 14.6 3 29.7 2.6 44.4.1 14.6-2.6 28.7-7.9 41-16.2l59.1 45.9c-21.3 19.7-48 33.1-76.2 39.6-31.2 7.1-64.2 7.3-95.2-1-24.6-6.5-47.7-18.2-67.6-34.1-20.9-16.6-38.3-38-50.4-62 20.3-15.7 40.6-31.5 60.9-47.3z" fill="#34A853"/></svg>
        </span>
        <span class="google-button__text">Sign in with Google</span>
      </button>
    </div>
  </b-container>
</template>

<script>
import firebase from 'firebase'
// import 'firebaseui/dist/firebaseui.css'

export default {
  name: 'Login',
  methods: {
    loginWithGoogle(){
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase
        .auth()
        .signInWithPopup(provider).then((result)=> {
          this.$store.commit('user', result.user)
          this.$store.commit('loginStatus', true)
          this.$router.push(
            this.$route.query.redirect ? this.$route.query.redirect : '/'
          )
        })
        .catch(function (error) {
          const errorCode = error.code
          const errorMessage = error.message
          alert(errorMessage)
        })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
html,
body {
  height: 100%;
}

body {
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #f5f5f5;
}

.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* Shared */
.loginBtn {
  box-sizing: border-box;
  position: relative;
  /* width: 13em;  - apply for fixed size */
  margin: 0.2em;
  padding: 0 15px 0 46px;
  border: none;
  text-align: left;
  line-height: 34px;
  white-space: nowrap;
  border-radius: 0.2em;
  font-size: 16px;
  color: #fff;
  cursor: pointer;
}
.loginBtn:before {
  content: "";
  box-sizing: border-box;
  position: absolute;
  top: 0;
  left: 0;
  width: 34px;
  height: 100%;
}
.loginBtn:focus {
  outline: none;
}
.loginBtn:active {
  box-shadow: inset 0 0 0 32px rgba(0, 0, 0, 0.1);
}

.google-button {
  height: 40px;
  border-width: 0;
  background: white;
  color: #737373;
  border-radius: 5px;
  white-space: nowrap;
  box-shadow: 1px 1px 0px 1px rgba(0,0,0,0.05);
  transition-property: background-color, box-shadow;
  transition-duration: 150ms;
  transition-timing-function: ease-in-out;
  padding: 0;
  box-shadow: 1px 4px 5px 1px rgba(0,0,0,0.1);
}

.google-button:hover {
  cursor: pointer;
}

.google-button:active {
  box-shadow: 3px 6px 7px 3px rgba(0,0,0,0.1);
  transition-duration: 10ms;
}

.google-button__icon {
  display: inline-block;
  vertical-align: middle;
  margin: 8px 0 8px 8px;
  width: 18px;
  height: 18px;
  box-sizing: border-box;
}

.google-button__icon--plus {
  width: 27px;
}

.google-button__text {
  display: inline-block;
  vertical-align: middle;
  padding: 0 24px;
  font-size: 14px;
  font-weight: bold;
  font-family: 'Roboto',arial,sans-serif;
}

</style>

npm run dev して表示してみると、こんな感じ。

image.png

コードが長くてちょっとアレですが、<template><style><script> それぞれみてみましょう。

まずtemplate部とstyle部いわゆる画面は、Googleログインのリンクボタンを追加しているだけです。styleはまあ長いですがボタンに適用しているcssですね。

つぎにボタンのクリックで呼ばれるメソッド loginWithGoogle (script部)ですが、

methods: {
  loginWithGoogle(){
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(......)
    ... 
  }
}

まずココまででポップアップが表示され、そのWindowsにGoogleがログイン画面を表示してくれます。

image.png

ポープアップ画面でGoogleへのログインとユーザの認可オペが完了すると、ポップアップは自動で閉じられ、thenに記述した下記のコールバックが呼び出されます。

then内のコールバック
result => {
  this.$store.commit('user', result.user)
  this.$store.commit('loginStatus', true)
  this.$router.push(
    this.$route.query.redirect ? this.$route.query.redirect : '/'
    // あとで詳細説明
  )
}

このコールバック処理ではvuexにあるuser/loginStatusを

  • コールバックに渡ってくるユーザ情報(result.user)
  • ログインステータスをtrue

で更新し、'/' へ画面遷移します。 '/'はルーティングでいままでのToDo画面が定義されているので、結果としてGoogle認証が完了するとToDo画面が表示されることになります。

image.png

ちなみにToDo画面であるHelloWorld.vue については、全体を

  <main v-if="$store.state.loginStatus" class="container">

で囲うことで、ログインステータスが trueのときのみコンテンツを表示するようにしています。

HelloWorld.vue(のtemplate部)
<template>
  <main v-if="$store.state.loginStatus" class="container">
    <h1>
      My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span>
      <span class='info' style='cursor:pointer' @click='checkAll()' v-if='!isAllChecked()'>すべてチェック/はずす</span>
      <span class='info' style='cursor:pointer' @click='unCheckAll()' v-if='isAllChecked()'>すべてチェック/はずす</span>
      <b-button size="sm" variant="secondary" @click="deleteEndTask">完了タスクの削除</b-button></h1>
    <ul>
      <li v-for='todo in todos' :key='todo.id'>
        <input type='checkbox' v-model='todo.isDone' @click='toggle(todo.id)' >
        <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
        <span @click='deleteTask(todo.id)' class='xButton'>[x]</span>
        </li>
    </ul>
    <form @submit.prevent='addTask'>
      <input type='text' v-model='newTask' placeholder="タスクを入力" >
      <b-button type='submit' variant="primary" style='margin:4px'>追加</b-button>
    </form>
  </main>
</template>

いま時点のソース

ここまでのソースは
https://github.com/masatomix/todo-examples/tree/for_qiita_auth_001

へコミット済みです。

一応このタグから構築する手順を示しておきます。

$ git clone --branch for_qiita_auth_001 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ npm run dev

これで、http://localhost:8080/#/login にブラウザでアクセスできるとおもいます。

いったん整理

さてここまでで、Googleアカウントでログインして、ToDoアプリケーションを使用するという基本的なところができあがりました。しかしながらまだ

などを対応したいので、つづけてやっていきます。

ヘッダ右上のドロップダウンの「ログアウト」を選んだら、ログイン画面に遷移させたい

ヘッダのログアウトを選択すると、logout()が呼ばれますが、

Header.vue(のscript部)
methods: {
  logout () {
    firebase.auth().signOut()
    // のちに画面遷移処理を追加する。
  }
}

にログアウト後のコールバックを追加します。

Header.vue(のscript部)
methods: {
  logout () {
    firebase.auth().signOut()
      .then(() => {
        this.$router.push('/')
        window.location.reload() // 保持してた不要な情報を一度クリア(vuexはきえない)
      })
      .catch(function (error) {
        const errorCode = error.code
        const errorMessage = error.message
        alert(errorMessage)
      })
  }
}

this.$router.push('/') だとログイン画面ではなくてToDo画面に行こうとするんですが、このあとの機能追加で 「ログイン済み状態じゃなかったらログイン画面に飛ばす」処理を入れるので、結果ログイン画面に遷移します。

ちなみに firebase.auth().signOut() のコールバックに入った時点でユーザ情報は変更され、その結果main.jsに追加した

src/main.js
firebase.auth().onAuthStateChanged(function (user) { // ユーザ情報が変更されたら呼ばれる
  if (user) {
    // User is signed in.
    store.commit('user', user)
    store.commit('loginStatus', true)
  } else {
    store.commit('user', {})
    store.commit('loginStatus', false)
  }
})

がうごきだして、vuex上の user,loginStatusはそれぞれ{}falseで更新されます。

画面に応じて、ログインが必要・必要でない、を制御したい

さて、認証されていない場合はログイン画面に飛ばしたいのですが、ログイン画面が認証が必要だと無限ループするので、画面によってログインの必要可否を制御したいですね。

ということで、画面ごとの定義を router/index.js に追加します。

router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/login',
      component: Login,
      meta: {
        isPublic: true  // このプロパティ追加
      }
    }
  ]
})

isPublicがtrueの場合は、認証状態をチェックしないで画面を表示します。デフォルトはfalseなので、ログイン画面以外は認証が必要という設定になりました。

この次の項の画面遷移の制御で、上記の定義を参照する機能を追加します。

画面にアクセスしたときに、ログイン済みでない場合はログイン画面を表示し、認証したら該当画面へ遷移する

さあもうすこしです。

src/main.js にナビゲーションガードを追加する

公式のナビゲーションガード に説明がありますが、ナビゲーションガード機構を使うことで、画面の遷移をフックして、状態をチェックするなどの処理を入れ込むことが出来ます。

こんな感じに、main.jsでナビゲーションガードを追加します。

src/main.js
router.beforeEach((to, from, next) => {
  const currentUser = store.state.user
  if (currentUser.uid) {
    if (to.path == '/login') {
      firebase.auth().signOut().then(() => next())
    }
  }

  if (to.matched.some(record => record.meta.isPublic)) {
    // alert('isPublic = true '+ to.path)
    next()
  } else {
    // alert('isPublic = false '+ to.path)
    if (currentUser.uid) {
      next()
    } else {
      next({
        path: '/login',
        query: {
          redirect: to.path
        }
      })
    }
  }
})

つまり、URLが変更されるときに変更前のURL情報(from)と変更後のそれ(to)が渡ってくるので、

  • vuex からユーザ情報が取れたときは、toがログイン画面(/login)へのアクセスだったら、いちどログアウトさせてユーザ情報をクリアしてから、ログイン画面へ遷移(next())させる
  • 遷移先(to)のURLについて、isPublicがtrueの場合は、そのまま遷移(next())させる
  • isPublicがfalseの場合は、
    • vuex からユーザ情報(store.state.user.uid)が取れたときはそのまま遷移(next())
    • vuex からユーザ情報(store.state.user.uid)が取れない場合は、ログイン画面へ遷移させる(※))

最後の処理(※)は

next({
  path: '/login',
  query: {
    redirect: to.path
  }
})

の処理のことです。

/login に遷移する際 redirect というクエリパラメタに to.pathという変数をセットしていますが、たとえば /ui001 に遷移しようとしてナビゲーションガードによってログイン画面に遷移させられたとき、遷移後のログイン画面(/login)のURLが

http://localhost:8080/#/login?redirect=%2Fui001

とクエリパラメタが後ろにつくようにしています。このように redirect にto.pathを渡しているのは、ログインが成功した際に本来行きたかった画面のURL(/ui001ですね) の情報が必要だからです。

Login.vue でのログイン成功後の遷移先

さて、渡されたredirectパラメタですが、Login.vue でログイン成功後の画面遷移 はこのようになっていました。

Login.vue(抜粋)
this.$router.push(
  this.$route.query.redirect ? this.$route.query.redirect : '/'
)

コレはつまり、redirectパラメタがある場合はそこのURLへ遷移、パラメタがない場合は '/' へ遷移(デフォルト値) するってことですね。

以上で、認証が必要な画面に行こうとした場合、ナビゲーションガードによって認証されているかがチェックされ、必要に応じてログイン画面を出した後、本来行きたい画面に遷移する流れを実装することが出来ました。

うーん、疲れましたね。。。

Firestore へのデータアクセスを、認証されたユーザのみに制限したい

さあFirebaseでの認証ができたので、せっかくなのでFirestoreへのアクセスを、認証されたユーザのみに出来たらよりセキュアですね。

そのための制御はFirebase側の画面にあります。

https://console.firebase.google.com から自分のプロジェクトに移動し、Database >> ルール に遷移します

image.png

ココですが、ワタクシまえに権限エラーに対応するため、下記の通りだれでもOK!ってしていたんですが、

だれでもOKの設定
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}

これを

認証されてるヒトだけOK
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}

にして「公開」ボタンを押せばOKです。ほどなくして反映されるようで、認証状態でないFirestoreへのアクセスにはエラーが返るようになりました。

ナビゲーションガードなどを入れたソース

最終形のソースは
https://github.com/masatomix/todo-examples/tree/for_qiita_auth_002

へコミット済みです。
下記手順でビルドできます。

$ git clone --branch for_qiita_auth_002 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ npm run dev

これで、http://localhost:8080/#/ にブラウザでアクセスできるとおもいます。

ログイン画面 '/login' とToDo画面 '/' の他に、'/ui001','/ui002'という画面を追加でコミットしています。/ui001だけ認証が必要な画面にしてあるので、

http://localhost:8080/#/ui001 などに直接アクセスしてみて、ログイン画面が表示され、ログインするとちゃんと行きたかった画面に遷移できること、などを確認してみてください。

まとめ

Firebase認証をつかえば、OAuthをつかったGoogle認証機能を簡単に実装することが出来ましたね。

ザックリまとめると、、ログイン状態はVuexで管理し、ログイン時やユーザ情報の変更に従ってVuexの情報を更新します。また、画面遷移時にログイン状態をチェックするナビゲーションガードを導入することで、必要な時だけログイン画面を挟むことができるようになりました。最後にFirestoreへのアクセスを、Firebaseログインしているユーザに制限することで、データを保護することも出来ました。

以上です。おつかれさまでした。

関連リンク

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

Node.jsで定期実行メモ

毎回忘れるので自分用メモです。
Node Cronを使います。

準備

$ npm init -y
$ npm i node-cron

Cronの書式

https://www.npmjs.com/package/node-cron#cron-syntax

 # ┌────────────── second (optional)
 # │ ┌──────────── minute
 # │ │ ┌────────── hour
 # │ │ │ ┌──────── day of month
 # │ │ │ │ ┌────── month
 # │ │ │ │ │ ┌──── day of week
 # │ │ │ │ │ │
 # │ │ │ │ │ │
 # * * * * * *
  • 6段階あって左から秒,分,時間,日,月,曜日
  • 秒はオプショナルなので5段階設定しておけば動きはします。

らしいです。

試す

公式サンプルが分単位のサンプルばかりで試しづらいので基本は秒単位の試し方を書いてます。

毎秒実行

  • cron.schedule()の*が6つ
app.js
'use strict';

const cron = require('node-cron');
cron.schedule('* * * * * *', () => console.log('毎秒実行'));

毎分実行

  • cron.schedule()の*が5つ
  • 毎秒実行よりも*が少ない
app.js
'use strict';

const cron = require('node-cron');
cron.schedule('* * * * *', () => console.log('毎分実行'));

毎分の1秒、10秒、30秒、55秒に実行

app.js
'use strict';

const cron = require('node-cron');
cron.schedule('1,10,30,55 * * * *', () => console.log('毎分1秒,10秒,30秒,55秒に実行'));

毎分の1秒~10秒に実行

  • 1-10みたいな記述が出来ます。
app.js
'use strict';

const cron = require('node-cron');
cron.schedule('1-10 * * * * *', () => console.log('毎分1秒~10秒に実行'));

実行すると、毎分10回実行されますね。

$ node app.js
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行
毎分1秒~10秒に実行

3秒ごとに実行

  • */3とすると3秒ごととか3分ごとみたいな指定です。
  • */10だと10分ごと
app.js
'use strict';

const cron = require('node-cron');
cron.schedule('*/3 * * * * *', () => console.log('3秒ごとに実行'));

毎日0時,6時,12時,18時に実行

結構使いそうな間隔

app.js
'use strict';

const cron = require('node-cron');
cron.schedule('0 0 0,6,12,18 * * *', () => console.log('毎日0時,6時,12時,18時に実行'));

3時間おきの指定時間(3,6,9,12,15,18,21)実行

'use strict';

const cron = require('node-cron');
cron.schedule('0 0 0,3,6,9,12,15,18,21, * * *', () => console.log('3時間おきの指定時間実行'));

3時間おき実行

'use strict';

const cron = require('node-cron');
cron.schedule('0 0 */3 * * *', () => console.log('3時間おきの実行'));

23時59分59秒に実行したい

app.js
'use strict';

const cron = require('node-cron');
cron.schedule('59 59 23 * * *', () => console.log('毎日23時59分59秒に実行'));

できなかったネタ

24時は指定できない

毎日24時に実行したくて、こんな指定をすると怒られます。

app.js
'use strict';

const cron = require('node-cron');
cron.schedule('0 0 24 * * *', () => console.log('毎日24時に実行'));
$ node app.js

/Users/n0bisuke/dotstudio/playground/cron-test/node_modules/node-cron/src/pattern-validation.js:54
      throw patterns[2] + ' is a invalid expression for hour';
      ^
24 is a invalid expression for hour

1日は0:00~23:59になるので24時ってのはほんとはダメなんですね。

毎日24時に実行させたい場合は0を指定しましょう。

app.js
'use strict';

const cron = require('node-cron');
cron.schedule('0 0 0 * * *', () => console.log('毎日24時に実行'));

60分や60秒も指定できない

24時同様に60分や60分も指定できません。

app.js
'use strict';

const cron = require('node-cron');
cron.schedule('60 60 0 * * *', () => console.log('60秒や60分の指定'));

怒られますね。

$ node app.js

/Users/n0bisuke/dotstudio/playground/cron-test/node_modules/node-cron/src/pattern-validation.js:50
      throw patterns[1] + ' is a invalid expression for minute';
      ^
60 is a invalid expression for minute

所感

地味に24H指定してハマってたので気をつけましょう笑

随時サンプルアップデートしたい

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

jsで26進数を処理する(Excelのカラム表記変換処理)

Excelのカラムの表記(26進数)と10進数をjsで相互変換するレアな要求があったのでその対応。

手動実装はしんどいなと思っていたところ、ライブラリがあるみたいなので試す。
とりあえずテストも書かれているbase26というのを触ってみる。他にもあるみたい。

インストール

npm install --save base26

実装

とりあえずindex.jsってファイルを作って記述。

index.js
//10進数を26進数に変換して大文字表示
console.log(base26.to(10));
console.log(base26.to(100));

//26進数を10進数に変換
console.log(base26.from('j'));
console.log(base26.from('cv'));

実行と確認

まあ、動きます。

node index.js

j
cv
10
100

大文字にしたければtoUpperCase()などを使えば良い。

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

ElectronでFileのOpenとSave

Electronでファイル操作する必要があったのでOpenとSaveの方法をメモ。

dialogの制御方法が知りたいのが主目的なので、ロジックはなし。
csvを読み込み表示。そのままsaveする。

仕様

下記のような感じ。

  • openボタンを押すとファイオープンし、textareaに表示。
  • saveでtextarea内の内容をfileにSave。

スクリーンショット 2019-01-27 7.14.03.png

textarea内を変更しても内容はファイルには反映されない。それをやるにはVueとかReact使う。

実装

package.json

エンドポイントはmain.jsとしました。

package.json
{
  "name": "openclose",
  "version": "1.0.0",
  "description": "sample",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^4.0.2"
  }
}

main.js

ElectronのQuick Startのmain.jsとほぼ一緒。
起動時の表示としてindex.htmlを呼び出している。

main.js
const { app, BrowserWindow } = require('electron')

let win

function createWindow() {

    //ウインドウの作成
    win = new BrowserWindow({ width: 800, height: 400 })

    //ウインドウに表示する内容
    win.loadFile('index.html')

    //デバッグ画面表示
    // win.webContents.openDevTools()

    //このウインドウが閉じられたときの処理
    win.on('closed', () => {
        win = null
    })
}

//アプリが初期化されたとき(起動されたとき)
app.on('ready', () => {
    createWindow()
})

//全ウインドウが閉じられたとき
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

//アクティブになったとき(MacだとDockがクリックされたとき)
app.on('activate', () => {
    if (win === null) {
        createWindow()
    }
})

index.html

画面要素を配置。ロジックを実装するindex.jsとstyles.cssを呼び出している。

index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>sample</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <h3>File Open/Save</h3>
    <button id="openFile">Open</button>
    <button id="saveFile">Save</button>
    <br>
    <textarea id="preview"></textarea>
    <script src="index.js"></script>
</body>
</html>

index.js

ここにいろいろ書く。やってることは、

  • OpenDialogで開くファイル名(Path)を取得し、その情報を利用してファイルをオープン。
  • SaveDialogで保存するファイル名(Path)を取得して、その情報を利用してファイルをSave。
index.js
const fs = require('fs');
const { BrowserWindow, dialog } = require('electron').remote;

//html内の要素取得とリスナーの設定
document.querySelector("#openFile").addEventListener('click', () => {
    openFile();
})

document.querySelector("#saveFile").addEventListener('click', () => {
    saveFile();
})

const preview = document.getElementById('preview');

//openFileボタンが押されたとき(ファイル名取得まで)
function openFile() {
    const win = BrowserWindow.getFocusedWindow();
    dialog.showOpenDialog(
        win,
        {
            properties: ['openFile'],
            filters: [
                {
                    name: 'Document',
                    extensions: ['csv', 'txt']
                }
            ]
        },
        (fileNames) => {
            if (fileNames) {
                // alert(fileNames[0]);
                readFile(fileNames[0]); //複数選択の可能性もあるので配列となる。
            }
        }
    )
}

//指定したファイルを読み込む
function readFile(path) {
    fs.readFile(path, (error, data) => {
        if (error != null) {
            alert("file open error.");
            return;
        }
        preview.textContent = data.toString();
    })
}

//saveFileボタンが押されたとき
function saveFile() {
    const win = BrowserWindow.getFocusedWindow();
    dialog.showSaveDialog(
        win,
        {
            properties: ['openFile'],
            filters: [
                {
                    name: 'Documents',
                    extensions: ['csv', 'txt']
                }
            ]
        },
        (fileName) => {
            if (fileName) {
                const data = preview.textContent;
                console.log(data);
                writeFile(fileName, data);
            }
        }
    )
}

//fileを保存(Pathと内容を指定)
function writeFile(path, data) {
    fs.writeFile(path, data, (error) => {
        if (error != null) {
            alert("save error.");
            return;
        }
    })
}

styles.css

まあ、おまけ程度ですが、いちおう。

styles.css
#preview{
    margin-top: 20px;
    width: 300px;
    height: 100px;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む