This module covers setting up Twitter authentication for this application which acts as one of the last few steps in closing the loop, from generating your the blurb and running plagarism checks to finally posting the Tweet.
4.1 Twitter Auth Configuration
4.2 NextJS APIs
4.3 Configure NextAuth
4.4 Frontend
In order to hook up Twitter with our application, we need a developer account and some consumer keys. Follow the steps below on how to setup if you have not already done so.
- Login to your Twitter account
- Create twitter developer account. Navigate to https://developer.twitter.com/en/portal/petition/essential/basic-info. This is the development account registration page.
- Sign up for free account. 250 Character use case textbox has to be filled in before you can create a new account. Just enter anything in here and Twitter should automatically provision one for you. Once provisioned you will be on the Free Plan which means 1500 tweets a month and 50 tweets every 24 hours max.
Note free account access is limited to ONLY the following endpoints:
- POST /2/tweets
- GET /2/users/me
- DELETE /2/tweets
See https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api for more details.
-
Navigate to the Twitter dev dashboard: https://developer.twitter.com/en/portal/dashboard
-
Scroll down to the
Apps
section and click on the button+ Add App
. Then enter in a name and hit the next button -
This should create an application. Proceed by clicking on
App Settings
button as we now need to configure oauth2 -
Under the section
User authentication settings
and click onSet Up
button. Next do the following:
a. Set App permissions toRead and write
b. Type of App to
Web App
c. Callback URI to
http://127.0.0.1:3000/api/auth/callback/twitter
. This is because localhost is not accepted here as a valid callback.d. Website URL to i.e. (http://example.com). It doesn't really matter for local development and prorotyping
-
Click on the save button and you will be taken to a page with your client ID and client secret. Copy these values down and store somewhere safe as these keys will be required for the
.env.local
file for local development. -
Lastly, we need to set a value for
NEXTAUTH_SECRET
env variable used by nextauth.js library which is used to encrypt and decrypt JWT tokens. See here for more documentation on generating a good value: https://next-auth.js.org/configuration/options
Run the following command in your terminal and copy output to your .env.local
file as NEXTAUTH_SECRET
value:
openssl rand -base64 32
Should look something like this:
NEXTAUTH_SECRET=+77tjH9yNylsQMBTRIAjCiYfgdfFLFbkHxSL94Wo6aE=
In order to tweet your post to Twitter, we need to create a NextJS API which will be called by the frontend. This API will be responsible for posting the tweet to Twitter.
Create a Tweet Post API
- Install the
next-auth
package. Command:pnpm i next-auth
- Create an Edge function named
tweetPost.ts
inpages/api
. - Obtain and validate the user's authentication (JWT) from the request (https://next-auth.js.org/configuration/options#jwt-helper)
- Validate the incoming request Body
- Post Tweet using the Twitter API (https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets)
- Deploy your API
Solution
- Create a file named
tweetPost.ts
inpages/api
. - Create a handler which takes a
req
parameter. - Obtain and validate the JWT token from the request.
- Validate the incoming request Body.
- Post Tweet using the Twitter API.
- Push your code to main to deploy your API.
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
type TweetRequest = {
message: string;
};
/*
Given Twitter has been authenticated
And a TweetRequest has been provided
Then post the tweet to Twitter
*/
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
// Validate Token
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
if (!token) {
throw new Error("Not authorised, please login to Twitter first");
}
// Validate Request
const body = JSON.parse(req.body) as TweetRequest;
if (!body.message) {
throw new Error("No message provided");
}
// Post Tweet
const response = await fetch("https://api.twitter.com/2/tweets", {
method: "POST",
headers: {
Authorization: `Bearer ${token.access_token}`,
"content-type": "application/json",
},
body: JSON.stringify({
text: body.message,
}),
});
const details = await response.json();
res.status(response.ok ? 201 : 400).send(details);
} catch (e) {
res.status(500).send((e as Error).message);
}
};
Now that we've setup our Twitter API, we need to configure NextAuth to use Twitter as an authentication provider. NextAuth is a library that abstracts away the complexity of authentication and provides a simple API for us to use. It also provides a number of authentication providers out of the box which we can use. In this case, we will be using Twitter as our authentication provider.
NextAuth is configured in the pages/api/auth/[...nextauth].ts
file. This file is a dynamic route which means it will match any route that starts with /api/auth/
and then anything after that. This is useful as it allows us to create multiple authentication providers in the same file. For example, we could have a Twitter and Facebook authentication provider in the same file.
Authentication is important as it allows us to identify the user and also obtain an access token which we can use to make API calls on behalf of the user. In this case, we will be using the Twitter API to post a tweet on behalf of the user.
Outline:
- Create a catch-all dynamic route named
[...nextauth].ts
inpages/api/auth
- Configure NextAuth to use Twitter (https://next-auth.js.org/providers/twitter)
- Ensure scope is set to
"users.read tweet.read tweet.write offline.access"
so the user's token will have access to get the user's Profile Picture, name and email as well as being able to write tweets
- "users.read" - allows us to get the user's profile picture, name and email
- "tweet.read" - allows us to read tweets
- "tweet.write" - allows us to write tweets
- "offline.access" - allows us to obtain a refresh token which can be used to obtain a new access token when the current one expires
- Bind the Twitter Provider to the NextAuth configuration
Learn more about NextJs Dynamic Routes (ie. [...nextauth]): https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes Learn NextAuth: https://next-auth.js.org/getting-started/introduction
Solution
import NextAuth from "next-auth";
import TwitterProvider from "next-auth/providers/twitter";
// File naming: the brackets [ define our API route as a parameter (or variable) and the ... tells Next.js that there can be more than one parameter
const twitterProvider = TwitterProvider({
clientId: process.env.TWITTER_CLIENT_ID,
clientSecret: process.env.TWITTER_CLIENT_SECRET,
authorization: {
url: "https://twitter.com/i/oauth2/authorize",
params: {
scope: "users.read tweet.read tweet.write offline.access",
},
},
version: "2.0",
});
export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async jwt({ account, token }) {
if (account) {
token.refresh_token = account.refresh_token;
token.access_token = account.access_token;
}
return token;
},
},
providers: [twitterProvider],
});
Finally, let's start creating the UI to show the user what their tweet will look like before posting it to Twitter.
Outline:
UI
- Create a file named
signinToolbar.tsx
incomponents
- Create a component named
SigninToolbar
which uses theuseSession
hook from next-auth to determine if the user is logged in or not - Add the
SigninToolbar
component to theHome
page
Solution
// components/signinToolbar.tsx
import { Box, Button } from "@mui/material";
import { useSession } from "next-auth/react";
import * as React from "react";
export default function SigninToolbar() {
const { data: session, status } = useSession();
return (
<Box position="absolute" top="1em" right="1em">
{status === "authenticated" ? "Logged in" : "Logged Out"}
</Box>
);
}
Login
- If the user is not logged in, show a login button
- The login button should call the
signIn
function from next-auth (https://next-auth.js.org/getting-started/example#frontend---add-react-hook)
Solution
// components/SigninToolbar.tsx
import { Box, Button } from "@mui/material";
import { useSession, signIn } from "next-auth/react";
import * as React from "react";
function UnauthenticatedContent() {
return (
<Button
variant="contained"
size="medium"
color="primary"
onClick={() => {
signIn("twitter", {
callbackUrl: process.env.NEXTAUTH_URL,
});
}}
>
Login With Twitter
</Button>
);
}
export default function SigninToolbar() {
const { data: session, status } = useSession();
return (
<Box position="absolute" top="1em" right="1em">
{status === "authenticated" ? "Logged in" : <UnauthenticatedContent />}
</Box>
);
}
Logout
- If the user is logged in, show a welcome message and a logout button
- The logout button should call the
signOut
function from next-auth (like you did in the Login step)
Solution
import { Box, Button } from "@mui/material";
import { signIn, signOut, useSession } from "next-auth/react";
import * as React from "react";
function AuthenticatedContent({ username }: { username?: string | null }) {
return (
<div>
<span className="mr-3">
Welcome <b className="text-green-500">{username}!</b>
</span>
<Button
variant="contained"
size="medium"
color="primary"
onClick={() => {
signOut({ redirect: true });
}}
>
Sign Out
</Button>
</div>
);
}
function UnauthenticatedContent() {
return (
<Button
variant="contained"
size="medium"
color="primary"
onClick={() => {
signIn("twitter", {
callbackUrl: process.env.NEXTAUTH_URL,
});
}}
>
Login With Twitter
</Button>
);
}
export default function SigninToolbar() {
const { data: session, status } = useSession();
return (
<Box position="absolute" top="1em" right="1em">
{status === "authenticated" ? (
<AuthenticatedContent username={session.user?.name} />
) : (
<UnauthenticatedContent />
)}
</Box>
);
}
You will also need to import SigninToolBar
into pages/index.tsx
and add it to the page.
return (
<Stack
component="main"
direction="column"
maxWidth="50em"
mx="auto"
alignItems="center"
justifyContent="center"
py="1em"
spacing="1em"
>
+ <SigninToolbar />
<Typography
variant="h1"
className="bg-gradient-to-br from-black to-stone-400 bg-clip-text text-center font-display text-4xl font-bold tracking-[-0.02em] text-transparent drop-shadow-sm md:text-7xl md:leading-[5rem]"
>
Generate your next Twitter post with ChatGPT
</Typography>
Once your app is deployed
- Update the
NEXTAUTH_URL
environment variable to be your deployed site's URL. - In your Twitter developer account, update your callback URL to be
<your_site_URL_here>/api/auth/callback/twitter
. To get to this setting see the step4.c
inSetting Up Twitter API Consumer & Client Keys
Test
- Deploy your changes
- Login with Twitter
- Logout
- Login again
- Create a file named
profilePicture.tsx
incomponents
. - The component should show the logged in user's profile picture (https://next-auth.js.org/getting-started/client). Since we're using Twitter, we can use the
image
property from the session object. - The component should be a circular image with a height of 3em and a margin-right of 1em.
- The component should be imported from
TweetPreview.ts
and used in the Dialog.
Solution
import { useSession } from "next-auth/react";
export const ProfilePicture = () => {
const { data: session } = useSession();
const twitterImage = session?.user?.image;
return (
<>
{twitterImage && (
<img
src={twitterImage}
alt="User's Twitter Profile Picture"
style={{
height: "3em",
width: "auto",
borderRadius: "50%",
marginRight: "1em",
}}
/>
)}
</>
);
};
Outline:
UI
- Create a TweetPreview Dialog component
Posting
- Create a button to tweet your blurb to the new tweetPost API
- Close the Dialog on success and show a success message
Error Handling
- Handle the response from the API
- Show an error on error
Solution
-
Create a file named
tweetPreview.tsx
incomponents
. -
Install the
@mui/icons-material
andreact-hot-toast
packages:pnpm i @mui/icons-material react-hot-toast
-
The component should declare a
blurb
parameter which gets injected by the Higher-ordered-Component, HoC. Higher-ordered-Components are parent components that wrap child components and inject props into them. In this case, the HoC is theHome
component and the child component is theTweetPreview
component. -
The component should have 4 states to manage:
editableBlurb
should be initialised with the blurb parameter. It's purpose is to allow the user to edit the blurb in the preview itself.loading
should be initialised withfalse
. It's purpose is to show a loading indicator when the user clicks the tweet button.showDialog
should be initialised withfalse
. It's purpose is to show the dialog when the user clicks the tweet button; likewise hide the Dialog when the user clicks the close button.error
should be initialised withundefined
. It's purpose is to show an error message when the API call fails. -
Tweet Handler should be async and do the following:
a. Set loading to true
b. Set error to undefined
c. Call thetweetPost
API you created earlier with theeditableBlurb
value
d. If the API call fails, set error to the error message
e. If the API call succeeds, close the Dialog and show a success message
f. Set loading to false
NOTE: On success, this will publish to your Twitter account!
import TwitterIcon from "@mui/icons-material/Twitter";
import { useState } from "react";
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
TextField,
} from "@mui/material";
import { CenterBox } from "./centerBox";
import { ProfilePicture } from "./profilePicture";
import { toast } from "react-hot-toast";
export const TweetPreview = ({ blurb }: { blurb: string }) => {
const [editableBlurb, setEditableBlurb] = useState(blurb);
const [loading, setLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
const [error, setError] = useState<string>();
const tweet = async () => {
try {
setLoading(true);
setError(undefined);
const res = await fetch("/api/tweetPost", {
method: "POST",
body: JSON.stringify({
message: blurb,
}),
});
const errors = (await res.json()).errors;
if (Array.isArray(errors) && errors.length > 0) {
throw new Error(errors[0].message);
} else {
toast("Tweet Posted!");
setShowDialog(false);
}
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
};
return (
<>
<TwitterIcon
className="cursor-pointer"
onClick={() => setShowDialog(true)}
/>
<Dialog
open={showDialog}
onClose={() => setShowDialog(false)}
fullWidth
sx={{ maxWidth: 600, mx: "auto" }}
>
<DialogTitle>Tweet Preview</DialogTitle>
<DialogContent sx={{ position: "relative" }}>
{loading && (
<CenterBox
sx={{
backgroundColor: "white",
zIndex: 1,
opacity: 0.5,
}}
>
<CircularProgress color="primary" />
</CenterBox>
)}
<Stack direction="row">
<ProfilePicture />
<Box width={"100%"}>
{error && <p className="text-red-500">{error}</p>}
<TextField
fullWidth
minRows={4}
multiline
onChange={(e) => setEditableBlurb(e.target.value)}
sx={{ "& textarea": { boxShadow: "none !important" } }}
value={editableBlurb}
variant="standard"
/>
</Box>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowDialog(false)} disabled={loading}>
Close
</Button>
<Button
onClick={tweet}
disabled={loading}
variant="contained"
color="primary"
>
Tweet
</Button>
</DialogActions>
</Dialog>
</>
);
};
NextAuth.js provides a session provider that enables session management in Next.js applications. The session provider handles the creation, storage, and retrieval of session data, including user authentication status and related information. As the final step, we need to wrap our application in a Session Provider.
Make the following changes in pages/_app.tsx
import "@/styles/globals.css";
import type { AppProps } from "next/app";
+ import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "@mui/material";
import theme from "../styles/theme";
export default function App({ Component, pageProps }: AppProps) {
return (
+ <SessionProvider>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
+ </SessionProvider>
);
}
Next let's add a toast pop up once the tweet has been successfully tweeted to our index.tsx
.
import { Button, Stack, TextField, Typography } from "@mui/material";
import { useCallback, useRef, useState } from "react";
import Blurb from "@/components/blurb";
import SigninToolbar from "@/components/signinToolbar";
+ import { Toaster } from "react-hot-toast";
export default function Home() {
...
<SigninToolbar />
<Typography
variant="h1"
className="bg-gradient-to-br from-black to-stone-400 bg-clip-text text-center font-display text-4xl font-bold tracking-[-0.02em] text-transparent drop-shadow-sm md:text-7xl md:leading-[5rem]"
>
Generate your next Twitter post with ChatGPT
</Typography>
+ <Toaster
+ position="top-center"
+ reverseOrder={false}
+ toastOptions={{ duration: 2000 }}
+ />
<TextField
multiline
Finally add the TweetPreview
component to your blurb
component so that it shows after the blurbs have finished generating.
The blurb.tsx
component's return statement should look like this:
return (
<>
<Stack direction="row" spacing="1em">
<Card sx={{ width: "37em" }}>
<CardContent>
{!blurbsFinishedGenerating ? (
generatingPost
) : (
<>
{highlightedHTMLBlurb}
<Box>
<Stack direction="row-reverse" spacing="0.5em">
<TweetPreview blurb={generatingPost} />
</Stack>
</Box>
</>
)}
</CardContent>
</Card>
<Stack
alignItems="center"
justifyContent="center"
width="12em"
className="bg-white rounded-xl shadow-md p-4 border"
>
<Plagiarism loading={plagiarismLoading} score={plagiarisedScore} />
</Stack>
</Stack>
</>
);