- 投稿日:2019-01-27T23:12:42+09:00
初心者のための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 awscliAWS 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
- Default region name について気になる人はリージョンとアベイラビリティーゾーン
- Default output format はいくつか種類がありますが気になる人は出力形式について
Serverless Framework のインストール
npm install -g serverlessServerless 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-profileはaws 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」を検索する
Lambda の画面で
- 左側に見える「関数」をクリック
- 今回デプロイしたもの「quick-start-dev-hello」をクリック
quick-start-dev-hello の画面
この画面を活用する方法は別記事で説明します。
デプロイしたものは、この様に確認できます。デプロイしたやつを実行する
--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 サーバーを構築するやつをやっていきましょう。
- 投稿日:2019-01-27T22:37:15+09:00
【Windows】pdfmakeでカスタムフォントを使用したPDFファイル生成
やりたいこと
pdfmakeでカスタムフォントを使用したPDF生成を行う
pdfmakeとは
Client/server side PDF printing in pure JavaScript
JavaScriptでクライアント側、サーバ側でのPDF生成ができるモジュールと理解しています。
利用する際に必要なツール
クライアント側でPDF作成できるかチェック
pdfmakeモジュールを取得
- 作成した作業ディレクトリにエクスプローラでアクセス
- 今回は
D:\pdfmakeTestを作業フォルダとします- パス表示部分をクリックし
cmdと入力
- 現在のディレクトリをカレントにしたコマンドプロンプトが立ち上がります
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が生成されることは確認しましたが、日本語はフォントを変えないと表示されないようです。
pdfmakeで利用するフォントの変更
pdfmakeで日本語が表示できるようにフォントのJSファイル(vfs_fonts.js)を更新します。フォントの入手
環境を問わず、日本語を表示できるIPAフォントを利用させていただきます。
IPAフォントは、誰でも無償で利用できる、高品位を目指した日本語フォントです。OSI認定の“IPAフォントライセンス”と、フォントのデファクトスタンダードであるOpenTypeフォントフォーマットを採用することで、プラットフォームの種類を問わず、多様な情報機器で共通に利用することができ、どの環境の下でも同じ形状の高品質な文字の表示・印刷を可能にします。本プロジェクトでは、IPAフォントの品質向上を図るとともに、各プラットフォームでの日本語文字表示に関する情報交換の場となることを目指しています。
- 公式サイトのIPA Fonts/IPAex Fonts 4書体パック_IPAフォント(Ver.003.03)からフォントファイルをダウンロード
- 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 の更新
以下の手順でフォント情報を更新します。
pdfmakeディレクトリに移動し、Node.jsモジュールを更新
npm install(ズラズラっとメッセージが出て更新が行われます)- 上記でインストールした
pdfmake\node_modulesのgulpを利用したvfs_fonts.jsの更新
pdfmakeTest\pdfmake\node_modules\.bin\gulp.cmd buildFontsを入力しvfs_fonts.jsを更新します- フォント情報を
pdfmakeに反映
pdfmakeのフォルダでnpm run buildを行い、モジュールをビルドしなおしますvfs_fonts.js の確認
テキストエディタでファイルを確認し、
ipagp.ttfという記述があるかを確認します。
カスタムフォントが正常に利用できるかチェック
利用フォントを設定した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>日本語が表示されているか確認
ハマったところ
カスタムフォント定義ファイルの更新
問題
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-日本語フォントテスト)
- 投稿日:2019-01-27T22:02:24+09:00
GitBook でドキュメントを書く準備を整える
ちょっと書いてみたくなったのでメモ
環境
ミドルウェア バージョン Node.js 10.15.0 GitBook cli 3.2.3 手順
- Node.js をあらかじめインストールしておく
GitBook CLI をグローバルにインストールする
$ npm install gitbook-cli -gGitBook コマンドで初期化する
$ gitbook init新規でディレクトリを作成したい場合は次のように入力する
$ gitbook init ./<作成したいディレクトリ名>現在のディレクトリ(もしくは新規作成したディレクトリ)に新しいファイルが作成されていることを確認する
$ ls README.md SUMMARY.mdサーバを立ち上げる
$ 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 は今後アクティブな開発は行われない(アナウンスがあった)
参考
- 投稿日:2019-01-27T21:23:58+09:00
NodejsでSlackにメッセージ投稿(2019年1月版)
いつの間にか仕様が変わってる…
本家の説明 => https://api.slack.com/methods/chat.postMessagepostMsgToSlack.jsconst 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);
- 投稿日:2019-01-27T20:54:47+09:00
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-SDKConnect With PayPal
https://developer.paypal.com/docs/integration/direct/identity/他の決済サーバと同じように、自由にいじれるSandboxがあるので、気兼ねなく試してみましょう。
毎度のことですが、Swaggerを使ったRESTfulサーバを立ち上げます。
以下ご参考まで。PayPal Developerアカウントの作成
まずは、以下のページからアカウントを作成しましょう。
PayPal Developer
https://developer.paypal.com/まず見ていただきたいのが、Sandbox Accountです。
左側のナビゲータから選択します。あらかじめ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 を選択します。そうすると、Client IDやSecretが表示されます。
後で使うので、覚えておきます。また、右上のところで、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.jsvar 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.jsclass Redirect{ constructor(url){ this.statusCode = 303; this.headers = {'Location' : url}; this.body = null; } }response.jsmodule.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.jsvar 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サーバを立ち上げてみましょう。
Paymentボタンを押下します。
そうすると、ログイン画面が表示されます。
Personalアカウントでログインします。自動で作られるXXXXX-buyer@YYYYY.yyy がありました。今度は、品目と価格の確認画面に変わります。
内容を確認したら「同意して続行」ボタンを押下します。めでたく最初のページに戻ってきて、ダイアログが表示できました。
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サーバを再起動して、ブラウザからアクセスしてみます。
PayPalのボタンが増えているのがわかると思います。
PERSONALのアカウントでログインします。PayPalのログインアカウント名が書かれたダイアログが表示されたかと思います。成功です。
感想
PayPalのドキュメント類は、すごく見にくかったのですが、REST APIに絞って熟読することで、なんとなく動作させることができました。
今後は、これを足掛かりに、他の機能も見ていこうと思います。以上
- 投稿日:2019-01-27T20:54:47+09:00
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-SDKConnect With PayPal
https://developer.paypal.com/docs/integration/direct/identity/他の決済サーバと同じように、自由にいじれるSandboxがあるので、気兼ねなく試してみましょう。
毎度のことですが、Swaggerを使ったRESTfulサーバを立ち上げます。
以下ご参考まで。PayPal Developerアカウントの作成
まずは、以下のページからアカウントを作成しましょう。
PayPal Developer
https://developer.paypal.com/まず見ていただきたいのが、Sandbox Accountです。
左側のナビゲータから選択します。あらかじめ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 を選択します。そうすると、Client IDやSecretが表示されます。
後で使うので、覚えておきます。また、右上のところで、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.jsvar 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.jsclass Redirect{ constructor(url){ this.statusCode = 303; this.headers = {'Location' : url}; this.body = null; } }response.jsmodule.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.jsvar 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サーバを立ち上げてみましょう。
Paymentボタンを押下します。
そうすると、ログイン画面が表示されます。
Personalアカウントでログインします。自動で作られるXXXXX-buyer@YYYYY.yyy がありました。今度は、品目と価格の確認画面に変わります。
内容を確認したら「同意して続行」ボタンを押下します。めでたく最初のページに戻ってきて、ダイアログが表示できました。
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サーバを再起動して、ブラウザからアクセスしてみます。
PayPalのボタンが増えているのがわかると思います。
PERSONALのアカウントでログインします。PayPalのログインアカウント名が書かれたダイアログが表示されたかと思います。成功です。
以下の呼び出しにより、アクセストークン等を取得しています。
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に絞って熟読することで、なんとなく動作させることができました。
今後は、これを足掛かりに、他の機能も見ていこうと思います。以上
- 投稿日:2019-01-27T17:20:00+09:00
Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)
「Vue.js の主要な機能をざっくりとつかってみたときのメモ」や「Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)」のつづきです。。
前回までで、FirebaseのFirestoreにTodoリストをCRUDしました。今回はFirebaseのOAuthの認証・認可機能を使ってみます。
OAuthってのは、いわゆる「Googleアカウントでログイン」などのリンクを押すと、Googleのサイトのポップアップが出てきて、このアカウントをつかってログインしていい?って聞かれるヤツです。本来は、認可サーバであるGoogleが「Firebaseをつかった(WEB)アプリに、あなたのGoogleアカウント情報を読ませてもいいかな?」って、Googleアカウントのオーナ(ユーザ)に許可つまり認可させるという「認可のための」仕組みなのですが、その際にポップアップでユーザを認証するため、結果として「Firebaseをつかった(WEB)アプリにGoogleアカウントでログインする(ユーザ情報が連携される)という処理シーケンスとなります。
参考:Authleteを使った認可サーバの構築と、OAuthクライアント(Webアプリケーション)からの疎通
そのまえに bootstrap-vue いれさせて
そのまえに、前回から、今回の記事作成までの最中にbootstrap-vue を導入しているので、そのインストールについて。
インストールとコードへの反映
$ npm install bootstrap-vue --saveインストールはこれだけであとは main.js に以下を追加します。
main.jsimport 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」を選択。
「ログイン方法」を選択すると、下記のようにログイン方法を選択する画面になります。今回はGoogleをつかうので、プロバイダのGoogleを「有効」にしておきましょう。
OAuthの認可サーバを使用するときって、認可サーバ側からWEBアプリに対して「client_id/client_secret」を払い出してもらい、それらをパラメタに載せることでWEBアプリ自体の認証を行うのですが、Google もFirebaseもGoogle だからですかね、この辺はあらかじめ設定済みになってるようです。
承認済みドメインを追加する
この認証機能を利用出来るドメインを指定します。いまはlocalhostで起動しているので設定不要ですが、外部に公開するサーバのURLが http://client.example.com:8080 などだった場合、 client.example.com を設定追加しておきましょう。
ライブラリのインストール
Firebaseのライブラリは前回インストール済みなのですが、ログインしているユーザ情報やログイン状態をjsやvue間で共有するために、オブジェクトの状態を管理するフレームワークVuexを導入します。
Vuexは、 Vuex.Store というオブジェクトに対して、
store.jsimport 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-persistedstatevuexはライブラリそのもの、vuex-persistedstate がそのデータをLocalStorageやSessionStorageに保存するためのライブラリです。
ソース追加・修正内容
さてソースについて。多いので一覧をつけました。
- src/main.js で、Firebaseのユーザ情報の更新を検知
- src/store.js: Vuexの追加
- components/Header.vue: ヘッダにログアウトリンクを追加
- router/index.js にログイン画面のルーティング追加
- components/Login.vue: /loginで呼ばれる画面を追加
順番に見ていきましょう。
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.jsimport 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して表示してみると、こんな感じ。コードが長くてちょっとアレですが、
<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がログイン画面を表示してくれます。
ポープアップ画面で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画面が表示されることになります。
ちなみに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アプリケーションを使用するという基本的なところができあがりました。しかしながらまだ
- ヘッダ右上のドロップダウンの「ログアウト」を選んだら、ログイン画面に遷移させたい(今はログアウトさせるだけ)
- 画面に応じて、ログインが必要・必要でない、を制御したい
- 画面にアクセスしたときに、ログイン済みでない場合はログイン画面を表示し、認証したら該当画面へ遷移するようにしたい
- Firestore へのデータアクセスを、認証されたユーザのみに制限したい
などを対応したいので、つづけてやっていきます。
ヘッダ右上のドロップダウンの「ログアウト」を選んだら、ログイン画面に遷移させたい
ヘッダのログアウトを選択すると、
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.jsfirebase.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.jsimport 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.jsrouter.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 >> ルール に遷移します
ココですが、ワタクシまえに権限エラーに対応するため、下記の通りだれでもOK!ってしていたんですが、
だれでもOKの設定service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write; } } }これを
認証されてるヒトだけOKservice 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ログインしているユーザに制限することで、データを保護することも出来ました。
以上です。おつかれさまでした。
関連リンク
- Vue vuexでfirebaseのログイン保持
- 面倒なログイン機能の実装はFirebase Authenticationに丸投げしよう 概要が整理されています
- Firebase Authentication を使って得られた知見まとめ - トークンの仕様や注意点など 戻り電文のJWTの仕様が丁寧に説明されています。
- Firebase Auth のユーザ認証機能を自前のデータベースと連携する Firebaseを認証だけにつかう場合。
- ブラウザでログインしたFirebaseのユーザー情報をサーバー側で取得する その2
- Authenticating a Vue JS Application With Firebase UI Firebase UI っていう、画面まで作ってくれちゃうライブラリをつかう場合。
- Vue.jsにおけるFirebaseの主要な機能の取扱い
- Vue Routerのナビゲーションガードによるアクセス制限を試した&コードを読み解いた
- 【備忘録】【Vue.js】routeを使ってログインしてないと見れないように設定する方法
- Firebase / Firestore を使って簡単な Chat を作ってみる。(JS, Vue)
- 投稿日:2019-01-27T16:55:01+09:00
Node.jsで定期実行メモ
毎回忘れるので自分用メモです。
Node Cronを使います。準備
$ npm init -y $ npm i node-cronCronの書式
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 hour1日は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指定してハマってたので気をつけましょう笑
随時サンプルアップデートしたい
- 投稿日:2019-01-27T08:49:04+09:00
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()などを使えば良い。
- 投稿日:2019-01-27T07:16:46+09:00
ElectronでFileのOpenとSave
Electronでファイル操作する必要があったのでOpenとSaveの方法をメモ。
dialogの制御方法が知りたいのが主目的なので、ロジックはなし。
csvを読み込み表示。そのままsaveする。仕様
下記のような感じ。
- openボタンを押すとファイオープンし、textareaに表示。
- saveでtextarea内の内容をfileにSave。
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.jsconst { 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.jsconst 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; }
























