PoWが必要なコメント機能の実装
このブログにコメント機能を実装しました。記事の最下部でコメントが投稿できます。よかったらコメントしてね。
PocketBaseという、Firebaseのような、バックエンドコンポーネントを提供してくれるものを使ってみたくなったのでやりました。 私の自宅内に PocketBase を動かしているサーバーを用意していますので、コメント機能については私の家で停電とか掃除とかしているとうっかりサーバー機のコンセントが引っこ抜けてたりします。怖いですね。ワンルームでホームラボとか正気じゃない。 ↩が起きると使えなくなります。
コメント機能の実装
マイグレーションファイルの作成
空のマイグレーションファイルはjs-migrationsにある通り、./pocketbase migrate create "new collection" で作れるのですが、マイグレーションファイルを作っても書き方の参考になるものが少ないので、ダッシュボードからコレクション作っていじったほうがいいです。
最初ちゃんと読んでなかったのですが、デフォで自動でマイグレーションファイル作ってくれるようになってます。保存をこまめにやっちゃうと思いがけない数生成されちゃったりはするのですが。
The prebuilt executable has the —automigrate flag enabled by default, meaning that every collection configuration change from the Dashboard (or Web API) will generate the related migration file automatically for you.
最初がんばって自力で書こうとしていましたが、そんな必要はありませんでした。
コメントビュー
基本的には取得も登録も同じスキーマになっちゃうので、コメントテーブルにメールアドレスを定義すると、取得でも普通にメールアドレスをかえしちゃいます。 フックして値を返す直前で編集しちゃうか、取得用のビューを用意する必要があります。
こちらはダッシュボードでコレクション作る時に、「ビュー」を選択すれば作れます。普通にSQL書くことになります。
PoW用のチャレンジ取得API
PocketBaseのカスタムルート作成方法に従ってチャレンジ取得APIを実装してます。
ちょっと省略してるけど大体以下のような実装です。コレクションの取得・登録用インスタンスの作成・保存・レスポンスの返却あたりの流れです。 本当は difficulty の自動調整とかもうちょっと凝ったことしないといけないのかもしれないけど、スパムとか来るまではまあいいや…
/// <reference path="../pb_data/types.d.ts" />
routerAdd("GET", "/api/amata/challenges", (e) => {
// 登録・するためのレコードインスタンスを作る
let collection = $app.findCollectionByNameOrId("challenges");
let record = new Record(collection);
// record.set("キー", "値") でもいいけどこれならオブジェクト渡せる
record.load({
challenge: genBase62(32)
difficulty: 4,
used: false,
expires: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5分後
});
// 保存の方法
$app.save(record);
// レスポンス
return e.json(200, {
challenge: record.get("challenge"),
difficulty: record.get("difficulty"),
expires: record.get("expires"),
});
});
コメント登録前のチェック
Event hook の onRecordCreate を使って、コメント登録がされる直前に割り込んで処理を入れてます。
結構楽に処理のカスタマイズができる感じでした。
適当にコードは省略しています。
/// <reference path="../pb_data/types.d.ts" />
// コメント作成前の検証
onRecordCreate((e) => {
// 登録しようとしている値は e.record.get("キー") で取得
let challenge = e.record.get("challenge");
let nonce = e.record.get("nonce");
// なんか色々検証 (省略)
// challenge と nonce を元に、所定の pow 課題が解けているか確認
const data = JSON.stringify({ challenge, nonce });
const hash = $security.sha256(data);
const difficulty = challengeRecord.get("difficulty");
if (!hash.startsWith("0".repeat(difficulty))) {
throw new BadRequestError("Invalid proof of work");
}
// 登録しようとしている値そのものを変更もできる
e.record.set("status", "approved");
e.next();
}, "comments");
コード全体については下記を見てください!
おわりに
ノーコード・ローコードソリューションみたいなもの考えた時に、こういう管理画面でコレクション作るみたいなものを思いつきがち…と、ちょっと疑いの気持ちすらあったのですが、使ってみると楽しいですね。 (まあ、結局この例ではカスタムルート・フック作っているので、「ノーコードではうまくいかない」を示した感じがありますが。)
適当に使えるDBが用意できたようなものなので、アンケートとか実装してみたいですね。(意味のある数だけアンケート回答が得られるような未来は来るのか…?)
以上!