TL;DR

Посмотреть демонстрацию
Посмотреть исходный код
Установить блог премиум-контента

У вас есть отличные идеи. Ваша голова переполнена контентом, который, как вы знаете, люди будут платить за чтение. Так с чего же начать? Вы, вероятно, склонны выбрать надежную платформу, такую ​​как Wordpress, но поскольку вы хотите предлагать платный контент своим пользователям, теперь вы сталкиваетесь с проблемой создания громоздкого решения еще более серьезной.

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

В духе простого и понятного решения, которое работает именно так, как мы хотим, мы будем использовать Cosmic JS для размещения нашего блога, управления его пользователями и хранения его контента.

Наш проект будет состоять из двух частей. Во-первых, мы создадим наш блог с помощью Express и Cosmic JS и будем использовать Stripe для обработки платежей и подписок в блоге. Затем мы воспользуемся функцией расширения Cosmic JS, чтобы создать панель инструментов, которая дает нам обзор серверной части нашего бизнеса.

Часть 1: Создание блога

1. Настройка Boilerplate

Чтобы сэкономить время на шаблоне, мы будем использовать Yeoman и Express Generator (который основан на официальном генераторе Express) для начала. Если у вас не установлен Yeoman, запустите npm i -g yo. Затем установите генератор с npm i -g generator-express и запустите с yo express. Следуйте инструкциям, чтобы настроить свой проект в новом каталоге (скажем, CosmicUserBlog), установить версию Basic и использовать Handlebars для вашего механизма просмотра.

Ваша структура каталогов теперь следующая:

CosmicUserBlog
| 
|--bin
|   |--www
|--(node_modules)
|--public
|--routes
|    |--users.js
     |--index.js
|--views
     |--layouts
     |--partials
     |--error.handlebars
     |--index.handlebars
|--.bowerrc
|--.gitignore
|--app.js
|--bower.json
|--gruntfile.js
|--package.json

2. Установки

Мы будем использовать следующие пакеты:

  • Async - мощная библиотека асинхронных утилит
  • Axios - простые HTTP-запросы на основе обещаний
  • Cors - стандартное промежуточное ПО cors
  • brcypt - Для хеширования паролей. (Если вы работаете в Windows, прочтите эти заметки)
  • CosmicJs - Официальный клиент
  • Экспресс-сессия - чтобы наши пользователи могли войти в систему
  • dateformat - интуитивно понятное средство форматирования даты, которое мы будем использовать в сообщениях.
  • Stripe - Официальный клиент
  • TruncateHTML - для сообщений

Вы можете установить их с помощью npm, но я выступаю за Yarn. Это значительно быстрее, и у нас нет времени терять зря. Итак, установите Yarn (в macOS мы сделаем brew install yarn), затем запустите yarn add async axios cors bcrypt cosmicjs expres-session dateformat stripe truncate-html. Мы почти готовы начать строительство.

3. Установите Cosmic JS.

Прежде чем мы начнем строить, нам нужно будет разработать схему для нашего Cosmic Bucket. Мы хотим хранить Posts, Users и Configs (чтобы редактировать конфигурации сайта на лету).

Эти три типа объектов будут иметь следующие поля mata (все типа text, заданные их Title):

Почта:

MetafieldValuePremium true или false

Пользователь:

MetafieldValueFirst namestringLast namestringPasswordHashed StringEmailstringStripe IdstringSubsription Typestring

Конфиг:

Объект: Подписки: | Метафилд | Значение | | - - - - - - - - | - - -: | | Ежемесячная цена | строка | | Квартальная цена | строка | | Годовая цена | строка | | Аннулирование | строка |

Объект: Сайт: | Метафилд | Значение | | - - - - - | - - -: | | Заголовок сайта | строка | | Домен | Строка |

После того, как вы добавите типы объектов Post, User и Config и создадите объекты Subscriptions и Site Config, мы настроим себя с помощью Stripe.

4. Настройте Stripe.

Поскольку мы будем взимать с пользователей плату за их премиальные подписки, нам понадобится платежный процессор. Благодаря надежному API, справедливой оплате по факту использования и проверенной безопасности использование Stripe не составляет труда. Двигаясь дальше, нам потребуются как «публикуемый», так и «секретный» ключ для Stripe's API, и нам необходимо настроить планы подписки для ежемесячных, годовых и квартальных подписок. Следуйте инструкциям Stripe, чтобы создать эти подписки и присвоить им идентификаторы ежемесячная подписка, ежеквартальная подписка и годовая подписка соответственно.

5. Настройте приложение Express.

У нас установлены пакеты, разработана схема данных и создана учетная запись Stripe. Теперь нам нужно настроить серверную часть Express.

Стандартный код Express создан до ES5, поэтому для единообразия стиля мы require будем использовать нужные нам пакеты.

Вверху приложения Express добавьте:

// app.js
var session = require('express-session')
var dateFormat = require('date-format')
var truncate = require('truncate-html')
var cors = require('cors')

Когда мы развертываем наше приложение, Cosmic JS предоставит наши ключи Bucket, а также любые пользовательские ключи, которые мы предоставим через process.env. Ниже инструкций require сделайте их доступными для всего приложения, сохранив их в app.locals

//app.js
var config = {
    bucket: {
        slug: process.env.COSMIC_BUCKET,
     read_key: process.env.COSMIC_READ_KEY,
      write_key: process.env.COSMIC_WRITE_KEY
    }
}
app.locals.config = config
app.locals.stripeKeyPublishable = process.env.STRIPE_PUBLISHABLE_KEY
app.locals.stripeKeySecret = process.env.STRIPE_SECRET_KEY

Последний шаг - подключить промежуточное ПО cors и session следующим образом:

//app.js
app.locals.stripeKeySecret = process.env.STRIPE_SECRET_KEY
app.use(session({
  secret: 'sjcimsoc',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}))
app.use(cors())

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

6. Создайте вид

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

Основной макет:

В ручках каждая страница будет отображаться внутри тега body макета по умолчанию. В нашем шаблоне это /views/layouts/main.handlebars.

Нам нужно внести три изменения.

  1. в теге title замените {{title}} на {{config.site_title}}, который мы передадим через res.locals позже.
  2. В конце тега head добавьте <script src="https://js.stripe.com/v3/"></script>. Это клиент браузера Stripe. Нам это нужно только для формы оформления заказа, однако Stripe рекомендует размещать его на каждой странице, чтобы помочь в обнаружении мошенничества.
  3. Включите Bootstrap. Где-то в теге head добавьте <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> и прямо перед концом тега body добавьте
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
 integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
 crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

Главный макет теперь выглядит так:

<!-- views/layouts/main.handlebars -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>{{config.site_title}}</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
    {{#if ENV_DEVELOPMENT}}
      <script src="http://localhost:35729/livereload.js"></script>
    {{/if}}
    <script src="https://js.stripe.com/v3/"></script>
  </head>
  <body>

  {{{body}}}
  <script
    src="https://code.jquery.com/jquery-3.2.1.min.js"
    integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
    crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  </body>
</html>

Частичный заголовок:

По умолчанию Handlebars ожидает, что в каталоге /views/partials будут найдены частичные файлы, поэтому мы сделаем там header.handlebars. Это будет выглядеть так:

<header>
  <div class="container">
    <div class="row">
      <div class="col-xs-12 text-center">
        <h1>{{config.site_title}}</h1>
      </div>
    </div>
  </div>
</header>
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse-header" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
    </div>
    <div class="collapse navbar-collapse" id="navbar-collapse-header">
      <ul class="nav navbar-nav">
        <li {{#if route_posts}}class="active"{{/if}}><a href="/posts">Posts<span class="sr-only">(current)</span></a></li>
        <li><a href="/premium">Premium Content</a></li>
      </ul>
      <ul class="nav navbar-nav navbar-right">
        {{#if logged_in}}
          <li class="navbar-text text-center">Welcome Back, {{user.first_name}}!</li>
          <li><a href="/logout">Logout</a></li>
        {{/if}}
        {{#unless logged_in}}
          <li {{#if route_login}}class="active"{{/if}}><a href="/login">Login</a></li>
          <li {{#if route_signup}}class="active"{{/if}}><a href="/plans">Sign Up</a></li>
        {{/unless}}
      </ul>
    </div>
  </div>
</nav>

Примечание. В части заголовка мы используем встроенные в if и unlesshelpers Handlebars, чтобы сделать части нашего кода зависимыми от маршрута. Позже мы передадим относительные логические значения в обработчики маршрутов.

Просмотр сообщений

На странице сообщений мы хотим:

  1. Отобразить заголовок
  2. Показывать сообщение об ошибке, если пользователь пытается получить доступ к премиум-сообщениям без учетной записи
  3. Абстрагируйте логику отображения поста до ее собственного партиала, чтобы сделать наш код более чистым и модульным.

Для сообщения об ошибке мы будем полагаться на if помощника Handlebars, как и раньше. Для отображения резюме сообщений мы будем передавать сообщения в представление в виде массива объектов сообщений. Это позволяет нам использовать блок-помощник Handlebars each для итерации по этому массиву (каждое сообщение доступно как this). Тогда наше представление сообщений будет выглядеть так:

{{> header}}
<div class="container">
  <div class="row">
    {{#if error}}
      <div class="alert alert-danger" role="alert">
        {{error}}
      </div>
    {{/if}}
    {{#each posts}}
      {{> post-container this}}
    {{/each}}
  </div>
</div>

Часть почтового контейнера:

Следующим очевидным шагом является создание контейнера для отображения резюме каждого сообщения на странице сообщений. Помимо отображения заголовка и содержания сообщения (к которому легко получить доступ с помощью this.title и т. Д.), Мы хотим показать пользователю, является ли сообщение премиум-класса, а также обрезать текст сообщения и дату его создания.

Мы снова обратимся к помощнику if, чтобы показать звездочку рядом с сообщением, если this.metadata.premium вернется true, что достаточно просто. Для рекламного объявления и даты создания нам нужно изменить свойства content и created_at объекта Post; в первую очередь, чтобы сократить его, во втором, потому что Cosmic хранит дату в формате ISO datetime. Чтобы логика отображения не попадала в наши представления, Handlebars предоставляет нам возможность писать собственные помощники.

Сначала установите код просмотра на место:

<!-- /views/partials/post-container.handlebars -->
<div style="border-bottom: 3px solid #337ab7" class="col-xs-12 col-md-8 col-md-offset-2">
  <h2>
    {{#if this.metadata.premium}}
      <span style="font-size: 0.5em" class="glyphicon glyphicon-star"></span>
    {{/if}}
    <a href="/post/{{this.slug}}">{{this.title}}</a>
  </h2>
  <p style="margin: 35px 40px">
    {{truncateText this.content 20}} <a href="/post/{{this.slug}}">Read more</a>
  </p>
  <em style="margin: 20px 0" class="pull-right">
    {{date this.created_at}}
  </em>
</div>

(Подсказка: наши помощники руля будут называться truncateText и date.)

Откройте app.js и найдите фрагмент кода, который устанавливает Handlebars в качестве механизма просмотра. exphbs - это ссылка на express-handlebars, и переданный ему объект содержит параметры, используемые для создания экземпляра механизма. Нам нужно добавить к этому объекту свойство helpers. Затем свойство helpers будет указывать на методы date и truncateText следующим образом:

// app.js
// etc...
app.engine('handlebars', exphbs({
  defaultLayout: 'main',
  partialsDir: ['views/partials/'],
  helpers: {
    date: function(date) {
      return dateFormat(new Date(date), "dddd, mmmm dS, yyyy")
    },
    truncateText: function(text, length) {
      return truncate(text, length, { stripTags: true, byWords: true })
    }
  }
}));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'handlebars');
// etc...

Чтобы проиллюстрировать, {{truncateText this.content 20}} указывает Handlebars отобразить результат truncate(this.content, 20, {...} ).

Просмотр сообщения:

Наши пользователи должны иметь возможность читать отдельные сообщения, поэтому мы создадим простое представление, которое будет передавать массив длиной в один пост. (В будущем, за пределами этого руководства, мы можем найти полезным использовать представление для последовательного отображения нескольких полных сообщений.)

Если сообщение не найдено, мы также выдадим сообщение об ошибке. Создайте вид поста так:

<!-- /views/post.handlebars -->
{{> header}}
{{#if not_found}}
  <div class="container">
    <div class="row text-center">
      <div class="col-xs12 col-md-6 col-md-offset-3">
        <div class="alert alert-danger">
          Post Not Found!
        </div>
        <a href="/posts">See All Posts</a>
      </div>
    </div>
  </div>
{{/if}}
{{#each post}}
  <article class="container">
    <div class="row">
      <div class="col-xs-12 col-sm-8 col-sm-offset-2">
        <h1>{{this.title}}</h1>
        <div class="lead" style="font-size: 1.5em">
          {{{this.content}}}
        </div>
        <em class="pull-right">
          <time>{{date this.created_at}}</time>
        </em>
      </div>
    </div>
  </article>
{{/each}}

Страница входа:

Страница входа в систему будет самой простой - это базовая форма, которая отправляет POST в маршрут входа.

<!-- /views/login.handlebars -->
{{> header}}
<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-4 col-sm-offset-4">
      <h1>Log In</h1>
      <p class="text-muted">
        Log in to view premium content.
      </p>
      <form method="post">
        <input class="form-control" type="email" name="email" placeholder="Email" required/>
        <input type="password" class="form-control" name="password" placeholder="Password" required />
        <button class="btn btn-lg btn-primary btn-block submit-btn" type="submit">Log in</button>
      </form>
    </div>
  </div>
</div>
<style>
  form > input, button {
    margin-top: 12px;
  }
</style>

Страница планов:

Очевидно, что наши пользователи должны будут иметь возможность зарегистрироваться, прежде чем они смогут войти в систему, но, поскольку мы даем им возможность выбрать один из трех планов подписки, мы создадим представление, которое покажет им их варианты непосредственно перед выездом. Позже нам нужно будет передать объект Subscriptions в это представление, чтобы мы могли устанавливать плановые цены из Cosmic, а не жестко их кодировать. Вид будет выглядеть так:

<!-- /views/plans.handlebars -->
{{> header}}
<div class="container">
  <div class="row text-center lead">
    <h1>Choose a Plan to Read Premium Content</h1>
    <p>
      Sign up to view Premium Content on {{config.site_title}}
    </p>
  </div>
  <div class="row">
    <div class="col-sm-4">
      <h3 class="text-center text-muted">
        Monthly
      </h3>
      <p class="text-center">
        <strong>Billed every month</strong>
      </p>
      <h1 class="text-center text-success">{{subscriptions.monthly_price}}</h1 class="text-center text-success">
      <ul class="list-unstyled lead" style="padding: 0 20px">
        <li>
          <span class="glyphicon glyphicon-ok text-success"></span>Here's a good benefit
        </li>
        <li>
          <span class="glyphicon glyphicon-ok text-success"></span>A reason to buy
        </li>
        <li>
          <span class="glyphicon glyphicon-ok text-success"></span>Why you have to have it
        </li>
        <li>
          <span class="glyphicon glyphicon-ok text-success"></span>Why you shouldn't miss out
        </li>
        <li>
          <span class="glyphicon glyphicon-ok text-success"></span>Believe it.
        </li>
      </ul>
      <a href="/signup?plan=monthly"><button class="btn btn-block btn-default btn-lg">Sign Up</button></a>
    </div>
    <div class="col-sm-4" style="border: 2px solid #3c763d">
      <h3 style="background: #3c763d;color: white;padding: 7px 0" class="text-center text-success">
        Yearly
      </h3>
      <p class="text-center">
        <strong>Billed every 12 months</strong>
      </p>
      <h1 class="text-center text-success">{{subscriptions.yearly_price}}</h1>
        <ul class="list-unstyled lead" style="padding: 0 20px">
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Here's a good benefit
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>A reason to buy
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Why you have to have it
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Why you shouldn't miss out
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Believe it.
          </li>
        </ul>
        <a href="/signup?plan=yearly"><button class="btn btn-block btn-default btn-lg">Sign Up</button></a>
    </div>
    <div class="col-sm-4">
      <h3 class="text-center text-muted">
        Quarterly
      </h3>
      <p class="text-center">
        <strong>Billed every 3 months</strong>
      </p>
      <h1 class="text-center text-success">{{subscriptions.quarterly_price}}</h1 class="text-center text-success">
        <ul class="list-unstyled lead" style="padding: 0 30px">
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Here's a good benefit
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>A reason to buy
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Why you have to have it
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Why you shouldn't miss out
          </li>
          <li>
            <span class="glyphicon glyphicon-ok text-success"></span>Believe it.
          </li>
        </ul>
        <a href="/signup?plan=quarterly"><button class="btn btn-block btn-default btn-lg">Sign Up</button></a>
    </div>
  </div>
</div>
<style>
  form > input, button {
    margin-top: 12px;
  }
  ul.lead {
    margin-top: 40px
  }
  .lead > li {
    margin: 8px 0
  }
  .glyphicon {
    margin-right: 12px
  }
  .btn {
    margin: 35px 0px;
  }
</style>

Обратите внимание, что каждая кнопка «Зарегистрироваться» связана с маршрутом signup, передавая параметр запроса, связанный с выбранным планом. Т.е. /signup?plan={:plan}

Страница регистрации:

И последнее, но не менее важное - производитель денег. Самый сложный вид мы оставили напоследок. Это наши требования:

  1. Нам нужно передать план, выбранный на странице ранее в planName. (Позже мы сделаем это в строке запроса URL)
  2. Нам нужно использовать API Stripe Elements для сбора информации о кредитных картах. Это то, что мы включили Stripe.js в основной макет ранее. В конце формы оформления заказа нам понадобятся два div; один ID был card-element, а другой ID card-errors для Stripe.js для внедрения после первоначального рендеринга DOM.
  3. Нам нужно передать наш доступный для публикации ключ с полосой, чтобы все заработало.

Начиная с HTML у нас есть:

<!-- /views/signup.handlebars -->
{{>header}}
<div class="container">
  <div class="row">
    <form method="post" id="payment-form">
      <div class="col-xs-12 text-center">
        <h4 class="lead"><em>You're one step away from a <u>{{planName}}</u> subscription to {{config.site_title}}!</em></h4>
      </div>
    </div>
    <div class="row"  style="margin-top: 30px">
      <div class="col-md-8 col-md-offset-2">
        <h4>Enter your account details and payment information</h4>
        <label for="first_name">First name:</label>
        <input type="text" name="first_name" class="form-control" placeholder="First name" required />
        <label for="last_name">Last name:</label>
        <input type="text" name="last_name" class="form-control" placeholder="Last name" required />
        <label for="email">Email:</label>
        <input type="email" name="email" class="form-control" placeholder="Email" required />
        <label for="password">Password</label>
        <input type="password" name="password" class="form-control" placeholder="Password" required />
        <label for="card-element">Credit or debit card</label>
        <div class="form-control" id="card-element"></div>
        <div id="card-errors" role="alert"></div>
        <button id="submit-button" class="btn-success btn btn-lg btn-block btn-default" >Submit Payment</button>
      </div>
    </div>
    </form>
</div>

Затем для простоты мы воспользуемся встроенным скриптом, который интегрирует Stripe:

<!-- views/signup.handlebars -->
<script>
  var stripe = Stripe('{{stripeKeyPublishable}}')
  var elements = stripe.elements()
  var card = elements.create('card')
  var style = {
      base: {
        color: '#32325d',
        lineHeight: '24px',
        fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
        fontSmoothing: 'antialiased',
        fontSize: '16px',
        '::placeholder': {
          color: '#aab7c4'
        }
      },
      invalid: {
        color: '#fa755a',
        iconColor: '#fa755a'
      }
    };
  card.mount('#card-element', {style: style})
  // Handle real-time validation errors from the card Element.
card.addEventListener('change', function(event) {
  var displayError = document.getElementById('card-errors');
  if (event.error) {
    displayError.textContent = event.error.message;
  } else {
    displayError.textContent = '';
  }
});
// Handle form submission
var form = document.getElementById('payment-form');
var submitButton = document.getElementById('submit-button');
form.addEventListener('submit', function(event) {
  event.preventDefault();
  submitButton.disabled=true
  stripe.createToken(card).then(function(result) {
    if (result.error) {
      // Inform the user if there was an error
      var errorElement = document.getElementById('card-errors');
      errorElement.textContent = result.error.message;
      submitButton.disabled=false
    } else {
      // Send the token to your server
      stripeTokenHandler(result.token);
    }
  });
});
function stripeTokenHandler(token) {
  // Insert the token ID into the form so it gets submitted to the server
  var form = document.getElementById('payment-form');
  var hiddenInput = document.createElement('input');
  hiddenInput.setAttribute('type', 'hidden');
  hiddenInput.setAttribute('name', 'stripeToken');
  hiddenInput.setAttribute('value', token.id);
  form.appendChild(hiddenInput);
  // Submit the form
  form.submit();
}
</script>
<style>
  label,button {
    margin-top: 22px;
  }
</style>

Вот что происходит:

  1. Мы создаем экземпляр Stripe с переданным ключом Publishable, назначаем его библиотеку elements его собственной переменной, используем ее для создания элемента card и, наконец, монтируем его к <div id="card-element">, который мы создали ранее. Объект карты обрабатывает проверку карты, поставляется с хорошими функциями UX и сообщает об ошибках пользователю в режиме реального времени.
  2. Мы прикрепляем прослушиватель событий к объекту карты, который реагирует на любое изменение введенного пользователем номера карты, CCV или даты истечения срока действия. Он устанавливает эту ошибку на <div id="card-errors">
  3. Мы обрабатываем отправку формы вручную. Во-первых, мы предотвращаем действие по умолчанию и отключаем многократную отправку. Затем, исключая ошибки, мы прикрепляем скрытое поле к форме, которое содержит токен проверки Stripe, предоставленный Stripe.js, и затем отправляем форму по маршруту Signup.

7. Постройте маршруты.

Создав наши представления, мы точно знаем, какие маршруты нужны нашему приложению. А именно:

  • Сообщения
  • Почта
  • Премиум
  • Авторизоваться
  • Выйти
  • Зарегистрироваться
  • Планы

По умолчанию ваше приложение имеет Users маршрут и Index маршрут. Удалите Users и сделайте Index перенаправление на Posts вот так:

// /routes/index.js
var express = require('express')
var router = express.Router()
router.get('/', function(req, res) {
  res.redirect('/posts')
});
module.exports = router

Маршрут сообщений:

В сообщениях будет использоваться async для объединения ряда асинхронных функций: одна использует клиент Cosmic JS для получения наших сообщений, а вторая использует Cosmic для получения конфигурации сайта и рендеринга postsview, передавая соответствующие локальные переменные. Если пользователь не находится в сеансе аутентификации, мы будем использовать lodash, чтобы отфильтровать сообщения, возвращаемые Cosmic, которые не были помечены как премиум (и поэтому их можно читать). Мы будем передавать сообщения, конфигурацию и данные представления, относящиеся к маршруту, через res.locals.

var express = require('express');
var router = express.Router();
var cosmic = require('cosmicjs');
var async = require('async');
var _ = require('lodash')
router.get('/', function(req, res) {
  async.series([
    function(cb) {
      cosmic.getObjectType(req.app.locals.config, { type_slug: 'posts' }, function(err, response) {
        (req.session.user) ? res.locals.posts = response.objects.all : res.locals.posts = _.filter(response.objects.all, function(post) {
          return !post.metadata.premium
        })
        cb()
      })
    },
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {
        res.locals.config = response.object.metadata
        res.locals.user = req.session.user
        res.locals.route_posts = true
        if (req.session.user) res.locals.logged_in = true
        return res.render('posts.handlebars')
      })=
    }
  ])
});
module.exports = router;

Почтовый маршрут:

Имея маршрут для всех сообщений, нам понадобится сопутствующий маршрут для отдельного сообщения, который принимает слаг сообщения в качестве параметра URL и возвращает это сообщение, если оно найдено, используя аналогичную логику, что и маршрут Posts.

// routes/post.js
var express = require('express');
var router = express.Router();
var cosmic = require('cosmicjs');
var async = require('async');
var _ = require('lodash')
router.get('/:slug', function(req, res) {
  async.series([
    function(cb) {
      cosmic.getObjectType(req.app.locals.config, { type_slug: 'posts' }, function(err, response) {
        res.locals.post = _.filter(response.objects.all, function(post) {
          return post.slug === req.params.slug
        })
        if (!res.locals.post) res.locals.not_found = true
        cb()
      })
    },
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {
        res.locals.config = response.object.metadata
        res.locals.user = req.session.user
        return res.render('post.handlebars')
      })
    }
  ])
});
module.exports = router

Способ входа:

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

Запрос GET отобразит форму входа. Затем мы обрабатываем POST-запрос формы, извлекая всех пользователей из Cosmic и перебирая всех пользователей с помощью серии из двух асинхронных функций: одна использует bcrypt для сравнения хэшей паролей, а вторая сохраняет данные пользователя в сеансе, если они найдены.

// routes/login.js
var express = require('express');
var router = express.Router();
var cosmic = require('cosmicjs');
var async = require('async');
var _ = require('lodash')
var bcrypt = require('bcrypt')
router.get('/', function(req, res) {
  async.series([
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {
        res.locals.config = response.object.metadata
        res.locals.route_login = true
        return res.render('login.handlebars')
      })
    }
  ])
});
router.post('/', function(req, res) {
  cosmic.getObjectType(req.app.locals.config, { type_slug: 'users' }, function (err, response) {
    if (err) res.status(500).json({ status: 'error', data: response })
    else {
      async.eachSeries(response.objects.all, function (user, eachCb) {
        if (!_.find(user.metafields, { key: 'email', value: req.body.email.trim().toLowerCase() }))
          return eachCb()
        const stored_password = _.find(user.metafields, { key: 'password' }).value
        bcrypt.compare(req.body.password, stored_password, function (err, correct) {
          if (correct) res.locals.user_found = user
          eachCb()
        })
      }, function () {
        if (res.locals.user_found) {
          req.session.user = {
            first_name: res.locals.user_found.metafield.first_name.value,
            last_name: res.locals.user_found.metafield.last_name.value,
            email: res.locals.user_found.metafield.email.value
          }
          req.session.save()
          return res.redirect('/posts')
        }
        return res.status(404).json({ status: 'error', message: 'User not found' })
      })
    }
  })
})
module.exports = router

Маршрут Планов:

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

var express = require('express');
var router = express.Router();
var cosmic = require('cosmicjs');
var async = require('async')
router.get('/', function(req, res) {
  async.series([
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {
        res.locals.config = response.object.metadata
        res.locals.route_signup = true
        cb()
      })
    },
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function(err, response) {
        res.locals.subscriptions = response.object.metadata
        res.render('plans.handlebars')
      })
    }
  ])
});
module.exports = router;

Путь регистрации:

Как вы, наверное, догадались, из них больше всего происходит процедура регистрации. Вот что нам нужно реализовать:

  1. По запросу GET визуализируйте форму регистрации и передайте наш публикуемый ключ Stripe через res.locals, чтобы Stripe.js работал.
  2. По почтовому запросу:
  3. Создайте экземпляр Stripe на стороне сервера с помощью нашего секретного ключа
  4. Если пользователь уже вошел в систему, перенаправьте его.
  5. Запустите серию из двух именованных асинхронных функций (чтобы мы могли использовать их возвращаемые значения после завершения обеих), чтобы получить данные нашей подписки из Cosmic и хэшировать пароль.
  6. Завершив шаг 3, мы используем Stripe API для создания нового клиента, связывая его способ оплаты через токен Stripe, который мы передали из формы регистрации.
  7. Затем мы взимаем с этого клиента плату в соответствии с выбранным планом и создаем новую подписку (снова через Stripe), чтобы регулярные платежи обрабатывались автоматически.
  8. Мы создаем новый объект User на основе нашей схемы Cosmic, добавляем его в нашу корзину, и как только это будет успешным, мы создаем новый сеанс для пользователя и перенаправляем его на маршрут Posts, где они теперь смогут просматривать премиум-контент.

Все готово, это будет выглядеть так:

// routes/signup.js
var express = require('express');
var router = express.Router();
var cosmic = require('cosmicjs');
var async = require('async');
var bcrypt = require('bcrypt')
router.get('/', function(req, res) {
  if (req.session.user) res.redirect('/')
  async.series([
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'site' }, function(err, response) {
        res.locals.config = response.object.metadata
        res.locals.route_signup = true
        cb()
      })
    },
    function(cb) {
      cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function(err, response) {
        res.locals.subscriptions = response.object.metadata
        res.locals.stripeKeyPublishable = req.app.locals.stripeKeyPublishable
        res.locals.planName = req.query.plan
        res.render('signup.handlebars')
      })
    }
  ])
});
router.post('/', function(req, res) {
  var stripe = require('stripe')(req.app.locals.stripeKeySecret)
  if (req.session.user) res.redirect('/')
  async.series({
    subscriptions: function(callback) {
      cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function(err, response) {
        callback(null, response.object.metadata)
      })
    },
    hash: function (callback) {
      bcrypt.hash(req.body.password, 10, function (err, hash) {
        callback(null, hash)
      })
    }
  }, function (err, results) {
    stripe.customers.create({
      email: req.body.email,
      source: req.body.stripeToken
    }).then(function (customer) {
      return stripe.charges.create({
        amount: results.subscriptions[req.query.plan + "_price"].replace(/[$]/,'') + '00',
        currency: "usd",
        customer: customer.id
      })
    }).then(function (charge) {
      stripe.subscriptions.create({
        customer: charge.customer,
        items: [
          {
            plan: 'subscription-' + req.query.plan
          }
        ]
      })
      var object = {
        type_slug: 'users',
        title: req.body.first_name + ' ' + req.body.last_name,
        metafields: [
          {
            title: 'First name',
            key: 'first_name',
            type: 'text',
            value: req.body.first_name
          },
          {
            title: 'Last name',
            key: 'last_name',
            type: 'text',
            value: req.body.last_name
          },
          {
            title: 'Password',
            key: 'password',
            type: 'text',
            value: results.hash
          },
          {
            title: 'Email',
            key: 'email',
            type: 'text',
            value: req.body.email
          },
          {
            title: 'Stripe Id',
            key: 'stripe_id',
            type: 'text',
            value: charge.customer
          },
          {
            title: 'Subscription Type',
            key: 'subscription_type',
            type: 'text',
            value: req.query.plan
          }
        ]
      }
      if (req.app.locals.config.bucket.write_key) object.write_key = req.app.locals.config.bucket.write_key
      cosmic.addObject(req.app.locals.config, object, function (err, reponse) {
        if (err)
          res.status(500).json({ data: reponse })
        else {
          req.session.user = {
            first_name: req.body.first_name,
            last_name: req.body.last_name,
            email: req.body.email
          }
          req.session.save()
          res.redirect('/posts')
        }
      })
    })
  })
})
module.exports = router

Путь выхода:

Для удобства пользователей нам нужно дать нашим пользователям возможность выйти из системы. Все, что для этого требуется, - это запрос POST на /logout и быстрый session.destroy() вызов.

var express = require('express')
var router = express.Router()
/* GET home page. */
router.get('/', function(req, res) {
  req.session.destroy()
  return res.redirect('/')
});
module.exports = router

Соединяем их вместе:

Создав все наши маршруты и подготовив их к работе по мере необходимости, мы require их все в нашем приложении и укажем на них связанные точки через app.use()

// app.js
var routes = require('./routes/index');
var posts = require('./routes/posts');
var post = require('./routes/post')
var login = require('./routes/login')
var logout = require('./routes/logout')
var signup = require('./routes/signup')
var plans = require('./routes/plans')
var premium = require('./routes/premium')
var api = require('./routes/api')
app.use('/', routes);
app.use('/post', post)
app.use('/posts', posts)
app.use('/login', login)
app.use('/logout', logout)
app.use('/signup', signup)
app.use('/plans', plans)
app.use('/premium', premium)
app.use('/api', api)

Переход к расширению:

Если вы сделали все правильно до этого момента, ваш блог теперь работает именно так, как вы ожидали. Чтобы проверить, запустите npm start, создайте несколько сообщений в Cosmic и убедитесь, что они загружаются. Затем создайте фиктивную учетную запись и убедитесь, что она хранится в Cosmic и зарегистрирована в Stripe. Затем мы создадим расширение для панели управления Cosmic.

Часть 2: Создание расширения

Stripe предоставляет нам впечатляющий объем аналитики, однако мы хотим, чтобы в центре можно было быстро просмотреть список всех наших пользователей, их план подписки и три ключевых показателя нашего блога: доход на сегодняшний день, активные подписки и отмены на сегодняшний день.

Cosmic JS дает нам возможность сделать это, используя функцию расширения, которая позволяет нам загружать SPA с index.html в качестве точки входа, которая загружается во фрейм на нашей панели инструментов Cosmic. Ключи сегментов затем предоставляются ему через строки запроса URL.

Мы будем создавать расширение с помощью React, потому что нашему расширению требуется только слой представления.

Настраивать

Чтобы сохранять организованность, мы будем хранить наше приложение в нашем CosmicUserBlog каталоге. Наше дерево будет выглядеть так:

CosmicUserBlog
|
|--extensions
| |--subscription-management
| | |--client
| | | |--components
| | | |--index.js
| | | |--index.html
| | |--dist

Когда ваша структура каталогов будет создана, запустите yarn init, и мы перейдем к установке.

Установки

Нам нужны эти пакеты:

  • асинхронный
  • аксиомы
  • babel-preset-2015
  • Вавилон-пресет-2016
  • cosmicjs
  • html-webpack-plugin - для генерации нашего html с помощью webpack
  • Lodash
  • строка запроса - простой способ разобрать ключи ведра
  • дорожка
  • реагировать
  • React-dom
  • реактивная загрузка
  • веб-пакет
  • вавилонское ядро
  • вавилонский погрузчик
  • Вавилон-предустановка-реакция

Беги yarn add async axios babel-preset-2015 babel-preset-2016 cosmicjs html-webpack-plugin lodash query-string path react react-dom react-loading webpack babel-core babel-loader-babel-preset-react, и мы нырнем.

Настроить Webpack и Babel

Сначала создайте .babelrc в корневой папке и расскажите, как транспилировать наш код:

// .babelrc
{
  "presets": [
    "es2016", "es2015", "react"
  ]
}

Затем, снова в CosmicUserBlog/extensions/subscription-management, сделайте webpack.config.js, чтобы мы могли указать Webpack, как упаковывать наши модули.

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: './client/index.js',
  output: {
    path: path.resolve('dist'),
    filename: 'index_bundle.js'
  },
  module: {
    loaders: [
      { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './client/index.html',
      filename: 'index.html',
      inject: 'body'
    })
  ]
}

dist будет содержать все наши выходные файлы (это index_bundle.js и index.html), и мы в конечном итоге сжимаем dist для загрузки в качестве нашего расширения. html-webpack-plugin возьмет наш html-шаблон из client/index.html и при сборке будет ссылаться на наш скомпилированный javascript в dist/index.html.

Создать файл входа

Мы хотим использовать Boostrap, и нам нужен div (который мы будем обозначать как root) для нашего приложения React, чтобы смонтировать на него. client/index.html должен выглядеть так:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
  </head>
  <body>
    <div id="root">
      <script
        src="https://code.jquery.com/jquery-3.2.1.min.js"
        integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
        crossorigin="anonymous"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  </body>
</html>

Создайте приложение React

1. Создайте точку входа

У нас есть все необходимое, чтобы мы начали пачкать руки. Мы будем использовать client/index.js как точку входа для нашего приложения React. Мы импортируем React, установим его как глобальный, передадим ему наши космические ключи в его реквизитах и ​​смонтируем компонент приложения (который мы создадим дальше) в <div id="root">.

import React from 'react'
import ReactDom from 'react-dom'
import App from './components/App'
import QueryString from 'query-string'
window.React = React
const url = QueryString.parse(location.search)
const cosmic = { bucket: {
    slug: url.bucket_slug,
    write_key: url.write_key,
    read_key: url.read_key
  }
}
ReactDom.render(
  <App cosmic={cosmic}/>,
  document.getElementById('root')
)

2. Создайте компонент приложения

Чтобы наше приложение оставалось модульным, у нас будет многоуровневая структура компонентов, которая выглядит следующим образом:

components
|
|--App.js
|--Header.js
|--SubscriberData
| |
| |--SubscriberContainer.js
| |--Loader.js
| |--StatsContainer.js
| |--StatTicker.js
| |--UserList.js

Header и SubscriberContainer будут непосредственными потомками App. Loader, UserList и StatsContainer будут непосредственными потомками SubcriberContainer. Наконец, StatsContainer будет состоять из StatTicker.

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

Начиная с вершины иерархии, мы создадим компонент приложения, который хранит наши ключи Cosmic в своем состоянии и отображает Header и SubscriberContainer.

// components/App.js
import { Component } from 'react'
import Header from './Header'
import SubscriberContainer from './SubscriberData/SubscriberContainer'
export default class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      cosmic: this.props.cosmic
    }
  }
  render() {
    return (
      <div>
        <Header
          bucket={this.state.cosmic.bucket.slug} />
        <SubscriberContainer
          cosmic={this.state.cosmic} />
      </div>
    )
  }
}

Следующим очевидным шагом является создание компонента Header.

Создайте заголовок:

Заголовок будет нашим первым функциональным компонентом без сохранения состояния, в котором в качестве опоры будет использоваться только наш слаг ведра:

const Header = ({ bucket }) =>
  <nav className="navbar navbar-default">
    <div className="container-fluid">
      <ul className="nav navbar-nav">
        <li className="navbar-text"><strong>Managing Subscriptions for: </strong><em>{bucket}</em></li>
      </ul>
    </div>
  </nav>
export default Header

Создайте контейнер подписчика:

SubscriberContainer будет обрабатывать всю логику, связанную с данными наших подписчиков, и отображать StatsContainer и UserList для отображения обрабатываемых данных.

SubscriberContainer будет классом React с отслеживанием состояния, содержащим следующее:

  1. Конструктор, который инициализирует состояние SubscriberContainer, чтобы содержать наши ключи Cosmic (которые мы передали как свойства) и отражать данные наших подписчиков. Мы инициализируем статистику доходов, пользователей и отмен на 'Loading…', а их состояние загрузки - на true.
  2. Переопределение для componentDidMount(), чтобы наш компонент извлекал нужные нам данные после его монтирования в DOM, а затем обновлял эти данные каждую минуту.
  3. Метод getRevenue() для получения (по общему признанию хакерским) нашего дохода путем перебора типов подписки наших активных пользователей из Cosmic и сохранения рассчитанного дохода в состоянии.
  4. Метод getUsers() для получения всех наших пользователей из Cosmic и сохранения их в массиве в состоянии, а также их общее количество.
  5. Метод getCancellations() для получения количества отмененных подписок из нашего объекта конфигурации Subscriptions. (Позже мы обновим это число с помощью веб-перехватчика от Stripe.)
  6. Метод render() для визуализации нашего Loader компонента (только если мы получаем данные), StatsContainer и UserList.

Все вместе мы получаем следующее:

import { Component } from 'react'
import Cosmic from 'cosmicjs'
import async from 'async'
import _ from 'lodash'
import StatsContainer from './StatsContainer'
import Loader from './Loader'
import UserList from './UserList'
const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 2
})
export default class App extends Component {
 
  constructor(props) {
    super(props)
    this.state = {
      cosmic: this.props.cosmic,
      stats: {
        revenue: 'Loading...',
        users: 'Loading...',
        cancellations: 'Loading...'
      },
      users: [],
      fetchingRevenue: true,
      fetchingUsers: true,
    }
  }
  fetchData() {
    this.getRevenue();this.getUsers();this.getCancellations()
  }
  componentDidMount() {
    this.fetchData()
    setInterval(() => {
      this.fetchData()
    }, 60000)
  }
  getRevenue(cosmic) {
    this.setState({ fetchingRevenue: true})
    async.series([
      callback => {
        Cosmic.getObject(this.state.cosmic, { slug: 'subscriptions' }, (err, response) => {
          callback(null, response.object)
        })
      },
      callback => {
        Cosmic.getObjectType(this.state.cosmic, { type_slug: 'users' }, (err, response) => {
          callback(null, response.objects.all)
        })
      }
    ], (err, results) => {
      let subscriptions = results[0], users = results[1];
      let currentStats = this.state.stats
      currentStats.revenue = formatter.format(users.map(user =>
        parseInt(subscriptions.metadata[`${user.metadata.subscription_type}_price`].replace('$', ''))
      )
      .reduce((sum, val) => sum + val))
      this.setState({ stats: currentStats })
      this.setState({ fetchingRevenue: false })
    })
  }
  getUsers(cosmic) {
    this.setState({ fetchingUsers: true })
    Cosmic.getObjectType(this.state.cosmic, { type_slug: 'users' }, (err, response) => {
      if (err) {
        currentStats = this.state.stats
        currentStats.users = 'Error'
        this.setState({ stats: currentStats })
      } else {
        let currentStats = this.state.stats
        currentStats.users = isNaN(response.total) ? 0 : response.total
        this.setState({ stats: currentStats })
        this.setState({ users: response.objects.all })
        this.setState({ fetchingUsers: false })
      }
    })
  }
  getCancellations(cosmic) {
    this.setState({ fetchingCancellations: true})
    Cosmic.getObject(this.state.cosmic, { slug: 'subscriptions' }, (err, response) => {
      if (err) {
        currentStats = this.state.stats
        currentStats.users = 'Error'
        this.setState({ stats: currentStats })
      } else {
        let currentStats = this.state.stats
        currentStats.cancellations = isNaN(response.object.metadata.cancellations) ? 0: response.object.metadata.cancellations
        this.setState({ stats: currentStats })
        this.setState({ fetchingCancellations: false })
      }
    })
  }
  render() {
    return (
      <div className="container">
        <Loader loadingState={this.state.fetchingUsers || this.state.fetchingRevenue || this.state.fetchingCancellations} />
        <StatsContainer stats={this.state.stats} />
        <UserList users={this.state.users}/>
      </div>
    )
  }
}

Теперь нам осталось создать четыре функциональных компонента без сохранения состояния. Эти:

1. Загрузчик:

import ReactLoading from 'react-loading'
const Loader = ({ loadingState }) =>
  <div className="row" style={{display: loadingState ? 'block' : 'none' }}>
    <div className="col-xs-12">
      <div className="pull-right">
        <ReactLoading height='20px' width='20px' type="spin" color="#444" />
      </div>
    </div>
  </div>
export default Loader

Что использует удобный пакет react-loading.

2. StatsContainer:

import StatTicker from './StatTicker'
const StatsContainer = ({ stats }) =>
  <div className="row">{Object.keys(stats).map((key, index) =>
      <div key={index} className="col-md-4 text-center"><StatTicker name={key} value={stats[key]} /></div>
    )}
  </div>

export default StatsContainer

3. StatTicker:

const StatTicker = ({ name, value }) =>
  <div><h3 className="lead text-muted">{name}</h3><h1 className="text-primary">{value}</h1></div>
export default StatTicker

и наконец…

4. UserList:

const UserList = ({ users, deleteUser }) =>
  <div style={{marginTop: 50 + 'px'}} className="row">
    <div className="col-xs-12">
      <h4 className="pull-left lead">All Users:</h4>
      <table className="table table-responsive table-hover">
        <thead>
          <tr>
            <th>Stripe ID</th>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user, index) =>
            <tr key={index}>
              <td>{user.metadata.stripe_id}</td>
              <td>{user.metadata.first_name}</td>
              <td>{user.metadata.last_name}</td>
              <td>{user.metadata.email}</td>
            </tr>
          )}
        </tbody>
      </table>
    </div>
  </div>

export default UserList

Интегрируйте Stripe Webhooks

Чтобы узнать количество отмененных подписчиков, мы обновим метаполе cancellations в нашем объекте Cosmic Subscriptions. Для этого мы будем получать веб-перехватчик от Stripe через наше приложение Express каждый раз, когда удаляем подписку на Stripe.

  1. Настройте веб-перехватчики в Stripe и укажите им домен, в котором развернуто ваше приложение Express.
  2. Создайте api маршрут в CosmicUserBlog/routes/api.js и require его в App.js.
  3. Обрабатывайте запросы POST с помощью оператора switch, действующего на req.body. Когда Stripe отправляет нам веб-перехватчик отмены подписки, req.body.type будет customer.subscription.deleted.
  4. Удалите объект User из Cosmic, получите объект Subscription из Cosmic, неглубоко скопируйте объект, увеличьте metadata.cancellations, затем используйте REST API Cosmic для внесения изменений в объект.
  5. Ответьте кодом 200, чтобы Stripe мог подтвердить получение веб-перехватчика.

Вот такой готовый продукт:

var express = require('express')
var router = express.Router()
var cosmic = require('cosmicjs')
var axios = require('axios')
router.post('/', function(req, res) {
  event = req.body
  switch (event.type) {
    case 'customer.subscription.deleted':
      cosmic.deleteObject(req.app.locals.config, { slug: 'user', write_key: req.app.locals.config.bucket.slug }, function (err, response) {
        cosmic.getObject(req.app.locals.config, { slug: 'subscriptions' }, function (err, response) {
          var currentObject = response.object
          currentObject.metadata.cancellations = currentObject.metadata.cancellations + 1
          currentObject.metafield.cancellations.value = currentObject.metadata.cancellations + 1
          currentObject.write_key = req.app.locals.config.bucket.write_key
          axios({
            method: 'put',
            url: `https://api.cosmicjs.com/v1/${req.app.locals.config.bucket.slug}/edit-object`,
            data: currentObject
          }).then(function (axRes) {
            console.log('Success')
          }).catch(function (axError) {
            console.log('Error')
          })
        })
      })
      return res.json({ received: true})
      break;
    default:
      return res.json({ received: false })
  }
});
module.exports = router

Развертывать

Чтобы сообщить Cosmic, какое у вас расширение, вам нужно добавить extension.json в свою distпапку. Мы настроим наше расширение так:

// dist/extension.json
{
  "title": "Subscription Management",
  "font_awesome_class": "fa-gears",
  "image_url": ""
}

Вывод

Используя Cosmic JS, Express, Stripe и React, мы создали как монетизируемый блог, который позволяет нашим читателям подписываться на чтение премиум-контента, так и удобную панель инструментов для просмотра данных о нашем блоге. Мы интегрировали Stripe для безопасных платежей и создали приложение, которое делает все, что мы хотим, с возможностью роста.

С учетом того, насколько быстро мы смогли создать наше приложение, а также с учетом простоты его развертывания и обслуживания, становится ясно, что Cosmic JS является единственным в своем роде подходом к управлению контентом через API. Ясно, что CosmisJS зарабатывает деньги.

Эта статья изначально появилась в блоге Cosmic JS.

Мэтт Кейн создает интеллектуальные веб-приложения и пишет о технологиях, используемых для их создания. Вы можете узнать о нем больше из его портфолио.