Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 9063 articles
Browse latest View live

【Node.js express Docker】 Docker環境変数を定義してnode.jsで読み込む方法

$
0
0

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

何パターンかあるみたいなんですが、とりあえずシンプルに。

app.env というファイルに環境変数を定義する。(名前は任意)

app.env
SECRET_KEY=himitu

docker-compose.ymlで上記ファイルを環境変数ファイルに指定

docker-compose.yml
env_file:./app.env

node.js側で環境変数ファイルを読み込む

constenv=process.env//envという名前でオブジェクトを格納。

取り出し方法

env.SECRET_KEY

以上です。


WebsocketとRedis Pub/Sub

$
0
0

RedisのPub/Sub機能を使うとWebsocketの通信情報を複数のサーバー間で同期させることができます。
スケールのためにWebsocketのサーバーを複数立てた場合、サーバー間で通信情報を同期する仕組みがないと、例えばチャットアプリケーションの場合、サーバーAに接続されたクライアントはサーバーBに接続されたクライアントとは正常にメッセージのやりとりができないことになってしまいます。
RedisのPub/Sub機能はそれを解消することができます。

スクリーンショット 2020-12-28 20.44.39.png

Redis Pub/Subとは

Pus/Subにでてくる主な概念です。

用語説明
Publish発行すること
Subscribe購読すること
Channel購読するチャンネル

ラジオで言えば
番組を配信することがPublish、
番組を聞くことがSubscribe、
番組そのものがChannelです。
ラジオの音は、番組を聞いている人にだけ配信されます。

Redis-cliを使ってpubsubを試してみます。

redis-server  //Redisを起動
redis-cli     //CLIを起動

ターミナルのタブを二つ開き、一方のターミナルで

SUBSCRIBE MorningShow

もう一方のターミナルで

PUBLISH MorningShow "Good Morning!"

を実行すると、Subscribeしている側のタブに"Good Morning!"のメッセージが届きます。
スクリーンショット_2020-12-27_17_42_18.png

Node.jsでサンプルを動かす

Node.jsでwebsocket + Redis PubSubを使った簡単なサンプルです。ソースコードはこちら

タブを複数開き、一方でメッセージを送ると両方で更新されます。※この時点ではまだpubsubは関係ありません
ezgif.com-crop.gif

まずはクライアント側です。接続ボタンが押されたらサーバーにアクセスします。サーバーからメッセージを受け取ったら画面に表示します。

public/client.js
wsButton.onclick=function(){wsSendButton.disabled=false;ws=newWebSocket(`ws://${location.host}`);ws.onopen=function(){showMessage('WebSocket connection established');};ws.onmessage=function(message){letdata=JSON.parse(message.data)showMessage(JSON.stringify(data.message));};--省略--};

次にサーバー側です。
websocketのリスナーを作成と、Redisへの接続を行います。今回はdocker-composeで立てたRedisコンテナに接続するため、Redisホストをコンテナ名の'redis'としています(参照)。publishとsubscribe用にRedisへの接続は2つ作っています。

server.js
constexpress=require('express');consthttp=require('http');constWebSocket=require('ws');constRedis=require('ioredis');constapp=express();app.use(express.static('public'));constserver=http.createServer(app);constwss=newWebSocket.Server({noServer:true});constredis=newRedis('redis');//redis containerconstclient=newRedis('redis');

次がpubsubの処理です。
あらかじめ一方のRedisへの接続で'newMessage'というチャンネルをsubscribeしておきます。
クライアントからメッセージを受け取ったら、もう一方の接続で'newMessage'のチャンネルに対して受け取ったメッセージをpublishします。
'newMessage'チャンネルに新規メッセージが来たら、全てのクライアントにメッセージを配信します。

server.js
functionsubscribeMessage(channel){client.subscribe(channel);client.on('message',function(channel,message){broadcast(JSON.parse(message))});}subscribeMessage('newMessage')// broadcast message to all clientsfunctionbroadcast(message){wss.clients.forEach(function(client){client.send(JSON.stringify({message:message}))})}wss.on('connection',function(ws,request){ws.on('message',function(message){redis.publish('newMessage',JSON.stringify(message))});--省略--});

ここまでは一台のサーバーで動かしてもpubsubを使う意味がありません。
なので、次にNode.jsのdockerコンテナを2つ立て、一方のコンテナにメッセージが来たらもう一方のコンテナにもpubsubを通して同期されることを試します。

Node.jsのコンテナ間で通信を同期させる

Node.jsのコンテナを立てるDockerfileです。公式にあるものをそのまま利用してます。

FROM node:12WORKDIR /usr/src/appCOPY package*.json ./RUN npm installCOPY . .CMD [ "node", "server.js" ]

Dockerfileを元にNode.jsのコンテナを2つ、またRedisコンテナをdocker-composeで作成します。
Nodeのコンテナは一つをlocalhost:3000、もう一つをlocalhost:3030でアクセスできるようにしています。

docker-compose.yml
version:'3'services:server1:build:.ports:-3000:3000server2:build:.ports:-3030:3000redis:image:redis:6ports:-6379:6379

最後にコンテナを起動します。

docker-compose up --build

起動したら、ブラウザのタブを二つ開き、一方をlocalhost:3000、もう一方をlocalhost:3030にアクセスします。
一方のタブで送信したメッセージが、もう一方にも表示されれば成功です。
ezgif.com-crop.gif

実際のアプリケーションはより複雑にはなりますが、概要を書きました。
より実践的なアプリケーション構成や実装方法は、参考にあるAWSのブログが分かりやすいと思います。

参考

Node.js: Async/Await を使って Redis のデータを読む

$
0
0
redis_read.js
#! /usr/bin/node
// ---------------------------------------------------------------////  redis_read.js////                      Dec/29/2020// ---------------------------------------------------------------constutil=require('util')constredis=require('redis')// ---------------------------------------------------------------asyncfunctionproc01(){constredisUrl='redis://127.0.0.1:6379'constclient=redis.createClient(redisUrl)client.get=util.promisify(client.get)client.keys=util.promisify(client.keys)constkeys=awaitclient.keys('*')//  console.log(keys)keys.forEach(asyncfunction(key,index){constvalue=awaitclient.get(key)console.log(key,value)})client.quit()}// ---------------------------------------------------------------console.error("*** 開始 ***")proc01()console.error("*** 終了 ***")// ---------------------------------------------------------------

実行結果

$ ./redis_read.js 
*** 開始 ***
*** 終了 ***
t1854 {"name":"大野","population":26957,"date_mod":"2001-10-12"}
t1855 {name: 宇都宮, population: 47236, date_mod: 2003-5-22}
t1858 {"name":"越前","population":71954,"date_mod":"2001-1-19"}
t1857 {"name":"あわら","population":52763,"date_mod":"2001-2-14"}
t1859 {"name":"坂井","population":41852,"date_mod":"2001-7-11"}
t1853 {"name":"小浜","population":15643,"date_mod":"2001-8-9"}
t1851 {name: 宇都宮, population: 49236, date_mod: 2003-5-22}
t1852 {"name":"敦賀","population":46729,"date_mod":"2001-9-17"}
t1856 {"name":"鯖江","population":32914,"date_mod":"2001-5-12"}

TestCafe入門 覚えておいたほうがよさそうなこと

$
0
0

TestCafeとは?

end-to-end testをnode.jsで自動化させたフレームワークです。

記事内容

今回、キャプチャ、cvポイントのテストにあたって、
これ多分次も使うかも?というやつを紹介していきます。

case.1 Basic認証のサイトのテスト

テスト対象サイトがBasic認証がかかっているサイトだと、
認証が通らなかった時の画面をスクリーンショットをとった結果が返ってくる。

こちらはドキュメントに全てが載っていました。

screenshotExample.js
fixture('スクリーンショット').page('https://example.com')// 追加.httpAuth({username:'example',//ユーザー名を設定password:'password'//パスワードを設定})test('スクリーンショット',asynct=>{awaitt.takeScreenshot({path:'example.png'})})

case.2 devlopment環境とproduction環境分けたい

case1にて Basic認証の際での対応を知ったのですが、
本番環境ではBasic認証がいらないので条件分けしたパターンがありました。

cross-envを使用して、解決しました
※今回はこのパターンでいきましたがその他効率良さそうなものがあればご教授お願いいたします

cross-envとは?

npmスクリプト実行時に任意の環境変数を変更できるもの。
cross-env npm

terminal
//cross-env パッケージをインストール
$npm i --save-dev cross-env
package.json
"scripts":{"screenshot:dev":"cross-env NODE_ENV=development testcafe chrome screenshotExample.js --page-load-timeout 10000 --skip-js-errors","screenshot:prod":"cross-env NODE_ENV=production testcafe chrome screenshotExample.js --page-load-timeout 10000 --skip-js-errors",},

このように scriptの中で cross-envを記述し、 NODE_ENV=環境名とすると
process.env.NODE_ENV === 指定した環境名とすることができる。

screenshotExample.js
constprocessENV=process.env.NODE_ENVconsttestUrl=processENV==='production'?'https://example.com':'https://dev-example.com'if(processENV==='development'){fixture('スクリーンショット').page(testUrl).httpAuth({username:'example',//ユーザー名を設定password:'password'//パスワードを設定})}else{fixture('スクリーンショット').page(testUrl)}test('スクリーンショット',asynct=>{awaitt.takeScreenshot({path:'example.png'})})

これで環境別で、条件分岐が可能になりました。

case.3 1つのフォームで送信パターンが複数存在する。

Image from Gyazo

ラジオボタンを押すとフォームの中身が切り替わる場合があります。
その際は2回テストを行わないといけません。

2つ関数を用意したり、2つファイルを作成するという方法もあると思われるのですが、
それだとコードが冗長になってしまいます。

今回はそれをeach文でまわして解決しました。

コード例

formTest.js
import{Selector}from'testcafe';consttakeContactTest=(testType)=>{fixture('テストフォーム').page('https://example.com');test('必須項目を入力し送信',async(t)=>{constdataTarget=awaitSelector('input');constradioButtonTarget=awaitSelector('[data-target-radio]');consttypeB=radioButtonTarget.withAttribute('value','1');constmail=dataTarget.withAttribute('name','email');constphone=dataTarget.withAttribute('name','phone_number');// typeごとに処理を分ける switch(testType){case'type1':awaitt.typeText(mail,'test_sample@gmail.com').click(submitBtn.withExactText('送信'))break;case'type2':awaitt.click(typeB).typeText(phone,'08012345678').typeText(mail,'test_sample@gmail.com').click(submitBtn.withExactText('送信'))break;}});}consttestName=['type1','type2']//こちらは判別する値です。任意の名前を設定。testName.forEach(testType=>takeContactTest(testType))

このようにして今回は実装しました。
今回はeach文で回したのですがほかにもパターンがあると思われます。
実装次第良作でしたら 内容を追加したいと思います。

おわり

今回は実装してみて、お!これは覚えないといけないかもというやつを内容にしました。
まだまだTestCafe初級者ですので、もっとテストケースを増やして、知見をアウトプットできたらいいなと思ってます。

参考

TestCafe ドキュメント
E2Eテストの始め方 TestCafe① -導入編-

プログラミング超初心者がNode.jsについて理解しようとした

$
0
0

JavaScript関連の分野について学習をしていると
Node.jsというものがどうしても目に入ってくるのですが、
プログラミング初学者の私にとっては、

わかりやすく解説しようとしてくれている記事を10本調べてみても
結局Node.jsがなんなのかわかりませんでした。。

もしかして同じような初心者の人いるのでは?と思い
理解が進んだ説明を書き留めておきます。
Node.js ってなんなん?ていつまで経っても解決しない方はぜひご覧ください。

Node.js
JavaScript(フロントエンドを担当する言語)という、
ユーザーの目に見える場所の挙動にあれこれ指示を出す言語
(マウスオーバーするとポップアップが出る、
webサイト上に画像のスライダーの挙動を実現させる、など)

しかし、サーバーサイドというデータベースに入っている情報を引っ張ってくる
いわゆるユーザーの目に見えないサーバーとの情報のやり取りをしてくれる
言語のような使い方ができるというもの。

本来webページには、登録された情報を引っ張ってくる指示が出されてから
サーバーに情報を取りに行って表示されるというプロセスがある

しかしデータベースに登録されたユーザー情報などをサーバーに連携して
取り出してきて表示、、、というように、

情報取得の要求(ユーザーから) → サーバーへ情報を探しに行く
→ 該当情報を返して表示

という一連の流れに時間がかかりすぎると、ちょっとサービスによっては
ユーザーの要求するイベントに対してタイムラグが発生し、
(LINEのやり取りや決済サービスなど)サービス的にイケてないものに
なってしまいそうな時に使われるものらしい。

特徴
・webサイトやサービス上で、
・入力された情報などをリアルタイムでサーバーに保存し反映し続ける(常時接続)

LINEがなんか挙動が止まって数秒経ってからようやくメッセージ送れる
みたいな状態だと結構ストレス溜まりますよね。

そうならないようにするための実装方法、という解釈ができました。

参考にさせていただいたサイト:
https://eng-entrance.com/what-is-nodejs

LINE公式でオンラインサロン作ってみたい

$
0
0

LINEでオンラインサロン

先日、LINE Payの決済について学びました。
ネット検索をしていると、LINEpayのサブクス決済をLINE公式アカウントで実装するコードを公開しているサイトを見つけました。

最近、wordpressでオンラインサロン、会員サイトを作りたいという要望がとても多いので
LINE公式アカウント上でサブスク管理が出来、限られた会員だけが使えるLINE公式アカウントがあっても面白いのかなと思い実装してみる事にしました。

動作の説明

まず、お友達登録をしてメッセージを送信
(LIFFで会員になるなどボタンを設置して、テキストを送るのも良いですね)
image.png

YESを選択

image.png

1円を支払うボタンをクリック
image.png

決済ボタンをクリック
image.png

決済完了するとスタンプと、オウム返し
image.png

決済か終了すると、正式にLINE公式アカウントのメンバーになった感じがしますね。
それでは、参考にしたサイトを紹介します。

LINE Payを使った決済をBotに組み込んでみよう

LINE Payを使った決済をBotに組み込んでみようというサイト

1.botにお友達追加
2.サブスク決済の案内
3.LINE Payで決済
4.決済完了するとLINE公式アカウントでの対話可能

こんな流れを実装出来ます。

はまった所1

githubのread meにあった手順書がスペルミスしていました。コピペで使うとエラーに
image.png

正しくは

node with_middleware.js

はまった所2

セキュリティにひっかかりました

image.png

LINE Pay公式のDocumentでホワイトリストに登録をしてあげなければならないです。
LINE Pay Developers White IP

LINE Pay管理画面にログインします
LINE Pay Login

image.png

【決済サーバーIP管理】をクリックして、エラーで表示されていたIPアドレスを入力
最後の数字は【24】を入力して登録します

image.png

出来た

Line Payのビジネスアカウント開設していたので、決済完了までの動きを見る事が出来ました。
完了するとスタンプ+メッセージ

その後はオウム返し

という部分までコードが実装されていました。

image.png

まとめ

今回実装した機能をベースに、今まで学んできた事を組合わせてどれだけの事が出来るのかチャレンジしたいと思います!

1.LINE Messaging API
2.web APIとの連携

を実装していきたいと思います。

その後、実装しました!
https://qiita.com/misa_m/items/f9c5c1adc5f4c96acb8a

Google Calenadr APIをnode.jsで使ってみる

$
0
0

google calendar APIからスケジュール取得

普段からgoogle calendarをよく利用しています。
google calendarにプライベートと仕事のスケジュールを全て管理しておき、お客様はスケジュールの空いている所にピンポイントで予約を入れられるので、スケジュール調整が自動化出来てとても便利です。

そこで、node.jsでgoogle calendar情報を取得させるコードを見つけたので実装してみる事にしました。
node.jsでgoogle calendarを扱う前に、Google APIsで認証キーの発行が必要です。

Google APIsでAPIキー発行

公式ドキュメントのEnable the Google Calendar APIボタンをクリックしてキーを発行します。

image.png

Google API でプロジェクト新規作成

image.png

プロジェクト名入力

image.png

APIを有効化

image.png

image.png

image.png

有効にします
image.png

OAuth同意

外部を選択して作成します
image.png

認証情報作成

image.png

今回、node.jsを利用します。

image.png

任意のサービスアカウント名を入力します。
ロールはオーナーにしました。

image.png

秘密キーが自動ダウンロードされました。

【認証情報】の中にサービスアカウントが増えています。
クリックしてみると
image.png

アクティブになっているのでOKです。
image.png

google calendarでのアクセス権限

カレンダーへアクセスして、歯車アイコンをクリックして設定をしていきます。
image.png

左サイドバーの【特定のユーザーと共有】をクリックします。
image.png

ユーザーを追加
image.png

先ほどのサービスアカウント名を入力
image.png

node.jsを利用

パッケージを利用します。

npm install node-google-calendar

configファイル作成

// ----------------------// ④ タイムゾーン(日本)// ----------------------constTIMEZONE='UTC+09:00';module.exports.calendarUrl=CALENDAR_URL;module.exports.serviceAcctId=SERVICE_ACCT_ID;module.exports.calendarId=CALENDAR_ID;module.exports.key=KEY;module.exports.timezone=TIMEZONE;

カレンダーの統合に書いてあるURLを②と③にいれます
image.png

/*************************
  sample.js
 *************************/// ----------------------// パッケージのインスタンス生成// ----------------------varCalendar=require('node-google-calendar'),config=require('./credentials/calendar-config'),calId=config.calendarId.myCal;varcal=newCalendar(config);// ----------------------// カレンダーイベントの取得// ----------------------// 取得する対象期間を指定// timeMinからtimeMaxの間のカレンダーイベントが取得されるvarparams={timeMin:'2020-12-01T06:00:00+09:00',timeMax:'2020-12-31T06:00:00+09:00',}cal.Events.list(calId,params).then(calEvents=>{console.log(calEvents);}).catch(err=>{console.log(err.message);});

timeの箇所を取得したい期間にします。

実際に取得をしたデータ

image.png

他にも色々出来る

カレンダー情報を受け取ったのですが、削除したり追加したり色々出来るみたいです。

Enentsの構造についてのリファレンスを見つけました。
Google Calendar API Enents

また、今回はcal.Event.listとしたのですが、
listの部分をdeleteやgetなど、色々変える事でgoogleカレンダーを便利に使えそうです。
Google Calendar API Methods

せっかくなので、LINEにデータを送りたい

取得されるcalEventsはJSON形式なので、LINEに送りたいと思って実装したところ

letmsg=calEvents;

カレンダーから取得した値をメッセージに代入できなくてはまりました。

image.png

調べると

console.log(JSON.stringify(value));

このような形式で書いてあげると解決できるみたいです。無事、出力されました。
参考サイト:console.logでJSON形式の値が[object Object]になる時

LINE botへメッセージを送る時も、【text: JSON.stringify(msg) 】と記述をしたところ、無事チャットボットに返答がありました!
【text: msg】だとbotに情報が何も送れず、この部分の解決にとても苦労しました。

cal.Events.list(calId,params).then(calEvents=>{console.log(calEvents);letmsg=calEvents;console.log(JSON.stringify(msg));returnclient.replyMessage(event.replyToken,{type:'text',text:JSON.stringify(msg)});}).catch(err=>{console.log(err.message);});

image.png

LINEへカレンダー情報が送れることが分かったので

特定LINEユーザーへ送信

LINE公式アカウントでオンラインサロンを運用する際に、空きスケジュールを自動取得、一斉送信という仕組みもあっても良いのかなと思ったので、LINE Messaging APIのmulticastという機能も使ってみる事にしました。

replayMessageをmulticastへ変更しています。

cal.Events.list(calId,params).then(calEvents=>{console.log(calEvents);letmsg=calEvents;console.log(JSON.stringify(msg));// return client.replyMessage(event.replyToken, {returnclient.multicast(['USER-ID'],{type:'text',text:JSON.stringify(msg)//← ここに入れた言葉が実際に返信されます});}).catch(err=>{console.log(err.message);});

まとめ

JSONで受けとったデータがそのまま送られているので、必要な情報だけを送れるように引き続き考察を続けていきたいと思います。

今回、Google Calendar APIを利用する為に参考にしたサイトは以下です。
とても分かりやすく、エラーなく実装が行えました。
node.jsでGoogleカレンダーを操作する方法。nodeパッケージ「node-google-calendar」使用。

googleカレンダーから情報を取得する方法は
LINE公式でオンラインサロン作ってみたいこちらに書きました

JWTを用いた認証方法(JWT発行と検証方法)

$
0
0

概要

Webアプリにおいて、クライアント側のSPA(=Single Page Application)からのAPIの認証に、JWTを利用する場合を考える。サーバー側でのJWT発行と、APIに含めれて送られてきたJWTを検証する方法を説明する。

言語はNode.jsとする。

クライアント側でJWTの有効性を検証する、つまりクライアント側で復号用の公開鍵を保持することを想定し、JWTはRSAで署名するものとする(クライアント側での検証をしない場合は共通鍵で署名してもよいが、割愛する)。

JWTの発行と検証そのものは、OSSライブラリ「jsonwebtoken」を用いて簡単にできる。

ref. https://github.com/auth0/node-jsonwebtoken#readme

本記事では、JWT発行の際に必要となる秘密鍵の準備方法をメインに、作成した鍵ペアで実際にJWTを生成し、検証する様を説明する。

鍵ペアの生成はWindows10上で実施するものとする。一部WSLを利用する。

公開鍵暗号のアルゴリズムにはRSAを用いる(他にElGamal等がある)。
RSAの鍵長は4096かそれ以上が一般に推奨されるが、
ここでの用途は暗号化ではなく署名であり、また期限付き認証キーである点を踏まえ、
デフォルトの2048を用いる(あまり長い文字列になると、AWSのElasticBeastallkの環境変数に入らないし)。

本記事で用いるサンプルコードは、以下へ全体が格納してある。

https://github.com/hoshimado/qiita-notes/tree/main/qiita-jwt-auth

JWTの発行(署名)に必要な秘密鍵を準備する

最初に、生成に使うツールについて説明し、続いて具体的な手順を説明する。

windows10上でのRSA鍵ペアの作成に用いるツールなど

Node.jsのライブラリ「jsonwebtoken」において署名に秘密鍵(とペアとなる公開鍵)を用いる場合は、
「PEMフォーマットを用いること」
と記載されているので、これを準備する。

secretOrPrivateKey is a string, buffer, or object containing either
the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA.

ref. https://github.com/auth0/node-jsonwebtoken

秘密鍵の生成には、SSHログイン向けPEM形式のRSA鍵ペアを生成するコマンドであるssh-keygen コマンドを利用する(SSHログイン向けであるが、RSA鍵ペア自体はSSH独自と言うわけではない)。

なお、鍵のフォーマットにはDER形式とPEM形式があり、これらは相互に変換は可能(ほかに、OpenSSH形式、もあるが割愛)。

ref. https://qiita.com/kunichiko/items/12cbccaadcbf41c72735

Windows10マシンの場合は、WSLを利用する。
WSLにはOpenSSHコマンドが入っている(バージョンはWSL環境に依存)。

$ ssh -H
OpenSSH_7.2p2 Ubuntu-4ubuntu2.4, OpenSSL 1.0.2g 1 Mar 2016

なお、ssh-keygenは、OpenSSHに内包されるツールであり、OpenSSHの7.8以上では、デフォルトフォーマットが(OpenSS*L*の)PEM形式ではなく、OpenSSH形式に変更されている。
その場合は、-m PEMを指定する必要がある。

ref. https://dev.classmethod.jp/articles/openssh78_potentially_incompatible_changes/

バージョンの確認は ssh -vで行える。

ssh-keygenコマンドで作成した鍵ペアのうち、
秘密鍵の「id_rsa」はPEM形式なのでそのまま利用できるが、公開鍵の「id_rsa.pub」はOpenSSHの独自形式のファイルとなる。
この独自形式は「ssh-keygen -f id_rsa.pub -e -m pem」とすることでPEM形式に変換可能。

JWT生成時には、これらの作成済みの秘密鍵と公開鍵を読み込む必要があるが、ファイルのままだとGit管理との整合が面倒なので、環境変数から文字列として読み込むのが望ましい。
この場合、ファイルをBase64変換した「1行のテキスト」として扱うことで、実現できる。

As mentioned in this comment, there are other libraries that
expect base64 encoded secrets (random bytes encoded using base64),
if that is your case you can pass Buffer.from(secret, 'base64'),
by doing this the secret will be decoded using base64
and the token verification will use the original random bytes.

ref. https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback

Windows10の場合は、WSL環境にあるBase64コマンドを利用して変換可能(具体的な方法は後述)。

WSLを用いたRSA秘密鍵と公開鍵の鍵ペアの作成手順

ssh-keygenで鍵ペアを生成

秘密鍵を格納するフォルダとして「.ssh」を作成しておく。

※Gitの.gitignoreファイルに、忘れずに「.ssh/」を追加して管理対象外としておくこと。

WSLに入り、フォルダ「.ssh」配下へ移動する。

秘密鍵作成コマンド「ssh-keygen -t rsa -b 2048」を入力してEnterキーを押し、次のように入力する。
パスフレーズは(これはSSHでのログイン向けのコマンドのため署名目的なら)省略も可能だが、
ここでは「qiitajwt」と入力する。
なお、passphraseのところは、実際には入力文字は画面に表示されない。

$ ssh-keygen -t rsa -b 2048
Generating public/private rsa key pair.
Enter file in which to save the key (/home/hoshimado/.ssh/id_rsa): ./id_rsa
Enter passphrase (empty for no passphrase): qiitajwt
Enter same passphrase again: qiitajwt
Your identification has been saved in ./id_rsa.
Your public key has been saved in ./id_rsa.pub.
The key fingerprint is:
SHA256:uqs6k7iUOu0GE0flMYqpy4REVPdG6RcSYL84IthYc44 hoshimado@DESKTOP-598V4QK
The key's randomart image is:
+---[RSA 2048]----+
|.o.o++.oo        |
|.o.ooo+o .       |
|oo+ o .+. .      |
|*+.=  o...       |
|==E..o .S        |
|=.o . ..         |
|.O .  .          |
|= *    .         |
|o=o+..o.         |
+----[SHA256]-----+

id_rsaid_rsa.pubという2つのファイルが生成されば成功。
共にテキストファイルなので、中身をテキストエディタで開いて参照することは可能。
なお、内容はBase64エンコードされている。

ref. http://rnakato.hatenablog.jp/entry/2019/05/18/134336

生成した公開鍵(OpenSSH独自形式)をPEM形式へ変換

id_rsa.pubはOpenSSHの独自形式なので、次のようにしてPEM形式に変換しておく。

$ ssh-keygen -f id_rsa.pub -e -m pem > id_rsa.pub.pem

生成した鍵ペアをBase64変換して文字列へ変換

次のコマンドで秘密鍵のファイルをBase64変換する。出力ファイルはMIMEの基準に従って76文字ごとに改行コードが入るため、続いて以下のコマンドで改行を削除しておく。

$ base64 id_rsa > id_rsa.base64.txt
$ cat id_rsa.base64.txt | tr -d '\n' > id_rsa.base64.oneline.txt

公開鍵のファイルも同様。

$ base64 id_rsa.pub.pem > id_rsa.pub.pem.base64.txt
$ cat id_rsa.pub.pem.base64.txt | tr -d '\n' > id_rsa.pub.pem.base64.oneline.txt

これで、秘密鍵、公開鍵ともに1行の文字列として準備ができた。

id_rsa.base64.oneline.txt
id_rsa.pub.pem.base64.oneline.txt

実際にJWTを生成してみる

JWTを生成するには、OSSモジュール「jsonwebtoken」をもちいて、例えば次のようにできる。

var jwt = require('jsonwebtoken');

var envFactory = {
    publicBase64     : process.env.JWT_PUBLIC_KEY,
    secretBase64     : process.env.JWT_PRIVATE_KEY,
    passphraseSecret : process.env.JWT_PASSPHRASE,
    issuerUri        : process.env.JWT_ISSUER,
    expireMin        : process.env.JWT_EXPIRE_MINS
};

/**
 * Jwt形式でアクセストークンを発行する。
 * 
 * @param {*} params - JWTのsub(ユーザー識別子)とaud(クライアント識別子)を持つこと
 */
var publishApiKeyAsJwt = function (params) {
    var env = envFactory;
    var claim = {
        iss : env.issuerUri,
        aud : params.aud,
        sub : params.sub,
        iat : Math.floor(Date.now() / 1000),
        exp : Math.floor(Date.now() / 1000) + (60 * env.expireMin)
    };
    var token = jwt.sign(
        claim, 
        { 
            key: Buffer.from(env.secretBase64, 'base64'),
            passphrase: env.passphraseSecret},
        {
            algorithm : 'RS256'
        }
    );
    return Promise.resolve(token);
};


publishApiKeyAsJwt({
    sub: 'idIsHere',
    aud: `clientName`
}).then((token)=>{
    console.log(token);
});

これを実行するには、環境変数に先ほど作成した秘密鍵を設定して次のようにする。

set JWT_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpQcm9jLVR5cGU6IDQsRU5DUllQVEVECkRFSy1JbmZvOiBBRVMtMTI4LUNCQywxNzRFM0I3NUNCOTQ0RTRBRjVGRUMxRUIxQ0RDRTdDMwoKUFkyakpYeVJTRTZrRHhiQzRjelhPZHZVbVh4OG8rTDcyZENBSzdSVkZvQ3J5Q25sSUNwVVM2aUZScHBsQXdRYgpEN1BMN2pXR3VhZDYvb3Z4VkhjSk8wYUUrTlNKSFRxRllhcDUraGxqUnZadjJOTWVrVjBTTGt1UTRDYlRNNWVnCmRDN0hGSUFaUmpWeDZsVTArVURiamxXVS9mRzdyTk5LNVkvaDM3SVEzelBHV1RXeVV1MTdCMExxanRQZ0w3OWQKakRpOHQwOHJoNEp1M1pKT2kzZWVmaTFYQ1gyalNwVWZ0MkxXNDdnT0FPR2ttOWFhaUlWZDNYWkV5VllVa3hEbwoxMTRUUUxTeTk2b3ZxeTVPQ1N4OGFkRHBpN0t6ZzlHaCtiTmg1MGNKSnRaSnZCcWZDTzRyVjd3Vk81d3NNdkpSCkVXUWErZVl5V05EUjhWemZieUtuMWxSeXA5VUt6M083Y3Rva1FTS0xkckxOT0krWEk4bzdacW83a2xHZmM2ZkoKaTgzdjVhOUtjM1p2OWUwWDN2RnVoaEJjeDNCaVRnRU9zeXBDQXlNZndYVzNCRFVjdGptcGhiRVJQMVRnYjIvaQpnSDc2UnM1bEEzLzdRMmI1di96TEV1T0xXQ2Q0d0owVlZpdW5QS3JCQ0pONWsvZytMK3BXRHFONnR5b0x6VHlOCnEwd0d6eXI4TUNJUWhoYWZuM0hMMHlUaURnMlFZZ1RSRHQxbFhjOVk0aVBLU3FrUWFSaHJZcmZuNllJWUpLTTEKVGpkVTdKdE8wSHFXYWtaZXk0NUpLeUZvbHNaditiVVZtRktVU0k5OTlTdVBpVGlnM2R0NVMvcG01NHJQdkcvQgoyRlJuSW1lRmo2bnBJMG1Ya280V2tvUWZJaGVKM0hLMUtDNmhrRWFHdzc5S1dFMU1iV2lSOWVpODVyYnQ5L1JjCm9MN2VDS0hHeGtHNm51SG42SXF2bkNJT3JqS1FJbjZGdjdEWEl4RDRWTjZPYlhHb3NNUnVyemtlWlNqc1R2ZmIKZGEvYmZRRCtLbk5sUnZrSmNlZDJFc3JLYWxWREQrazlhV0dTWncrQlI0S0trRGliUkpRcTZmTllRSHh6RjYzcgp5TnhTOTY1ZkdjNXQ0c0VZd3A3UXp1MmNEVnBjOHA5YmVCQnAxU2pDdWVoWENsV3dFTEZMNndFb0w2K0h6d3M2CisrTCtoUVYrOXJsWjljekt3NnlHNEF4V3VjV3lTVCtjMHBFbHU0MUV2bGUxQUg1KzZCRll2RWRxbUJkSVJPWUYKQ2k4RnE0YkVVNGhCakxyUVR4NzUvRlkzVFZnU2NZL1BJNHFhaWs5eE1YOVBTRnIvZXR6NVZEZDhMdmEvRHB1OQprUDdPd3R0bXZJRlFsVDdHekxHUVE5UWxXWi9UdDdtWm9iVDQrSUx1b29SWmVpYUZYYTBXL0V5RlY2cVNSZmFvCmxQSk52c3FiSTFaYWJHQ1ZaSEFCa0RVZzFzRjRvUnVmcUNYSXlWU25DMGxjT2w2R250UHo0aXVvRENZLzd4K2MKV3U2cFRXdEJHOUJ6M3lza1BMVXNNaTcvQUE3UktKeCtYYlEwMExxd0FzSzFuaUh1cUtFTTFRR284VldZQTVDQgpvejd5V1ErMUFxVWR4alp6SmtRcURCUW4zYUVuczFRUldGVUx6SkVxellpOEk2QVgydjZtM284S2JDOFl0cW00Cnhja2tGWmZtZ0pWK0dzMUN1alJ0eVgrWEl3OXg4UHhOWWdDUzFWZ0M4eTFnMjdrTVVHcVNLQXIvSTQ2THYzNXQKcHhvY1NQRFAxTk5Kd3hMYWpjU1c0TXJVZlpRclJZVU5Ib0NmcU04dHk4bE5zOTh2d0diU2tvbWd0bmlTTTNzLwpZdW4zcExWS2RKZlRQNG1FRCtaUzdwOE9icU1LRFNpRGN5bmhpNlpGZlFIeWVNWmlFN0l6a2VqUDNxbDNuRTBGCmZ1ZDNYaUJGYmFIVVNJSVk5YUFhS2UwZi9SR1ZjQVlHQ3BsbFAvU1ZKMDlITkd2Zk8yOXlpWGhSRlFjVzBuL2wKcmx2Qm16QlVLVkFGOC9hdzlxVGxVdnBZa2h3NFVjcEEyaitxRUVKa01EdTkrUy9RR1Uyb0RDM0R5SkNOWElLVgotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
set JWT_PASSPHRASE=qiitajwt

set JWT_ISSUER=sample.qiitajwt
set JWT_EXPIRE_MINS=1

node publishJwt.js

これを検証するには、同じようにOSSモジュール「jsonwebtoken」をもちいて、例えば次のようにできる。

var verifyApiKeyAsJwt = function (accessTokenAwJwt, audience) {
    var env = envFactory.getInstance();
    var publicKey = Buffer.from(env.publicBase64, 'base64');
    var token = accessTokenAwJwt;
    var options = {algorithms: ['RS256']};

    var promise;
    if(token && publicKey){
        if(audience){
            options['audience'] = audience;
        }
        return promise = new Promise((resolve,reject)=>{
            // https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
            jwt.verify(
                token, 
                publicKey, 
                options, 
                (err, decoded)=>{
                    if(err){
                        reject(err);
                    }else{
                        resolve(decoded)
                    }
                }
            );
        });
    }else{
        promise = Promise.reject();
    }
    return promise;
};

verifyApiKeyAsJwt(
    process.argv[2],
    `clientName`
).then((token)=>{
    console.log(token);
}).catch((err)=>{
    console.log(err);
});

これを実行するには、環境変数に先ほど作成した公開鍵を設定して次のようにする。

set JWT_PUBLIC_KEY=LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXVLM1pwSUNDc0VnSVlPczJhbHdJN08zeFhWUnJ2MXRZODlqNEczbWZYNkxPMk9iYm9RTFoKK0EwUlY5V3pJTTdSUDcvcWxOUnpjYmN0YW9jKzF5aGhSbGZJZjFhY0NpdzgrdUdEbEE0S0VWM0hQa1dKL0IxYwpiNmI3cGJDNzVHT0YvVEtXc0RRZlViTURFWEFkNmFReURTbHlHZGdJTExiUkVvaW1XQlhZNzZDUnNlZVFZd1oyCnRKUlpHMEhnTmJzMll3MDBjN0J3Q3YvVUozTGZWaHN5TS9qQ2pxQ2x2WkRHT081a09HTko0WmVDdVphMGRYaDYKWXZWSkd3cjdDME1Tb29mNmhBZEtMZVEwTEViY0pDR1FVMmZpZFRIQVdYYUpjSjI4eEhMTUQ0S1UzOHdTMllWYgpDcDFRQUVPODRhOUJsMkh0cEY3TzRYaW5rSU5idzMwaFZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K

node verifyJwt.js   eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUucWlpdGFqd3QiLCJhdWQiOiJjbGllbnROYW1lIiwic3ViIjoiaWRJc0hlcmUiLCJpYXQiOjE2MDg0NzAwMzYsImV4cCI6MTYwODQ3MDA5Nn0.htfTjPxWtrJdfG84cyeWtPDJxMjOcq-yxYycOVQV6UMSDqwzSPAMWhI1Wg_5YoahRDaahyTUF7oJsxrPvDw8dJCVlLwwbAdfMw1xYM8wf9OOyA7PDkpHc234corPxoOlH05GvIopw2MgEVXKSHdgf7G16xrrvtWpYoROMicanIfa9pi41BkuiaATFPtlcnFBUX1Yq3EA_cQZhHCTJW7HGzRgzebEtO9Djc41vBpNAfJE07QeuLr6kpMImrlb6PTguZxLrYnlxYZb7VRd4uaUf3VudUzCHp_ymzIrb5HmSqFu8JlYwnAEZf98FwSxDRI7YIza2yGswc_S-71rdt1WYQ

なお、上記の実行結果は、「TokenExpiredError: jwt expired」となる。
当方がこの記事を書く際に生成したJWTの検証であり、有効期限は1分なので、この記事を書いているうちに切れている。
なので、検証に成功するためには、「node publishJwt.js」で生成したJWTを、1分以内に「node verifyJwt.js [生成したJWT(文字列)]」として実行する必要がある。

検証に成功すると、例えば次のように結果が表示される。

{
  iss: 'sample.qiitajwt',
  aud: 'clientName',
  sub: 'idIsHere',
  iat: 1608470825,
  exp: 1608470885
}

なお、上記に記載した秘密鍵と公開鍵のペアは、本記事向けに専用に作成したものなので、これを流用しても何か悪さができるわけでは無いのであしからず(WSL環境が使えない、等の理由で鍵ペア生成ができない環境で試したい場合は、本鍵ペアを流用してもらっても構わない)。

また、本記事では簡単のためにWSL環境のssh-keygenコマンドを利用したが、SSH for Windows等と一緒に提供されるssh-keygenコマンドを使っても、もちろん構わない。

エラーについて

jsonwebtokenが「PEM_do_header:bad password read」のようなエラーを吐く場合がある。
これは、公開鍵(か秘密鍵)のフォーマット不適切、もしくは、パスフレーズを設定した鍵なのに、パスフレーズのインプットが無い、場合に起きるので、PEM形式になっているか?パスフレーズを設定したか?を確認のすること。

記事中に記載以外の参考URL


Node.js: bluebird を使って Redis のデータを読む

$
0
0

次のプログラムを bluebird を使って書き換えました。
Node.js: Async/Await を使って Redis のデータを読む

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

sudo npm install-g bluebird
bluebird_read.js
#! /usr/bin/node
// ---------------------------------------------------------------////  bluebird_read.js////                  Dec/30/2020// ---------------------------------------------------------------'use strict'constbluebird=require("bluebird")constredis=require('redis')bluebird.promisifyAll(redis.RedisClient.prototype)bluebird.promisifyAll(redis.Multi.prototype)// ---------------------------------------------------------------asyncfunctionproc01(){constredisUrl='redis://127.0.0.1:6379'constclient=redis.createClient(redisUrl)constkeys=awaitclient.keysAsync('*')keys.forEach(asyncfunction(key,index){constvalue=awaitclient.getAsync(key)console.log(key,value)})awaitclient.quitAsync()}// ---------------------------------------------------------------console.error("*** 開始 ***")proc01()console.error("*** 終了 ***")// ---------------------------------------------------------------

実行スクリプト

export NODE_PATH=/usr/lib/node_modules
./bluebird_read.js

nodejsでtail

$
0
0

electron(nodejs)上で特定のファイルをtailする

tailライブラリを使用する。
https://github.com/lucagrulla/node-tail

$ yarn add tail
App.tsx
importReact,{useEffect,useState}from'react'import{render}from'react-dom'import{Tail}from'tail'consttail=newTail('file-path')constApp=()=>{let[texts,setTexts]=useState<string[]>([])useEffect(()=>{tail.on('line',(data:string)=>{texts=[...texts,data]setTexts(texts)})return()=>{tail.unwatch()}},[])return(<>{texts.map((text,index)=>{return(<divkey={index}>{text}</div>
)})}</>
)}

Node.js: Redis のデータを作成 (Create)

$
0
0
redis_create.js
#! /usr/bin/node
// ---------------------------------------------------------------//  redis_create.js////                  Dec/30/2020//// ---------------------------------------------------------------'use strict'constutil=require('util')constredis=require('redis')process.on('unhandledRejection',console.dir)// ---------------------------------------------------------------functiondict_append_proc(dict_aa,id_in,name_in,population_in,date_mod_in){varunit_aa={}unit_aa['name']=name_inunit_aa['population']=population_inunit_aa['date_mod']=date_mod_indict_aa[id_in]=unit_aareturndict_aa}// ---------------------------------------------------------------functiondata_prepare_proc(){vardict_aa=newObject()dict_aa=dict_append_proc(dict_aa,'t1851','福井',95714,'1950-9-12')dict_aa=dict_append_proc(dict_aa,'t1852','敦賀',28157,'1950-3-15')dict_aa=dict_append_proc(dict_aa,'t1853','小浜',67241,'1950-10-2')dict_aa=dict_append_proc(dict_aa,'t1854','大野',32169,'1950-6-26')dict_aa=dict_append_proc(dict_aa,'t1855','勝山',41358,'1950-8-14')dict_aa=dict_append_proc(dict_aa,'t1856','鯖江',64792,'1950-9-12')dict_aa=dict_append_proc(dict_aa,'t1857','あわら',38251,'1950-3-21')dict_aa=dict_append_proc(dict_aa,'t1858','越前',52486,'1950-7-26')dict_aa=dict_append_proc(dict_aa,'t1859','坂井',25397,'1950-11-9')returndict_aa}// ---------------------------------------------------------------asyncfunctionmain(options){vardict_aa=data_prepare_proc()constredisUrl='redis://127.0.0.1:6379'constclient=redis.createClient(redisUrl)client.setAsync=util.promisify(client.set)client.quitAsync=util.promisify(client.quit)constkeys=Object.keys(dict_aa)keys.forEach(asyncfunction(key,index){conststr_json=JSON.stringify(dict_aa[key])awaitclient.setAsync(key,str_json)})awaitclient.quitAsync()}// ---------------------------------------------------------------console.error("*** 開始 ***")main({argv:process.argv})console.error("*** 終了 ***")// ---------------------------------------------------------------

実行コマンド

./redis_create.js

Node.js: Redis のデータを更新 (Update)

$
0
0
redis_update.js
#! /usr/bin/node
// ---------------------------------------------------------------//  redis_update.js////                  Dec/30/2020//// ---------------------------------------------------------------'use strict'constutil=require('util')constredis=require('redis')process.on('unhandledRejection',console.dir)// ---------------------------------------------------------------functionget_current_date_proc(){consttoday=newDate()varddx=(1900+today.getYear())+"-"+(today.getMonth()+1)ddx+="-"+today.getDate()returnddx}// ---------------------------------------------------------------asyncfunctionmain(options){varargv=options.argvconstkey_in=argv[2]constpopulation_in=argv[3]console.log(key_in+"\t"+population_in)constredisUrl='redis://127.0.0.1:6379'constclient=redis.createClient(redisUrl)client.getAsync=util.promisify(client.get)client.setAsync=util.promisify(client.set)client.quitAsync=util.promisify(client.quit)constvalue=awaitclient.getAsync(key_in)console.log(value)try{constunit_aa=JSON.parse(value)varout_str=key_in+"\t"out_str+=unit_aa.name+"\t"out_str+=unit_aa.population+"\t"out_str+=unit_aa.date_modconsole.log(out_str)unit_aa.population=population_inunit_aa.date_mod=get_current_date_proc()constjson_out=JSON.stringify(unit_aa)console.log(json_out)awaitclient.setAsync(key_in,json_out)}catch(error){console.error("*** error *** from JSON.parse ***")console.error(error)console.error(key_in)}awaitclient.quitAsync()}// ---------------------------------------------------------------console.error("*** 開始 ***")main({argv:process.argv})console.error("*** 終了 ***")// ---------------------------------------------------------------

実行コマンド

./redis_update.js t1857 82597400

Node.js: Redis のデータを削除 (Delete)

$
0
0
redis_delete.js
#! /usr/bin/node
// ---------------------------------------------------------------//  redis_delete.js////                  Dec/30/2020//// ---------------------------------------------------------------'use strict'constutil=require('util')constredis=require('redis')process.on('unhandledRejection',console.dir)// ---------------------------------------------------------------asyncfunctionmain(options){varargv=options.argvconstkey_in=argv[2]console.log(key_in)constredisUrl='redis://127.0.0.1:6379'constclient=redis.createClient(redisUrl)client.delAsync=util.promisify(client.del)client.quitAsync=util.promisify(client.quit)awaitclient.delAsync(key_in)awaitclient.quitAsync()}// ---------------------------------------------------------------console.error("*** 開始 ***")main({argv:process.argv})console.error("*** 終了 ***")// ---------------------------------------------------------------

実行コマンド

./redis_delete.js t1856

APIGWとlambdaでGoogleスマートホームアプリを作成してみた[前編]

$
0
0

初めに

業務で初めてスマートホームスピーカーの開発を行うことになった
今回はその中でもGoogleHomeの開発について備忘録的に、実施事項を記載していく

注意事項

本投稿記事は2020年12月時点の仕様に基づいた実装となります

参考記事

Google Assistant 公式ドキュメント

この記事で書いていくこと

  • cognitoを利用したアカウントリンクの実装 <- ★本記事ではこちらを実施
  • APIGateway+lambdaを利用して、Googleスマートホームアプリのコード実装と各種設定

この記事で書かないこと

  • 基本的なGoogleスマートホームアプリの作成手順について

事前に用意しておくもの

  • Googleアカウント
  • Googleアカウントに紐づく(Cloud IAM)GCPアカウント

1. 事前設定

プロジェクトやアプリの初期セットアップについては、以下の記事を参考に実施しました
Googleスマートホームアプリの作成方法

2. [AWS]Cognito初期設定

  1. AWSコンソールから[cognito]->[ユーザプールを作成する]を押下
  2. プール名は任意のものを入力(例ではAccountlink_googleと入力)
  3. 画面下の[デフォルトを確認する]を押下
  4. [プールの作成]を押下 Screen Shot 2020-12-30 at 17.20.42.png

3. [AWS]Cognitoアカウントリンク設定

  1. cognitoの設定画面から[アプリクライアント]からapiを実行するための新規IDとシークレットキーを作成
    Screen Shot 2020-12-30 at 17.35.24.png

  2. [ドメイン名]から任意のcognitoドメインを作成
    Screen Shot 2020-12-30 at 17.38.58.png

  3. [アプリクライアントの設定]にて以下を設定

    • 有効な ID プロバイダ: Cognito User Pool
    • コールバック URL: https://oauth-redirect.googleusercontent.com/r/[Actions on googleのプロジェクトID]
    • サインアウト URL: https://gala-demo.appspot.com/ ※Googleで提供しているサンプルUIを利用します
    • 許可されている OAuth フロー : [Authorization code grant]にチェック
    • 許可されている OAuth スコープ :email, openid Screen Shot 2020-12-30 at 17.43.33.png

4. [Actions on google]アカウントリンク設定

  1. アクションのコンソールにログイン
  2. [Account linking]にて以下を設定
    • Client ID issued by your Actions to Google: cognitoのクライアントID
    • Client secret : cognitoのシークレットキー
    • Authorization URL: https://<ドメイン名>.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize
    • Token URL: https://<ドメイン名>.auth.ap-northeast-1.amazoncognito.com/oauth2/token
    • scope: emailとopenidを一つずつ追加 Screen Shot 2020-12-30 at 18.02.11.png

一旦ここまででアカウントリンクの実装までは完了
APIGWとlambda部分の実装については後編にて記載します

【アプリ開発 1】【Node.js express Docker】 Dockerを用いてNode.js Express MongoDB(mongoose)の環境を構築する【2020年12月】

$
0
0

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

Node.js Express MongoDBを用いたアプリ開発

受託開発でNode.js Express MongoDBを用いたアプリ開発を行うことになりました。
開発の中で得られた知見を、支障のない範囲で記録していきたいと思います。
アプリの内容はさておき、チームで開発するため、まずはDockerを用いて開発環境を構築を行います。

構築する環境

  • docker-composeを用いて、アプリコンテナ、mongodbコンテナを作成する。
  • アプリコンテナとmongodbコンテナの接続テスト(mongoose)を用いて、テストモデルにデータ保存。
  • mongo compassからデータの挿入を確認
  • mongodbに認証を設ける。作成するユーザーの権限は"root","read","owner"
  • データベースのパスワード等は環境変数で管理(gitignoreにてgitの管理から外す。)

※注意点
<アプリ名> <パスワード>となっているところは、各自で適当なものを記入してください。
そのまま記載するとエラーとなります。
今回は、パスワードは全て共通としています。

初期インストールするパッケージ一覧

 "bcrypt"
 "body-parser"
 "connect-flash"
 "cookie-parser"
 "debug"
 "ejs"
 "express"
 "express-ejs-layouts"
 "express-generator"
 "express-session"
 "express-validator"
 "http-errors"
 "http-status-codes"
 "method-override"
 "mongoose"
 "morgan"
 "nodemon"
 "passport"
 "passport-local-mongoose"

下準備

最初に用意するファイル群

.
├── .env
├── .gitignore
├── data
│   └── db  //空ディレクトリ
├── docker-compose.yml
├── docker_app
│   └── Dockerfile
├── secret_file
│   ├── db.env
│   └── db_init
│         └──mongo_init_user.js
└── src
    ├── controllers
    │     └── initTestsController.js
    ├── models
    │     └── init_test.js
    └── package.json

env.
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=<パスワード>
MONGO_INITDB_DATABASE=<DB名>
gitignore.
node_modules/
data/
.env
secret_file/
docker-compose.yml
version:'3'services:app:build:./docker_appcontainer_name:<アプリ名>_app_cntports:-"8080:3000"restart:alwaysworking_dir:/apptty:truevolumes:-/etc/passwd:/etc/passwd:ro-/etc/group:/etc/group:ro-./src:/appenv_file:-./secret_file/db.envcommand:bashnetworks:-<アプリ名>-networkdepends_on:-mongomongo:image:mongo:latestcontainer_name:<アプリ名>_db_cntports:-"27018:27017"restart:alwaysenvironment:MONGO_INITDB_ROOT_USERNAME:${MONGO_INITDB_ROOT_USERNAME}MONGO_INITDB_ROOT_PASSWORD:${MONGO_INITDB_ROOT_PASSWORD}MONGO_INITDB_DATABASE:${MONGO_INITDB_DATABASE}volumes:-./data/db:/data/db-./secret_file/db_init/:/docker-entrypoint-initdb.denv_file:-./secret_file/db.envcommand:-mongodnetworks:-<アプリ名>-networknetworks:<アプリ名>-network:external:true
Dockerfile.
FROM node:12
WORKDIR /app
RUN npm install
db.env
DB_USER=owner
DB_PASS=<パスワード>
DB_NAME=<アプリ名>_db
mongo_init_user.js
letusers=[{user:"read",pwd:"<パスワード>",roles:[{role:"read",db:"<アプリ名>_db"}]},{user:"owner",pwd:"<パスワード>",roles:[{role:"dbOwner",db:"<アプリ名>_db"}]},{user:"readWriteUser",pwd:"<パスワード>",roles:[{role:"readWrite",db:"<アプリ名>_db"}]}];for(leti=0,length=users.length;i<length;++i){db.createUser(users[i]);}
initTestsController.js
"use strict";constInitTest=require('../models/init_test');consttest=()=>{letinitTest=newInitTest({name:"Taro",age:20})initTest.save((error,data)=>{if(error){console.log(error);}console.log(data);})}module.exports={test};
init.test.js
"use strict";constmongoose=require("mongoose");constinitTestSchema=newmongoose.Schema({name:String,age:Number});module.exports=mongoose.model("InitTest",initTestSchema);
package.json
{}

環境作成

コンテナ作成

ホスト.
docker-compose build

下記の様な警告がでるが問題ない。

npm WARN saveError ENOENT: no such file or directory, open '/app/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/app/package.json'
npm WARN app No description
npm WARN app No repository field.
npm WARN app No README data
npm WARN app No license field.
ホスト.
#コンテナを一時的に起動コンテナ内に入る(--rmで停止後削除する。コンテナ起動後、bashに入る)
docker-compose run --rm app /bin/bash
appコンテナ内.
# express-generatorでアプリケーションのひな形を生成
npx express-generator --view=ejs

下記の用に即abortingで拒絶されたら再度実行

destination is not empty, continue? [y/N]
aborting

二回目は質問で待ってくれるので y でエンター

destination is not empty, continue? [y/N] y

下記の様な各種ファイルが作成される。

   create : public/
   create : public/javascripts/
   create : public/images/
   create : public/stylesheets/
   create : public/stylesheets/style.css
   create : routes/
   create : routes/index.js
   create : routes/users.js
   create : views/
   create : views/error.ejs
   create : views/index.ejs
   create : app.js
   create : package.json
   create : bin/
   create : bin/www

package.jsonに下記の内容を上書き

package.json
{"name":"uniq_app","version":"0.0.0","private":true,"scripts":{"start":"nodemon ./bin/www"},"dependencies":{"bcrypt":"^5.0.0","body-parser":"^1.19.0","connect-flash":"^0.1.1","cookie-parser":"~1.4.4","debug":"~2.6.9","ejs":"^3.1.5","express":"~4.16.1","express-ejs-layouts":"^2.5.0","express-generator":"^4.16.1","express-session":"^1.17.1","express-validator":"^6.7.0","http-errors":"~1.6.3","http-status-codes":"^2.1.4","method-override":"^3.0.0","mongoose":"^5.11.9","morgan":"~1.9.1","nodemon":"^2.0.6","passport":"^0.4.1","passport-local-mongoose":"^6.0.1"}}

app.jsに下記の内容を上書き

app.js
constcreateError=require('http-errors');constexpress=require('express');constpath=require('path');constcookieParser=require('cookie-parser');constlogger=require('morgan');constmongoose=require("mongoose");constindexRouter=require('./routes/index');constusersRouter=require('./routes/users');constinitTestController=require('./controllers/initTestsController');constapp=express();mongoose.connect(`mongodb://${process.env.DB_USER}:${process.env.DB_PASS}@mongo:27017/<アプリ名>_db`,{userNewParser:true});mongoose.set("useCreateIndex",true);// view engine setupapp.set('views',path.join(__dirname,'views'));app.set('view engine','ejs');app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({extended:false}));app.use(cookieParser());app.use(express.static(path.join(__dirname,'public')));app.get('/initTest',initTestController.test);/* 初期テストルーティング */app.use('/',indexRouter);app.use('/users',usersRouter);// catch 404 and forward to error handlerapp.use(function(req,res,next){next(createError(404));});// error handlerapp.use(function(err,req,res,next){// set locals, only providing error in developmentres.locals.message=err.message;res.locals.error=req.app.get('env')==='development'?err:{};// render the error pageres.status(err.status||500);res.render('error');});module.exports=app;

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

appコンテナ内.
npm install

一度コンテナを抜ける

appコンテナ内.
#コンテナを抜ける(この仮コンテナは削除される)
exit

コンテナを再起動

ホスト.
docker-compose up

下記クリックで接続確認
http://localhost:8080/

mongooseとの接続確認

下記クリックでデータが作成されるか確認
http://localhost:8080/initTest

下記の様なログが出力されれば成功(docker-compose upしたターミナルで)

uniq_app_cnt |   _id: 5fec5a6213a2fd002d89acca,
uniq_app_cnt |   name: 'initTestUser',
uniq_app_cnt |   age: 20,
uniq_app_cnt |   __v: 0
uniq_app_cnt | }
uniq_app_cnt | 【ログ】--接続成功--【ログ】

mongodbの権限周り確認

ターミナルを別タブで開き、dbコンテナに入る

docker exec -it <アプリ名>_db_cnt bash
mongo
//mongodbに接続

show dbs
//何も表示されない。(認証でロックされていることを確認)

use admin
//adminデータベースへ接続

db.auth("root", "<パスワード>")

//1 と返れば認証成功

db.system.users.find().pretty()
//作成されたユーザー確認

以下の様な、データベースに対する各権限者が作成されていたらオッケー。

{"_id":"admin.root","roles":[{"role":"root","db":"admin"}]}{"_id":"uniq_db.read","roles":[{"role":"read","db":"<データベース名>_db"}]}{"_id":"uniq_db.owner","roles":[{"role":"dbOwner","db":"<データベース名>_db"}]}{"_id":"uniq_db.readWriteUser","roles":[{"role":"readWrite","db":"<データベース名>_db"}]}

mongo compassからの接続確認。

image.png


Sequelize-cli-esmでSequelize をESモジュールで作成する

$
0
0

目的

  • nodeがESモジュールに対応したので、SequelizeをESモジュールで使用したい。
  • フロントエンド、バックエンドともにimportで統一した方が理解しやすい。
    • 初めて開発する人にfrontendはimport、backendはrequire()を使用する理由を説明しづらい。

前提

サンプルでは下記を使用。sequelizeについて基礎的な知識があること。

  • Node.js v14以降 (ESモジュール対応済み)
  • sequelize:6.3.5
  • sequelize-cli-esm:5.0.6 (ESモジュール版cli)
  • sqlite3:5.0.0 (データベース)

  • package.jsonに「"type":"module"」を追加しnodeのデフォルトをESモジュールにする。


概要

  1. sequelize-cli-esm (ESモジュール対応版Sequelize CLI)を利用して、ESモジュールでひな形を生成する
  2. package.jsonに「"type":"module"」を追加(nodeのデフォルトをESモジュールにする)
  3. sequelize-cli-esmでテンプレートファイルの作成と、マイグレーションの実行を行う
  • 今回の手順ではUsersテーブルを作成し、サンプルデータを登録する。
  • sequelize-cli-esm はnode_module/.binに「sequelize-esm」という名前でインストールされる。(yarnやnpxで実行する際、コマンド名を間違えないこと)
  • 生成されるファイルはesモジュール形式となるため「"type":"module"」を追加するか、ファイルの拡張子を「.mjs 」にする。

手順

必要なパッケージをインストール

yarn init -y
yarn add sequelize sqlite3
yarn add sequelize-cli-esm -D

package.json

{"name":"sequelize-cli-esm","version":"1.0.0","main":"index.js","license":"MIT","type":"module","dependencies":{"sequelize":"^6.3.5","sqlite3":"^5.0.0"},"devDependencies":{"sequelize-cli-esm":"^5.0.6"}}

Sequelize CLI による初期設定

yarn sequelize-esm init

config、migrations、models、seedersの各フォルダが作成される。

/config/config.jsonを修正する

dialectを"sqlite"、storageにデータベースの保存ファイルを指定する。

{"development":{"database":"database_development","dialect":"sqlite","storage":"database.sqlite3"}}

モデルの作成

yarn sequelize-esm model:generate --name User --attributes name:string,email:string,password:string
  • modelsフォルダに「users.js」ファイルが作成される(モデル定義)
  • micrationsフォルダに「yyyymmddhhmmss-create-users.js」というファイルが作成される(マイグレーション用テーブル定義)

マイグレーションを行いテーブルを作成する

yarn sequelize-esm db:migrate

マイグレーションを取り消す場合

ひとつ前に戻す

yarn sequelize-esm db:migrate:undo

マイグレーションを全て取り消す

yarn sequelize-esm db:migrate:undo:all

データを追加する(seed)

yarn sequelize-esm seed:generate --name test-users

下記ファイルが作成される。(ESモジュールで作成されている)

  • 修正前
exportdefault{asyncup(queryInterface,Sequelize){/**
     * Add seed commands here.
     *
     * Example:
     * await queryInterface.bulkInsert('People', [{
     *   name: 'John Doe',
     *   isBetaMember: false
     * }], {});
    */asyncdown(queryInterface,Sequelize){/**
     * Add commands to revert seed here.
     *
     * Example:
     * await queryInterface.bulkDelete('People', null, {});
     */}};
  • 修正後
exportdefault{asyncup(queryInterface,Sequelize){constnow=newDate();returnawaitqueryInterface.bulkInsert("Users",[{name:"name1",username:"username1",email:"email1",password:"password1",createdAt:now,updatedAt:now},{name:"name2",username:"username2",email:"email2",password:"password2",createdAt:now,updatedAt:now},{name:"name3",username:"username3",email:"email3",password:"password3",createdAt:now,updatedAt:now},{name:"name4",username:"username4",email:"email4",password:"password4",createdAt:now,updatedAt:now},],{});},asyncdown(queryInterface,Sequelize){returnawaitqueryInterface.bulkDelete("Users",null,{});}};

データ登録用のコマンドを実行する

yarn sequelize-esm db:seed:all
  • データが登録されていることを確認する
idnameusernamemailpasswordcreatedAtupdatedAt
1name1username1email1password12020-12-30 14:36:38.895 +00:002020-12-30 14:36:38.895 +00:00
2name2username2email2password22020-12-30 14:36:38.895 +00:002020-12-30 14:36:38.895 +00:00
3name3username3email3password32020-12-30 14:36:38.895 +00:002020-12-30 14:36:38.895 +00:00
4name4username4email4password42020-12-30 14:36:38.895 +00:002020-12-30 14:36:38.895 +00:00

登録したデータを削除する場合

yarn sequelize-esm db:seed:undo:all

参考ページ

https://mseeeen.msen.jp/sequelize-for-es6/

create-react-appで躓いたこと

$
0
0

create-react-appで Reactアプリを作成したときに、うまくアプリの起動ができなかったため、その経緯と対応を記載します。

1. create-react-app の実行手順

初めに、npmnodeの version確認を行います。

cmd
C:\Users\ユーザー名>npm-v6.12.1C:\Users\ユーザー名>node-vv13.1.0

npmnodeがインストールされていることを確認したので、
create-react-appを実行します。

cmd
C:\Users\ユーザー名>npxcreate-react-appmy-app途中の表示は省略Wesuggestthatyoubeginbytyping:

  cdmy-appnpmstartHappyhacking!

create-react-appにて、Reactアプリが作成できました。

2. エラーの検出と対応

create-react-appもうまくいったみたいなので、意気揚々と実行してみる。

cmd
C:\Users\ユーザー名>cdmy-appC:\Users\ユーザー名\my-app>npmstart

むむむ、こんなエラーが。

cmd
>atelier@0.1.0startC:\Users\ユーザー名\my-app>react-scriptsstartinternal/modules/cjs/loader.js:548throwe;^Error:Packageexportsfor'C:\Users\ユーザー名\my-app\node_modules\postcss-safe-parser\node_modules\postcss'donotdefineavalid'.'targetatresolveExportsTarget(internal/modules/cjs/loader.js:545:13)atapplyExports(internal/modules/cjs/loader.js:459:14)atresolveExports(internal/modules/cjs/loader.js:508:12)atFunction.Module._findPath(internal/modules/cjs/loader.js:577:20)atFunction.Module._resolveFilename(internal/modules/cjs/loader.js:879:27)atFunction.Module._load(internal/modules/cjs/loader.js:785:27)atModule.require(internal/modules/cjs/loader.js:956:19)atrequire(internal/modules/cjs/helpers.js:74:18)atObject.<anonymous>(C:\Users\ユーザー名\my-app\node_modules\postcss-safe-parser\lib\safe-parse.js:1:17)atModule._compile(internal/modules/cjs/loader.js:1063:30){code: 'MODULE_NOT_FOUND'}npmERR!codeELIFECYCLEnpmERR!errno1npmERR!my-app@0.1.0start: `react-scripts start`npmERR!Exitstatus1npmERR!npmERR!Failedatthemy-app@0.1.0startscript.npmERR!Thisisprobablynotaproblemwithnpm.Thereislikelyadditionalloggingoutputabove.npmERR!Acompletelogofthisruncanbefoundin:
npmERR!C:\Users\ユーザー名\AppData\Roaming\npm-cache\_logs\2020-11-29T14_05_22_479Z-debug.log

Google先生にエラーを尋ねてみると、以下の記事にヒントが。
https://github.com/vercel/next.js/issues/18828

どうやら nodeの versionが合っていないことが原因みたいでした。

windowsでの nodeの update方法は、https://nodejs.org/ja/からインストーラーをダウンロードしてきて、それをインストールすればOKです。
インストール後、nodeの versionを確認します。

cmd
C:\Users\ユーザー名>node-vv14.15.3

nodeの versionが更新されていることを確認したので、再度 npm startを実行します。

cmd
C:\Users\ユーザー名>cdmy-appC:\Users\ユーザー名\my-app>npmstart>my-app@0.1.0startC:\Users\ユーザー名\my-app>nodescripts/start.jsiwds:Projectisrunningathttp://xxx.xxx.x.xx/iwds:webpackoutputisservedfromiwds:ContentnotfromwebpackisservedfromC:\Users\ユーザー名\my-app\publiciwds:404swillfallbackto/Startingthedevelopmentserver...Compiledsuccessfully!Youcannowviewmy-appinthebrowser.Local:http://localhost:3000OnYourNetwork:http://xxx.xxx.x.xx:3000Notethatthedevelopmentbuildisnotoptimized.Tocreateaproductionbuild,usenpmrunbuild.

起動に成功しました!
create-react-app画像.png

3. まとめ

  • create-react-appにとって、nodeの versionが鬼門
  • windows版だと、nodeのアップデートは、公式サイトからインストーラーをダウンロードしないといけない

【冬休みオトナの自由研究】「2700円でスマートハウス化してみた」の巻

$
0
0

きっかけ

年末年始だ〜。まとまった時間がとれる〜なんて思っていたら、ムクムクと制作意欲が湧いてきました。
そんな折に、家の洗面台の電球が切れ、Amazonで買おうなんて検索していたら見つけたのがこれ!!


【Amazon Alexa認定 LED電球】OHLUXスマート電球

え、スマートLED 2個で2700円!?
アレクサとかにも繋がるし、ちょっと調べてみたらIFTTTとか、Tuya API(?)など利用して自動化もできそう。
「これは安いぞ」と思い大人の自由研究ということで、スマートホーム化で遊んでみることにしました。

なにを作るか

最初はアレクサのアプリを作って音で操作とかも考えましたが、正直ハードウェアスイッチの利便性には及ばないし、子供に遊ばれるのが目に見えてる。。。。

なんて思っていたら

朝起きて、洗面台で顔洗ってる時に、色で、今日の天気がしれたら最高じゃん」

ということに気付きました。
なので今回はそれを作ります。

どういう構成にするか

LEDの制御方法は主に3つあります。

1.LED操作スマホアプリ「SmartLife」での操作

SmartLifeアプリを用いることで、スマホから色とか変えられます。
また簡単な自動化レシピもつくることができます。
ただ、なぜかトリガーに設定できるのが「現在の天気」だけだったり、
色も選べなかったりと痒いところに手が届きません。

SmartLife

2. IFTTT連携での操作

購入時は、このIFTTT連携を採用しようと考えていました。
LEDはSmartLifeというアプリと連動でき、SmartLifeはIFTTTと連動できるためです。
しかし、いざレシピを作ろうとしてみるとIFTTTでのONトリガーが謎のエラーにより一向に作れませんでした。

Screen Shot 2020-12-29 at 23.11.28.png

ググっても、いまいちhitせず、うまく連携できていない様でした。
また、IFTTTは有料化して無料はかなりレスポンス悪くなってきてるとのこと。残念です。

3. Tuya APIでの操作

現在、中華製のスマートホームデバイスは多くがTuya APIのプロトコル採用(OEM?)しているらしく
自身の購入したものもそうでした。そのためこのTuya APIで操作することにしました。

また、Tuya APIで操作するためには、LEDと同一のwifiネットワーク上で操作元のプログラムが動作している必要があります。
そのため今回のLED操作プログラムを動かす家内サーバを用意する必要がありました。
調べたところ Node.js にて Tuya APIは動かせる様です。

ずっとノートPCをONにしとくわけには行かないと思っていたところ、
押入れの中に「RaspberryPi」が眠っていることに気付きました!!!これを使わない手はありません。

最終的に構成は、下記の様になりました。
IMG_6645.jpg

動作させるまで + 実際のコード

大きく3つのパートに別れます。

1. Tuya API動作させるまで

ここが今回一番特徴的なところでした。
とちゅう変なバグ(後述)を踏んでしまったのですが、それ以外は先人の方をならえばいけると思います。

めちゃ丁寧に書かれてる本家setup
https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md

参考記事
https://qiita.com/plageoj/items/dd2f1c8b55c39625d550

TuyAPI: これを使ってNode.jsからTuyaAPIを叩けます
https://github.com/codetheweb/tuyapi

大きな流れとしては
1. Tuya Iotのサイトでディベロッパー登録し、プロジェクトを作ってプロジェクトのAPI KEYとAPI SERCRETを取得
2. tuya-cli linkコマンドをつかって、LEDとリンクし、LED操作用のAPI IDとKEYを取得する

という流れです。

(自身が踏んでしまった変なバグ)
自分が踏んだのは、1でディベロッパー登録した後になぜか、認証を求められプロジェクトが作れないというバグ。
サポートに問い合わせても判然とせず、、、ググったら、同様の現状になっている人がいました。

Screen Shot 2020-12-30 at 0.13.31.png

アカウント再発行で行けたよって書いてあったので別のメアドでアカウント作り直したら行けました。謎です。w

2. 実際のプログラム本体を書く

ここが本体なのですが、実は一番さくっとできた部分かもしれません。w
動けばOKという思想で作られておりますので、コードはツッコミだらけかもしれませんがご了解ください。
本家のサンプルコードをいじって作りました
https://github.com/codetheweb/tuyapi

Yahoo RSSから天気を取得し、それにより表示される色を変えています。
また、雪(緑) => 雨(青) => 曇り(グレー) => 晴(オレンジ)
の順番で重要なステータスと判断し、それぞれの文字が含まれるかで判定してます。
ここら辺は改善の余地ありそうです。

// main.jsrequire('date-utils');constfs=require('fs');constclient=require('cheerio-httpcli');constTuyAPI=require('tuyapi');// 1時間以内に、LED色変更を行っている場合は何もしないで終了するtry{letlastOutputDate=fs.readFileSync('lastoutputdate.txt');if((newDate().getTime()-newDate(lastOutputDate).getTime())<60*60*1000){console.log("Exit because the last update is too soon. lastOutputDate:"+lastOutputDate);return;}}catch{console.log('lastoutputdate.txt not found!');}// 0:00 - 17:00以前なら今日の天気, 17:00 - 24:00なら明日の天気を取得するvartargetDay=newDate(newDate().getTime()+7*60*60*1000).toFormat("DD");vartargetDayReg=newRegExp(targetDay+"")constdevice=newTuyAPI({id:'XXXXXXXXXXXXXXXXXX',key:'YYYYYYYYYYYYYYYYYY'});letstateHasChanged=false;// Find device on networkdevice.find().then(()=>{// Connect to devicedevice.connect();});// Add event listenersdevice.on('connected',()=>{console.log('Connected to device!');});device.on('disconnected',()=>{console.log('Disconnected from device.');});device.on('error',error=>{console.log('Error!',error);});device.on('data',data=>{console.log('Data from device:',data);//DPS MEMO://[dps 1]: switch ON/OFF(true/false)//[dps 2]: mode("white"/"colour")//[dps 5]: color code「RGB00HSV」形式 (ex. red=ff00000000ffff)if(!stateHasChanged){// Yahoo RSSから天気取得letRSS="https://rss-weather.yahoo.co.jp/rss/days/3610.xml";//福島県のYahoo天気RSSclient.fetch(RSS,{},function(err,$,res){if(err){console.log("error");return;}$("item > title").each(function(idx){vartitle=$(this).text();if(title.match(targetDayReg)){console.log(title);// 雪かどうかdevice.set({dps:2,set:'colour'});if(title.match(/雪/)){console.log("雪です")device.set({dps:5,set:'00ff00004511ff'});}elseif(title.match(/雨/)){console.log("雨です")device.set({dps:5,set:'0000ff004511ff'});}elseif(title.match(/曇/)){console.log("曇りです")device.set({dps:5,set:'808080004511ff'});}elseif(title.match(/晴/)){console.log("晴れです");device.set({dps:5,set:'ff4500004511ff'});}else{console.log("想定外の何か");device.set({dps:5,set:'660099004511ff'});}//LED変更を記録stateHasChanged=true;fs.writeFileSync("lastoutputdate.txt",newDate().toString());}});});}});// Disconnect after 10 secondssetTimeout(()=>{device.disconnect();},10000);

3. ラズパイでプログラムを走らせる

ここも結構苦労しました。ラズパイを触るのはほぼ初めてだったためと、
結構いろんな角度からの情報がネット上にあり、いいとこ取りするのに時間がかかったためです。w
IMG_6642.jpg

やったことと参考にしたページを箇条書きします。

ちなみに上記を参考にcrontabは下記の様に設定しました。
15秒おきにスクリプト走らせています。

# crontabLANG=ja_JP.UTF-8
*****for i in`seq 0 15 59`;do(sleep${i}; /home/pi/.nvm/versions/node/v14.15.3/bin/node /home/pi/dev/weather-led/main.js >> /home/pi/dev/weather-led/log.txt;)& done;

動作の様子

0:00 - 17:00以前なら今日の天気, 17:00 - 24:00なら明日の天気を取得するようにしてあります。
そして写真の様に
雪(緑)
雨(青)
曇り(グレー)
晴(オレンジ)で光ります。
洗面台は普通にハードウェアスイッチでのON/OFFです。
明日は雪予報なので、今日夜は緑に光ってました!
CE863D58-9F36-4152-86F4-5E852CDB6368.JPG

実際に便利なの?

これは正直まだ未知数です。

  • ラズパイ+LEDが安定動作するのか。
  • 家族が受け入れてくれるか
  • 自分が実際便利になるか(天気アプリを開く回数が減るのか)

こちらは一ヶ月ぐらい使ってみてわかってくると思います。
何か特筆事項がでてきたら追記します。

また、

  • ラズパイの死活監視
  • 実運用でのチューニング
  • 現在はLED ONかどうかを頻繁に問い合わせ(ポーリング)形式になっている => LEDがONになった時点でイベント発火する形式にできれば良いのだが、、、

などはFuture Workですね。

まとめ

感想ですが、

  • スマートLEDはすごい安くてびっくりした。Tuya API利用までは少し遠回りしたけど、わかってしまえばシンプルで使いやすいAPI
  • 家に眠っていた「ラズパイ(Model3 B)」が大活躍した。数千円というこの値段で小型PCが買えてしまうというのが凄すぎる。まだまだ潜在能力を秘めていそう!
  • 冬休み自由研究やることで、今まで触ったことのない新しい技術に触れられたのは良かった!!
  • 先人の皆様のお力をお借りしまくり ました。ありがとうございました!!!

是非良かったら皆さまも、冬休みの自由研究してみてください〜!!!

あと、お年玉としてLGTMいただけたら嬉しいです。m(_ _)m 良いお年を〜〜。

WordPressテーマのバージョンをpackage.jsonと同期する

$
0
0

image.png

管理画面に表示されるバージョンを動的に更新したい。

postcss使ってるから、package.jsonと同時に更新してみよう!

postcssとpostcss-replaceの導入

postcssに関しては省略。

postcss-replace

https://github.com/gridonic/postcss-replace

npm install--save-dev postcss-replace

meta.cssを作る

このデータはWordPressしか読まないんですから、ファイルは隔離していいでしょう。

postcss/meta.css
/*
Theme Name: my_theme
Theme URI: https://example.com/
Author: me
Author URI: https://me.me
Description: This is my theme
Version: {{ version }}
License: GNU General Public License v3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
Text Domain: my_theme
*/

{{ version }}が変数になります。

postcssの設定

postcss.config.js
constpack=require('./package.json')constpackVer=pack.versionmodule.exports={plugins:[require('postcss-replace')({data:{'version':packVer}}),],};

data: {'CSS内の変数名': 渡す変数}で変数を指定。

書き出し

cssnanoとかに関してはコメントを抹消してしまうため場合分けしましょう。

postcss.config.js
// 前略module.exports={plugins:[// 中略// 圧縮process.env.NODE_ENV!=="meta"&&require('cssnano')({autoprefixer:false}),],};

こんな感じにして

cross-env NODE_ENV=meta postcss ./postcss/meta.css -o ./style.css

こうすればWordPressテーマのメタ情報だけがstyle.cssに書き出されます。

それ以外の部分はpurgecssとか使うでしょうから、こんな感じで別に分ければ良いでしょう。

pm2のプロファイリングを試してみる(前準備)

$
0
0

あけましておめでとうございます。
この記事を編集していたら年を越していました。

EC2を作成

keyvalue
インスタンスタイプt2.micro
vCPU1

Node.js, pm2をインストール

curl -sL https://rpm.nodesource.com/setup_12.x | bash -
yum install nodejs -y
node -v
> v12.20.0
npm -v
> 6.14.8
npm install pm2 -g
> 4.5.1

pm2の設定

# 再帰的にディレクトリを作成する
mkdir -p /var/www/test
# pm2の設定ファイルを作成
touch pm2.yaml
# pm2の設定ファイルを編集
vi pm2.yaml
pm2.yaml
apps:-script   :./app.jsname     :'tes-app'instances:-1# cpuの数-1のプロセスを起動。(clusterモードに限る)# instances: 0 # cpuの数に応じて最大のプロセスを起動。(clusterモードに限る)exec_mode:cluster# n個のインスタンスを起動し、クラスターモジュールに負荷分散を処理させます。

PM2 - Cluster Mode

コア数、環境もによりますが、instances: -1がよさそうです。(コア数8なら7プロセス起動)(脳死でMaxはNG)
またt2.microは1コアのはずですが、instances: 4にするとプロセスが4つ起動しました。(どういう状態?)

参考: Optimising NginX, Node.JS and networking for heavy workloads - GoSquared Blog

Expressの準備

# expressをインストール(4.17.1: 2020/12/30)
npm install express

Node.js アプリケーションの簡単なプロファイリング | Node.js

app.js
constexpress=require("express");constapp=express();server=app.listen(3000,function(){console.log("Node.js is listening to PORT:"+server.address().port);});app.get('/newUser',(req,res)=>{letusername=req.query.username||'';constpassword=req.query.password||'';username=username.replace(/[!@#$%^&*]/g,'');if(!username||!password||users.username){returnres.sendStatus(400);}constsalt=crypto.randomBytes(128).toString('base64');consthash=crypto.pbkdf2Sync(password,salt,10000,512,'sha512');users[username]={salt,hash};res.sendStatus(200);});

pm2を起動

pm2 start pm2.yaml 
[PM2][WARN] Applications test-app not running, starting...
[PM2] App [test-app] launched (1 instances)
┌─────┬─────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name        │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼─────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ test-app    │ default     │ 1.0.0   │ cluster │ 24606    │ 0s     │ 0    │ online    │ 0%       │ 30.1mb   │ root     │ disabled │
└─────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

設定を変更したい場合は一度レジストリを削除してから再度スタートします。(restartだと変更した設定は反映されない)

pm2 delete test-app
pm2 start pm2.yaml 

今回はここまで。

Viewing all 9063 articles
Browse latest View live