From dc34d36f8dd0135e037a930fc1d7902710ff6c35 Mon Sep 17 00:00:00 2001 From: Logan Anderson Date: Wed, 2 Aug 2023 14:24:52 -0400 Subject: [PATCH] added auth docs --- .../authentication-provider/next-auth.md | 345 +++++++++++++++++- .../authentication-provider/tina-cloud.md | 64 +++- 2 files changed, 407 insertions(+), 2 deletions(-) diff --git a/content/docs/self-hosted/authentication-provider/next-auth.md b/content/docs/self-hosted/authentication-provider/next-auth.md index fdabbce28..7c282c357 100644 --- a/content/docs/self-hosted/authentication-provider/next-auth.md +++ b/content/docs/self-hosted/authentication-provider/next-auth.md @@ -5,4 +5,347 @@ prev: '/docs/self-hosted/authentication-provider/overview' next: '/docs/self-hosted/authentication-provider/tina-cloud' --- -TODO +> Note: NextAuth requires using Next.js if you are using a differnt framework you will need to use a different authentication provider. + +The NextAuth Authentication Provider allows you to use [NextAuth.js](https://next-auth.js.org/) to authenticate users with your TinaCMS instance. + +## Getting Started + +To get started you will need to install the following dependencies: + +```bash +yarn add next-auth next-auth-tinacms +``` + +## Setup + +NextAuth requires a databse to store user and session data. This provider uses [vercel KV]() to presist users. If you have not already, you will need to setup a Vercel DB database. + +Once you have done that you can copy the environment variables from the Vercel dashboard. + +![Vercel Dashboard](https://res.cloudinary.com/forestry-demo/image/upload/v1690998148/tina-io/docs/self-hosted/Screenshot_2023-08-02_at_1.29.58_PM.png) + +Then add the following environment variables to your project: + +```env +KV_URL="redis://***" +KV_REST_API_URL="********" +KV_REST_API_TOKEN="********" +KV_REST_API_READ_ONLY_TOKEN="********" +NEXTAUTH_CREDENTIALS_KEY=tinacms_users +``` + +## Setup NextAuth + +Create a file called `tina/auth.{ts,js}` this will contain your NextAuth configuration. + +```ts +import { RedisUserStore, TinaCredentialsProvider } from "tinacms-next-auth"; + +const { + NEXTAUTH_CREDENTIALS_KEY: authCollectionName = "tinacms_users", + NEXTAUTH_SECRET: secret, + KV_REST_API_URL: url = "missing-url", + KV_REST_API_TOKEN: token = "missing-token", +} = process.env; + +const userStore = new RedisUserStore(authCollectionName, { url, token }); +const authOptions = { + pages: { + error: "/auth/signin", // Error code passed in query string as ?error= + signIn: "/auth/signin", + }, + secret, + providers: [ + TinaCredentialsProvider({ name: "VercelKVCredentialsProvider", userStore }), + ], +}; + +export { authOptions, userStore }; +``` + +## Create a NextAuth API Route + +Create a new file at `pages/api/auth/[...nextauth].ts` and add the following: + +```ts +import NextAuth from "next-auth"; +import { authOptions } from "../../../tina/auth"; + +export default NextAuth(authOptions); +``` + +Create a file called `pages/api/credentials/register.{ts,js}` + +```ts +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { userStore } from "../../../tina/auth"; + +export default async function handler(req, res) { + const { username, password } = req.body; + if (req.method === "POST") { + if (!username || !password) { + res.status(400).json({ message: "Missing username or password" }); + } else { + try { + const success = await userStore.addUser(username, password); + if (success) { + res.status(200).json({ message: "User added" }); + } else { + res.status(400).json({ message: "User already exists" }); + } + } catch (e) { + console.error(e); + res.status(500).json({ message: "Internal server error" }); + } + } + } else { + res.status(400).json({ message: "Invalid request" }); + } +} +``` + + +## Create a Signin and Register Page + +`pages/auth/signin.{tsx,js}` +```ts +import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next" +import { getCsrfToken } from "next-auth/react" +import { Redis } from "@upstash/redis" + +export default function SignIn({ csrfToken, error, userSetupRequired }: InferGetServerSidePropsType) { + if (userSetupRequired) { + return ( +
+
+
+ TinaCMS Logo +
User setup required. Click here to add your first user.
+
+
+
+ ) + } + return ( +
+
+
+ TinaCMS Logo + {error && ( +
+ Sign In Failed [{error}] +
+ )} +
+
+ +
+ + + +
+
+
+
+ ) +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + let userSetupRequired = false + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN && process.env.NEXTAUTH_CREDENTIALS_KEY) { + const kv = new Redis({ + url: process.env.KV_REST_API_URL, + token: process.env.KV_REST_API_TOKEN, + }) + const users = await kv.json.get(process.env.NEXTAUTH_CREDENTIALS_KEY) + if (!users || Object.keys(users).length === 0) { + userSetupRequired = true + } + } + return { + props: { + csrfToken: await getCsrfToken(context), + error: context.query?.error || '', + userSetupRequired + }, + } +} +``` + +`pages/auth/register.{tsx,js}` + +```ts +import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next" +import { getCsrfToken } from "next-auth/react" +import { Redis } from "@upstash/redis" + +export default function SignIn({ csrfToken, error, userSetupRequired }: InferGetServerSidePropsType) { + if (userSetupRequired) { + return ( +
+
+
+ TinaCMS Logo +
User setup required. Click here to add your first user.
+
+
+
+ ) + } + return ( +
+
+
+ TinaCMS Logo + {error && ( +
+ Sign In Failed [{error}] +
+ )} +
+
+ +
+ + + +
+
+
+
+ ) +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + let userSetupRequired = false + if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN && process.env.NEXTAUTH_CREDENTIALS_KEY) { + const kv = new Redis({ + url: process.env.KV_REST_API_URL, + token: process.env.KV_REST_API_TOKEN, + }) + const users = await kv.json.get(process.env.NEXTAUTH_CREDENTIALS_KEY) + if (!users || Object.keys(users).length === 0) { + userSetupRequired = true + } + } + return { + props: { + csrfToken: await getCsrfToken(context), + error: context.query?.error || '', + userSetupRequired + }, + } +} + +``` + + + + +## Update your Tina Config + +Add the following to your `tina/config.{ts.js}` file + +```ts +import { createTinaNextAuthHandler } from "tinacms-next-auth/dist/tinacms"; + +export default defineConfig({ + //... + contentApiUrlOverride: "/api/gql", + admin: { + auth: { + useLocalAuth: isLocal, + customAuth: !isLocal, + ...createTinaNextAuthHandler({ + callbackUrl: "/admin/index.html", + isLocalDevelopment: isLocal, + name: "VercelKVCredentialsProvider", + }), + }, + } + //... +}) +``` + + +## Protect your GraphQL API endpoint + +Add the following to your `pages/api/gql.ts` file + +```ts +import { NextApiHandler } from "next"; +import databaseClient from "../../tina/__generated__/databaseClient"; +import { withNextAuthApiRoute } from "tinacms-next-auth"; +import { authOptions } from "../../tina/auth"; + +const nextApiHandler: NextApiHandler = async (req, res) => { + const { query, variables } = req.body; + const result = await databaseClient.request({ query, variables }); + return res.json(result); +}; + +export default withNextAuthApiRoute(nextApiHandler, { + authOptions, + isLocalDevelopment: process.env.TINA_PUBLIC_IS_LOCAL === "true", +}); +``` + + +## Testing the authentiation flow locally + +To test the athentication flow locally you will need to set the following environment variables: + +```bash +TINA_PUBLIC_IS_LOCAL=false +``` + +This will enable the authentication flow to use the `KV_REST_API_URL` and `KV_REST_API_TOKEN` environment variables to connect to your Upstash database. + +you can start the dev server by running: + +```bash +yarn dev +``` + + +## Managing users + +The first time you run the authentication flow you will be redirected to the `/auth/register` page. This page will allow you to add your first user. Once you have added your first user you not be able to add anymore users via the UI. You can add them by running our local CLI that will allow you to add users to your database. + +```bash +npx tinacms-next-auth setup +``` + +This will allow you to add and delete users. diff --git a/content/docs/self-hosted/authentication-provider/tina-cloud.md b/content/docs/self-hosted/authentication-provider/tina-cloud.md index 9122da325..df64f3d08 100644 --- a/content/docs/self-hosted/authentication-provider/tina-cloud.md +++ b/content/docs/self-hosted/authentication-provider/tina-cloud.md @@ -5,4 +5,66 @@ prev: '/docs/self-hosted/authentication-provider/next-auth' next: '/docs/self-hosted/authentication-provider/bring-your-own' --- -TODO: +You can use Tina Cloud for your authenticaion. This is the easiet way to get up and running quickly. + +If you have not alreay you can [create a tina cloud account](app.tina.io) and create a new project. + +Once you have created a project you will need to add the following environment variables to your project: + +```env +NEXT_PUBLIC_TINA_CLIENT_ID=*** +``` + +## Configure the authentication provider + +Since we are using Tina Cloud no extra setup is required. + +`tina/config.{ts,js}` +```ts +//... +export defualt defineConfig({ + // Make sure this is set to point to your graphql endpoint + contentApiUrlOverride: "/api/gql", + clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID!, + //... + admin: { + auth: { + useLocalAuth: process.env.TINA_PUBLIC_IS_LOCAL === "true", + }, + }, +}) +``` + +## Protecting routes + +Update your graphql endpoint to look like the following + +`/pages/api/gql.{ts,js}` +```ts +import { NextApiHandler } from "next"; +import { isUserAuthorized } from "@tinacms/auth"; +import { databaseRequest } from "../../lib/databaseConnection"; + +const nextApiHandler: NextApiHandler = async (req, res) => { + // Example if using TinaCloud for auth + const tinaCloudUser = await isUserAuthorized({ + clientID: process.env.NEXT_PUBLIC_TINA_CLIENT_ID, + token: req.headers.authorization, + }); + + const isAuthorized = + process.env.TINA_PUBLIC_IS_LOCAL === "true" || + tinaCloudUser?.verified || + false; + + if (isAuthorized) { + const { query, variables } = req.body; + const result = await databaseRequest({ query, variables }); + return res.json(result); + } else { + return res.status(401).json({ error: "Unauthorized" }); + } +}; + +export default nextApiHandler; +```