/spwmini

Библиотека для создания мини приложений для сайта sp worlds

Primary LanguageTypeScript

SPWMini

NPM Version NPM downloads

SPWorldsSPWorlds APIDiscord
УстановкаКлиентСерверТипыБезопасность

SPWMini - Библиотека для создания мини приложений для сайта SPWorlds.

Она состоит из клиентской части, серверной части и набора типов.

Установка

Ты умеешь устанавливать пакеты своим любимым менеджером пакетов. Наш называется spwmini.

npm install spwmini

yarn add spwmini

pnpm install spwmini

Клиентская часть

Пакет предоставляет импорт класса SPWMini из spwmini/client

import SPWMini from 'spwmini/client';

Инициализация

Инициализация класса требует обязательный аргумент - ID приложения, который можно взять со страницы /[server]/apps/[id-приложения].

const spm = new SPWMini('123e4567-e89b-12d3-a456-426655440000');

При работе с фреймворками, опционально, можно отключить автоматическую инициализацию, чтобы предупредить работу с window до появления возможности его использования.

Метод initialize позволит произвести инициализацию в нужный момент, а метод dispose - удалить все хендлеры по необходимости.

const spm = new SPWMini('123e4567-e89b-12d3-a456-426655440000', {
  autoinit: false
});

onMounted(() => spm.initialize());
onUnmounted(() => spm.dispose());

Так как для валидации данных о пользователе требуется запрос к серверу, отдельной опцией может быть кастомизирован и метод fetch.

const spm = new SPWMini('123e4567-e89b-12d3-a456-426655440000', {
  customFetch: ofetch
});

Общение с SPWorlds

Во время инициализации сразу же на SPWorlds отправляется запрос init с id приложения.

Если id запущенного приложения и вашего совпадут, в ответ будет отправлено сообщение initResponse с данными о пользователе. Иначе - initError с данными об ошибке.

Все сообщения прослушиваются с помощью метода on или addEventListener.

spm.on('initResponse', user => {
  console.log(`Logged in as ${user.username} / ${user.minecraftUUID}`);
});

spm.on('initError', message => console.error(`Log in error: ${message}`));

Сразу после инициализации и события ready данные о пользователе доступны как параметр user.

spm.on('ready', () => {
  console.log('App is ready!');
  console.log('Current user:', spm.user);
})

Все описанные далее методы будут работать только после инициализации приложения (после события initResponse или ready).

Валидация пользователя

Так как эти данные может легко подделать любой пользователь, предоставлен и метод валидации информации, отправляющий запрос к API и тесно связанный с серверной частью этого пакета.

Метод validateUser принимает URL и опциональные параметры fetch. Он делает запрос к бекенду, передавая информацию о пользователе.

Бэкенд должен вернуть простой текстовый ответ, содержащий число 1 в случае, если всё хорошо и 0 в случае, если пользователь подделан. Подробнее в секции про серверную часть.

const isUserValid = await spm.validateUser('/validate');

const isUserValid = await spm.validateUser('/validate', { credentials: 'include' });

Открытие URL в новом окне

Для открытия ссылки в новом окне используется метод openURL. Он принимает только ссылки с протоколом https://. Вызов метода с валидной ссылкой открывает диалоговое окно, предлагающее пользователю перейти по ссылке.

spm.openURL('https://google.com');

В случае успеха будет отправлен ответ openURLResponse со строкой success. Любую ошибку открытия можно поймать событием openURLError и получить строку с информацией о ней.

spm.on('openURLResponse', () => console.log('Окно открытия URL успешно открыто'));
spm.on('openURLError', err => console.error(`Ошибка запроса окна открытия URL: ${err}`));

Окно оплаты

Открытие окна оплаты требует создание заранее транзакции и получение кода от API SPWorlds. Затем, достаточно передать этот код в метод openPayment и у пользователя откроется окно оплаты.

const payment = await fetch('/api/buyPremium').then(r => r.json());
spm.openPayment(payment.code);

Да, нужно на сервере самостоятельно создать транзакцию. Для этого можно воспользоваться либо эндпоинтом API SPWorlds напрямую, либо одной из библиотек, созданных коммьюнити.


Оплата состоит из двух этапов, так что и пар событий две: открытие окна оплаты и сама оплата.

Событие openPaymentReponse со строкой success отправляется при удачном открытии окна, иначе - openPaymentError со строкой, содержащей информацию об ошибке. Например, транзакция по коду уже оплачена или срок оплаты истёк.

spm.on('openPaymentReponse', () => console.log('Окно успешно открыто, ждём оплату'));
spm.on('openPaymentError', err => console.error(`Не удалось открыть окно оплаты: ${err}`));

Событие paymentResponse со строкой success отправляется при удачной оплате пользователем товара. Это не значит, что нужно верить, что товар оплачен, стоит проверить это и на бэкенде, но это позволит фронтенду знать, что проверить уже пора. (Хотя, наверное, перед проверкой стоит подождать секунду-другую, пока хук до твоего бекенда дойдёт и обработается)

spm.on('paymentReponse', async () => {
  console.log('Оплата успешно произведена');

  const premiumStatus = await fetch('/api/premium').then(r => r.json());
  if (premiumStatus.active)
    user.showShinyBadge = true;
});

В случае же ошибки оплаты, например, если средств недостаточно, это будет отправлено событием paymentError в виде строки, как и в случае любого другого события ошибки.

spm.on('openPaymentError', err => console.error(`Оплатить не удалось! Ошибка: ${err}`));

В случае ошибки новую транзакцию создавать не нужно. Стоит предложить пользователю попробовать оплатить ещё раз и запросить открытие окна оплаты с тем же кодом.

Серверная часть

Из spwmini/middleware могут быть импортированы функции, помогающие с валидацией пользователя.

Действительно middleware

Функция spwmValidate принимает как обязательный аргумент токен приложения и как опциональный - настройки. Возвращает она другую функцию, уже используемую как посредник.

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

Возвращаемая функция-посредник принимает http2 запрос и http2 ответ аргументами, а потому может быть встроена посредником как в http2 сервер, так в express.js сервер и иные.

Для корректной работы в случае с express.js, она должна быть встроена как можно раньше, до применения трансформации тела запроса в json, так как она обрабатывает его самостоятельно.

Наверное это нужно переделать, сделать лучше, но неизвестно как. Контрибуторство приветствуется.

import express from 'express';
import { validate } from 'spwmini/middleware';

const app = express();
app.use('/validate', validate('SECRET_TOKEN'));
app.use(express.json());

После подобного применения посредника, его URL можно указать в клиентской части для проверки пользователя.

Единственной настройкой этой функции является отключение проверки метода. По умолчанию функция принимает только POST запросы, отклоняя любые другие методы. Указав для checkPostMethod значение false, можно позволить посреднику принимать вообще любые методы.

// Принимает совершенно любые методы
app.use('/validate', validate('SECRET_TOKEN', { checkPostMethod: false }));

// Принимает только метод PUT
app.put('/validate', validate('SECRET_TOKEN', { checkPostMethod: false }));

// Отклоняет PUT запросы с ошибкой, другие методы не принимает
app.put('/validate', validate('SECRET_TOKEN'));

Только проверка

Функция checkUser принимает пользователя, имеющегося на клиенте после инициализации, и секретный токен приложения. Возвращается логическое значение, означающее валидность информации о пользователе, что ни одна часть структуры не подменена.

import express from 'express';
import { checkUser } from 'spwmini/middleware';

const app = express();
app.use(express.json());

app.use('/validate-user', (req, res) => {
  if (!req.body.user || typeof req.body.user !== 'object')
    return res.status(400).send({ message: "Invalid user provided" });

  res.send({ valid: checkUser(req.body.user) })
});

Набор типов

Все типы импортируются из spwmini/types.

«Это немного, но это честная работа».

Уголок безопасности

Чтобы ваш сайт невозможно было использовать внутри других сайтов, кроме spworlds, нужно написать в <head> элементе:

<meta http-equiv="Content-Security-Policy" content="frame-ancestors https://spworlds.ru;">

Благодаря одной лишь этой строчке вы легко сделаете свой сайт безопаснее.

После https://spworlds.ru через пробел можно указать и свой сайт, если это где-то требуется для тестирования:

<meta http-equiv="Content-Security-Policy" content="frame-ancestors https://spworlds.ru https://example.com;">

А если есть возможность модифицировать заголовки запросов, можно для фронтенда сайта указать

X-Frame-Options: ALLOW-FROM https://spworlds.ru https://example.com