Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信
序章
シリーズの前のパートでは、ユーザーが既存の請求書を作成および表示できるようにする請求書発行アプリケーションのユーザーインターフェイスを作成する方法について説明しました。 シリーズのこの最後のパートでは、クライアントで永続的なユーザーセッションを設定し、請求書の単一のビューを構成します。
前提条件
この記事を適切にフォローするには、次のものが必要です。
インストールを確認するには、次のコマンドを実行します。
- node --version
- npm --version
結果としてバージョン番号を取得した場合は、問題ありません。
ステップ1—JWTokenを使用したクライアントでのユーザーセッションの永続化
アプリケーションが安全であり、許可されたユーザーのみがリクエストを実行できることを確認するために、JWTokenを使用します。 JWTokens 、またはJSON Webトークンは、リクエストのヘッダー、ペイロード、署名を含む3つの部分からなる文字列で構成されます。 その中心的な考え方は、認証されたユーザーごとに、バックエンドサーバーへのリクエストを実行するときに使用するトークンを作成することです。
開始するには、に変更します invoicing-app
ディレクトリ。 その後、 jsonwebtoken
JSONWebトークンの作成と検証に使用されるノードモジュール:
cd invoicing-app
npm install jsonwebtoken nodemon --save
nodemon
ファイルの変更が発生するとサーバーを再起動するノードモジュールです。
今、更新します 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
ファイル:
// 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
ルート:
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
これで、アプリはログインと登録が成功するとトークンを作成します。 次のステップは、着信要求のトークンを確認することです。 これを行うには、保護するルートの上に次のミドルウェアを追加します。
[...]
// 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
これにより、ユーザーがアプリケーションを使用しているときに、さまざまなページにまたがって存続できるようになります。 これを行うには、 login
と register
あなたの方法 frontend/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");
}
}
[...]
ログイン方法も更新しましょう。
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;
}
});
以前は、ユーザーデータはルートパラメータを使用して渡されていましたが、アプリはローカルストレージからデータを取得するようになりました。 これによりコンポーネントがどのように変化するかを見てみましょう。
The Dashboard
以前のコンポーネントは次のようになりました。
<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
ページ、そして user
のプロパティ Dashboard
コンポーネントはそれに応じて更新されました。 ユーザーがページを更新することを決定した場合、それ以降、ユーザーを特定する方法はありません。 this.$route.params.user
もはや存在しない。
あなたの編集 Dashboard
ブラウザのを使用するためのコンポーネント localStorage
:
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は次のようになります。
<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
これで、アプリケーションのルートは、前に設定したトークンミドルウェアで保護されます。 このエラーを修正するには、リクエストに追加してください。
<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>
The v-for
ディレクティブは、特定の請求書に対してフェッチされたすべてのトランザクションをループできるようにするために使用されます。
コンポーネントの構造を以下に示します。 まず、必要なモジュールとコンポーネントをインポートします。 コンポーネントが mounted
、 POST
を使用してリクエスト axios
データをフェッチするためにバックエンドサーバーに対して行われます。 応答が得られたら、それらをそれぞれのコンポーネントプロパティに割り当てます。
<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()
現在空のメソッド。 記事を読み進めると、必要な機能を追加する理由と方法について理解を深めることができます。
コンポーネントには、次のスコープスタイルがあります。
<!-- 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>
ここで、アプリケーションに戻って、 TO INVOICEボタンをクリックすると、 View Invoices
タブには、単一の請求書ビューが表示されます。
ステップ3—電子メールで請求書を送信する
これは、請求アプリケーションの最後のステップであり、ユーザーが請求書を送信できるようにすることです。 このステップでは、 nodemailer
バックエンドサーバー上の指定された受信者に電子メールを送信するモジュール。 開始するには、最初にモジュールをインストールします。
- npm install nodemailer
モジュールがインストールされたので、 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アカウントの安全でないログインを一時的に許可する必要があります。
// 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
次のようにしてコンポーネントを作成します。
<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>
また、コンポーネントのプロパティは次のように更新されます。
<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とブラウザーのローカルストレージを使用してユーザーのサインインを維持する方法について説明しました。 また、単一の請求書のビューも作成しました。