Flarum: добавление адреса Web3 в профиль пользователя

Запрос разрешения и получение учётных записей Программирование и разработка

В нашем первом руководстве по Flarum — мы рассмотрели, как добавить новое настраиваемое поле в профиль пользователя в невероятно быстром и чрезвычайно расширяемом программном обеспечении форума с открытым исходным кодом под названием Flarum. Поле, которое мы добавили, было web3addressучетной записью пользователя Web3.

Примечание: Экосистема Web3 — это новый Интернет с децентрализованным хостингом, собственными данными и устойчивой к цензуре коммуникации. Для праймера по Web3, пожалуйста, посмотрите этот 15-минутный доклад на FOSDEM.

Криптографическое добавление Web3

На данный момент наше настраиваемое поле работает, настраивается пользователями и редактируется администраторами форума. Теперь давайте убедимся, что пользователи могут добавлять свой адрес криптографически надёжным способом.

Это означает, что пользователь сможет добавить только адрес, владельцем которого он является. Вы подтверждаете право собственности на адрес, подписывая сообщение закрытым ключом этого адреса. Только лицо, имеющее закрытый ключ из пары открытого и закрытого ключей, считается владельцем этой пары ключей. Открытая часть пары открытых и закрытых ключей — это основа, на которой математически выводится адрес Web3.

Чтобы получить какой-либо адрес (а), пользователь должен установить расширение Polkadot JS и создать учётную запись. Пользовательский интерфейс должен быть самим за себя, если это необходимо.

Поле, которое мы добавили в предыдущем руководстве, в настоящее время позволяет пользователям вручную устанавливать любое значение, но это означает, что пользователи могут вводить любой адрес или даже какую-то тарабарщину. Мы хотим, чтобы они добавляли только свои собственные, поэтому мы заменим его кнопкой, которая будет:

  • запросить разрешение на доступ к расширению браузера, содержащему учётные записи;
  • загрузить учётные записи и предложить раскрывающийся список, чтобы выбрать одну из них;
  • попросите пользователя подписать сообщение с этим адресом и проверить эту подпись;
  • зарегистрируйте эту учетную запись в качестве адреса Web3 пользователя.

Кнопка

Сначала нам нужно изменить поле ввода Web3 на раскрывающееся. Создадим components/Web3Dropdown.js:

import Component from "flarum/Component";
import Dropdown from "flarum/components/Dropdown";

export default class Web3Dropdown extends Component {
  view() {
    return (
      <Dropdown
        buttonClassName="Button"
        onclick={this.handleClick.bind(this)}
        label="Add Web3 Account"
      >
      </Dropdown>
    );
  }

  handleClick(e) {
    console.log("Pick something");
  }
}

Мы создаём новый компонент в стиле, который Web3Field.jsмы создали ранее, но теперь мы возвращаем экземпляр компонента Dropdown. Компонент Dropdown — один из нескольких стандартных компонентов JS в Flarum. Мы также даём ему класс «Button», чтобы он соответствовал стилю остального форума. По щелчку мы печатаем сообщение.

Компонент представляет собой кнопку с возможностью вызова раскрывающегося списка из переданных элементов, что очень похоже на меню «Элементы управления», которое администратор форума может видеть в профиле пользователя:

Зависимости

В папке JS нашего расширения мы добавим две зависимости:

yarn add @polkadot/util-crypto @polkadot/util @polkadot/extension-dapp

Примечание ⚠: не забудьте остановить процесс, если вы всё ещё работаете, yarn devи не забудьте запустить его снова после установки этих зависимостей!

util-cryptoсодержит некоторые служебные функции для криптографических операций. utilсодержит некоторые базовые утилиты, такие как превращение строк в байты и т. д. (Здесь есть документация для обоих.) extension-dapp- это вспомогательный уровень, который позволяет JS, который мы пишем, взаимодействовать с установленным нами расширением Polkadot JS. (Посетите документы здесь.)

Запрос разрешения и получение учётных записей

Давайте изменим наш Dropdown сейчас, чтобы запросить у пользователя разрешение на доступ к своим учётным записям Web3:

  import { web3Accounts, web3Enable } from "@polkadot/extension-dapp";

  // ...

  async handleClick(e) {
    await web3Enable("Flarum Web3 Address Extension");
    const accounts = await web3Accounts();
    console.log(accounts);
  }

Обратите внимание, что мы изменили handleClickфункцию на async! Нам это нужно, чтобы можно было использовать awaitобещания в коде. В противном случае мы застряли бы с thenвызовами вложенности.

Сначала мы звоним web3Enable, который спрашивает у нас разрешение на доступ к расширению. Затем мы берём все учётные записи пользователей и выводим их в консоль. Если у вас установлено расширение Polkadot JS и загружены некоторые учётные записи, вы можете попробовать это прямо сейчас.

Запрос разрешения и получение учётных записей

Запрос разрешения и получение учётных записей2

Но что, если у кого-то расширение не установлено? У нас может быть настройка на уровне администратора, которая позволяет нам выбирать, скрывать ли кнопку, если расширения нет рядом, или перенаправлять пользователя на её URL-адрес, но пока давайте выберем последнее:

  import { web3Accounts, web3Enable, isWeb3Injected } from "@polkadot/extension-dapp";

  // ...

  async handleClick(e) {
    await web3Enable("Flarum Web3 Address Extension");
    if (isWeb3Injected) {
      const accounts = await web3Accounts();
      console.log(accounts);
    } else {
      window.location = "https://github.com/polkadot-js/extension";
    }
  }

Выбор учётной записи

Следующий шаг — позволить пользователю выбрать учётную запись и в то же время попросить его подписать с ней сообщение. После проверки эта подпись неопровержимо подтверждает право собственности на учётную запись.

Компонент Dropdown принимает itemsдля отображения массив элементов. Чаще всего это массив Buttonэлементов, где Button — общий компонент Flarum. Чтобы дать нашему компоненту свойство данных для всего компонента, которым мы можем манипулировать и основывать изменения, мы определяем его в oninit:

 oninit() {
    this.web3accounts = [];
  }

Вместо того, чтобы просто console.logвставить accounts, мы затем устанавливаем accountsэтот новый атрибут:

this.web3accounts = accounts;
m.redraw();

Примечание ⚠: redrawздесь мы используем make mithril( m) для повторного рендеринга нашего компонента. Если мы этого не сделаем, компонент сначала отобразит пустой раскрывающийся список (у него ещё нет учётных записей) и потребуется ещё одно закрытие — открытие раскрывающегося списка для отображения учётных записей (что вызывает перерисовку). Мы хотим, чтобы учётные записи в раскрывающемся списке сразу после их загрузки были сильными>, даже если раскрывающийся список уже открыт и не имеет элементов, так что это поможет. Если вам нужно применить изменения к вашему компоненту динамически без триггеров пользовательского интерфейса, обычно на основе некоторых удалённых выборок или обработки данных, вы можете использовать это нормально m.redraw().

Читайте также:  Что такое ASCII

Наконец, мы заставляем viewфункцию, отвечающую за наш рендеринг, реагировать на это изменение:

  view() {
    const items = [];
    if (this.web3accounts.length) {
      for (let i = ; i < this.web3accounts.length; i++) {
        items.push(
          <Button
            value={this.web3accounts[i].address}
            onclick={this.handleAccountSelect}
          >
            {this.web3accounts[i].address}
            {this.web3accounts[i].meta.name
              ? ` - ${this.web3accounts[i].meta.name}`
              : ""}
          </Button>
        );
      }
    }
    return (
      <Dropdown
        buttonClassName="Button"
        onclick={this.handleClick.bind(this)}
        label="Set Web3 Account"
      >
        {items}
      </Dropdown>
    );
  }

Сначала мы определяем пустой массив заполнителей. Затем, если web3accountsв этом компоненте хранится больше нуля, мы перебираем их, чтобы создать кнопку для каждой учётной записи со значением, установленным на адрес учётной записи, и меткой, установленной на комбинацию адреса и метки, определённой в расширении. Наконец, мы передаём эти кнопки в компонент Dropdown.

Нам также необходимо импортировать компонент Button:

import Button from "flarum/components/Button";

Примечание ℹ: обратите внимание, что мы не привязаны thisк каждому onclickобработчику события Button. Это связано с тем, thisчто контекст кнопки изменится на родительский компонент раскрывающегося списка, а не на кнопку, на которую нажимают кнопку, и сделает выборку значения кнопки менее простой.

Далее нам нужно отреагировать на щелчок пользователя по одному из адресов в меню:

  handleAccountSelect() {
    console.log(this.value);
  }

Тестирование покажет, что мы действительно можем выбрать учётную запись Web3 в нашем расширении, и что после выбора она регистрируется в консоли.

Выбор учётной записи

 

Проверка учётной записи

Наконец, нам нужно попросить пользователя подписать сообщение. Скажем, сообщение — «Крайняя собственность». Это предложит им ввести пароль во всплывающем окне расширения и вернуть подписанное сообщение.

Во-первых, импорт:

import {
  web3Accounts,
  web3Enable,
  isWeb3Injected,
  web3FromAddress,  // <-- this is new
} from "@polkadot/extension-dapp";
import { stringToHex } from "@polkadot/util"; // <-- this is new

web3FromAddress- удобный метод создания объекта Web3, стандартного объекта для взаимодействий Web3, с заданным адресом в качестве «главного героя». stringToHexиспользуется для преобразования строки в шестнадцатеричное представление, которое является форматом данных, ожидаемым подписывающей стороной (байты):

  async handleAccountSelect() {
    const address = this.value;
    const web3 = await web3FromAddress(address);
    const signer = web3.signer;
    const hexMessage = stringToHex("Extreme ownership");
    try {
      const signed = await signer.signRaw({
        type: "bytes",
        data: hexMessage,
        address: address,
      });
      console.log(signed);
    } catch (e) {
      console.log("Signing rejected");
      return;
    }
  }

Сначала мы превращаем функцию в asyncодну, чтобы можно было использовать await. Затем мы создаём web3экземпляр из нашего адреса, как описано выше, и извлекаем подписавшего. Подписывающая сторона — это криптографический инструмент, который автоматически извлекает открытый ключ из адреса и подписывает данное сообщение в байтах. (Это то, для чего нам нужно hexMessage- преобразование нашей строки в байты, представленные в шестнадцатеричном виде.)

Единственный способ получить signed- подписать; всё остальное вызывает ошибку.

Сохранение Аккаунта

Наконец, мы выполняем тот же процесс, что и раньше, Web3Field.js- передаём адрес в save:

  async handleAccountSelect() {
    const address = this.value;
    const web3 = await web3FromAddress(address);
    const signer = web3.signer;
    const hexMessage = stringToHex("Extreme ownership");
    try {
      const signed = await signer.signRaw({
        type: "bytes",
        data: hexMessage,
        address: address,
      });
      console.log(signed);
      const user = app.session.user;
      user
        .save({
          web3address: address,
        })
        .then(() => m.redraw());
    } catch (e) {
      console.log("Signing rejected");
      return;
    }
  }

Примечание ℹ: мы добавляем, m.redrawчтобы обновить значение на экране после сохранения. Повторная отрисовка вызовет обновление JavaScript расширения и чтение данных из экземпляра User, возвращённого операцией сохранения, с отображением нашего обновлённого адреса, если сохранение было успешным.

Проверка на стороне сервера

Это достаточно безопасно. Даже если кто-то взломает наш JS и вставит адрес Web3, который ему не принадлежит, они не смогут ничего с этим поделать. Они могут просто представить себя кем-то, кем они не являются. Тем не менее, мы также можем обойти это, выполнив некоторую проверку на стороне сервера.

Поскольку в настоящее время нет набора инструментов для PHP для криптографических функций, которые мы используем, нам нужно проявить творческий подход. В частности, мы напишем сценарий командной строки на TypeScript, который мы будем вызывать из PHP.

В js/src/forumсоздайте scriptsпапку и добавьте файл verify.js:

let util_crypto = require("@polkadot/util-crypto");

util_crypto
  .cryptoWaitReady()
  .then(() => {
    const verification = util_crypto.signatureVerify(
      process.argv[2], // message
      process.argv[3], // signature
      process.argv[4] // address
    );
    if (verification.isValid === true) {
      console.log("OK");
      process.exitCode = ;
    } else {
      console.error("Verification failed");
      process.exitCode = 1;
    }
  })
  .catch(function (e) {
    console.error(e.message);
    process.exit(1);
  });

Пакет утилит для шифрования содержит вспомогательные методы для всего, что нам нужно. cryptoWaitReadyожидает запуска крипто-операций — в частности, sr25519, который мы здесь используем, нуждается в разогреве WASM. Затем мы проверяем подпись с помощью signatureVerifyфункции, обрабатывая предоставленные аргументы.

Мы можем протестировать это локально (получить значения из полезной нагрузки запроса на сохранение после установки адреса в раскрывающемся списке или вручную подписав сообщение «Экстремальное владение» в пользовательском интерфейсе Polkadot ):

$ node src/forum/scripts/verify.js "Extreme ownership" 0x2cd37e33c18135889f4d4e079e69be6dd32688a6bf80dcf072b4c227a325e94a89de6a80e3b09bea976895b1898c5acb5d28bccd2f8742afaefa9bae43cfed8b 5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB
> OK

$ node src/forum/scripts/verify.js "Wrong message" 0x2cd37e33c18135889f4d4e079e69be6dd32688a6bf80dcf072b4c227a325e94a89de6a80e3b09bea976895b1898c5acb5d28bccd2f8742afaefa9bae43cfed8b 5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB
> Verification failed

Наш скрипт проверки работает.

Читайте также:  Лучшие проекты JavaScript для начинающих

Примечание ℹ: одно и то же сообщение, подписанное одним и тем же адресом, будет каждый раз давать разный хэш. Не рассчитывайте на то, что они такие же. Например, эти три полезные данные являются «Экстремальным владением» и подписаны одним и тем же адресом 3 раза:

// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0x0c837b9a5ba43e92159dc2ff31d38f0e52c27a9a5b30ff359e8f09dc33f75e04e403a1e461f3abb89060d25a7bdbda58a5ff03392acd1aa91f001feb44d92c85"}""
// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0x3857b37684ee7dfd67304568812db8d5a18a41b2344b15112266785da7741963bdd02bb3fd92ba78f9f6d5feae5a61cd7f9650f3de977de159902a52ef27d081"}""
// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0xa66438594adfbe72cca60de5c96255edcfd4210a8b5b306e28d7e5ac8fbad86849311333cdba49ab96de1955a69e28278fb9d71076a2007e770627a9664f4a86"}""

Нам также нужно изменить наш app.session.user.saveвызов в Dropdownкомпоненте, чтобы он действительно отправлял подписанное сообщение в серверную часть:

  user
    .save({
      web3address: address,
      signedMessage: signed.signature,
    })
    .then(() => console.log("Saved"));

Когда наше web3addressзначение сохраняется для пользователя, нам нужно перехватить эту операцию, проверить подпись, только если это пользователь, а не администратор, и сохранить, если всё в порядке, или отклонить (желательно с сообщением об ошибке), если нет.

Давайте изменим handleфункцию в SaveUserWeb3Address.php:

if (isset($attributes['web3address'])) {
    if (!$isSelf) {
        $actor->assertPermission($canEdit);
    }

    chdir(__DIR__ . "/../../js");
    $command = "node src/forum/scripts/verify.js \"Extreme ownership\" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    exec($command, $out, $err);

    if ($err) {
        return false;
    }
    $user->web3address = $attributes['web3address'];
    $user->save();
}

Мы добавили строки с 6 по 12: мы меняем каталог на тот, который содержит наш сценарий проверки. Затем мы составляем вызов сценария из командной строки, передавая необходимые параметры. И, наконец, если код ошибки $errотличается от ложного (это будет, 0если всё пойдёт хорошо), мы останавливаем процесс сохранения.

Однако это не позволяет администраторам изменять значение по своему желанию, поэтому давайте добавим это. Согласно документации, у an $actorесть isAdminпомощник. Окончательная версия нашего handleметода теперь:

public function handle(Saving $event)
{
    $user = $event->user;
    $data = $event->data;
    $actor = $event->actor;

    $isSelf = $actor->id === $user->id;
    $canEdit = $actor->can('edit', $user);
    $attributes = Arr::get($data, 'attributes', []);

    if (isset($attributes['web3address'])) {
        if (!$isSelf) {
            $actor->assertPermission($canEdit);
        }

        if (!$actor->isAdmin()) {
            chdir(__DIR__ . "/../../js");
            $command = "node src/forum/scripts/verify.js \"Extreme ownership\" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
            exec($command, $out, $err);

            if ($err) {
                return false;
            }
        }
        $user->web3address = $attributes['web3address'];
        $user->save();
    }
}

Ясность ошибок

Последнее, что нам следует сделать, это сделать ошибку более удобной для пользовательского интерфейса, если проверка адреса не удалась. A return falseне очень полезен; пользовательский интерфейс просто ничего не сделает. Поскольку это ошибка проверки (нам не удалось подтвердить право собственности пользователя на этот адрес), мы можем выдать ValidationException:

if ($err) {
    throw new Flarum\Foundation\ValidationException(["Signature could not be verified."]);
}

Теперь, если наша проверка не удалась, мы увидим это в удобном сообщении об ошибке:

Теперь, если наша проверка не удалась, мы увидим это в удобном сообщении об ошибке

Предупреждение перед развёртыванием

Поскольку мы находимся в режиме разработки, наше расширение имеет доступ к Node и Yarn и может устанавливать зависимости Polkadot, необходимые для криптографии. Однако в производственной среде нет простого способа автоматического запуска yarn installпакета, установленного Composer. Поэтому наш сценарий проверки не будет работать без значительного вмешательства пользователя. Нам нужно связать verify.jsскрипт в файл, который будет запускаться NodeJS напрямую без менеджеров пакетов. Это по-прежнему означает, что на нашем производственном сервере должен быть установлен NodeJS. Но это всё, что ему нужно — по крайней мере, до тех пор, пока криптографические функции, которые мы используем, также не появятся в версии PHP.

Чтобы связать наш скрипт, внутри папки JS расширения мы можем запустить:

npx browserify src/forum/scripts/verify.js > dist/verify.js

Это запустит Browserify без его установки, объединит все зависимости и выведет один JS-объект, в который мы сохраняем dist/verify.js. Теперь мы можем зафиксировать этот файл в репозитории расширения и указать его, если он существует. Фактически, мы можем сделать так, чтобы наше расширение определяло, находится ли форум в debugрежиме, и нацелив его на файл source vs dist на основе этого флага:

if (!$actor->isAdmin()) {
    chdir(__DIR__ . "/../../js");
    if (app(\Flarum\Foundation\Config::class)->inDebugMode()) {
        $command = "node src/forum/scripts/verify.js \"Extreme ownership\" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    } else {
        $command = "node dist/verify.js \"Extreme ownership\" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    }
    exec($command, $out, $err);

    if ($err) {
        throw new ValidationException(["Signature could not be verified."]);
    }
}

Наш слушатель прочитает исходную версию, если она inDebugModeвернет true, или dist/verify.jsиначе.

Заключение

Пользователи нашего форума теперь могут добавлять свои адреса Web3 в свой профиль.

Поскольку некоторые пользователи могут не использовать Chrome или Firefox и не иметь доступного расширения, а также поскольку некоторые могут предпочесть альтернативные методы создания учётных записей, мы разрешаем администраторам вручную вводить адреса для определённых учётных записей пользователей при условии, что эти пользователи подтвердят право собственности на их адрес. Однако, поскольку это ручной процесс, процесс доказательства выходит за рамки данного руководства.

Это закладывает основу для будущего использования этих адресов. Они будут использоваться для выплаты очков участия — крипто-токенов — за активность пользователей на форуме, поощрение оживлённых дискуссий и качественного взаимодействия. Мы увидим, как это сделать в следующем руководстве.

 

Оцените статью
bestprogrammer.ru
Добавить комментарий