Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信
序章
シリーズの前のパートでは、ユーザーが既存の請求書を作成および表示できるようにする請求書発行アプリケーションのユーザーインターフェイスを作成する方法について説明しました。 シリーズのこの最後のパートでは、クライアントで永続的なユーザーセッションを設定し、請求書の単一のビューを構成します。
前提条件
この記事を適切にフォローするには、次のものが必要です。
インストールを確認するには、次のコマンドを実行します。
node --version npm --version
結果としてバージョン番号を取得した場合は、問題ありません。
ステップ1—JWTokenを使用したクライアントでのユーザーセッションの永続化
アプリケーションが安全であり、許可されたユーザーのみがリクエストを実行できることを確認するために、JWTokenを使用します。 JWTokens 、またはJSON Webトークンは、リクエストのヘッダー、ペイロード、署名を含む3つの部分からなる文字列で構成されます。 その中心的な考え方は、認証されたユーザーごとに、バックエンドサーバーへのリクエストを実行するときに使用するトークンを作成することです。
開始するには、invoicing-app
ディレクトリに移動します。 その後、JSONWebトークンの作成と検証に使用されるjsonwebtoken
ノードモジュールをインストールします。
cd invoicing-app npm install jsonwebtoken nodemon --save
nodemon
は、ファイルの変更が発生するとサーバーを再起動するノードモジュールです。
次に、次を追加してserver.js
ファイルを更新します。
server.js
// import node modules [...] const jwt = require("jsonwebtoken"); // create express app [...] app.set('appSecret', 'secretforinvoicingapp'); // this will be used later
次に行うことは、/register
および/login
ルートを微調整してトークンを作成し、ユーザーが正常に登録またはログインしたときにトークンを返すことです。 これを行うには、server.js
ファイルに次を追加します。
server.js
// edit the /register route app.post("/register", multipartMiddleware, function(req, res) { // check to make sure none of the fields are empty [...] bcrypt.hash(req.body.password, saltRounds, function(err, hash) { // create sql query [...] db.run(sql, function(err) { if (err) { throw err; } else { let user_id = this.lastID; let query = `SELECT * FROM users WHERE id='${user_id}'`; db.all(query, [], (err, rows) => { if (err) { throw err; } let user = rows[0]; delete user.password; // create payload for JWT const payload = { user: user } // create token let token = jwt.sign(payload, app.get("appSecret"), { expiresInMinutes: "24h" // expires in 24 hours }); // send response back to client return res.json({ status: true, token : token }); }); } }); db.close(); }); }); [...]
/login
ルートについても同じようにします。
server.js
app.post("/login", multipartMiddleware, function(req, res) { // connect to db [...] db.all(sql, [], (err, rows) => { // attempt to authenticate the user [...] if (authenticated) { // create payload for JWT const payload = { user: user }; // create token let token = jwt.sign( payload, app.get("appSecret"),{ expiresIn: "24h" // expires in 24 hours }); return res.json({ status: true, token: token }); } return res.json({ status: false, message: "Wrong Password, please retry" }); }); });
これが完了したので、次に行うことはそれをテストすることです。 次のコマンドを使用してサーバーを実行します。
nodemon server.js
これで、アプリはログインと登録が成功するとトークンを作成します。 次のステップは、着信要求のトークンを確認することです。 これを行うには、保護するルートの上に次のミドルウェアを追加します。
server.js
[...] // unprotected routes [...] // Create middleware for protecting routes app.use(function(req, res, next) { // check header or url parameters or post parameters for token let token = req.body.token || req.query.token || req.headers["x-access-token"]; // decode token if (token) { // verifies secret and checks exp jwt.verify(token, app.get("appSecret"), function(err, decoded) { if (err) { return res.json({ success: false, message: "Failed to authenticate token." }); } else { // if everything is good, save to request for use in other routes req.decoded = decoded; next(); } }); } else { // if there is no token // return an error return res.status(403).send({ success: false, message: "No token provided." }); } }); // protected routes [...]
SignUp.vue
ファイルでは、サーバーから取得したトークンとユーザーデータをlocalStorage
に保存して、ユーザーがアプリケーションを使用しているときに異なるページ間で保持できるようにする必要があります。 これを行うには、frontend/src/components/SignUp.vue
ファイルのlogin
およびregister
メソッドを次のように更新します。
フロントエンド/src/components/SignUp.vue
[...] export default { name: "SignUp", [...] methods:{ register(){ const formData = new FormData(); let valid = this.validate(); if(valid){ // prepare formData [...] // Post to server axios.post("http://localhost:3128/register", formData) .then(res => { // Post a status message this.loading = ""; if (res.data.status == true) { // store the user token and user data in localStorage localStorage.setItem('token', res.data.token); localStorage.setItem('user', JSON.stringify(res.data.user)); // now send the user to the next route this.$router.push({ name: "Dashboard", }); } else { this.status = res.data.message; } }); } else{ alert("Passwords do not match"); } } [...]
ログイン方法も更新しましょう。
フロントエンド/src/components/SignUp.vue
login() { const formData = new FormData(); formData.append("email", this.model.email); formData.append("password", this.model.password); this.loading = "Signing in"; // Post to server axios.post("http://localhost:3128/login", formData).then(res => { // Post a status message console.log(res); this.loading = ""; if (res.data.status == true) { // store the data in localStorage localStorage.setItem("token", res.data.token); localStorage.setItem("user", JSON.stringify(res.data.user)); // now send the user to the next route this.$router.push({ name: "Dashboard" }); } else { this.status = res.data.message; } });
以前は、ユーザーデータはルートパラメータを使用して渡されていましたが、アプリはローカルストレージからデータを取得するようになりました。 これによりコンポーネントがどのように変化するかを見てみましょう。
Dashboard
コンポーネントは、以前は次のようになりました。
フロントエンド/src/components/Dashboard.vue
<script> import Header from "./Header"; import CreateInvoice from "./CreateInvoice"; import ViewInvoices from "./ViewInvoices"; export default { name: "Dashboard", components: { Header, CreateInvoice, ViewInvoices, }, data() { return { isactive: 'create', title: "Invoicing App", user : (this.$route.params.user) ? this.$route.params.user : null }; } }; </script>
これは、ユーザーがサインインまたは登録すると、Dashboard
ページにリダイレクトされ、Dashboard
コンポーネントのuser
プロパティがそれに応じて更新されることを意味します。 ユーザーがページを更新することを決定した場合、this.$route.params.user
が存在しなくなるため、ユーザーを識別する方法はありません。
Dashboard
コンポーネントを編集して、ブラウザのlocalStorage
を使用するようにします。
フロントエンド/src/components/Dashboard.vue
import Header from "./Header"; import CreateInvoice from "./CreateInvoice"; import ViewInvoices from "./ViewInvoices"; export default { name: "Dashboard", components: { Header, CreateInvoice, ViewInvoices, }, data() { return { isactive: 'create', title: "Invoicing App", user : null, }; }, mounted(){ this.user = JSON.parse(localStorage.getItem("user")); } };
これで、ページを更新した後もユーザーデータが保持されます。 リクエストが行われるときは、トークンをリクエストに追加する必要もあります。
ViewInvoices
コンポーネントを見てください。 コンポーネントのJavaScriptは次のようになります。
フロントエンド/src/components/ViewInvoices.vue
<script> import axios from "axios"; export default { name: "ViewInvoices", components: {}, data() { return { invoices: [], \ user: '', }; }, mounted() { this.user = JSON.parse(localStorage.getItem('user')); axios .get(`http://localhost:3128/invoice/user/${this.user.id}`) .then(res => { if (res.data.status == true) { console.log(res.data.invoices); this.invoices = res.data.invoices; } }); } }; </script>
現在ログインしているユーザーの請求書を表示しようとすると、トークンがないために請求書を取得するときにエラーが発生します。
これは、アプリケーションのinvoice/user/:user_id
ルートが、前に設定したトークンミドルウェアで保護されるようになったためです。 このエラーを修正するには、リクエストに追加してください。
フロントエンド/src/components/ViewInvoices.vue
<script> import axios from "axios"; export default { name: "ViewInvoices", components: {}, data() { return { invoices: [], user: '', }; }, mounted() { this.user = JSON.parse(localStorage.getItem('user')); axios .get(`http://localhost:3128/invoice/user/${this.user.id}`, { headers: {"x-access-token": localStorage.getItem("token")} } ) .then(res => { if (res.data.status == true) { console.log(res.data.invoices); this.invoices = res.data.invoices; } }); } }; </script>
これを保存してブラウザに戻ると、請求書を正常に取得できるようになりました。
ステップ2—請求書の単一ビューを作成する
TO INVOICE ボタンをクリックしても、何も起こりません。 これを修正するには、SingleInvoice.vue
ファイルを作成し、次のように編集します。
<template> <div class="single-page"> <Header v-bind:user="user"/> <!-- display invoice data --> <div class="invoice"> <!-- display invoice name here --> <div class="container"> <div class="row"> <div class="col-md-12"> <h3>Invoice #{{ invoice.id }} by {{ user.company_name }}</h3> <table class="table"> <thead> <tr> <th scope="col">#</th> <th scope="col">Transaction Name</th> <th scope="col">Price ($)</th> </tr> </thead> <tbody> <template v-for="txn in transactions"> <tr :key="txn.id"> <th>{{ txn.id }}</th> <td>{{ txn.name }}</td> <td>{{ txn.price }} </td> </tr> </template> </tbody> <tfoot> <td></td> <td style="text-align: right">Total :</td> <td><strong>$ {{ total_price }}</strong></td> </tfoot> </table> </div> </div> </div> </div> </div> </template>
v-for
ディレクティブは、特定の請求書に対してフェッチされたすべてのトランザクションをループできるようにするために使用されます。
コンポーネントの構造を以下に示します。 まず、必要なモジュールとコンポーネントをインポートします。 コンポーネントがmounted
の場合、axios
を使用したPOST
要求がバックエンドサーバーに対して行われ、データがフェッチされます。 応答が得られたら、それらをそれぞれのコンポーネントプロパティに割り当てます。
<script> import Header from "./Header"; import axios from "axios"; export default { name: "SingleInvoice", components: { Header }, data() { return { invoice: {}, transactions: [], user: "", total_price: 0 }; }, methods: { send() {} }, mounted() { // make request to fetch invoice data this.user = JSON.parse(localStorage.getItem("user")); let token = localStorage.getItem("token"); let invoice_id = this.$route.params.invoice_id; axios .get(`http://localhost:3128/invoice/user/${this.user.id}/${invoice_id}`, { headers: { "x-access-token": token } }) .then(res => { if (res.data.status == true) { this.transactions = res.data.transactions; this.invoice = res.data.invoice; let total = 0; this.transactions.forEach(element => { total += parseInt(element.price); }); this.total_price = total; } }); } }; </script>
注:現在空のsend()
メソッドがあります。 記事を読み進めると、必要な機能を追加する理由と方法について理解を深めることができます。
コンポーネントには、次のスコープスタイルがあります。
フロントエンド/src/components/SingleInvoice.vue
<!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #426cb9; } .single-page { background-color: #ffffffe5; } .invoice{ margin-top: 20px; } </style>
ここで、アプリケーションに戻ってView Invoices
タブのTO INVOICE ボタンをクリックすると、単一の請求書ビューが表示されます。
ステップ3—電子メールで請求書を送信する
これは、請求アプリケーションの最後のステップであり、ユーザーが請求書を送信できるようにすることです。 このステップでは、nodemailer
モジュールを使用して、バックエンドサーバー上の指定された受信者に電子メールを送信します。 開始するには、最初にモジュールをインストールします。
npm install nodemailer
モジュールがインストールされたので、server.js
ファイルを次のように更新します。
server.js
// import node modules [...] let nodemailer = require('nodemailer') // create mail transporter let transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: '[email protected]', pass: 'userpass' } }); // create express app [...]
このメールはバックエンドサーバーに設定され、ユーザーに代わってメールを送信するアカウントになります。 また、セキュリティ設定でテスト目的のために、Gmailアカウントの安全でないログインを一時的に許可する必要があります。
server.js
// configure app routes [...] app.post("/sendmail", multipartMiddleware, function(req, res) { // get name and email of sender let sender = JSON.parse(req.body.user); let recipient = JSON.parse(req.body.recipient); let mailOptions = { from: "[email protected]", to: recipient.email, subject: `Hi, ${recipient.name}. Here's an Invoice from ${ sender.company_name }`, text: `You owe ${sender.company_name}` }; transporter.sendMail(mailOptions, function(error, info) { if (error) { return res.json({ status: 200, message: `Error sending main to ${recipient.name}` }); } else { return res.json({ status: 200, message: `Email sent to ${recipient.name}` }); } }); });
この時点で、POST
リクエストが/sendmail
ルートに対して行われたときに機能するように電子メールを構成しました。 また、ユーザーがフロントエンドでこのアクションを実行できるようにし、受信者の電子メールアドレスを入力するためのフォームをユーザーに提供する必要があります。 これを行うには、次のようにしてSingleInvoice
コンポーネントを更新します。
フロントエンド/src/components/SingleInvoice.vue
<template> <Header v-bind:user="user"/> <!-- display invoice data --> <div class="invoice"> <!-- display invoice name here --> <div class="container"> <div class="row"> <div class="col-md-12"> // display invoice </div> </div> <div class="row"> <form @submit.prevent="send" class="col-md-12"> <h3>Enter Recipient's Name and Email to Send Invoice</h3> <div class="form-group"> <label for="">Recipient Name</label> <input type="text" required class="form-control" placeholder="eg Chris" v-model="recipient.name"> </div> <div class="form-group"> <label for="">Recipient Email</label> <input type="email" required placeholder="eg [email protected]" class="form-control" v-model="recipient.email"> </div> <div class="form-group"> <button class="btn btn-primary" >Send Invoice</button> {{ loading }} {{ status }} </div> </form> </div> </div> </div> </template>
また、コンポーネントのプロパティは次のように更新されます。
フロントエンド/src/components/SingleInvoice.vue
<script> import Header from "./Header"; import axios from "axios"; export default { name: "SingleInvoice", components: { Header }, data() { return { invoice: {}, transactions: [], user: '', total_price: 0, recipient : { name: '', email: '' }, loading : '', status: '', }; }, methods: { send() { this.status = ""; this.loading = "Sending Invoice, please wait...."; const formData = new FormData(); formData.append("user", JSON.stringify(this.user)); formData.append("recipient", JSON.stringify(this.recipient)); axios.post("http://localhost:3128/sendmail", formData, { headers: {"x-access-token": localStorage.getItem("token")} }).then(res => { this.loading = ''; this.status = res.data.message }); } }, mounted() { // make request to fetch invoice data } }; </script>
これらの変更を行うと、ユーザーは受信者の電子メールを入力して、アプリから「送信済み請求書」通知を受け取ることができるようになります。
ノードメーラーガイドを確認すると、メールをさらに編集できます。
結論
シリーズのこのパートでは、JWTokenとブラウザーのローカルストレージを使用してユーザーのサインインを維持する方法について説明しました。 また、単一の請求書のビューも作成しました。