Introduction: A Comprehensive Walkthrough of Grafbase Integration with Next.js 13 for Database and Authentication-NextAuth
Get ready to embark on a journey that's part coding wizardry and part digital detective work, as you and I unravel the mysteries of connecting Grafbase with Next.js 13 and dancing hand-in-hand with Authentication-NextAuth. By the time you're done, you'll be slinging queries and authenticating like a tech-savvy Sherlock Holmes, and you might find yourself giving your database a standing ovation. So, grab your magnifying glass and let's dive into this delightful tech tango! π΅οΈββοΈπ©π
That's by the way.
So in this walkthrough, I'll take you from creating and deploying your Grafbase project from the CLI, and then integrate NextAuth within our Next.js 13 project while using Grafbase Graphql API. And in case you're worried if you don't understand GraphQL and how it works, then I'm here for you, I promise to explain thoroughly. So relax and follow along as I take you through this walkthrough.
Before you start, let me talk about what Grafbase is capable of, and how amazing it is.
What really is Grafbase?
Grafbase is a platform that allows and gives power to developers to build GraphQL API fast and effectively and also allows developers to connect any type of database using resolvers and connectors. It also allows developers to set authentication and permission rules to an endpoint.
So in a nutshell, Grafbase is a backend for your mobile application or web application that's all the complex stuff for you and then lets you handle the easy part, but that's just the surface level, its capabilities goes beyond what I described.
So at least you have a fair of what Grafbase is about.
To start your journey, first, you either sign up or sign in to grafbase in order to use their services.
After you have completed the registration or login, you'll be redirected to your dashboard, then from there, move to your terminal.
Now I'll show how easy it is to create, link, and deploy a project. Continuing, create a directory called grafbase-auth
Project Setup
To set up our project, let's first initial a Next.js project, by entering the command below in the terminal.
npx create-next-app ./
Say yes to all the options that will be given on the terminal. And it should look like this below:
β Would you like to use TypeScript with this project? ... No / Yes
β Would you like to use ESLint with this project? ... No / Yes
β Would you like to use `src/` directory with this project? ... No / Yes
β Would you like to use experimental `app/` directory with this project? ... No / Yes
β What import alias would you like configured? ... @/*
Creating a new Next.js app in C:\Users\essel_r\desktop\grafbase-auth.
Using npm.
Installing dependencies:
- react
- react-dom
- next
- @next/font
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
After that, you need to install the packages below to use our GraphQL seamlessly in our walkthrough.
npm install @apollo/client graphql-request
You'll be using NextAuth for authenticating your application in this walkthrough and might need other packages like bcrypt
for password hashing and JWT
for our custom token, so install that below.
npm install next-auth jsonwebtoken @types/jsonwebtoken bcrypt @types/bcrypt axios react-icons encoding
After the above installation is done, let's move on to the main dish of this walkthrough π.
Now to install Grafbase globally in your terminal, run this command on your terminal.
npm install -g grafbase
After this step, you can now run Grafbase(Your Backend), with only a few commands, init, create, link, deploy, dev
You'll be using the above commands frequently, so let's explain what each command does.
For npx grafbase init
: Is used for setting up the current or existing project in your directory, or for new projects. Where you will be asked to choose whether to have your project in GraphQL
or TypeScript
.
npx grafbase init
So now, run the following command in your terminal, then run ls
in your parent directory, a directory called grafbase
is created and inside of it is your schema.graph
. You'll then get an output like so:
$ npx grafbase init
Grafbase CLI 0.30.0
> What configuration format would you like to use? GraphQL
β¨ Your project was successfully set up for Grafbase!
Your new schema can be found at .\grafbase\schema.graphql
After you can run the project in the terminal run the command below npx grafbse dev
and that's for the next step. We'll talk about the pathfinder later, and how to start query data, but if you want to check that before continuing then here is a link to the Grafbase doc
npx grafbase dev
The output for the above command should look like this:
$ npx grafbase dev
Grafbase CLI 0.30.0
π‘ Listening on port 4000
- Pathfinder: http://127.0.0.1:4000
- Endpoint: http://127.0.0.1:4000/graphql
So now let's stop or kill the terminal, and then see what npx grafbase create
allows you to do. The command below allows us to create a project with your existing project, meaning in order to create a project, you first need to initial a grafbase project which has either grafbase/grafbase.config.ts
or grafbase/schema.graphql
, or you will get an error saying: Error: could not find grafbase/grafbase.config.ts or grafbase/schema.graphql in the current or any parent directory
.
npx grafbase create
After running this project you will be asked a few questions related to your project. Then your project will be created and deployed. The output:
$ npx grafbase create
Grafbase CLI 0.30.0
> What should your new project be called? grafbase-auth
> In which account should the project be created? Rocky Essel (rockyessel)
> In which region should your database be created? Frankfurt (eu-central-1)
> Please confirm the above to create and deploy your new project Yes
β¨ grafbase-auth was successfully created!
Endpoints:
- https://grafbase-auth.....grafbase.app/graphql
- https://grafbase-auth-.....grafbase.app/graphql
The next step is the npx grafbse deploy
, it helps you deploy your existing or new projects. But in case your project is an existing project you clone from Github, then in order for you to deploy any changes to the GraphQL API endpoint, you have to link
the existing project to the project in Grafbase.
npx grafbase link && npx grafbase deploy
So this explains the relationship between link
and deploy
.
npx grafbase login
With the login, this gives you the authorisation to perform the above operation explains. So now I think you're good to go!!!!
Now your project has been initialised
, created
and deployed
. Let's start the fun.
Walkthrough - Part 1
Inside of our schema.graph
type Post @model @search {
title: String!
# Other field
}
type Comment @model @search {
post: Post!
# Other field
}
type User @model {
name: String!
# Other field
}
Let's go over the GraphQL schema but from the perspective of how JavaScript developers build their model
using mongoose
in Node.js
GraphQL Basic
Firstly, GraphQL is a query language for your API, and the schema you see above is like a contract that outlines the types of data that can be queried and manipulated using GraphQL operations. In contrast to RESTful APIs, where you typically have multiple endpoints for different resources, GraphQL uses a single endpoint (https://...../graph)
and allows clients to request exactly the data they need, which can reduce over-fetching and under-fetching of data.
Here is the breakdown of the default schema generated by Grafbase:
Post Type:
Represents a blog post.
@model
indicates that this type is backed by some data source (like a database table), and likely operations (like CRUD) can be performed on it.@search
might indicate that there's search functionality integrated, possibly through some plugin or service.title
: A non-null string field representing the title of the post.slug
: A non-null unique string field, possibly for SEO-friendly URLs.content
: An optional string field for the content of the post.publishedAt
: A DateTime field indicating when the post was published.comments
: An array ofComment
objects associated with this post.likes
: An integer field with a default value of 0, representing the number of likes on the post.tags
: An array of strings, possibly representing tags associated with the post.author
: AUser
an object representing the author of the post.
Comment Type:
Represents a comment on a post.
post
: A required reference to anPost
object, indicating which post this comment belongs to.body
: The content of the comment.likes
: An integer field with a default value of 0, representing the number of likes on the comment.author
: AUser
object representing the author of the comment.
User Type:
Represents a user.
name
: A non-null string field representing the name of the user.email
: AnEmail
scalar type (which seems to be a custom scalar) representing the email of the user.posts
: An array ofPost
objects authored by this user.comments
: An array ofComment
objects authored by this user.
Remember, GraphQL can provide a more flexible and efficient way to fetch and manipulate data compared to traditional RESTful APIs, especially when dealing with complex data relationships and avoiding over-fetching or under-fetching data.
Setting up Authentication With NextAuth
Earlier in our walk-through, you install next-auth
, for those that don't know what it is. next-auth
is an authentication library is that designed to work with any OAuth services, to allow secured and flexible login with passwordless login or signups.
These OAuth services include Google, Facebook, Linkedin, Github, and Twitter, allowing users to authenticate using them. And that's next-auth
Below is how your next.js project directory should look. In the [...nextauth]
directory you create a TypeScript file called router.ts
.
src
ββββapp
ββββapi
ββββauth
ββββ[...nextauth]
src/app/api/auth/[...nextauth]
and not src/app/api/[...nextauth]
or this src/app/api/auth/[...nextAuth]
, otherwise, you definitely get an error.So this is how your router.ts code should be like
// route.ts
import NextAuth, { AuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
const authOptions: AuthOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
debug: process.env.NODE_ENV === 'development',
session: {
strategy: 'jwt',
},
secret: process.env.NEXTAUTH_SECRET!,
}
const handler = NextAuth(authOptions);
export {handler as GET, handler as POST};
Let's talk about the above code.
Imports
You first import the modules to use next-auth or integrate next-auth in our next.js project for authentication. And our case users will be authenticating with their GitHub accounts.
Options Configuration
The options part is the configuration process for our auth set-up so for the authOptions
object keys and value and here is what the keys represent;
providers
: An array of authentication providers. In this case, it's using theGitHubProvider
with the GitHub client ID and client secret obtained from environment variables.debug
: Determines whether debugging information should be displayed. It's set to true in development mode and false in other environments.session
: Specifies the session strategy. Here, it's using JSON Web Tokens (JWT) for session management.secret
: The secret key used to sign and verify tokens. It's obtained from an environment variable.
Handler
const handler = NextAuth(authOptions);
This line of code initializes the authentication handler using the NextAuth
function and the previously configured authOptions
. The handler
will be responsible for handling the authentication requests.
Exporting Handlers
This exports the handler
function under different names (GET
and POST
). Exporting it as GET
and POST
allows to make both GET
and POST
request. So when you login in or sign up you're making a POST
request, and then when you navigate between routes, it makes a GET request to retrieve your information.
And that's it for our authentication, now you're done, by doing it this way, we have left everything to handle in default, but since we want to store user information like username, name, image, password, and email. That means you need to customize the src/app/api/auth/[...nextauth]/route.ts
so can save the data to our Grafbase GraphQL backend, and return the information to use in our application.
Believe me, I would love to explain everything from my thinking process to how you can customize the however you want to your needs, but that will be pretty long, so I will paste the code here can explain at a go.
Customizing the authentication process.
But let's refactor the directory as so;
//route.ts
import { authOptions } from '@/lib/auth';
import NextAuth from 'next-auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
What we did was export authOptions
from a lib
directory you should create the src
directory, reason it was, exporting it in the route.ts
would cause an error, and also you will so use it later in your code.
root dir
ββββapp
β ββββapi
β ββββauth
β ββββ[...nextauth]
ββββlib
β ββββapi
β β ββββindex.ts
β ββββauth
β β ββββindex.ts
β ββββproviders
ββββstyles
So in index.ts
you have the code below, the code has been expanded to support Google, GitHub, and Credentials providers.
import { AuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
import jsonwebtoken from 'jsonwebtoken';
import { JWT } from 'next-auth/jwt';
import bcrypt from 'bcrypt';
import { createUserUsingProvider, findUserByEmail } from '../api';
// Configuration options for authentication
export const authOptions: AuthOptions = {
providers: [
// GitHub authentication provider configuration
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
// Google authentication provider configuration
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
// Credentials authentication provider configuration
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'email', type: 'text' },
password: { label: 'password', type: 'password' },
},
// Custom authorization logic for credentials-based authentication
async authorize(credentials) {
try {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid credentials');
}
// Find user by email from GraphQL API
const user = await findUserByEmail(credentials?.email);
const isCorrectPassword = await bcrypt.compare(
credentials?.password,
user?.password
);
if (!isCorrectPassword) {
throw new Error('Invalid credentials');
}
if (user && isCorrectPassword) {
return user;
} else {
throw new Error('User does not exist');
}
} catch (error) {
console.error('Error making GraphQL request:', error);
throw error;
}
},
}),
],
// Configure JSON Web Token (JWT) encoding and decoding
jwt: {
// Encoding function: Create and sign a JWT token
encode: ({ secret, token }) => {
return jsonwebtoken.sign(
{
...token,
iss: 'https://grafbase.com', // Issuer of the token
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 60, // Expiration time (1 hour from now)
},
secret
);
},
// Decoding function: Verify a JWT token and decode its data
decode: async ({ secret, token }) => {
return jsonwebtoken.verify(token!, secret) as JWT;
},
},
// Configure authentication callbacks
callbacks: {
// JWT callback: Executed whenever a JWT is created or updated
async jwt({ profile, token }) {
let userId;
if (token) {
// Find user by email from GraphQL API
const user = await findUserByEmail(token.email!);
if (user) {
userId = user.id;
token.sub = userId;
} else {
// Create a custom user using token data
let customUser = {
username: '',
name: token.name,
email: token.email,
image: token.picture,
};
// If GitHub profile information is available, use it
if (profile && 'avatar_url' in profile && 'login' in profile) {
customUser.image = profile.avatar_url as string;
token.username = profile.login as string;
}
// Create the user and get the user's ID
const createdUser = await createUserUsingProvider(customUser);
userId = createdUser;
token.sub = userId;
}
}
return token;
},
// Session callback: Populate session data based on JWT information
async session({ session, token }) {
// Find user by email from GraphQL API
const user = await findUserByEmail(token.email!);
if (user) {
const userId = user.id;
const customUser = {
username: token.username as string | null | undefined,
name: token.name,
email: token.email,
image: token.picture,
id: userId as string | null | undefined,
};
session.user = customUser; // Set custom user data in the session
}
return session;
},
},
};
createUserUsingProvider
& findUserByEmail
later after setting up our next-auth provider. But note that are they custom functions you will write later.Explanation:
When users login or sign up on a website, the system needs to ensure they are genuine users and grant them access to the website's features. To achieve this, the system needs to know if these users are new or returning. In other words, it needs to determine if they already have an account or if they need to be registered as new users.
For instance, consider a situation where users can log in using their email and password. The system should check whether the provided email already exists in its database. If the email exists, it's likely that the user already has an account. If not, the system needs to create a new account for them.
This decision-making process is facilitated by a callback function. Think of a callback function as a piece of code that gets triggered at a specific point in the authentication process. In this case, it's invoked when a JSON Web Token (JWT) is being created for the user.
The reason for using a callback function is twofold:
Customization of Token Information: The callback function gives developers the flexibility to customize the information that's included in the JWT token. This customization is important because the token's contents are used for creating a secure digital signature. This signature ensures that the token hasn't been tampered with.
User Information Enrichment: Developers can use this callback to enrich the token with additional user data. For example, the token could contain the user's name, email, profile picture, and any other information that helps the system recognize and authenticate the user.
The sequence of events goes like this:
A user tries to log in or sign up.
The system checks if the provided email exists in its records.
Before the system signs and issues the JWT token, it passes the token and other relevant information to the callback function.
Inside the callback function, the system can make decisions based on whether the user's email is already known or not. It can create new user records if needed.
The callback function customizes the token's contents and returns it to the system.
The system finally signs the token, making it ready for use.
In essence, the callback function acts as a bridge between the user's authentication and the token creation process. It helps ensure that user data is accurately reflected in the token and that the token's integrity is maintained for secure authentication and authorization.
Token Customization use case (Unrelated you can skip this part).
So Imagine you're building a web application using Next.js, and you're using the next-auth
library to handle user authentication. This library offers a lot of flexibility, especially in how it creates and manages authentication tokens.
Now, let's say you have another part of your application that's running on a separate Node.js server. This server is responsible for handling certain protected parts of your application that require user authorization. It's like a security checkpoint that only allows users with the right credentials to access specific areas.
In order to perform this authorization check, your Node.js server needs to see if the user's request is backed by a valid "token." A token is like a special pass that Next.js creates when a user logs in. It's a way of saying, "Hey, this user is allowed in."
However, to make this system work, your Node.js server needs to understand and trust these tokens. This is where the "signature" comes in. Think of it as a digital lock on the token. The lock's pattern is created using a secret that only your application knows. If your Node.js server can unlock the token using the same pattern, it knows the token is genuine.
So, both your Next.js application and your Node.js server need to be on the same page regarding how they create these signatures. If they're not in sync, it's like trying to unlock a door with the wrong key.
When a user logs in on your Next.js app, they get this token. When they try to access a protected area on your Node.js server, they send this token along as proof of their identity. The server then uses the secret pattern to unlock the token and confirm the user's authenticity.
To make this process smooth, you need to ensure that the way tokens are created in your Next.js app matches the way they're verified in your Node.js server. This consistency ensures that everything works seamlessly. You avoid any confusion or errors that could arise if the two parts of your application had different ways of handling tokens.
To actually send the token from Next.js to your Node.js server, you can include it in the headers of the user's request or in cookies. Alternatively, you can set up a special route in your Next.js app that your server can request to get the token.
So, by keeping the token process consistent between your Next.js app and your Node.js server, you're ensuring a secure and reliable way to let users access protected parts of your application.
Setting Up Next-Auth Provider
Configuring your authentication provider with next-auth
is straightforward. Just navigate to src/lib/providers/next-auth.tsx
in your project directory. In this file, you'll include the following code. The purpose of this code is to ensure that any HTML element with the children
prop is accessible within the scope of the next-auth
provider. This allows these elements to utilize the provider's methods and functions seamlessly, without encountering any errors.
// next-auth.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
type Props = {
children?: React.ReactNode;
};
export const NextAuthProvider = (props: Props) => {
return <SessionProvider>{props.children}</SessionProvider>;
};
In this code, the NextAuthProvider
component is defined. It takes in a props
object, where the children
prop represents the HTML elements enclosed within this provider. The purpose of SessionProvider
from next-auth/react
is to manage the authentication session and make its methods and functions available to the enclosed elements.
By wrapping your application's relevant components with this NextAuthProvider
, you ensure that they are within the scope of the next-auth
provider. This allows them to interact with authentication features without any hiccups. The SessionProvider
is responsible for seamlessly handling the session-related functionality and sharing it with your app's components.
You're about to embark on the login and registration project. To get started, navigate to your /src/app
directory. Once there, you'll spot two key files: layout.tsx
and page.tsx
. In the layout.tsx
file, we're going to envelop the next-auth
provider within the <body>
element like this:
// layout.tsx
import '@/styles/global.css';
import type { Metadata } from 'next';
import { LayoutRootProps } from '@/interface';
import { NextAuthProvider } from '@/lib/providers/next-auth';
export const metadata: Metadata = {
title: 'A Comprehensive Walkthrough of Grafbase Integration with Next.js 13 for Database and Authentication-NextAuth',
description: `So in this walkthrough, I'll take you from creating and deploying your Grafbase project from the CLI, and then integrate NextAuth within our Next.js 13 project while using Grafbase Graphql API. And in case you're worried if you don't understand GraphQL and how it works, then I'm here for you, I promise to explain thoroughly. So relax and follow along as I take you through this walkthrough.`,
};
export default function RootLayout(props: LayoutRootProps) {
return (
<html lang='en'>
<NextAuthProvider>
<body>
<main>{props.children}</main>
</body>
</NextAuthProvider>
</html>
);
}
This strategic placement ensures that authentication-related functionality is available throughout the application.
Creating Custom Function with Grafbase GraphQL Query & Mutation - Users
During the authentication set-up, there were two functions that we used to create a user and find an existing user. They were createUserUsingProvider
, and findUserByEmail
To create these functions, we need to query and mutate how graphql then return the necessary value you need. But before we continue, let's explore Grafbase Pathfinder.
Modify user modal
type User @model {
email: Email! @unique
name: String
username: String!
password: String
image: String
}
Run your endpoint locally:
npx grafbase dev
Then head to Pathfinder:
http://127.0.0.1:4000
there you should see the same screen as me:
User - Mutation
In the video, I explained the query and mutate user objects using Grafbse Pathfinder.
Here is a link for now https://youtu.be/BZ2USK9Dn_k or view it here below.
In the videos, you saw how one can create, update, delete, and query the user. But how do you do that in your Next.js application, well, it is simple, go back your Pathfinder and copy the query for the user, and it should be like this:
Query
query User {
user(by: {email:"rockyessell@gmail.com"}) {
name
username
password
email
id
}
}
So to query this, let's first create a GraphQL directory, and create a file called query.ts
is file will contain all our query data. And this is how the file should look like.
// graphql/query.tsx
import { gql } from '@apollo/client';
export const queryUserByEmail = gql`
query User($email: Email!) {
user(by: { email: $email }) {
id
email
name
username
password
image
}
}
`;
export const queryUserById = gql`
query User($id: ID!) {
user(by: { id: $id }) {
id
email
name
username
password
image
}
}
`;
The $email
parameter in the query definition is used to make the query dynamic, allowing you to pass different email values when executing the query.
Mutation
// mutation.ts
import { gql } from '@apollo/client';
export const createUserByProviders = gql`
mutation UserCreate(
$email: Email!
$username: String
$image: String
$name: String
) {
userCreate(
input: {
email: $email
username: $username
image: $image
name: $name
}
) {
user {
id
}
}
}
`;
export const createUserByCredential = gql`
mutation UserCreate($email: Email!, $password: String) {
userCreate(input: { email: $email, password: $password }) {
user {
id
email
username
name
image
}
}
}
`;
This is how your folder structure should look like.
ββββapp
β β
β ββββapi
β ββββauth
β ββββ[...nextauth]
ββββgraphql // Create i query.ts & mutation.ts
β ββββquery.ts // Create this file
β ββββmutation.ts // Create this file
ββββinterface
ββββlib
ββββapi // Here we handle function here
β ββββindex.ts // Create this file
ββββauth // AuthOptions
ββββproviders // NextAuth Provider
With src/lib/api/index.ts
created, you have your functions look like this.
import { createUserByProviders } from '@/graphql/mutation';
import { queryUserByEmail } from '@/graphql/query';
import { DocumentNode } from '@apollo/client';
import { GraphQLClient, Variables } from 'graphql-request';
export const authenticatedQuery = async (query: DocumentNode | string, variables: Variables) => {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
'x-api-key': process.env.GRAFBASE_API_KEY!,
};
const client = new GraphQLClient('http://127.0.0.1:4000/graphql', {
headers,
});
const data = await client.request(query, variables);
return data;
};
export const findUserByEmail = async (email: string) => {
const { user } = (await authenticatedQuery(queryUserByEmail, { email })) as any;
return user;
};
export const createUserUsingProvider = async (variables: Variables): Promise<string> => {
const data = (await authenticatedQuery(createUserByProviders, variables)) as any;
return data.userCreate.user.id as string;
};
// I'll leave this one for you to figure it out.
export const findUserById = async (id: string) => {};
Overall, this code provides utility functions for querying and interacting with a GraphQL API server. It handles authentication headers, makes GraphQL requests, and extracts relevant data from query and mutation responses. It seems to be part of a broader application that interacts with a GraphQL backend.
Let's break down each part of the code and understand its purpose:
Import Statements:
It imports
DocumentNode
from@apollo/client
, which represents a GraphQL query or mutation document.It imports
GraphQLClient
andVariables
fromgraphql-request
library, which is used to make GraphQL requests to the server.
authenticatedQuery
Function:This function is used to make authenticated GraphQL queries to the server.
It takes a
query
parameter (which can be a GraphQL query document or a string) andvariables
to be passed to the query.It constructs the necessary headers, including the
x-api-key
header with the API key from the environment.It creates a
GraphQLClient
instance and uses it to send a request to the specified GraphQL endpoint with the provided query and variables.It returns the response data from the request.
findUserByEmail
Function:This function is designed to retrieve user data by querying for a user using their email.
It uses the
authenticatedQuery
function to execute thequeryUserByEmail
query, passing the email variable.It extracts and returns the user data from the query result.
createUserUsingProvider
Function:This function is intended to create a user using provider-specific information.
It uses the
authenticatedQuery
function to execute thecreateUserByProviders
mutation with the provided variables.It extracts the user ID from the mutation result and returns it as a string.
User Authentication - Almost Done
Before you start implementing the authentication, let's handle env
. Create an .env
file in your parent directory, then paste the following data:
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
NEXTAUTH_SECRET="it-can-be-anything"
NEXTAUTH_URL=""
NEXT_PUBLIC_GRAFBASE_API_URL=""
GRAFBASE_API_KEY=""
Find the above information yourself. Now let's head to page.tsx
in app
directory.
src/app/page.tsx
In your page.tsx
, you will only have to create a components folder, and inside of it is a user.tsx
, add the following code, I would love to explain the code below, but it will only make the article length than it already is, but I added a comment to explain, so that makes up for it.
'use client';
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { UserProps } from '@/interface'; // Import UserProps interface
import { signOut, useSession } from 'next-auth/react'; // Import signOut and useSession functions
const User = () => {
// Create a ref for the user dropdown
const userDropdownRef = React.useRef<HTMLDivElement | null>(null);
// State to track whether user dropdown should be shown
const [showUserDropdown, setShowUserDropdown] = React.useState(false);
// Get user session data and status using useSession hook
const { data: session, status } = useSession();
// Extract user information from the session
const user = { ...session?.user } as UserProps;
// Toggle user dropdown visibility
const toggleUserDropdown = () => {
setShowUserDropdown(!showUserDropdown);
};
// Effect to handle clicks outside the user dropdown
React.useEffect(() => {
const handleOutsideClick = (event: any) => {
// If dropdown is shown and click is outside dropdown, hide it
if (
showUserDropdown &&
userDropdownRef.current &&
!userDropdownRef.current.contains(event.target)
) {
setShowUserDropdown(false);
}
};
// Add click event listener for outside clicks
document.addEventListener('click', handleOutsideClick);
// Clean up the event listener when component unmounts
return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [showUserDropdown]);
return (
<>
{status === 'authenticated' ? (
<div className='relative'>
<button
onClick={toggleUserDropdown}
type='button'
className='z-20 rounded-full flex mx-3 text-sm bg-gray-800 md:mr-0 focus:ring-4 focus:ring-gray-300'
>
<span className='sr-only'>Open user menu</span>
{/* Display user image */}
{user.image ? (
<Image
src={user.image}
width={50}
height={50}
className='w-full h-full object-cover object-center rounded-full'
alt={user.name}
priority
/>
) : (
<Image
src={'/248387.jpg'}
width={50}
height={50}
className='w-12 h-12 object-cover object-center rounded-full'
alt={'248387'}
priority
/>
)}
</button>
{/* Show user dropdown if enabled */}
{showUserDropdown && (
<div
ref={userDropdownRef}
className='absolute top-[3.6rem] left-12 right-0 mt-3 mr-2 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow'
>
{/* Display user name and email */}
<div className='py-3 px-4 text-gray-500'>
<span className='block text-sm font-semibold'>
{user?.name}
</span>
<span className='block text-sm font-light truncate '>
{user?.email}
</span>
</div>
{/* Display sign out option */}
<ul className='py-1 font-light text-gray-500 cursor-pointer'>
<li
onClick={() => signOut()}
className='block py-2 px-4 text-sm'
>
Sign out
</li>
</ul>
</div>
)}
</div>
) : (
<div className='relative'>
<button
onClick={toggleUserDropdown}
type='button'
className='z-20 rounded-full border-2 border-black flex mx-3 text-sm bg-gray-800 md:mr-0 focus:ring-4 focus:ring-gray-300'
>
<span className='sr-only'>Open user menu</span>
{/* Display default avatar */}
<Image
src={'/avator.png'}
width={50}
height={50}
className='w-full h-full object-cover object-center rounded-full'
alt={'Avator'}
priority
/>
</button>
{/* Show user dropdown if enabled */}
{showUserDropdown && (
<div
ref={userDropdownRef}
className='absolute top-[3.6rem] left-12 right-0 mt-3 mr-2 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow'
>
{/* Display link to authentication */}
<ul className='py-1 font-light text-gray-500 cursor-pointer'>
<li className='block py-2 px-4 text-sm'>
<Link href={`/account`}>Authenticate</Link>
</li>
</ul>
</div>
)}
</div>
)}
</>
);
};
export default User;
Now it is left with one more component, and that's the sign page. In the above, you can see we are redirecting the user to `/account`
, so create an account
folder in the app
directory, with a file called page.tsx
then paste the following code here.
'use client';
import React from 'react';
import axios from 'axios';
import Image from 'next/image';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { FaGithub, FaGoogle } from 'react-icons/fa';
// Initial form values
const initialFormValue = { email: '', password: '' };
const Account = () => {
const router = useRouter(); // Initialize the useRouter hook
const { status } = useSession(); // Retrieve the authentication status using the useSession hook
// React effect to redirect after authentication status changes
React.useEffect(() => {
// Check if the authentication status is 'authenticated'
if (status === 'authenticated') {
// Redirect to the homepage when the user is authenticated
router.push('/');
}
}, [router, status]);
// State to hold form values
const [form, setForm] =
React.useState<typeof initialFormValue>(initialFormValue);
// Handler for form input changes
const handleFormChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { target } = event;
setForm((initialValue) => ({
...initialValue,
[target.name]: target.value,
}));
};
// Handler for form submission
const handleSubmission = async (event: React.SyntheticEvent) => {
event.preventDefault();
if (form) {
// Make a POST request to register user
await axios.post('/api/register', { ...form });
// Sign in using credentials and bypass redirection
await signIn('credentials', {
...form,
redirect: false,
});
}
};
// Component rendering
return (
<main className='flex min-h-screen flex-col items-center justify-between p-24'>
<section className='flex flex-col'>
<div className='flex items-center justify-center'>
<div className='min-w-fit flex-col border bg-white px-6 py-14 shadow-md rounded-[4px] '>
<div className='mb-8 flex flex-col items-center justify-center'>
<Image
src='/grafbase.svg'
width={50}
height={50}
alt='Grafbase'
/>
<p className='font-rathetta'>Grafbase</p>
</div>
<form onSubmit={handleSubmission}>
<fieldset className='flex flex-col text-sm rounded-md'>
{/* Email input */}
<input
className='mb-5 rounded-[4px] border p-3 hover:outline-none focus:outline-none hover:border-yellow-500 '
type='text'
placeholder='Username or Email id'
value={form.email}
onChange={handleFormChange}
name='email'
/>
{/* Password input */}
<input
className='border rounded-[4px] p-3 hover:outline-none focus:outline-none hover:border-yellow-500'
type='password'
placeholder='Password'
value={form.password}
onChange={handleFormChange}
name='password'
/>
</fieldset>
{/* Sign-in button */}
<button
className='mt-5 w-full border p-2 bg-gradient-to-r from-gray-800 bg-gray-500 text-white rounded-[4px] hover:bg-slate-400 scale-105 duration-300'
type='submit'
>
Sign in
</button>
</form>
{/* Forgot password and sign-up links */}
<div className='mt-5 flex justify-between text-sm text-gray-600'>
<a href='#'>Forgot password?</a>
<a href='#'>Sign up</a>
</div>
{/* Social login options */}
<div className='flex justify-center mt-5 text-sm'>
<div className='inline-flex items-center justify-center w-full'>
<hr className='w-64 h-px my-8 bg-gray-200 border-0 dark:bg-gray-700' />
<span className='absolute px-3 font-medium -translate-x-1/2 bg-white left-1/2 dark:text-white dark:bg-gray-900'>
or
</span>
</div>
</div>
<div className='mt-2.5 flex justify-center gap-3 '>
{/* GitHub login */}
<FaGithub
onClick={() => signIn('github')}
className='w-10 h-10 p-1 rounded-md bg-gray-500 text-white text-2xl'
/>
{/* Google login */}
<FaGoogle
onClick={() => signIn('google')}
className='w-10 h-10 p-1 rounded-md bg-gray-500 text-white text-2xl'
/>
</div>
{/* Privacy and terms */}
<div className='mt-5 flex text-center text-sm text-gray-400'>
<p>
This site is protected by reCAPTCHA and the Google <br />
<a className='underline' href=''>
Privacy Policy
</a>
and
<a className='underline' href=''>
Terms of Service
</a>
apply.
</p>
</div>
</div>
</div>
</section>
</main>
);
};
export default Account;
The above code is a page users can authenticate with, then get redirected to the homepage, where their profile is displayed.
Finally in page.tsx
in the app
directory, you just need to add your User
components and you're done. So here is how page.tsx
should be:
import React from 'react';
import Image from 'next/image';
import User from '@/components/user';
export default function Home() {
return (
<main className='flex min-h-screen flex-col items-center justify-between p-24'>
<div className='z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex'>
<div className='inline-flex items-center gap-1'>
<User /> // added your hUser components herer.
<p className='fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30'>
Get started by editing
<code className='font-mono font-bold'>src/app/page.tsx</code>
........
// Other code
</main>
);
}
Done!!!!!!
Conclusion
The overarching message of the walkthrough encourages developers to embark on this journey with a sense of assurance. By elucidating the potential of Grafbase and demonstrating its practical implementation, the walkthrough fosters an environment of learning and exploration. As the software development landscape continues to evolve, platforms like Grafbase play a pivotal role in equipping developers with the tools they need to build sophisticated applications effectively and efficiently.
#grafbasehackathon #grafbase