開発者ドキュメント

Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信

序章

シリーズの前のパートでは、ユーザーが既存の請求書を作成および表示できるようにする請求書発行アプリケーションのユーザーインターフェイスを作成する方法について説明しました。 シリーズのこの最後のパートでは、クライアントで永続的なユーザーセッションを設定し、請求書の単一のビューを構成します。

前提条件

この記事を適切にフォローするには、次のものが必要です。

インストールを確認するには、次のコマンドを実行します。

  1. node --version
  2. npm --version

結果としてバージョン番号を取得した場合は、問題ありません。

ステップ1—JWTokenを使用したクライアントでのユーザーセッションの永続化

アプリケーションが安全であり、許可されたユーザーのみがリクエストを実行できることを確認するために、JWTokenを使用します。 JWTokens 、またはJSON Webトークンは、リクエストのヘッダー、ペイロード、署名を含む3つの部分からなる文字列で構成されます。 その中心的な考え方は、認証されたユーザーごとに、バックエンドサーバーへのリクエストを実行するときに使用するトークンを作成することです。

開始するには、に変更します invoicing-app ディレクトリ。 その後、 jsonwebtoken JSONWebトークンの作成と検証に使用されるノードモジュール:

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"
        });
      });
    });

これが完了したので、次に行うことはそれをテストすることです。 次のコマンドを使用してサーバーを実行します。

  1. 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 これにより、ユーザーがアプリケーションを使用しているときに、さまざまなページにまたがって存続できるようになります。 これを行うには、 loginregister あなたの方法 frontend/src/components/SignUp.vue このように見えるファイル:

フロントエンド/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;
            }
          });

以前は、ユーザーデータはルートパラメータを使用して渡されていましたが、アプリはローカルストレージからデータを取得するようになりました。 これによりコンポーネントがどのように変化するかを見てみましょう。

The 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 ページ、そして user のプロパティ Dashboard コンポーネントはそれに応じて更新されました。 ユーザーがページを更新することを決定した場合、それ以降、ユーザーを特定する方法はありません。 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>

The v-for ディレクティブは、特定の請求書に対してフェッチされたすべてのトランザクションをループできるようにするために使用されます。

コンポーネントの構造を以下に示します。 まず、必要なモジュールとコンポーネントをインポートします。 コンポーネントが mountedPOST を使用してリクエスト 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() 現在空のメソッド。 記事を読み進めると、必要な機能を追加する理由と方法について理解を深めることができます。

コンポーネントには、次のスコープスタイルがあります。

フロントエンド/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>

ここで、アプリケーションに戻って、 TO INVOICEボタンをクリックすると、 View Invoices タブには、単一の請求書ビューが表示されます。

ステップ3—電子メールで請求書を送信する

これは、請求アプリケーションの最後のステップであり、ユーザーが請求書を送信できるようにすることです。 このステップでは、 nodemailer バックエンドサーバー上の指定された受信者に電子メールを送信するモジュール。 開始するには、最初にモジュールをインストールします。

  1. 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: 'COMPANYEMAIL@gmail.com',
        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: "COMPANYEMAIL@gmail.com",
        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 chris@invoiceapp.com" 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とブラウザーのローカルストレージを使用してユーザーのサインインを維持する方法について説明しました。 また、単一の請求書のビューも作成しました。

モバイルバージョンを終了