Безголовый WordPress как API для приложения Next.js

WordPress уже давно является основной CMS для управления контентом вашего веб-сайта, но быстрое развитие интерфейсных технологий открывает новые возможности. Давайте окунемся в шумиху и увидим смесь старого и нового: CMS WordPress с внешним интерфейсом, обслуживаемым приложением Next.js!

Наша мотивация

Последние годы показали, что популярность архитектуры Jamstack растет, и можно с уверенностью предположить, что она не изменится в ближайшее время. Каждый год рождаются новые JavaScript-фреймворки, а существующие становятся все популярнее и приобретают новых пользователей.

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

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

Идеальное решение для фронтенд- и бэкэнд-менеджеров, а также для контент-менеджеров, не занимающихся кодированием.

Это бесплатно (как в пиве, так и в свободе) и легко настраивается даже нетехническим пользователем. Более того, многие пользователи уже знакомы с панелью администратора WordPress. Слой WordPress CMS быстро развивается (просто взгляните на редактор Gutenberg — его любят или ненавидят, но он становится все более важным инструментом в вашей настройке WordPress), и у него есть большое сообщество, которое может предоставить доступ к бесчисленным темам и плагинам, позволяющим вам изменить свой сайт в соответствии с вашими прихотями.

С другой стороны, дизайн темы WordPress требует знания PHP , которого может не быть у среднего разработчика интерфейса, и не позволяет использовать современные, готовые к использованию интерфейсные фреймворки. Даже когда это возможно, реализовать тему WordPress с помощью React непросто и неудобно, а простая реализация может привести к плохой оценке SEO.

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

Ниже вы увидите доказательство концепции такого решения с Next.js в качестве фреймворка, с помощью которого мы создадим внешний слой. Таким образом, мы будем стремиться к достижению высоких показателей SEO благодаря рендерингу на стороне сервера.

Приложение будет работать в паре с WordPress как безголовая CMS. Если вы хотите сразу перейти к коду, перейдите в репозиторий GitHub с приложением Next.js и соответствующей безголовой темой WordPress , содержащей упомянутое доказательство концепции.

Получение данных WordPress для приложения Next.js

Для удобства мы будем использовать WordPress API, но не встроенный WordPress REST API. Вместо этого мы будем использовать GraphQL API для получения контента WordPress. Для этого нам понадобится несколько плагинов.

Давайте установим плагин WPGraphQL (и, кроме того, плагин WPGraphQL Offset Pagination ), чтобы расширить нашу WordPress CMS функциональностью GraphQL API и заменить разбиение на страницы на основе курсора на разбиение на страницы на основе страниц. Это понадобится вам для создания приложения Next.js с теми же маршрутами, которые используются на сайте WordPress.

Плагин WPGraphQL

Мы будем использовать Axiosдля получения данных из API. Давайте сначала напишем клиент API.

const client = axios.create({ baseURL: `${process.env.WP_PROTOCOL}://${process.env.WP_DOMAIN}/graphql` });

client.interceptors.response.use(
  ({ data }) => data,
  (error) => Promise.reject(error)
);

function graphQL(query: string, variables: Record<string, any>, options?: AxiosRequestConfig) {
  return client.post("/", { query, variables }, options);
}

страницы WordPress

Мы начнем с реализации представления, отображающего страницу WordPress, так как это немного проще, чем реализация записи WordPress. Мы будем извлекать содержимое WordPress каждой страницы по ее слагу в формате getStaticProps. Нам также нужно получить список страниц, которые будут использоваться в файлах getStaticPaths. Давайте сначала создадим наши запросы.

export async function getPageBySlug(slug: string) {
  return graphQL(
    `query PageBySlug($slug: String!) {
      pageBy(uri: $slug) {
        title
        content(format: RENDERED)
        slug
      }
    }`,
    { slug }
  );
}

export async function getPages(page: number, perPage: number) {
  return graphQL(
    `
    query AllPages($size: Int!, $offset: Int!) {
      pages(where: {offsetPagination: { size: $size, offset: $offset }}) {
        edges {
          node {
            title
            slug
          }
        }
        pageInfo {
          offsetPagination {
            total
          }
        }
      }
    }
  `,
    { offset: (page - 1) * perPage, size: perPage }
  );
}

Теперь пришло время использовать эти запросы. Мы будем использовать маршрут по умолчанию src/pages/[...slug]/index.tsxдля отображения наших страниц там. Сначала мы реализуем наши функции выборки данных.

export const getStaticPaths: GetStaticPaths = async () => {
  // Returns a list of { slug: string } objects, uses getPages underneath
  const pages = await getAllPages();

  return {
    paths: pages.map(({ slug }) => ({ params: { slug: slug.split("/") } })),
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async (ctx) => {
  if (!ctx.params || !ctx.params.slug) {
    return { notFound: true };
  }

  const {
    data: { pageBy: props },
  } = await getPageBySlug(ctx.params.slug.join("/"));

  return { props };
};

Теперь мы можем использовать данные, возвращаемые getStaticProps компонентом React, отображаемым на странице.

import parseHTML from "html-react-parser";


const Page = (page: PageProps) => (
  <>
    <Head>
      <title>{page.title}</title>
    </Head>
    <article>
      <header className={styles.header}>
        <h1 className={styles.title}>{page.title}</h1>
      </header>
      <div className={styles.content}>{parseHTML(page.content)}</div>
    </article>
  </>
);

export default Page;
страницы wordpress next.js

сообщения WordPress

Аналогичным образом мы реализуем представление, отображающее сообщения WordPress. Опять же, сначала мы реализуем запросы данных.

export type Post = {
  id: string;
  author: { node: Author };
  date: string;
  content: string;
  slug: string;
  title: string;
  categories: { edges: { node: Term }[] };
  tags: { edges: { node: Term }[] };
};

export async function getPostBySlug(slug: string): Promise<{data: {post: Post}}> {
  return graphQL(
    `query PostBySlug($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        author {
          node {
            name
            slug
            avatar {
              url
            }
          }
        }
        id
        title
        content(format: RENDERED)
        slug
        date
        tags {
          edges {
            node {
              name
              slug
            }
          }
        }
        categories {
          edges {
            node {
              name
              slug
            }
          }
        }
      }
    }`,
    { slug }
  );
}

export async function getPosts(page: number, perPage: number) {
  return graphQL(
    `
    query AllPosts($size: Int!, $offset: Int!) {
      posts(where: {offsetPagination: { size: $size, offset: $offset }, orderby: { field: DATE, order: DESC }}) {
        edges {
          node {
            id
            author {
              node {
                name
                slug
                avatar {
                  url
                }
              }
            }
            title
            content(format: RENDERED)
            slug
            date
            tags {
              edges {
                node {
                  name
                  slug
                }
              }
            }
            categories {
              edges {
                node {
                  name
                  slug
                }
              }
            }
          }
        }
        pageInfo {
          offsetPagination {
            total
          }
        }
      }
    }
  `,
    { offset: (page - 1) * perPage, size: perPage }
  );
}

Давайте перейдем к функциям выборки данных WordPress далее в src/pages/[year]/[month]/[day]/[slug]/index.tsx. На этот раз мы создадим только несколько первых сообщений (обратите внимание fallback: "blocking"на getStaticPaths). Остальные будут сгенерированы в первый раз, а затем запрошены и кэшированы на будущее. Это сократит продолжительность сборки. Вполне вероятно, что нам никогда не понадобится отображать большинство старых сообщений, так зачем нам их предварительно генерировать?

export async function getStaticPaths() {
  const {
    data: {
      posts: { edges },
    },
  } = await getPosts(1, POSTS_PER_PAGE);

  return {
    paths: edges.map(({ node: { date, slug } }) => ({
      params: {
        year: format(parseISO(date), "yyyy"),
        month: format(parseISO(date), "MM"),
        day: format(parseISO(date), "dd"),
        slug,
      },
    })),
    fallback: "blocking",
  };
}

export const getStaticProps: GetStaticProps = async (ctx) => {
  if (!ctx.params || !ctx.params.slug) {
    return { notFound: true };
  }

  const {
    data: { post: props },
  } = await getPostBySlug(ctx.params.slug);

  return { props };
};

Теперь мы можем написать компонент, отображающий один пост WordPress.

import { Post } from "components/post/Post";

const PostPage = (post: PostPageProps) => (
  <>
    <Head>
      <title>{post.title}</title>
    </Head>
    <Post post={post} />
  </>
);

export default PostPage;
wordpress публикует скриншот next.js

Страницы архива

Итак, мы реализовали просмотры записей и страниц. Теперь пришло время реализовать страницу архивов. В /page/<pageNumber>/URL-адресе мы хотели бы отобразить список сообщений. Кроме того, мы хотели бы отображать первую страницу в корне приложения и перенаправлять /page/1/` to `/. Во-первых, давайте добавим перенаправление в next.config.js файл.

module.exports = {
  ...,
  async redirects() {
    return [
      {
        source: "/page/1/",
        destination: "/",
        permanent: true,
      }
    ];
  },
  trailingSlash: true,
  ...
};

Теперь давайте реализуем страницу архива в формате src/pages/page/[page]/index.tsx. Как и раньше, мы начнем с функций, извлекающих данные (то есть список сообщений) для страницы архивов. Мы будем использовать генерацию статического сайта (функция инкрементной статической генерации Next.js), чтобы сначала отображать их, когда будет запрошена страница разбиения на страницы, и в следующий раз обслуживать кешированную версию.

export const getStaticPaths: GetStaticPaths = async () => ({
  paths: [],
  fallback: "blocking"
});

Метод getStaticPropsдля страницы архивов будет выглядеть следующим образом:

export const getStaticProps = async ({ params }: GetStaticPropsContext<{ page: string }>) => {
  if (!params || !params.page) {
    return { notFound: true };
  }

  const page = parseInt(params.page);
  const {
    data: {
      posts: {
        edges,
        pageInfo: {
          offsetPagination: { total },
        },
      },
    },
  } = await getPosts(page, POSTS_PER_PAGE);
  const totalPages = Math.ceil(total / POSTS_PER_PAGE);

  return edges.length > 0
    ? {
        props: {
          posts: edges.map(({ node }) => node),
          pagination: { currentPage: page, totalPages, href: "/" },
        },
      }
    : { notFound: true };
};

Осталось отобразить данные, возвращенные getStaticPropsв представлении.

import { Pagination } from "components/pagination/Pagination";
import { Post } from "components/post/Post";

export const ArchivesPage = ({ posts, pagination }: ArchivesPageProps) => (
  <div>
    <div>
      {posts.map((post) => (
        <Post key={post.id} post={post} />
      ))}
    </div>
    {pagination.totalPages > 1 && <Pagination {...pagination} className={styles.pagination} />}
  </div>
);
export const ArchivesPage;
архивы wordpress в next.js

Это касается всех страниц архива, кроме первой. Этот случай будет рассмотрен на индексной странице в src/pages/index.tsxфайле.

К счастью, с этим можно справиться с помощью нескольких строк кода. Мы будем использовать тот же компонент React. Более того, то, что getStaticProps мы написали ранее, можно использовать повторно!

import { GetStaticPropsContext } from "next";

import ArchivesPage, { getStaticProps as getPostsArchiveStaticProps } from "pages/page/[page]";

export default ArchivesPage;

export const getStaticProps = async (ctx: GetStaticPropsContext<{}>) =>
  getPostsArchiveStaticProps({ ...ctx, params: { page: "1", ...ctx.params } });

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

Запуск сборки Next.js при обновлении поста/страницы

Хорошо, у нас работает приложение. Приложение Next.js заполняется данными с сайта WordPress. Тем не менее, было бы неплохо обновлять данные всякий раз, когда запись или страница изменяется в панели администратора WordPress.

Предположим, что приложение Next.js размещается на Vercel . На самом деле подойдет любая платформа, если она позволяет инициировать новое развертывание с помощью запроса POST по указанному URL-адресу. В случае с Vercel мы будем использовать Deploy Hooks .

next.js создает безголовый wordpress

безголовый WordPress API с рядом

 

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

Мы будем использовать плагин Vercel Deploy Hooks для WordPress, чтобы инициировать новое развертывание при каждом изменении поста/страницы. Давайте создадим URL-адрес для Deploy Hook, как описано в документации Vercel. Скопируйте его в конфигурацию плагина, установите флажок «Активировать развертывание после обновления» и вуаля!

вордпресс следующий

Каждый раз, когда страница/сообщение обновляется или создается новое сообщение, будет запускаться новое развертывание, чтобы гарантировать, что содержимое нашего приложения Next.js всегда будет актуальным.

Предварительный просмотр постов и страниц

Во-первых, мы сделаем еще несколько предположений о наших приложениях, чтобы предварительный просмотр работал:

  • Сайт WordPress использует тему Headnext ,
  • И экземпляр WordPress, и веб-приложения Next.js находятся в поддоменах общего домена (например, admin.domain.comи next.domain.com)  .
  • В качестве альтернативы, сайт WordPress находится в поддомене (например admin.domain.com) домена приложения Next.js (то есть domain.com),
  • Имена файлов cookie WordPress устанавливаются в домене с точкой в ​​начале, что потребует добавления строки ниже в wp-config.php.
define('COOKIE_DOMAIN', '.domain.com');

Тема Headnext изменяет URL-адрес предварительного просмотра WordPress по умолчанию. Остальные предположения гарантируют, что приложение Next.js сможет получить доступ к файлам cookie WordPress для аутентификации и получения данных предварительного просмотра.

Маршрут предварительного просмотра в приложении Next.js

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

Мы будем использовать режим предварительного просмотра Next.js, чтобы переопределить getStaticProps данные для этих страниц. Таким образом, нам не нужно отказываться от скорости статической генерации, но при этом иметь возможность повторно использовать существующий код.

Но сначала мы реализуем конечную точку API для предварительного просмотра поста/страницы в src/pages/api/[type]/[id]/preview/index.tsфайле. И тип предварительного просмотра (запись/страница), и идентификатор записи/страницы будут переданы в URL-адресе. Мы также передадим nonce строку запроса для доступа к защищенным данным из GraphQL API.

import Cookies from "cookies";
import format from "date-fns/format";
import parseISO from "date-fns/parseISO";
import type { NextApiRequest, NextApiResponse } from "next";

import { getPageRevisions } from "api/pages";
import { getPostById, getPostRevisions } from "api/posts";

import { authorizationCookieName } from "utils/wordpress";

enum PreviewType {
  Post = "post",
  Page = "page",
}

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const cookies = new Cookies(req, res);

  if (!req.query.nonce) {
    return res.status(401).json({ message: "Invalid nonce token!" });
  }

  const cookieName = authorizationCookieName();
  const id = parseInt(req.query.id as string, 10);
  const headers = { "X-WP-Nonce": req.query.nonce as string, Cookie: `${cookieName}=${cookies.get(cookieName)}` };

  try {
    let revisions;

    if (req.query.type === PreviewType.Page) {
      const { data } = await getPageRevisions(id, { headers });
      revisions = data.page.revisions.edges;
    } else {
      const { data } = await getPostRevisions(id, { headers });
      revisions = data.post.revisions.edges;
    }

    const databaseId = revisions.find(({ node: { isPreview } }) => isPreview)?.node.databaseId || null;

    const {
      data: { post },
    } = databaseId ? await getPostById(databaseId, { headers }) : { data: { post: null } };

    if (!databaseId || !post) {
      res.status(200).json({ message: "No preview." });
      return;
    }

    const url =
      req.query.type === PreviewType.Page
        ? `/${post.slug}`
        : `/${format(parseISO(post.date), "yyyy/MM/dd")}/${post.slug}/`;

    res.setPreviewData({ id: databaseId, headers });
    res.redirect(url);
  } catch (error) {
    console.log(error);
    res.status(200).json({ error });
  }
};

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

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

И теперь мы можем использовать режим предварительного просмотра. Давайте установим идентификатор предварительного просмотра и заголовки в данных предварительного просмотра, используя setPreviewData. К сожалению, его размер ограничен 2 КБ (это в основном файл cookie, но его размер ограничен самим Next.js), поэтому мы не сможем передать туда все данные предварительного просмотра.

Наконец, давайте перенаправим на страницу/публикацию, где мы будем использовать ранее установленные данные.

Post View

Теперь, в представлении публикации, мы должны настроить getStaticProps функцию. Когда данные предварительного просмотра установлены, они доступны в контексте. Давайте посмотрим на обновленную функцию.

export const getStaticProps: GetStaticProps<PostPageProps, Params, PreviewData> = async (ctx) => {
  if (ctx.preview && ctx.previewData) {
    const { id, headers } = ctx.previewData;

    const {
      data: { post: post },
    } = await getPostById(id, { headers });

    return { props: post };
  }

  if (!ctx.params || !ctx.params.slug) {
    return { notFound: true };
  }

  const {
    data: { post: props },
  } = await getPostBySlug(ctx.params.slug);

  return { props };
};

Что изменилось? Первый ifбыл добавлен в функцию. Здесь происходит вся магия. Если данные предварительного просмотра доступны, мы хотим переопределить отображение кэшированного представления и снова получить данные предварительного просмотра. o вы помните об ограничении размера данных предварительного просмотра? Затем мы можем вернуть данные предварительного просмотра в качестве данных публикации, которые будут использоваться в компоненте React.

Что с просмотром страницы? Аналогичным образом if следует добавить аналогичный пункт. Чтобы быть кратким, мы не будем подробно останавливаться на этом. Взгляните на POC-код сами 😉

Что еще предстоит сделать? Мы должны убедиться, что WordPress укажет нашим редакторам на конечную точку предварительного просмотра API, а не на страницу предварительного просмотра WordPress по умолчанию.

Тема Headnext WordPress

Тема Headnext — это базовая тема WordPress без заголовка, позволяющая пользователям изменять URL-адрес по умолчанию для предварительного просмотра WordPress. Кроме того, он не содержит ненужного кода и отображает только ссылку для входа на индексную страницу темы — больше ничего не нужно, так как приложение Next.js будет обрабатывать внешний интерфейс.

 

тема wordpress headnext next.jsтема wordpress headnext next.js

headnext next.js без головы

Честно говоря, возможность изменить URL-адрес страницы предварительного просмотра WordPress может (и, вероятно, должна быть) извлечена из плагина, чтобы разрешить настройку URL-адреса предварительного просмотра WordPress в общем виде, а не жестко закодированном (сейчас он позволяет настраивать только Next.js). домен приложения). Для проверки концепции этого вполне достаточно.

Из кода видно, что пользователь, нажимающий Previewкнопку во время публикации примера публикации, будет перенаправлен на https://domain.com/api/post/1/preview?nonce=b192fc4204URL-адрес предварительного просмотра страницы примера:https://domain.com/api/page/2/preview?nonce=796c7766b1

Убираться

Честно говоря, мы еще не закончили. Данные предварительного просмотра сохранятся, так как это файл cookie. Мы должны очистить его сами. Давайте создадим новый маршрут API в src/pages/api/exit-preview/index.ts файле.

export default (_: NextApiRequest, res: NextApiResponse) => {
  try {
    res.clearPreviewData();

    return res.status(200).json({ message: "Everything clear!" });
  } catch (error) {
    return res.status(500).json({ message: error });
  }
};

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

export const usePreviewModeExit = () => {
  useEffect(() => {
    axios.post("/api/exit-preview")
  }, [])
}

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


const PostPage = (post: PostPageProps) => {
  usePreviewModeExit();

  return (
    <>
      <Head>
        <title>{post.title}</title>
      </Head>
      <Post post={post} />
    </>
  );
};

Мы обработали отображение превью как для внешнего интерфейса, так и для уровня CMS. Это должно работать как шарм!

Так вы говорите, что это просто доказательство концепции?

Но да, это все еще доказательство концепции. Есть несколько вещей, которые необходимо сделать, чтобы считать это готовым к производству. Приведенный ниже список, вероятно, может быть еще более исчерпывающим. Итак, что нужно сделать?

  • Стилизация в редакторе WordPress Gutenberg. Наши пользователи должны иметь представление о том, как будет выглядеть их контент, без необходимости использовать кнопку предварительного просмотра ( theme.jsonможет быть использована новая функция или вы можете использовать традиционные стили CSS),
  • Введение нескольких настроек в WordPress, чтобы заменить все его перенаправления на внешний слой. вместо этого наша тема должна перенаправить его в приложение Next.js (поскольку оно перенаправляет в случае предварительного просмотра),
  • Стилизация в приложении Next.js. Он должен обрабатывать хотя бы базовый набор (не очень) новых компонентов редактора WordPress Gutenberg,
  • Реализация оптимизации изображения в приложении Next.js с использованием компонента изображения Next.js,
  • Как изображения, так и компоненты редактора WordPress Gutenberg могут потребовать интенсивного использования файлов html-react-parser,
  • Если SEO является проблемой (и, вероятно, это так, если мы рассматриваем платформу Next.js), вы можете использовать плагины Yoast SEO и WPGraphQL Yoast SEO Addon .

Ссылка: https://tsh.io/blog/headless-wordpress-as-an-api-for-a-next-js-application/

#nextjs #javascript #wordpress

What is GEEK

Buddha Community

Безголовый WordPress как API для приложения Next.js

NBB: Ad-hoc CLJS Scripting on Node.js

Nbb

Not babashka. Node.js babashka!?

Ad-hoc CLJS scripting on Node.js.

Status

Experimental. Please report issues here.

Goals and features

Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js.

Additional goals and features are:

  • Fast startup without relying on a custom version of Node.js.
  • Small artifact (current size is around 1.2MB).
  • First class macros.
  • Support building small TUI apps using Reagent.
  • Complement babashka with libraries from the Node.js ecosystem.

Requirements

Nbb requires Node.js v12 or newer.

How does this tool work?

CLJS code is evaluated through SCI, the same interpreter that powers babashka. Because SCI works with advanced compilation, the bundle size, especially when combined with other dependencies, is smaller than what you get with self-hosted CLJS. That makes startup faster. The trade-off is that execution is less performant and that only a subset of CLJS is available (e.g. no deftype, yet).

Usage

Install nbb from NPM:

$ npm install nbb -g

Omit -g for a local install.

Try out an expression:

$ nbb -e '(+ 1 2 3)'
6

And then install some other NPM libraries to use in the script. E.g.:

$ npm install csv-parse shelljs zx

Create a script which uses the NPM libraries:

(ns script
  (:require ["csv-parse/lib/sync$default" :as csv-parse]
            ["fs" :as fs]
            ["path" :as path]
            ["shelljs$default" :as sh]
            ["term-size$default" :as term-size]
            ["zx$default" :as zx]
            ["zx$fs" :as zxfs]
            [nbb.core :refer [*file*]]))

(prn (path/resolve "."))

(prn (term-size))

(println (count (str (fs/readFileSync *file*))))

(prn (sh/ls "."))

(prn (csv-parse "foo,bar"))

(prn (zxfs/existsSync *file*))

(zx/$ #js ["ls"])

Call the script:

$ nbb script.cljs
"/private/tmp/test-script"
#js {:columns 216, :rows 47}
510
#js ["node_modules" "package-lock.json" "package.json" "script.cljs"]
#js [#js ["foo" "bar"]]
true
$ ls
node_modules
package-lock.json
package.json
script.cljs

Macros

Nbb has first class support for macros: you can define them right inside your .cljs file, like you are used to from JVM Clojure. Consider the plet macro to make working with promises more palatable:

(defmacro plet
  [bindings & body]
  (let [binding-pairs (reverse (partition 2 bindings))
        body (cons 'do body)]
    (reduce (fn [body [sym expr]]
              (let [expr (list '.resolve 'js/Promise expr)]
                (list '.then expr (list 'clojure.core/fn (vector sym)
                                        body))))
            body
            binding-pairs)))

Using this macro we can look async code more like sync code. Consider this puppeteer example:

(-> (.launch puppeteer)
      (.then (fn [browser]
               (-> (.newPage browser)
                   (.then (fn [page]
                            (-> (.goto page "https://clojure.org")
                                (.then #(.screenshot page #js{:path "screenshot.png"}))
                                (.catch #(js/console.log %))
                                (.then #(.close browser)))))))))

Using plet this becomes:

(plet [browser (.launch puppeteer)
       page (.newPage browser)
       _ (.goto page "https://clojure.org")
       _ (-> (.screenshot page #js{:path "screenshot.png"})
             (.catch #(js/console.log %)))]
      (.close browser))

See the puppeteer example for the full code.

Since v0.0.36, nbb includes promesa which is a library to deal with promises. The above plet macro is similar to promesa.core/let.

Startup time

$ time nbb -e '(+ 1 2 3)'
6
nbb -e '(+ 1 2 3)'   0.17s  user 0.02s system 109% cpu 0.168 total

The baseline startup time for a script is about 170ms seconds on my laptop. When invoked via npx this adds another 300ms or so, so for faster startup, either use a globally installed nbb or use $(npm bin)/nbb script.cljs to bypass npx.

Dependencies

NPM dependencies

Nbb does not depend on any NPM dependencies. All NPM libraries loaded by a script are resolved relative to that script. When using the Reagent module, React is resolved in the same way as any other NPM library.

Classpath

To load .cljs files from local paths or dependencies, you can use the --classpath argument. The current dir is added to the classpath automatically. So if there is a file foo/bar.cljs relative to your current dir, then you can load it via (:require [foo.bar :as fb]). Note that nbb uses the same naming conventions for namespaces and directories as other Clojure tools: foo-bar in the namespace name becomes foo_bar in the directory name.

To load dependencies from the Clojure ecosystem, you can use the Clojure CLI or babashka to download them and produce a classpath:

$ classpath="$(clojure -A:nbb -Spath -Sdeps '{:aliases {:nbb {:replace-deps {com.github.seancorfield/honeysql {:git/tag "v2.0.0-rc5" :git/sha "01c3a55"}}}}}')"

and then feed it to the --classpath argument:

$ nbb --classpath "$classpath" -e "(require '[honey.sql :as sql]) (sql/format {:select :foo :from :bar :where [:= :baz 2]})"
["SELECT foo FROM bar WHERE baz = ?" 2]

Currently nbb only reads from directories, not jar files, so you are encouraged to use git libs. Support for .jar files will be added later.

Current file

The name of the file that is currently being executed is available via nbb.core/*file* or on the metadata of vars:

(ns foo
  (:require [nbb.core :refer [*file*]]))

(prn *file*) ;; "/private/tmp/foo.cljs"

(defn f [])
(prn (:file (meta #'f))) ;; "/private/tmp/foo.cljs"

Reagent

Nbb includes reagent.core which will be lazily loaded when required. You can use this together with ink to create a TUI application:

$ npm install ink

ink-demo.cljs:

(ns ink-demo
  (:require ["ink" :refer [render Text]]
            [reagent.core :as r]))

(defonce state (r/atom 0))

(doseq [n (range 1 11)]
  (js/setTimeout #(swap! state inc) (* n 500)))

(defn hello []
  [:> Text {:color "green"} "Hello, world! " @state])

(render (r/as-element [hello]))

Promesa

Working with callbacks and promises can become tedious. Since nbb v0.0.36 the promesa.core namespace is included with the let and do! macros. An example:

(ns prom
  (:require [promesa.core :as p]))

(defn sleep [ms]
  (js/Promise.
   (fn [resolve _]
     (js/setTimeout resolve ms))))

(defn do-stuff
  []
  (p/do!
   (println "Doing stuff which takes a while")
   (sleep 1000)
   1))

(p/let [a (do-stuff)
        b (inc a)
        c (do-stuff)
        d (+ b c)]
  (prn d))
$ nbb prom.cljs
Doing stuff which takes a while
Doing stuff which takes a while
3

Also see API docs.

Js-interop

Since nbb v0.0.75 applied-science/js-interop is available:

(ns example
  (:require [applied-science.js-interop :as j]))

(def o (j/lit {:a 1 :b 2 :c {:d 1}}))

(prn (j/select-keys o [:a :b])) ;; #js {:a 1, :b 2}
(prn (j/get-in o [:c :d])) ;; 1

Most of this library is supported in nbb, except the following:

  • destructuring using :syms
  • property access using .-x notation. In nbb, you must use keywords.

See the example of what is currently supported.

Examples

See the examples directory for small examples.

Also check out these projects built with nbb:

API

See API documentation.

Migrating to shadow-cljs

See this gist on how to convert an nbb script or project to shadow-cljs.

Build

Prequisites:

  • babashka >= 0.4.0
  • Clojure CLI >= 1.10.3.933
  • Node.js 16.5.0 (lower version may work, but this is the one I used to build)

To build:

  • Clone and cd into this repo
  • bb release

Run bb tasks for more project-related tasks.

Download Details:
Author: borkdude
Download Link: Download The Source Code
Official Website: https://github.com/borkdude/nbb 
License: EPL-1.0

#node #javascript

Top 10 API Security Threats Every API Team Should Know

As more and more data is exposed via APIs either as API-first companies or for the explosion of single page apps/JAMStack, API security can no longer be an afterthought. The hard part about APIs is that it provides direct access to large amounts of data while bypassing browser precautions. Instead of worrying about SQL injection and XSS issues, you should be concerned about the bad actor who was able to paginate through all your customer records and their data.

Typical prevention mechanisms like Captchas and browser fingerprinting won’t work since APIs by design need to handle a very large number of API accesses even by a single customer. So where do you start? The first thing is to put yourself in the shoes of a hacker and then instrument your APIs to detect and block common attacks along with unknown unknowns for zero-day exploits. Some of these are on the OWASP Security API list, but not all.

Insecure pagination and resource limits

Most APIs provide access to resources that are lists of entities such as /users or /widgets. A client such as a browser would typically filter and paginate through this list to limit the number items returned to a client like so:

First Call: GET /items?skip=0&take=10 
Second Call: GET /items?skip=10&take=10

However, if that entity has any PII or other information, then a hacker could scrape that endpoint to get a dump of all entities in your database. This could be most dangerous if those entities accidently exposed PII or other sensitive information, but could also be dangerous in providing competitors or others with adoption and usage stats for your business or provide scammers with a way to get large email lists. See how Venmo data was scraped

A naive protection mechanism would be to check the take count and throw an error if greater than 100 or 1000. The problem with this is two-fold:

  1. For data APIs, legitimate customers may need to fetch and sync a large number of records such as via cron jobs. Artificially small pagination limits can force your API to be very chatty decreasing overall throughput. Max limits are to ensure memory and scalability requirements are met (and prevent certain DDoS attacks), not to guarantee security.
  2. This offers zero protection to a hacker that writes a simple script that sleeps a random delay between repeated accesses.
skip = 0
while True:    response = requests.post('https://api.acmeinc.com/widgets?take=10&skip=' + skip),                      headers={'Authorization': 'Bearer' + ' ' + sys.argv[1]})    print("Fetched 10 items")    sleep(randint(100,1000))    skip += 10

How to secure against pagination attacks

To secure against pagination attacks, you should track how many items of a single resource are accessed within a certain time period for each user or API key rather than just at the request level. By tracking API resource access at the user level, you can block a user or API key once they hit a threshold such as “touched 1,000,000 items in a one hour period”. This is dependent on your API use case and can even be dependent on their subscription with you. Like a Captcha, this can slow down the speed that a hacker can exploit your API, like a Captcha if they have to create a new user account manually to create a new API key.

Insecure API key generation

Most APIs are protected by some sort of API key or JWT (JSON Web Token). This provides a natural way to track and protect your API as API security tools can detect abnormal API behavior and block access to an API key automatically. However, hackers will want to outsmart these mechanisms by generating and using a large pool of API keys from a large number of users just like a web hacker would use a large pool of IP addresses to circumvent DDoS protection.

How to secure against API key pools

The easiest way to secure against these types of attacks is by requiring a human to sign up for your service and generate API keys. Bot traffic can be prevented with things like Captcha and 2-Factor Authentication. Unless there is a legitimate business case, new users who sign up for your service should not have the ability to generate API keys programmatically. Instead, only trusted customers should have the ability to generate API keys programmatically. Go one step further and ensure any anomaly detection for abnormal behavior is done at the user and account level, not just for each API key.

Accidental key exposure

APIs are used in a way that increases the probability credentials are leaked:

  1. APIs are expected to be accessed over indefinite time periods, which increases the probability that a hacker obtains a valid API key that’s not expired. You save that API key in a server environment variable and forget about it. This is a drastic contrast to a user logging into an interactive website where the session expires after a short duration.
  2. The consumer of an API has direct access to the credentials such as when debugging via Postman or CURL. It only takes a single developer to accidently copy/pastes the CURL command containing the API key into a public forum like in GitHub Issues or Stack Overflow.
  3. API keys are usually bearer tokens without requiring any other identifying information. APIs cannot leverage things like one-time use tokens or 2-factor authentication.

If a key is exposed due to user error, one may think you as the API provider has any blame. However, security is all about reducing surface area and risk. Treat your customer data as if it’s your own and help them by adding guards that prevent accidental key exposure.

How to prevent accidental key exposure

The easiest way to prevent key exposure is by leveraging two tokens rather than one. A refresh token is stored as an environment variable and can only be used to generate short lived access tokens. Unlike the refresh token, these short lived tokens can access the resources, but are time limited such as in hours or days.

The customer will store the refresh token with other API keys. Then your SDK will generate access tokens on SDK init or when the last access token expires. If a CURL command gets pasted into a GitHub issue, then a hacker would need to use it within hours reducing the attack vector (unless it was the actual refresh token which is low probability)

Exposure to DDoS attacks

APIs open up entirely new business models where customers can access your API platform programmatically. However, this can make DDoS protection tricky. Most DDoS protection is designed to absorb and reject a large number of requests from bad actors during DDoS attacks but still need to let the good ones through. This requires fingerprinting the HTTP requests to check against what looks like bot traffic. This is much harder for API products as all traffic looks like bot traffic and is not coming from a browser where things like cookies are present.

Stopping DDoS attacks

The magical part about APIs is almost every access requires an API Key. If a request doesn’t have an API key, you can automatically reject it which is lightweight on your servers (Ensure authentication is short circuited very early before later middleware like request JSON parsing). So then how do you handle authenticated requests? The easiest is to leverage rate limit counters for each API key such as to handle X requests per minute and reject those above the threshold with a 429 HTTP response. There are a variety of algorithms to do this such as leaky bucket and fixed window counters.

Incorrect server security

APIs are no different than web servers when it comes to good server hygiene. Data can be leaked due to misconfigured SSL certificate or allowing non-HTTPS traffic. For modern applications, there is very little reason to accept non-HTTPS requests, but a customer could mistakenly issue a non HTTP request from their application or CURL exposing the API key. APIs do not have the protection of a browser so things like HSTS or redirect to HTTPS offer no protection.

How to ensure proper SSL

Test your SSL implementation over at Qualys SSL Test or similar tool. You should also block all non-HTTP requests which can be done within your load balancer. You should also remove any HTTP headers scrub any error messages that leak implementation details. If your API is used only by your own apps or can only be accessed server-side, then review Authoritative guide to Cross-Origin Resource Sharing for REST APIs

Incorrect caching headers

APIs provide access to dynamic data that’s scoped to each API key. Any caching implementation should have the ability to scope to an API key to prevent cross-pollution. Even if you don’t cache anything in your infrastructure, you could expose your customers to security holes. If a customer with a proxy server was using multiple API keys such as one for development and one for production, then they could see cross-pollinated data.

#api management #api security #api best practices #api providers #security analytics #api management policies #api access tokens #api access #api security risks #api access keys

Autumn  Blick

Autumn Blick

1601381326

Public ASX100 APIs: The Essential List

We’ve conducted some initial research into the public APIs of the ASX100 because we regularly have conversations about what others are doing with their APIs and what best practices look like. Being able to point to good local examples and explain what is happening in Australia is a key part of this conversation.

Method

The method used for this initial research was to obtain a list of the ASX100 (as of 18 September 2020). Then work through each company looking at the following:

  1. Whether the company had a public API: this was found by googling “[company name] API” and “[company name] API developer” and “[company name] developer portal”. Sometimes the company’s website was navigated or searched.
  2. Some data points about the API were noted, such as the URL of the portal/documentation and the method they used to publish the API (portal, documentation, web page).
  3. Observations were recorded that piqued the interest of the researchers (you will find these below).
  4. Other notes were made to support future research.
  5. You will find a summary of the data in the infographic below.

Data

With regards to how the APIs are shared:

#api #api-development #api-analytics #apis #api-integration #api-testing #api-security #api-gateway

Eva  Murphy

Eva Murphy

1625751960

Laravel API and React Next JS frontend development - 28

In this video, I wanted to touch upon the functionality of adding Chapters inside a Course. The idea was to not think much and start the development and pick up things as they come.

There are places where I get stuck and trying to find answers to it up doing what every developer does - Google and get help. I hope this will help you understand the flow and also how developers debug while doing development.

App url: https://video-reviews.vercel.app
Github code links below:
Next JS App: https://github.com/amitavroy/video-reviews
Laravel API: https://github.com/amitavdevzone/video-review-api

You can find me on:
Twitter: https://twitter.com/amitavroy7​
Discord: https://discord.gg/Em4nuvQk

#next js #api #react next js #next #frontend #development

An API-First Approach For Designing Restful APIs | Hacker Noon

I’ve been working with Restful APIs for some time now and one thing that I love to do is to talk about APIs.

So, today I will show you how to build an API using the API-First approach and Design First with OpenAPI Specification.

First thing first, if you don’t know what’s an API-First approach means, it would be nice you stop reading this and check the blog post that I wrote to the Farfetchs blog where I explain everything that you need to know to start an API using API-First.

Preparing the ground

Before you get your hands dirty, let’s prepare the ground and understand the use case that will be developed.

Tools

If you desire to reproduce the examples that will be shown here, you will need some of those items below.

  • NodeJS
  • OpenAPI Specification
  • Text Editor (I’ll use VSCode)
  • Command Line

Use Case

To keep easy to understand, let’s use the Todo List App, it is a very common concept beyond the software development community.

#api #rest-api #openai #api-first-development #api-design #apis #restful-apis #restful-api