Узнайте, как использовать Strapi при создании приложения-форума и реализовать в нем аутентификацию и авторизацию пользователей.

Автор: Виктори Тудуо

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

Цели

В этом руководстве рассматривается создание веб-сайта форума и обеспечение аутентификации и авторизации пользователей на сайте.

Предпосылки

Чтобы полностью понять этот урок, вам нужно:

  • Node.js установлен
  • Знание NextJs.

Что такое Страпи?

Strapi — это безголовая CMS с открытым исходным кодом, построенная на Node.js. Strapi позволяет разработчикам создавать и управлять содержимым своих приложений. Strapi предоставляет пользователям панель администратора, которая используется для управления пользовательским контентом. Контент можно создавать с помощью таблиц, как в традиционной базе данных. Что еще? Strapi предоставляет функциональные возможности для интеграции API и обеспечивает совместный доступ к контенту, чтобы несколько пользователей могли получать доступ к сохраненным данным и управлять ими.

Настройка страпи

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

mkdir forumapp

Это создает папку для нашего проекта strapi-forum. Далее переходим в эту папку:

cd forumapp

Затем устанавливаем Strapi:

npx create-strapi-app@latest forum-backend --quickstart

Приведенная выше команда настраивает Strapi для нашего приложения со всеми необходимыми зависимостями. Здесь forum-backend — это имя папки нашего проекта. Параметр --quickstart настраивает Strapi с базой данных sqlite.

После завершения процесса он запускается strapi develop и открывает панель администратора на локальном хосте. Если вы откроете свой браузер по URL-адресу, вы получите форму.

Заполните свои данные в форме и нажмите «Начать». Он войдет в систему и перенаправит вас на панель инструментов.

Коллекция и настройка поля

В этом разделе мы создадим нашу коллекцию, содержащую post и comments для нашего приложения форума.

  • Нажмите на кнопку «Создать свой первый тип контента». Он открывает страницу для создания нашего контента. Есть модальное окно, где вы можете ввести название вашей коллекции. Здесь мы создадим коллекцию под названием strapi-forum.

  • Затем нажмите «Продолжить». Появится еще одно модальное окно, в котором вы задаете поле для своего типа коллекции. Нажмите «Текст», затем добавьте текстовые поля.

  • Добавьте «Заголовок» в текстовое поле и нажмите «Добавить другое поле». Повторите процесс для двух дополнительных полей, но на этот раз мы будем использовать форматированный текст.

Теперь у нас есть пять полей: title, answers, questions, username и answername для наших сообщений на форуме.

  • Нажмите на кнопку «Сохранить». Теперь на боковой панели «Тип коллекции» мы видим коллекцию «Форумы Strapi».
  • Нажмите «Контент-менеджер» в левом меню навигации, затем нажмите кнопку «Создать новую запись»:

  • Вы получите страницу, на которой вы можете создать запись для приложения Forum. Введите любой текст в эти поля, затем нажмите на кнопки save и publish.

Новая запись будет добавлена ​​в коллекцию «Форумы Strapi».

  • Чтобы наше приложение разрешало доступ к контенту без авторизации, вы должны перейти в «Настройки», затем «Роли».

  • Нажмите «Общедоступно».

  • Нажмите «Выбрать все», затем «Сохранить».

Получение коллекции

Вы можете получить данные из CollectionCollection с помощью тестера API. Введите URL-адрес: http://localhost:1337/api/strapi-forums. Отправьте запрос, и вы получите ответ от Strapi:

Создание нашего интерфейса

Мы создадим интерфейс нашего приложения для форума с помощью Next.js. Next.js — это фреймворк с открытым исходным кодом, построенный на Node.js, позволяющий отображать приложения React на стороне сервера.

Чтобы установить Next.js:

npx create-next-app forum

Приведенная выше команда устанавливает платформу Next.js в папку проекта forum. Наше готовое приложение будет иметь две страницы: одна для отображения форума, а другая для публикации новых вопросов. На изображениях ниже показано, как будет выглядеть наше приложение:

Страница форума дисплея:

Опубликовать новую страницу вопроса:

В папке проекта forum структура нашего приложения будет выглядеть так:

┣ 📂pages
 ┃ ┣ 📂api
 ┃ ┃ ┗ 📜hello.js
 ┃ ┣ 📂Components
 ┃ ┃ ┣ 📜Displayforum.js
 ┃ ┃ ┗ 📜Uploadforum.js
 ┃ ┣ 📜index.js
 ┃ ┣ 📜upload.js
 ┃ ┗ 📜_app.js
 ┣ 📂public
 ┃ ┣ 📜favicon.ico
 ┃ ┗ 📜vercel.svg
 ┣ 📂styles
 ┃ ┣ 📜globals.css
 ┃ ┗ 📜Home.module.css

Здесь наш файл index.js является нашей страницей формы отображения, и он использует компонент Displayforum.js, а файл upload.js служит нашей страницей для публикации новых вопросов. Он содержит компонент Uploadforum.js. Все наши стили находятся в Home.module.css.

В Index.js у нас есть следующие коды:

import styles from "../styles/Home.module.css";
    import Displayforum from "./Components/Displayforum";
    export default function Home() {
      return (
        <div className={styles.container}>
          <Displayforum />
        </div>
      );
    }

Здесь мы добавили компонент Displayforum на нашу страницу. В Displayforum.js у нас есть:

import React, { useState } from "react";
    import style from "../../styles/Home.module.css";
    import Link from "next/link";
    function Displayforum() {
      const [show, setShow] = useState(false);
      return (
        <div>
          <div className={style.topcont}>
            <h1 className={style.heading}>Display forum</h1>
            <div>
              <Link href="/upload">
                <button>Ask a question</button>
              </Link>
              <button>Login</button>
            </div>
          </div>
          <h2 className={style.subheading}>Questions</h2>
          <div className={style.userinfo}>
            <p>Posted By: Victory Tuduo</p>
          </div>
          <div className={style.questioncont}>
            <p className={style.question}>Description of the Question</p>
          </div>
          <div className={style.answercont}>
            <h2 className={style.subheading}>Answers</h2>
            <div className={style.inputanswer}>
              <form>
                <textarea type="text" placeholder="Enter your answer" rows="5" />
                <button>Post</button>
              </form>
            </div>
            <button className={style.showanswer} onClick={() => setShow(!show)}>
              {show ? "Hide Answers" : "Show Answers"}
            </button>
            {show ? (
              <div className={style.answers}>
                <div className={style.eachanswer}>
                  <p className={style.username}>Miracle</p>
                  <p className={style.answertext}>Try doing it Like this</p>
                </div>
              </div>
            ) : null}
          </div>
        </div>
      );
    }
    export default Displayforum;

Этот компонент обрабатывает макет нашей отображаемой страницы форума. У нас также есть кнопка, которая направляет пользователя на страницу для загрузки новых вопросов. Между тем, в upload.js у нас есть следующее:

import React from "react";
    import Uploadforum from "./Components/Uploadforum";
    function upload() {
      return (
        <div>
          <Uploadforum />
        </div>
      );
    }
    export default upload;

Здесь мы просто добавили импорт компонента Uploadforum на нашу страницу. В файле Uploadforum.js у нас есть простая форма для создания новых вопросов:

import React from "react";
    import style from "../../styles/Home.module.css";
    import Link from "next/Link";
    function Uploadforum() {
      return (
        <div className={style.uploadpage}>
          <div className={style.topcont}>
            <h1>Ask a question</h1>
            <Link href="/">
              <button>Forum</button>
            </Link>
          </div>
          <div className={style.formcont}>
            <form className={style.uploadform}>
              <input type="text" placeholder="Enter your title" maxLength="74" />
              <textarea type="text" placeholder="Enter your description" rows="8" />
              <button>Submit Question</button>
            </form>
          </div>
        </div>
      );
    }
    export default Uploadforum;

Наконец, у нас есть следующие стили в Home.module.css:

.container {
      min-height: 100vh;
      padding: 0 0.5rem;
      height: 100vh;
      font-family: monospace;
    }
    /* display forum page */
    .topcont {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 5px 8px;
    }
    .topcont button,
    .inputanswer button,
    .formcont button,
    .showanswer {
      border: none;
      color: #fff;
      background: dodgerblue;
      border-radius: 8px;
      padding: 10px 15px;
      outline: none;
      margin: 8px;
    }
    .topcont button:hover {
      cursor: pointer;
      transform: scale(1.2);
    }
    .heading {
      font-weight: bold;
    }
    .subheading {
      font-weight: 500;
      text-transform: uppercase;
    }
    .userinfo {
      font-size: 18px;
      font-weight: 600;
    }
    .questioncont {
      min-height: 300px;
      padding: 15px 14px;
      box-shadow: 12px 12px 36px rgba(0, 0, 0, 0.12);
    }
    .answercont {
      min-height: 300px;
      padding: 5px 3px 5px 15px;
    }
    .answers {
      height: 300px;
      overflow-x: scroll;
    }
    .inputanswer {
      margin-bottom: 8px;
    }
    .inputanswer textarea {
      width: 100%;
      resize: none;
      padding: 5px 8px;
    }
    .showanswer {
      border: 1px solid dodgerblue;
      background: #fff;
      color: dodgerblue;
      transition: 0.4s ease-in-out;
    }
    .showanswer:hover {
      background: dodgerblue;
      color: #fff;
    }
    .eachanswer {
      border-radius: 15px;
      background: #e7e7e7;
      padding: 8px 15px;
      margin-bottom: 10px;
    }
    .username {
      font-weight: bold;
      text-transform: uppercase;
    }
    .answertext {
      font-family: Montserrat;
      font-size: 14px;
      font-weight: 500;
    }
    /* upload a question page */
    .uploadpage {
      min-height: 100vh;
    }
    .formcont {
      min-width: 100vw;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .uploadform {
      display: flex;
      flex-direction: column;
      min-width: 500px;
      padding-top: 10px;
    }
    .uploadform input,
    .uploadform textarea {
      resize: none;
      width: 100%;
      margin: 8px;
      padding: 5px;
    }

Все это составляет макет наших страниц.

Получение данных от Strapi

На этом этапе мы будем получать данные из Strapi в качестве следующего шага.

Настройка нашего запроса на выборку

В этом разделе мы будем получать данные из Strapi и отображать их в нашем приложении. Мы будем использовать Axios для выполнения наших операций выборки.

Мы установим это через CLI:

npm install axios

Создайте файл index.js в папке API. Здесь мы настроим наш запрос на выборку:

import axios from "axios";
    const url = "http://localhost:1337/api/strapi-forums";
    export const readForum = () => axios.get(url);

Выше мы добавили импорт для axios, URL-адрес для получения наших данных и экспортированные функции для чтения и создания данных с нашего форума. Мы импортируем эти функции в наше приложение в нашем файле index.js:

import { readForum, createQuestion } from "./api";

Получение данных из Strapi

Мы получим данные из Strapi в нашем файле index.js и передадим их компоненту Displayforum.js для отображения:

import { react, useState, useEffect } from "react";
    ...
    const [question, setQuestions] = useState({});
      const [response, setResponse] = useState([]);
      useEffect(() => {
        const fetchData = async () => {
          const result = await readForum();
          setResponse(result.data.data);
        };
        fetchData();
      }, []);

Здесь мы извлекли наши данные из Strapi и присвоили их response с помощью хука React useState. У нас есть функция useEffect, которая делает запрос при монтировании нашего компонента. Теперь мы передаем этот response нашему компоненту Displayforum.

<Displayforum response={response} />

Отображение данных из Strapi

Чтобы отобразить наши данные в нашем файле Displayforum.js, мы сопоставим наши ответы и визуализируем наши компоненты. Мы обработаем это в компоненте Displayforum:

//...
    function Displayforum({ response }) {
    //...
      {response.map((response, index) => (
      <div key={index}>
      <h2 className={style.subheading}>{response.attributes.Title}</h2>
      <div className={style.userinfo}>
      //...
       <p className={style.answertext}>Try doing it Like this</p>
              </div>
            </div>
          ) : null}
        </div>
      </div>
      ))}

Здесь мы завернули наши компоненты, чтобы отобразить через response и отобразить этот компонент столько раз, сколько ответов. Чтобы отобразить наши данные Strapi, мы просто ссылаемся на них. Мы можем получить Username с помощью этого кода:

response.attributes.Username

Теперь мы можем добавить это в наш компонент и отобразить его:

<p>Posted By: {response.attributes.Username}</p>
    ...
    <p className={style.question}>{response.attributes.Questions}</p>
    ...

Чтобы отобразить наши ответы, мы сопоставим содержимое Answers, возвращенное Strapi:

{response.attributes.Answers.map((answers, i) => (
      <div className={style.eachanswer} key={i}>
        <p className={style.username}>{response.attributes.Answername}</p>
        <p className={style.answertext}>{answers}</p>
      </div>
    ))}

Мы успешно добавили данные из нашей коллекции CollectionCollection в наш front-end для просмотра в браузере. Выполните следующую команду в CLI:

npm run dev

В вашем браузере у вас будет вывод, подобный изображенному ниже:

После этого мы добавим функционал для добавления новых вопросов в Strapi.

Добавление данных в Strapi

В наш файл Uploadforum.js мы добавим функциональность для загрузки содержимого формы в Strapi. Во-первых, мы создадим две переменные состояния для хранения текста из нашего файла inputs.

import { React, useState } from "react";
    ...
    const [name, setName] = useState("");
    const [description, setDescription] = useState("");

Затем мы устанавливаем эти переменные в значение нашей формы input.

<input
        type="text"
        placeholder="Enter your title"
        maxLength="74"
        value={name}
        onChange={(e) => setName(e.target.value)}
    />
    <textarea
        type="text"
        placeholder="Enter your description"
        rows="8"
        value={description}
        onChange={(e) => setDescription(e.target.value)}
    />

Кроме того, мы добавим функцию для отправки этих переменных, когда мы щелкаем наш button.

<button  onClick={(e) => {
          e.preventDefault();
        sendData();
      }}
    }>Submit Question</button>

Мы создадим функцию sendData над оператором return для обработки отправки вновь созданных вопросов в Strapi.

const sendData = () => {
    };

Для нашей функции создания мы импортируем функцию createQuestion, которую мы определили в нашей папке api.

import axios from "axios";

Затем мы передаем наши данные этой функции.

const url = "http://localhost:1337/api/strapi-forums";
    const sendData = () => {
      axios.post(url, {
        data: {
          Title: name,
          Questions: description,
        },
      });

Теперь мы можем загружать новые вопросы в наш Strapi collection. Мы добавим Username, когда будем рассматривать аутентификацию пользователей.

Далее мы добавим функциональность для ответов на вопросы в нашем компоненте Displayforum.

Добавление новых ответов

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

import axios from "axios";
    //...
    const [answer, setAnswer] = useState("")
    const [id, setId] = useState("");
    const [a, formerArray] = useState([]);

Мы сохраним ввод из textarea в answer. Мы будем использовать переменную id для ссылки на коллекцию, в которую мы хотим добавить ответ. Затем в нашей форме textarea:

<textarea
      type="text"
      placeholder="Enter your answer"
      rows="5"
      value={answer}
      onChange={(e) => {
        formerArray(response.attributes.Answers);
        setAnswer(e.target.value);
        setId(response.id);
      }}
    />
    <button
      onClick={(e) => {
        submitAnswer();
      }}
    >
    }}>Post</button>

Затем в функции submitAnswer:

const submitAnswer = () => {
        try {
          axios.put(`http://localhost:1337/api/strapi-forums/${id}`, {
            Answers: [...a, answer],
          });
        } catch (error) {
          console.log(error);
        }
      };

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

Аутентификация пользователя с помощью NextAuth

В этом разделе будет использоваться Nextauth, пакет NextJs для аутентификации для реализации входа в Google для нашего приложения. Мы также настроим защищенные маршруты, чтобы только авторизованные пользователи могли создавать вопросы и просматривать их.

Чтобы установить next-auth, запустите:

npm i next-auth

Для нашей аутентификации мы будем использовать токен JWT. JWT — это стандарт, используемый для создания токенов доступа для приложения. Мы создадим файл для обработки аутентификации пользователя. Для этого создайте папку с именем auth в своей папке api и внутри нее создайте файл [...nextauth].js со следующим кодом:

import NextAuth from "next-auth"
    import GoogleProvider from "next-auth/providers/google"
    
    
    export default NextAuth({
      secret: process.env.SECRET,
      providers: [
        GoogleProvider({
          clientId: process.env.GOOGLE_ID,
          clientSecret: process.env.GOOGLE_SECRET,
          authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code',
        }),
      ],
    })

Приведенный выше код устанавливает Google Authentication для нашего приложения. Чтобы использовать его, нам нужно обернуть наше приложение в _app.js с компонентом Google Provider:

...
    import { SessionProvider } from "next-auth/react"
    function MyApp({ Component, pageProps }) {
      return (
        <SessionProvider session={pageProps.session}>
          <Component {...pageProps} />
        </SessionProvider>
      );
    }
    export default MyApp;

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

import {signIn, signOut, useSession} from 'next-auth/react'
    //...
    const { data: session } = useSession()

Мы будем использовать useSession, чтобы узнать, авторизован ли наш пользователь. Если есть session, мы вернем остальную часть компонента, а если нет session, мы отобразим кнопку sign in для доступа к приложению.

return(
    <div>
    {!session && (
                <>
                  <h1>Sign in to access forum</h1>
                  <button onClick={() => signIn()}>Sign In</button>
                </>
              )}
        {session && (
         <>
         {/*rest of our app*/}
         </>
         )}
        </div>

Мы также установим для нашей кнопки значение Sign out:

//...
    <Link href="/upload">
      <button>Ask a question</button>
    </Link>
    <button onClick={() => signOut()}>Signout</button>
    //...

Чтобы использовать Google authentication в нашем приложении, нам потребуются учетные данные для доступа из консоли Google Cloud. Для этого перейдите в браузере на Google Cloud.

Нажмите OAuth Client ID и заполните поля на открывшейся новой странице.

Наконец, установите URL-адрес перенаправления: http://localhost/api/auth/callback/google. Чтобы использовать учетные данные в файле […nextauth].js, вы можете создать файл .env и настроить переменные среды:

GOOGLE_CLIENT_ID: id
    GOOGLE_CLIENT_SECRET: secret
    secret: any string

Затем мы настроим наш компонент Uploadforum.js на нашей странице upload в качестве защищенного маршрута, чтобы неавторизованные пользователи не могли получить доступ к маршруту. Для этого в upload.js добавляем следующий код:

import { getSession, useSession } from 'next-auth/react'

Затем внизу:

export async function getServerSideProps(context) {
      const session = await getSession(context);
      if (!session) {
        context.res.writeHead(302, { Location: '/' });
        context.res.end();
        return {};
      }
      return {
        props: {
          user: session.user,
        }
      }
    }
    export default Upload;

Теперь, если вы запустите приложение с npm run dev в CLI, мы реализуем Google authentication. Также мы не можем получить доступ к пути /upload без входа в систему.

Добавление имени пользователя в коллекцию

Теперь, когда мы добавили аутентификацию в наше приложение, мы можем добавить username, полученное из Google Login, в качестве поля Answername при ответе на вопрос:

//...
    axios.put(`http://localhost:1337/api/strapi-forums/${id}`, {
          Answers: [...a, answer],
          Answername: session.user.name,
        });

Теперь, если я добавлю новый ответ в форму:

Когда я нажимаю кнопку Post, я получаю:

Ответ был добавлен, и в поле Answername было установлено мое user.name из нашего session.

Наконец, мы также добавим username при публикации вопроса в наш collection. Мы сделаем это в нашем файле upload.js:

const { data: session } = useSession();

Затем мы передаем значение session нашему компоненту Uploadforum:

<Uploadforum session={session} />

Теперь мы можем использовать данные session в нашем компоненте Uploadforum:

function Uploadforum({session}) {
    //...
     axios.post(url, {
          data: {
            Title: name,
            Questions: description,
            Answers: [],
            Username: session.user.name,
          },
        });

Любые новые добавленные вопросы теперь принимают поле Username как username, полученное от session. Если мы добавим новые ответы, поскольку Answername является полем, оно перезапишет предыдущие данные, и все answers будут использовать одно и то же имя. Чтобы исправить это, мы просто изменим наше поле Answers типа JSON, чтобы оно содержало как ответы, так и имя пользователя человека, предоставляющего ответы.

Затем мы можем получить эти данные и отобразить их в нашем компоненте Displayforum:

<div className={style.answers}>
      {response.attributes.Answers.map((answers, i) => (
        <div className={style.eachanswer} key={i}>
          <p className={style.username}>{answers[0]}</p>
          <p className={style.answertext}>{answers[1]}</p>
        </div>
      ))}

answer[0] — это имя пользователя, а answers[1] — ответ.

Наконец, мы изменим код, чтобы добавить новые ответы:

...
     axios.put(`http://localhost:1337/api/strapi-forums/${id}`, {
            Answers: [...a, [session.user.name, answer]],
        });
        } catch (error) {
          console.log(error);

Теперь мы можем добавлять новые ответы на наши вопросы, не перезаписывая предыдущие данные.

Когда я нажимаю на сообщение, я получаю новый ответ:

Заключение

Мы подошли к концу этого урока. В этом уроке мы узнали, как использовать Strapi CMS и подключить ее к внешнему интерфейсу NextJ. В этом процессе мы создали сайт-форум и реализовали на нем аутентификацию и авторизацию пользователей.

Ресурсы

Исходный код, используемый в этом руководстве, можно найти в репозитории GitHub: Forum Application.