👋

Wix 会員エリア の権限に関する機能を使って、WixChatに機能の制限を加える

2023/07/16に公開

はじめに

ここ最近、Wix Chatと OpenAI のAPIを組み合わせ、いくつか機能を追加したのですが、現状チャットにアクセスできる人は誰でも同じ操作ができてしまいます。
そこで、特定の権限のあるユーザーにだけ操作を許可するような制限を加えたくなり、作業に着手しました。

当初のゴールは、管理者から特別な権限を付与されたユーザーのみが、チャットでの投稿に回答してもらえるようにすることでした。

アプリWix 会員エリアをインストールし、ユーザー登録、ログインを実現し、登録ユーザー(サイト会員)にバッジを付与し、そのバッジの有無を判定して処理を分岐させる予定で、実際、その実装で着地しました。

この結果だけ記載すれば記事は短くまとまったのですが、本文中にあるように、いくつか想定外の事象が発生し、かなりの調査作業が発生しました。

これについてはVeloやWixに対する理解をより深めるきっかけになったことと、まだ解決できていない疑問点も残っており、冗長になるかと思いますが、記録を残すことにしました。

タイトルから逸脱する内容が多いですが、ご了承いただければと思います。

前提

本記事は上記記事の続きのような位置付けです。
必要であればあわせてご参照いただければと思います。

下準備

Velo by Wixの話題がメインなのでWix自体の設定は駆け足で進みたいと思います。

  • サイト構築時にWix 会員エリア機能がインストールされていない人は、上記ページなどからインストールしてください。
  • 作成したサイトに、新規会員登録・ログイン画面が表示されていない場合は上記の記事など参考に、サイトを訪問した人が会員登録、ログインできるようにしてください。
    ※ここでの「会員登録」「ログイン」はWix自体のユーザー登録のことではなく、Wixで作成したサイトの「会員登録」「ログイン」のことなのでお気をつけください。

以下が設定後の画面例です。アプリを追加すると自動的にメニューに導線が追加されていました。
(レイアウト崩れたままスクショ撮っていますが、見た目は今回気にしないので悪しからず)

  • 会員登録・ログイン導線
    未ログイン状態

    ログイン後

  • 会員登録画面

  • ログイン画面
    未ログイン状態で会員登録画面に遷移後"Already a member?Log In"の"Log In"をクリックで遷移できます。

上記の見た目はテンプレにより異なると思います。また自分で遷移・導線含めて調整可能かと思いますので、試してみてください。

開発用のユーザーの準備

テスト用に以下のユーザーを作成or管理します。

  1. 非サイト会員・・・未ログインユーザー(ログアウト後のユーザー)で再現します。
  2. 通常サイト会員・・・新規登録しログインすることで再現します。権限の付与は行いません。
  3. プレミアム会員(通常サイト会員に特別な権限を付与)・・・通常サイト会員に特別な権限を付与しログインします。

それぞれユーザーを作成、ブラウザを切り替えて以降のテストを行います。
権限の付与は

  • サイト会員の役割
  • バッジの管理
    の2つを試しました。
    冒頭で触れたように、当初バッジだけの予定でしたが、類似機能があることに気がついたため両方試すことにしました。
    後述しますが「サイト会員の役割」についてはチャット(のイベントハンドラ)では判定に使う方法が見つけられませんでした。

「サイト会員の役割」の作成と権限付与

  • ダッシュボードから「連絡先」→「サイト会員]

  • 「サイト会員の役割を管理」をクリックで以下に遷移
  • 「新しい役割」をクリックで以下に遷移
  • 必要項目を入力し「保存」、以下のページに遷移(「権限」タブ)今は何もしません
  • 「サイト会員」タブをクリックすると以下に遷移するので「サイト会員を追加」を実行します
  • プレミアム権限を付与したいユーザーを選択します

バッジの作成とサイト会員への付与

  • 「サイト会員」にて「その他」をクリックして、表示されるメニューから「バッジの管理」を選択

  • 「新しいバッジ」をクリック

  • 必要項目を入力し「保存」をクリック

  • 「バッジの管理」に戻ると作成したバッジが表示されます。カーソルを合わせると「バッジの授与」ボタンが表示されるので、クリックします。

  • 遷移先にバッジ授与者の一覧と「バッジの授与」リンクが表示されているので、「バッジの授与」をクリックし、選択画面に遷移したら、バッジを授与したいユーザーを選択します。

各種データ、IDの確認方法

これでサイト会員、「サイト会員の役割」、「バッジ」それぞれのレコードの作成と紐付けが終わりました。

「サイト会員の役割」については先ほどの編集画面にてIDが確認できます。
「サイト会員」と「バッジ」についてはサイトエディッタの「データベース」で確認できます。

サイトエディッタへは、いくつか方法がありますが左サイトメニューの「サイト概要」→「ウェブサイト」→「サイトを編集」で遷移できます。

サイトエディッタに遷移後は開発モードにする必要があります、
ヘッダー上部の「開発モード」を有効にしておいてください。

その後表示される一番左メニューの上から5番めのデータベースを選択します。

表示される「Wixアプリコレクション」の中から "Members/Badges" を探し、右側の"・・・"マークをクリックするとコンテンツマネージャーが開きます。

コンテンツマネージャーでは、デフォルトでIDが非表示になっているかと思います。「フィールドを管理」をクリックすると、表示するカラムを追加できるのでIDを追加してください。

これでBadgesのIDが確認できるようになりました。

  • Members/Badges にバッジの情報(誰がバッジを持っているか)
  • Members/FullData にサイト会員の情報
    があることを覚えておいてください。

チャット投稿+ログ確認(events.js)

チャット受信(送信)時、events.js内でのサイト会員IDの確認

wix-chat-backend > Events > onMessage()

上記API仕様書「onMessage Parameters」に「participantId」があります。

The sender's member ID. For a message sent from the site's business, the site owner's member ID.

この値がサイト会員のIDとなります。

サンプルコード

import { sendChatMessage, execChat } from 'backend/chat'

export async function wixChat_onMessage(event) {
  const chatMessage = event.payload.text;
  const channelId = event.channelId
  const participantId = event.participantId;
  if(event.direction == "VisitorToBusiness") {
    const ans = await execChat(chatMessage, participantId)
    await sendChatMessage(channelId, ans)
  }
}

上記のようにサイト会員のID(participantId)を取得します。
events.jsが肥大化するのを避けるため、別ファイルから読み込んだ関数に渡して利用します。

「サイト会員の役割」の有無を確認する実装(2023.07.17修正)

API仕様書やWEB検索などを使い、それらしきAPIを探します。
結果見つけられたのが

wix-members-backend > CurrentMember > getMember()

wix-members-backend > CurrentMember > getRoles() です。

どういう値が取れるか、テスト用のコードを埋め込み実行してみます。

const currentMember = await currentMember.getMember()

例外が投げられ、catchした箇所にて以下のエラーが投げられていました。

デベロッパーツールのログを抜粋します。(IDは伏字にしてます)

"jsonPayload":{
"message":"["message: Permission Denied\ndetails:\n applicationError:\n description: Forbidden\n code: FORBIDDEN\n data: {}"]"

※7.17追記、上記テスト時の発話者(チャット入力ユーザー)はバッジ・「サイト会員の役割」を付与したサイト会員(ログイン済みユーザー)です。なお、未ログインユーザーでも同様の結果となります。
後述する通常ページ(非events.js)で、同等の実装とログ取得をした際は未ログインユーザーの時のみ上記エラーが出現します。

続いて

const currentRole = await currentMember.getRoles()

チャットを送信したメンバーに割り当てていたロールが取得できせんでした。(空の値が返ってくる)

events.js(イベントハンドラー)で実行したので、実行ユーザーが発話者ではなく、例えばsystemユーザーといった可能性はあります。

バッジの有無を確認する実装

次は以下の方法になります。

サンプルコードは以下になります。

import { badges } from 'wix-members-backend';
// (中略)

//バッジのIDからサイト会員の一覧を取得する方法
try {
  const badgeId = '作成したバッジのID'
  const listMembers = await badges.listMembers(badgeId)
  console.log(listMembers, 'listMembers')
} catch (e) {
  console.log(e, 'LOG1')
}

//サイト会員のIDから所有するバッジの一覧を取得
try {
  const memberIds = [participantId];
  const memberBadges = await badges.listMemberBadges(memberIds)
  console.log(memberBadges, 'memberBadges')
} catch (e) {
  console.log(e, 'LOG2')
}

上記を適当な場所に埋め込み実行すると以下の結果が出ました。

  1. listMembers() は値の取得ができました。
  2. listMemberBadges() は 403エラーが出ました。

デベロッパーツールのログを抜粋します。

"jsonPayload":{
"message":"["Status code: 403, message: {\"message\":\"\",\"details\":{}}","test"]"
}

2については、events.js (イベントハンドラ)がトリガーのため、サイト会員本人が直接操作した処理ではないので実行権限がないという可能性はあります。
ただ、バッジ保有者の一覧情報を取得できる時点で矛盾は感じます。

別の方法も試してみます。
wix-data を使い、直接コレクションを覗いてみます。

try {
  const results = await wixData.query("Members/Badges")
    .eq('_id', badgeId)
    .hasSome('members', [participantId]).find()
  console.log(results, 'results')
} catch(e) {
  console.log(e, 'LOG3')
}

こちらは結果を取得することができました。
ただし、_idで検索できるもののroleIdだと
WDE0076: Validation error. Invalid filter operation $eq for field 'roleId こんなエラーが出てしまいました。私がタイポに気が付けてないだけだろうか???
※この件はわからずじまいです。

色々ありましたが、
2つ、目的達成をする方法が見つかりました。
しかし、前者(指定したバッジを保有しているサイト会員の一覧を全て取得)は会員数が多い場合大量のレコードを読み込む実装なため(私が遊びで作るサイトで会員数が1万、10万というのは考えられないですが)、後者を使うこととします。

実装の紹介は省きますが、専用のバッジを保有していない場合は

"このチャットはプレミアムユーザー限定です。"

とメッセージを返す分岐を加えました。

チャット以外のページで試す

先ほど listMemberBadges() で権限エラーが出た理由として events.js(イベントハンドラー)で実行したため、アクセス権限に問題が出た可能性があります。
また、events.js以外でもサイト会員の権限により処理の書き分けが使えないか、チェックしておきたいこともあり、通常の公開ページで検証してみます。

通常のページ(ホーム)にテスト用要素を作成

任意の場所にテキストを作成し、IDを"memberStatus"とします。

要素を選択後、画面右下以下の入力欄から変更できます。

この要素を訪問しているユーザーの状態を使って、動的に出し分けたいと思います。

バックエンド検証コード作成

画面左側メニュー上から2番めの「公開・バックエンド」から「新規Webモジュール」を作成します。
Webモジュールとすることでフロント(公開エリア)のコードから呼び出せます。
Webモジュール自体はサーバーサイド実装のようにセキュアな場所に配置されています。

今回はファイル名をmember.jsw とし以下の実装をしました。
今回events.jsで試した処理を移植しています。

※ID系は念の為伏字にしてます

import { currentMember, badges } from 'wix-members-backend';
import wixData from 'wix-data';

export async function hasPremiumRole() {
    try {
        const roles = await currentMember.getRoles()
        const hasId = roles.some(item => item._id === "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
        return hasId
    } catch (e) {
        console.log(e, 'hasPremiumRole Error')
        return false
    }
}

export async function isMember() {
    try {
        const member = await currentMember.getMember()
        return member && member._id
    } catch (e) {
        console.log(e, 'isMember Error')
        return false
    }
}

export async function hasPremiumBadge() {
    try {
        const member = await currentMember.getMember()
        const badgeId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
        const listMembers = await badges.listMembers(badgeId)
        const hasId = listMembers.includes(member._id);
        return hasId
    } catch (e) {
        console.log(e, 'hasPremiumBadge Error')
        return false
    }
}

export async function hasPremiumBadge2() {
    const badgeId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    try {
        const member = await currentMember.getMember()
        const memberIds = [member._id];
        const memberBadges = await badges.listMemberBadges(memberIds)
        return memberBadges.length == 1
    } catch (e) {
        console.log(e, 'hasPremiumBadge2 Error')
    }
}

export async function hasPremiumBadge3() {
    const badgeId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    try {
        const member = await currentMember.getMember()
        const memberIds = [member._id];
        const results = await wixData.query("Members/Badges")
            .eq('_id', badgeId).hasSome('members', memberIds).find()
        if (results?.items.length == 1) {
            return true
        }
    } catch (e) {
        console.log(e, 'hasPremiumBadge3 Error')
        return false
    }
}

フロントエンド検証コード実装

次にフロント側の実装です。
画面左側のメニューから「ページコード」を選択し、「ホーム」のコードを以下のように編集します。

$w.onReady(async function () {
    if (await isMember()) {
        $w('#memberStatus').text = await hasPremiumRole() ? "プレミアム会員" : "サイト会員"
    } else {
        $w('#memberStatus').text = "未ログインユーザー"
    }
});

「サイト会員の役割」で判定

まず、 events.js で失敗した currentMember.getMember(), currentMember.getRoles()を使った処理でテストします。

フロントのコードを保存・公開したら、それぞれのユーザーでアクセスします。

  • 該当の「サイト会員の役割」を与えたユーザ
    →「サイト会員の役割」を取得できた
  • 該当の「サイト会員の役割」がないサイト会員
    →「サイト会員の役割」を付与してないので正しく判定
  • 未ログインユーザー
    →例外発生、デベロッパーツールのエラーログ抜粋は以下になります。
"jsonPayload":{
"message":"["message: Permission Denied\ndetails:\n applicationError:\n description: Forbidden\n code: FORBIDDEN\n data: {}","isMember Error"]"
}

ここでevents.jsでも発生したエラーが出て、events.jsの実行ユーザーが、チャットの発言したユーザーと異なるという予測が正しい気がしてきました。
※ただし裏付けとなる公式資料は見つからず

バッジを使った判定(badges.listMembers())

フロントを以下に変更します。

$w.onReady(async function () {
    if (await isMember()) {
        $w('#memberStatus').text = await hasPremiumBadge() ? "プレミアム会員" : "サイト会員"
    } else {
        $w('#memberStatus').text = "未ログインユーザー"
    }
});

保存・公開したら、それぞれのユーザーでアクセスします。
結果は「サイト会員の役割」と同等になりました。

バッジを使った判定(listMemberBadges())

フロントを以下に変更します。
疲れてきたので関数名が手抜きです。
(こういう時ミスするんですよね)

$w.onReady(async function () {
    if (await isMember()) {
        $w('#memberStatus').text = await hasPremiumBadge2() ? "プレミアム会員" : "サイト会員"
    } else {
        $w('#memberStatus').text = "未ログインユーザー"
    }
});

保存・公開したら、それぞれのユーザーでアクセスします。
結果は「サイト会員の役割」と同等になりました。

バッジを使った判定(wixData)

最後にwixDataのクエリも試します。

フロントを以下に変更します。

$w.onReady(async function () {
    if (await isMember()) {
        $w('#memberStatus').text = await hasPremiumBadge3() ? "プレミアム会員" : "サイト会員"
    } else {
        $w('#memberStatus').text = "未ログインユーザー"
    }
});

保存・公開したら、それぞれのユーザーでアクセスします。
結果は「サイト会員の役割」と同等になりました。

やはり events.js(イベントハンドラ)に何か課題がありそうですが、通常ページ、イベントハンドラ共通で使えるという点で、当面はバッジを活用して権限関連の処理を実装してみようと思います。

まとめ・課題

今回はWix Chatのメール送信イベントハンドラ内でサイト会員の権限情報を取得して処理を分岐する方法について考えました。
そして、バッジや「サイト会員の役割」を用いて実装を試みましたが、今回は「サイト会員の役割」についてはイベントハンドラで扱えず、バッジについてもWixDataでやや変則的に扱う結果となりました。

一方、通常のページでは、イベントハンドラと異なり、バッジも「サイト会員の役割」でも期待される実装で処理に成功しました。

今回記事中で直接扱わなかったのですが、Veloのフォーラムでは今回と類似の事象についての記事がいくつかあります。今見つけた範囲では結論となるような返信はないようなので、もう少し自分で調べ、議論にも加わってみたいという気持ちもあります。

直近やりたいことや記事にしたいことが溜まってきているので、すぐに追加で記事にするかはわかりませんが、機会があれば追加で調査したいと思います。

追記

上記に伴い「まとめ」の wix-groups-backend に関する記述を削除しました。

Discussion