KurocoとNuxt.jsで、コンテンツにコメント機能を追加する

Kurocoを利用したNuxt.jsプロジェクトで、アクティビティ:コメント機能の実装方法を紹介します。
今回は例として、下記流れにて処理を実装します。

  1. API・エンドポイントの作成
  2. ニュース詳細画面にコメント機能を追加

事前準備

Nuxt.jsプロジェクトの作成について

このページは、KurocoとNuxt.jsでのプロジェクトが構築済み、 かつ既に何らかの記事が閲覧できること、またprofileエンドポイントが有効であることを前提としています。
まだNuxt.jsプロジェクトを構築していない場合、チュートリアル ->KurocoとNuxt.jsで、コンテンツ一覧ページを作成するを参照してください。
またprofileなどはチュートリアル ->KurocoとNuxt.jsで、ログイン画面を構築するを参考にしてください。 今回はcookieによるログイン制御を前提とします。

作成済みの「お知らせ詳細」のエンドポイントをコピー、未ログインメンバーによるコメントの許可設定をした後、お知らせ詳細とそれに紐づくコメントのフォームを作成します。

APIの作成

未承認ユーザーからの操作を許可するために、新しくAPIを作成します。
Kuroco管理画面のAPIより「追加」をクリックします。
Image from Gyazo

API作成画面が表示されます。
Image from Gyazo

下記入力し「追加する」をクリックします。
Image from Gyazo

項目設定内容
タイトルComment Test
1.0
ディスクリプションコメント機能の確認用API

CORSの設定

次にCORSの設定をします。[CORSを設定する] をクリックします。
Image from Gyazo

CORS_ALLOW_ORIGINSの [Add Origin] をクリックし、下記を追加します。

CORS_ALLOW_METHODSの [Add Method] をクリックし、下記を追加します。

  • GET
  • POST
  • OPTIONS

設定できたら[保存する]をクリックし、CORSの設定が完了です。
Image from Gyazo

ログイン不要でコメント機能の動作を確認する

アクティビティ定義の作成

未承認ユーザーからのコメント操作を許可するために、アクティビティ定義を編集します。
[アクティビティ定義]をクリックします。
Image from Gyazo

[追加する]をクリックします。
Image from Gyazo

APIリクエスト制限、投稿制限を閲覧可即公開として[追加する]をクリックします。
Image from Gyazo

後ほど利用するので、作成したアクティビティIDをメモしておきます。
Image from Gyazo

エンドポイントの作成

エンドポイントを作成します。今回は下記エンドポイントを作成します。

  • newsdetailエンドポイント -> ニュース詳細
  • commentsエンドポイント -> コメントの取得用
  • commentエンドポイント -> コメントの追加用
  • comment_deleteエンドポイント -> コメントの削除用

Comment TestのAPIで「新しいエンドポイントの追加」をクリックし、それぞれ作成します。
Image from Gyazo

newsdetailエンドポイントの作成

newsdetailエンドポイントを下記設定にて作成します。

Image from Gyazo Image from Gyazo

項目設定内容
パスnewsdetail
カテゴリーコンテンツ
モデルTopics v1
オペレーションdetails
認証None
topics_group_id表示するコンテンツ定義ID(9)

設定完了後、「追加」をクリックしnewsdetailエンドポイント完成です。

commentsエンドポイントの作成

commentsエンドポイントを下記設定にて作成します。
Image from Gyazo Image from Gyazo

項目設定内容
パスcomments
カテゴリーアクティビティ
モデルComment v1
オペレーションlist
認証None
idアクティビティID(37)
module_typetopics
new_order_flgチェックを入れる

設定完了後、「追加」をクリックしcommentsエンドポイント完成です。

commentエンドポイントの作成

commentエンドポイントを下記設定にて作成します。
Image from Gyazo

項目設定内容
パスcomment
カテゴリーアクティビティ
モデルComment v1
オペレーションinsert
認証None
idアクティビティID(37)

設定完了後、「追加」をクリックしcommentエンドポイント完成です。

comment_deleteエンドポイントの作成

comment_deleteエンドポイントを下記設定にて作成します。
Image from Gyazo

項目設定内容
パスcomment_delete
カテゴリーアクティビティ
モデルComment v1
オペレーションdelete
認証None
idアクティビティID(37)

設定完了後、「追加」をクリックしcomment_deleteエンドポイント完成です。

今回の例では、コメントの追加/削除するエンドポイントはセキュリティNoneにしていますが、実際のサイトではコメントの追加/削除を許可するグループを設定してください。

フロントエンドの設定

コメント機能付きニュース詳細ページの追加

今回は既存のニュース詳細画面を参考に画面を作成し、同ページにコメント機能を追加します。
まずは未承認のユーザーでもコメントの閲覧/投稿がきるようにし動作の確認をします。

画面を表示する際にニュースへ紐づく全てのコメントを取得し、表示します。

ユーザー名入力欄とコメント投稿のフォームが存在し、ユーザー名入力のあと、追加したコメントは即時に画面へ反映されます。
またコメントそれぞれには削除ボタンが存在します。

コメントの削除はコメントの投稿・削除にパスワード(delkey)を使用するように構築するか、ログインを前提として、コメントを投稿した本人でないとできません。
この段階では削除ボタンをクリックした場合、Code 422 "パスワードが一致しないか、書き込んだ本人でないため削除できません。" のエラーが発生します。

pages/news/test_with_comment.vueを追加します。

<template>
  <div>
    <h1 class="title">{{ response.details.subject }}</h1>
    <div class="post" v-html="response.details.contents"></div>
    <p v-if="resultMessage !== null">
      {{ resultMessage }}
    </p>
    <div>
        please type your name: <input v-model="userName" type="text" placeholder="your name">
    </div>
    <div>
        <ul v-for="comment in comments" :key="comment.comment_id">
            <li>
                {{ comment.note }} by {{ comment.name }}
                <button type="button" @click="() => deleteComment(comment.comment_id)">
                    delete
                </button>
            </li>
        </ul>
        <form @submit.prevent="submitComment">
            <input v-model="inputComment" type="text" placeholder="comment">
            <button type="submit" :disabled="inputComment === '' || userName === ''">
                submit
            </button>
        </form>
    </div>
  </div>
</template>

<script>
async function getAllComments (topics_id) {
    const { list } = await this.$axios.$get(
        process.env.BASE_URL + '/rcms-api/21/comments',
        {
            params: {
                module_id: topics_id,
                cnt: 999
            }
        }
    )
    return list
}

export default {
  async asyncData({ $axios, params }) {
    try {
      const response = await $axios.$get(
        process.env.BASE_URL + '/rcms-api/21/newsdetail/1047'
      )
      return { response, comments: await getAllComments.call({ $axios }, response.details.topics_id) }
    } catch (e) {
      console.log(e.message)
    }
  },
  data () {
      return {
          userName: '',
          response: null,
          comments: [],
          inputComment: '',
          resultMessage: null,
      }
  },
  methods: {
    async submitComment () {
        await this.$axios.$post('/rcms-api/21/comment', {
            module_id: this.response.details.topics_id,
            name: this.userName,
            note: this.inputComment
            })
            this.comments = await getAllComments.call(this, this.response.details.topics_id)
            this.inputComment = ''
    },
    async deleteComment (commentId) {
      try{
        await this.$axios.$post(`/rcms-api/21/comment_delete/${commentId}`, {})
        this.comments = await getAllComments.call(this, this.response.details.topics_id)
        this.inputComment = ''
      } catch (error) {
         this.resultMessage = error.response.data.errors[0].message
      }
    }
  }
}
</script>

以下のようにコメントの追加ができることを確認します。
Image from Gyazo

コメントをログイン必須にする

コメント機能のような変更/削除のためのPOSTエンドポイントを無施策に開放してしまうと、DOM攻撃(1万件一気にPOST->DBがパンクして落ちる)に弱くなります。
Kurocoのエンドポイントでは、同じIPアドレスから短時間に何回もコメントをされた場合に、投稿を受け付けないエラーを返す機能を持っていますが、本チュートリアルではページの閲覧、コメントの表示・投稿・削除にログインを必須とさせるように実装します。

アクティビティ定義の更新

[アクティビティ定義]をクリックします。
Image from Gyazo

作成したアクティビティのタイトルをクリックします。
Image from Gyazo

未ログインメンバーのAPIリクエスト制限、投稿制限を閲覧不可受け付けないとして[更新する]をクリックします。
Image from Gyazo

フロントエンドの更新

pages/news/test_with_comment.vueを次のように修正します。

<template>
  <div>
    <h1 class="title">{{ response.details.subject }}</h1>
    <div class="post" v-html="response.details.contents"></div>
    <div>
        <p v-if="resultMessage !== null">
          {{ resultMessage }}
        </p>
        <ul v-for="comment in comments" :key="comment.comment_id">
            <li>
                {{ comment.note }} by {{ comment.name }}
                <button type="button" @click="() => deleteComment(comment.comment_id)">
                    delete
                </button>
            </li>
        </ul>
        <form @submit.prevent="submitComment">
            <input v-model="inputComment" type="text" placeholder="comment">
            <button type="submit" :disabled="inputComment === ''">
                submit
            </button>
        </form>
    </div>
  </div>
</template>

<script>
async function getAllComments (topics_id) {
    const { list } = await this.$axios.$get(
        process.env.BASE_URL + '/rcms-api/21/comments',
        {
            params: {
                module_id: topics_id,
                cnt: 999
            }
        }
    )
    return list
}

import { mapActions } from 'vuex'
export default {
  middleware: 'auth',
  async asyncData ({ $axios, params }) {
    try {
      const profile = await $axios.$get(process.env.BASE_URL + '/rcms-api/18/profile')
      const response = await $axios.$get(
        process.env.BASE_URL + '/rcms-api/21/newsdetail/1047'
      )
      return { profile, response, comments: await getAllComments.call({ $axios }, response.details.topics_id) }
    } catch (e) {
      console.log(e.message)
    }
  },
  data () {
      return {
          userName: '',
          response: null,
          comments: [],
          inputComment: '',
          resultMessage: null,
      }
  },
  methods: {
    async submitComment () {
        await this.$axios.$post('/rcms-api/21/comment', {
            module_id: this.response.details.topics_id,
            name: `${this.profile.name1} ${this.profile.name2}`,
            mail: this.profile.email,
            note: this.inputComment
            })
            this.comments = await getAllComments.call(this, this.response.details.topics_id)
            this.inputComment = ''
    },
    async deleteComment (commentId) {
      try{
        await this.$axios.$post(`/rcms-api/21/comment_delete/${commentId}`, {})
        this.comments = await getAllComments.call(this, this.response.details.topics_id)
        this.inputComment = ''
      } catch (error) {
         this.resultMessage = error.response.data.errors[0].message
      }
    }
  }
}
</script>

最後に動作確認をして完了です。
Image from Gyazo

お探しのページは見つかりましたか?解決しない場合は、問い合わせフォームからお問い合わせいただくか、Slackコミュニティにご参加ください。