User Authorisation Trpc + Nextjs. pt - 1, 2

  1. GitHub Repository Link
  2. Youtube Video Backend Part
  3. Youtube Video Frontend Part

» What we building?

  1. In this blog, we will use tools such as tRPC + Next.js + Prisma + Postgres + JSON web tokens to authorize the user and once they are authorized we will show them our PRO features or you can say blogs. You can of course make changes on whatever you want to show the users.

  2. For Demo, please visit the youtube video and look for what we are building section.

  3. So let’s start.

» Initialize the project

  1. Open the terminal and run the following command to check if you have docker and node already installed on your computer:
node -v
docker -v
  1. Then run the below command to create a next.js
pnpm create next-app trpc-nextjs-app
  1. And select yes for TS, ESlint, tailwind, App router. Then let’s open the project in VS code.

» Install dependencies

  1. And now let’s install the dependencies.

To use tRPC and JWTs we have dependencies to install, so in the terminal of the code editor or a local one, run the following command:

pnpm add @trpc/server superjson zod jsonwebtoken bcryptjs prisma @prisma/client 
@types/bcryptjs @types/jsonwebtoken

» Docker setup

  1. Now open the docker desktop on your computer and log into it because while pulling an image of postgres on our computer we need docker daemon running.

  2. Next, create a docker-compose.yml in the root directory.

  3. In this file basically we are going to define two services: “postgres” and “pgAdmin”.

version: '3'
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    ports:
      - '6500:5432'
    volumes:
      - progresDB:/var/lib/postgresql/data
    env_file:
      - ./.env
  pgAdmin:
    image: dpage/pgadmin4
    container_name: pgAdmin
    env_file:
      - ./.env
    ports:
      - '5050:80'
volumes:
  progresDB:
  1. Now let’s run this container by using the command:
docker compose up -d
  1. Let’s check that the container is up and running: Run the command in terminal:
docker ps -a
  1. And there you should have two containers running. Alright?

NOTE: Please, if you get confuse anywhere, don’t forget to look into the video provided above.

» Connect to Database

  1. Now let’s Connect the App to the Postgres database using Prisma. To connect our application to the database, open the .env file and replace the content with the following:
# PostgreSQL Configuration
POSTGRES_HOST=127.0.0.1  
# Our PostgreSQL database is hosted on the local machine.

POSTGRES_PORT=6500      
 # It's running on port 6500.

POSTGRES_USER=postgres   
# We connect using the username 'postgres'.

POSTGRES_PASSWORD=password  
# And the password is 'password'.

POSTGRES_DB=trpc_prisma  
# The database we're using is named 'trpc_prisma'.

# Database URL for Prisma
DATABASE_URL=postgresql://postgres:password@localhost:6500/trpc_prisma?schema=public
# This URL is specifically for Prisma, providing all the details 
# needed to connect to our PostgreSQL database.

# PGAdmin Configuration
PGADMIN_DEFAULT_EMAIL=[email protected]  
# The default email to access PGAdmin is '[email protected]'.

PGADMIN_DEFAULT_PASSWORD=password  
# And the default password is 'password'.

JWT_SECRET=my_secure_jwt_secret
## add jwt key
  1. And with that also dont forget to add this .env file into the .gitignore file, so that it does not get pushed to the public repo.

  2. Now open schema.prisma file: In this file we are going to define how our database will store user-related information, and its also going to include an enum for user roles (either ‘user’ or ‘admin’). It’s like a blueprint for our database structure.

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

model User {
    id       String   @id @default(uuid())
    name     String   @db.VarChar(255)
    email    String   @unique
    verified Boolean? @default(false)

    password String
    role     RoleEnumType? @default(user)

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt

    provider String? @default("local")

    @@map(name: "users")
}

enum RoleEnumType {
    user
    admin
}
  1. Prisma is going to use this schema to generate a client that can be used to interact with the database in a type-safe manner.

  2. After this open the terminal and run the below command to generate a migration file

pnpm prisma migrate dev --name init

» Open pgAdmin

  1. Now let’s use pgAdmin app if you have it on your local machine or you can use it on browser on url http://localhost:5050/login and here log into it by using the credentials which we provided in our .env file.

  2. Now when we click on the database section in side menu and then here we have a trpc_prisma section inside which we have schemas and then in tables we have two tables one for prisma migrations which is used to track schema changes and then then users which reprsent user model we defined in prisma.schema file above.

  3. Now let’s create a lib folder inside which we create a prisma.ts file. This file is going to be used to set up a singleton pattern for the Prisma client,its a tool for database access.

import { PrismaClient } from '@prisma/client';

const prismaClientSingleton = () => {
    return new PrismaClient();
};

declare global {
    var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

const prisma = globalThis.prisma ?? prismaClientSingleton();

if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;

export { prisma };

» Use Zod for validating the schema

  1. Now we are going to use zod to validate the user schema. This is going to help us enforce a consistent and validated structure for user input while user creation and login processes.

  2. So create a user-schema.ts file inside the lib folder and in this start by importing object string and type of functions and types from zod library.

import { object, string, TypeOf } from 'zod';

export const createUserSchema = object({
    name: string({ required_error: 'Name is required' }).min(
        1,
        'Name is required'
    ),
    email: string({ required_error: 'Email is required' })
        .min(1, 'Email is required')
        .email('Invalid email'),
    password: string({ required_error: 'Password is required' })
        .min(1, 'Password is required')
        .min(8, 'Password must be more than 8 characters')
        .max(32, 'Password must be less than 32 characters'),
    passwordConfirm: string({
        required_error: 'Please confirm your password',
    }).min(1, 'Please confirm your password'),
}).refine((data) => data.password === data.passwordConfirm, {
    path: ['passwordConfirm'],
    message: 'Passwords do not match',
});

export const loginUserSchema = object({
    email: string({ required_error: 'Email is required' })
        .min(1, 'Email is required')
        .email('Invalid email or password'),
    password: string({ required_error: 'Password is required' }).min(
        1,
        'Password is required'
    ),
});

export type CreateUserInput = TypeOf<typeof createUserSchema>;
export type LoginUserInput = TypeOf<typeof loginUserSchema>;

  1. These are all the validation rules and custom error messages for createUserSchema.

  2. Also we use .refine to ensure that the password and password confirmation match.

  3. And then inside the loginUserSchema which is an object has email and password as a feilds that are required for a user to login.

  4. Also we have Input Types: CreateUserInput: and LoginUserInput that CreateUserInput is going to represent the expected input type for creating a user, based on the createUserSchema.

  5. LoginUserInput: is going to represent the expected input type for user login, based on the loginUserSchema.

» tRPC Authentication Middleware

  1. Now we are going to create a trpc authentication middlware which is going to ensure that only users with valid JWTs can access RPC(remote procedure call).

  2. Cretae a server folder in the root directory and inside it create a folder named routers and inside it create a file auth-middleware.ts

import { TRPCError } from '@trpc/server';
import jwt from 'jsonwebtoken';
import { prisma } from 'src/lib/prisma';
import { cookies } from 'next/headers';

export const deserializeUser = async () => {
    const cookieStore = cookies();
    try {
        let token;
        if (cookieStore.get('token')) {
            token = cookieStore.get('token')?.value;
        }
        const notAuthenticated = {
            user: null,
        };

        if (!token) {
            return notAuthenticated;
        }
        const secret = process.env.JWT_SECRET!;
        const decoded = jwt.verify(token, secret) as { sub: string };

        if (!decoded) {
            return notAuthenticated;
        }

        const user = await prisma.user.findUnique({ where: { id: decoded.sub } });

        if (!user) {
            return notAuthenticated;
        }

        const { password, ...userWithoutPassword } = user;
        return {
            user: userWithoutPassword,
        };
    } catch (err: any) {
        throw new TRPCError({
            code: 'INTERNAL_SERVER_ERROR',
            message: err.message,
        });
    }
}

» tRPC Procedure Handlers

  1. Now we will create a tRPC Procedure Handler function for handling user registration, login, and logout in our app. Create a file into server/routers/auth-controller.ts
import { CreateUserInput, LoginUserInput } from 'src/lib/user-schema'
import bcrypt from 'bcryptjs';
import { prisma } from 'src/lib/prisma';
import { TRPCError } from '@trpc/server';
import jwt from 'jsonwebtoken';
import { cookies } from 'next/headers';

export const registerHandler = async ({
    input,
}: {
    input: CreateUserInput;
}) => {
    try {
        const hashedPassword = await bcrypt.hash(input.password, 12);

        const user = await prisma.user.create({
            data: {
                email: input.email,
                name: input.name,
                password: hashedPassword,
            },
        });

        const { password, ...userWithoutPassword } = user;

        return {
            status: 'success',
            data: {
                user: userWithoutPassword,
            },
        };
    } catch (err: any) {
        if (err.code === 'P2002') {
            throw new TRPCError({
                code: 'CONFLICT',
                message: 'Email already exists',
            });
        }
        throw err;
    }
}

export const loginHandler = async ({
    input,
}: {
    input: LoginUserInput;
}) => {
    try {
        const user = await prisma.user.findUnique({
            where: { email: input.email },
        });

        if (!user || !(await bcrypt.compare(input.password, user.password))) {
            throw new TRPCError({
                code: 'BAD_REQUEST',
                message: 'Invalid email or password',
            });
        }

        const secret = process.env.JWT_SECRET!;
        const token = jwt.sign({ sub: user.id }, secret, {
            expiresIn: 60 * 60,
        });

        const cookieOptions = {
            httpOnly: true,
            path: '/',
            secure: process.env.NODE_ENV !== 'development',
            maxAge: 60 * 60,
        };

        cookies().set('token', token, cookieOptions);

        return {
            status: 'success',
            token,
        };
    } catch (err: any) {
        throw err;
    }
}

export const logoutHandler = async () => {
    try {
        cookies().set('token', '', {
            maxAge: -1,
        });
        return { status: 'success' };
    } catch (err: any) {
        throw err;
    }
};


» User Controller

  1. Now we are going to create getUserHandler which will be responsible for fetching and returning the details of the currently authenticated user. It relies on a custom context (ctx), which will hold user information, and throws a TRPCError in case of any unexpected errors, ensuring a more controlled and structured error handling approach. Create a file into server/routers/user-controllers.ts
import type { Context } from 'utils/trpc-context';
import { TRPCError } from '@trpc/server';

export const getUserHandler = ({ ctx }: { ctx: Context }) => {
    try {
        const user = ctx.user;
        return {
            status: 'success',
            data: {
                user,
            },
        };
    } catch (err: any) {
        throw new TRPCError({
            code: 'INTERNAL_SERVER_ERROR',
            message: err.message,
        });
    }
};

» tRPC context

  1. Now that we’ve created the RPC handler functions, the next step is to establish routes thatare going to trigger these functions. To kick things off, we’ll configure the tRPC context to make use of the JWT middleware function.

  2. We start by creating a file named trpc-context.ts within the ‘utils’ directory"

import { deserializeUser } from 'server/routers/auth-middleware'
import { inferAsyncReturnType } from '@trpc/server';

export const createContext = async () => deserializeUser();

export type Context = inferAsyncReturnType<typeof createContext>;

» Set up tRPC procedures

  1. Now we are going to set up tRPC procedures and a router for handling user registration, login, and logout in a secure and organized way.

  2. Create auth-route.ts file in the server/routers.

import { createUserSchema, loginUserSchema, CreateUserInput, LoginUserInput } 
from 'src/lib/user-schema';
import { protectedProcedure, pubicProcedure, t } from 'utils/trpc-server';
import { loginHandler, logoutHandler, registerHandler } from './auth-controller';

const authRouter = t.router({
    registerUser: pubicProcedure
        .input(createUserSchema)
        .mutation(({ input }: { input: CreateUserInput }) => registerHandler({ input })),
    loginUser: pubicProcedure
        .input(loginUserSchema)
        .mutation(({ input }: { input: LoginUserInput }) => loginHandler({ input })),
    logoutUser: protectedProcedure.mutation(() => logoutHandler()),
});

export default authRouter;

» Configure tRPC for our application

  1. Now we will are going to set up and configure tRPC for a our application. So in utils folder create a file in trpc-server.ts
import { TRPCError, initTRPC } from '@trpc/server';
import SuperJSON from 'superjson';
import { Context } from './trpc-context';

export const t = initTRPC.context<Context>().create({
    transformer: SuperJSON,
});

const isAuthed = t.middleware(({ next, ctx }) => {
    if (!ctx.user) {
        throw new TRPCError({
            code: 'UNAUTHORIZED',
            message: 'You must be logged in to access this resource',
        });
    }
    return next();
});

export const pubicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);

» Create a unified appRouter.

  1. Now we are going to set up tRPC routers and create a unified appRouter.

  2. So Navigate to the app directory inside it create an api folder. Then Inside it, create another folder named trpc and within the trpc folder, create a file named trpc-router.ts(app/api/trpc/trpc-router.ts)

  3. This is going to set up tRPC routers for status checking, authentication, and user-related queries. We have created a unified ‘appRouter’ by merging these routers and we also provide functions for creating callers with and without an asynchronous context, to enhancethe flexibility of handling tRPC operations within our application.

import authRouter from '@/server/auth-route';
import { getUserHandler } from '@/server/user-controller';
import { createContext } from '@/utils/trpc-context';
import { protectedProcedure, t } from '@/utils/trpc-server';

const statusCheckRouter = t.router({
    statuschecker: t.procedure.query(() => {
        return {
            status: 'success',
            message: 'Welcome to the trpc server!',
        };
    }),
});

const userRouter = t.router({
    getUser: protectedProcedure.query(({ ctx }) => getUserHandler({ ctx })),
});

export const appRouter = t.mergeRouters(
    statusCheckRouter,
    authRouter,
    userRouter
);

export const createCaller = t.createCallerFactory(appRouter);

export const createAsyncCaller = async () => {
    const context = await createContext();
    return createCaller(context);
};

export type AppRouter = typeof appRouter;
  1. Next in the trpc folder create a dynamic folder [trpc] and inside it create a route.ts folder. (app/api/trpc/[trpc]/route.ts). This is a tRPC HTTP route.

  2. In this file we’re going to set up a request handler using tRPC and Fetch.

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '../trpc-router';
import { createContext } from '@/utils/trpc-context';

const handler = (request: Request) => {
    console.log(`incoming request ${request.url}`);
    return fetchRequestHandler({
        endpoint: '/api/trpc',
        req: request,
        router: appRouter,
        createContext: createContext,
    });
};

export { handler as GET, handler as POST };
  1. Now that we are done creating routers, and we try to access the getUser RPC at(http://localhost:3000/api/trpc/getUser) we will get an error which says we must be signed in to access this url. because we are not still authenticated. http://localhost:3000/api/trpc/getUser

  2. But if you try to access statusChecker RPC at (http://localhost:3000/api/trpc/statuschecker) you will look we get it. Because this status checker rpc can be accessed by anyone, even if they are unauthenticated.

» FRONTEND PART

Note: The Frontend Part video of this project is this.

  1. Now let’s build the front-end part of our application. Open the terminal and run the command:
pnpm add @tanstack/[email protected] @tanstack/[email protected]
 @tanstack/eslint-plugin-query react-hook-form @hookform/resolvers 
 tailwind-merge react-hot-toast @trpc/client @trpc/react-query

» Update Tailwind config file.

  1. Let’s now update the Tailwind CSS configuration file. We’re going to customize our theme to extend its default settings.
  theme: {
    extend: {
    //   We're start by adding a custom font family called 'Poppins'.
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
    //   And configure the container to be centered, with some padding, and
    //  specific screen width limits. On larger screen its going to be 1125px,
    //  xl and 2xl its also the same.
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },

» React Query and the tRPC client for the Next.js

  1. let’s now move on to setting up React Query and the tRPC client within the Next.js project. First, we’ll need to initialize the tRPC client. So to do that navigate to the utils directory and create a file named trpc.ts. (utils/trpc.ts)
import type { AppRouter } from '@/app/api/trpc/trpc-router';
import { createTRPCReact } from '@trpc/react-query';

export const trpc = createTRPCReact<AppRouter>();

  1. Now we want to set up a QueryClient from React Query, which will create a new instance that will manage data caching and fetching in our application.

  2. So create another file in utils folder and name it query-client.ts. (utils/query-client.ts)

import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default queryClient;

» Create Client Component


  1. As all components that we have inside the ‘app‘ directory are now server components by default, and since we want to render the tRPC and React Query providers only on the client side, we need to create a client component. In Next.js, this is achieved by including the ‘use client‘ directive at the top of the file.

  2. So create a file name trpc-provider.tsx in the utils folder. (utils/trpc-provider.tsx)

'use client';

import { QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, getFetch, loggerLink } from '@trpc/client';
import { useState } from 'react';
import superjson from 'superjson';
import { trpc } from './trpc';
import queryClient from './query-client';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
    children,
}) => {
    const url = process.env.NEXT_PUBLIC_VERCEL_URL
        ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
        : 'http://localhost:3000/api/trpc/';

    const [trpcClient] = useState(() =>
        trpc.createClient({
            links: [
                loggerLink({
                    enabled: () => true,
                }),
                httpBatchLink({
                    url,
                    fetch: async (input, init?) => {
                        const fetch = getFetch();
                        return fetch(input, {
                            ...init,
                            credentials: 'include',
                        });
                    },
                }),
            ],
            transformer: superjson,
        })
    );
    return (
        <trpc.Provider client={trpcClient} queryClient={queryClient}>
            <QueryClientProvider client={queryClient}>
                {children}
                <ReactQueryDevtools />
            </QueryClientProvider>
        </trpc.Provider>
    );
};
  1. Now we want to invoke the procedures on the server, so that we dont need an HTTP request. This is going to be a sever side function. It’s utilizing createAsyncCaller function we created before.

  2. Create a file named utils/get-auth-user.ts in the utils directory. (utils/get-auth-user.ts)

'use server';

import { createAsyncCaller } from '@/app/api/trpc/trpc-router';
import { redirect } from 'next/navigation';

export const getAuthUser = async ({
    shouldRedirect = true,
}: {
    shouldRedirect?: boolean;
} = {}) => {
    const caller = await createAsyncCaller();
    return caller
        .getUser(undefined)
        .then((result) => result.data.user)
        .catch((e) => {
            if (e.code === 'UNAUTHORIZED' && shouldRedirect) {
                redirect('/login');
            }

            return null;
        });
};
  1. So in short, getAuthUser, uses tRPC to fetch authenticated user information. It allows for optional redirection to the login page in case of unauthorized access. The function also theb returns the user data if successfully fetched, or it’s going to return null if there’s an error or the user is unauthorized, with the option to redirect if needed.

» Create reusable components

  1. Now, we proceed to create reusable components that can be utilized in various sections of our application.

  2. We start by creating a header component which will render navigation links dynamically based on the user authentication.

  3. We will fetch users data on server and render the links based on that information.

  4. So we will convert our header component into a server component

  5. To start off Let’s create a components folder in the root dircetory. in the components create a file auth-menu.tsx. (components/auth-menu.tsx)

'use client';

import queryClient from '@/utils/query-client';
import { trpc } from '@/utils/trpc';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast';

export default function AuthMenu() {
    const router = useRouter();

    const { mutate: logoutFn } = trpc.logoutUser.useMutation({
        onError(error) {
            toast.error(error.message);
            console.log('Error message:', error.message);
        },
        onSuccess() {
            queryClient.clear();
            toast.success('logout successful');
            router.push('/login');
        },
    });

    return (
        <>
            <li>
                <Link href='/ProPage' className='text-white font-semibold
                 hover:text-pink-500'>
                    Pro Page
                </Link>
            </li>
            <li className='cursor-pointer text-white font-semibold
             hover:text-pink-500' 
            onClick={() => logoutFn()}>
                Logout
            </li>
        </>
    );
}

» Header component

  1. Let’s create a header component now. In this we use getAuthUSer function to fecth suer data by invoking the getUser RPC on server.
import Link from 'next/link';
import AuthMenu from './auth-menu';
import { getAuthUser } from '@/utils/get-auth-user';

const Header = async () => {
    const user = await getAuthUser({ shouldRedirect: false });

    return (
        <nav className="sticky top-0 z-10 backdrop-filter backdrop-blur-lg bg-opacity-0 
        border-b border-gray-200">
            <div className="max-w-5xl mx-auto px-4">
                <div className="flex items-center justify-between h-16">
                    <div>
                        <Link href='/' className='bg-gradient-to-r from-pink-500 to-yellow-500 
                        bg-clip-text 
                        text-transparent text-2xl font-bold'>
                            All Star.
                        </Link>
                    </div>
                    <ul className='flex items-center gap-4'>
                        <li>
                            <Link href='/' 
                            className='text-white hover:text-pink-500 
                            font-semibold'>
                                Home
                            </Link>
                        </li>
                        {!user && (
                            <>
                                <li>
                                    <Link href='/register' 
                                    className='text-white hover:text-pink-500 
                                    font-semibold'>
                                        Register
                                    </Link>
                                </li>
                                <li>
                                    <Link href='/login' 
                                    className='text-white hover:text-pink-500 
                                    font-semibold'>
                                        Login
                                    </Link>
                                </li>
                            </>
                        )}
                        {user && <AuthMenu />}
                    </ul>
                </div>
            </div>
        </nav>
    );
};

export default Header;

» Spinner and a LoadingButton components

  1. Let’s create two components: a Spinner and a LoadingButton that uses the Spinner component.

  2. Create a file names spinner.tsx in the components fodler.

import React from 'react';
import { twMerge } from 'tailwind-merge';
type SpinnerProps = {
    width?: string;
    height?: string;
    color?: string;
    bgColor?: string;
};
const Spinner: React.FC<SpinnerProps> = ({
    width = '1.25rem',
    height = '1.25rem',
    color,
    bgColor,
}) => {
    return (
        <svg
            role='status'
            className={twMerge(
                'mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600',
                `${color} ${bgColor}`
            )}
            style={{ height, width }}
            viewBox='0 0 100 101'
            fill='none'
            xmlns='http://www.w3.org/2000/svg'
        >
            <path
                d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 
                100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 
                0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 
                50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 
                91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 
                72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 
                9.08144 50.5908Z'
                fill='currentColor'
            />
            <path
                d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 
                33.5539C95.2932 28.8227 
                92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 
                75.2124 7.41289C69.5422 
                4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 
                41.7345 
                1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 
                9.04874 41.5694 10.4717 44.0505 
                10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 
                10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 
                15.2552C75.2735 17.9648 79.3347 21.5619 
                82.5849 25.841C84.9175 28.9121 86.799
                 32.2913 
                88.1811 35.8758C89.083 38.2158 91.5421 
                39.6781 93.9676 39.0409Z'
                fill='currentFill'
            />
        </svg>
    );
};

export default Spinner;
  1. Create a file names loading-button.tsx in the components fodler. (components/loading-button.tsx)
import React from 'react';
import { twMerge } from 'tailwind-merge';
import Spinner from './spinner';

type LoadingButtonProps = {
    loading: boolean;
    btnColor?: string;
    textColor?: string;
    children: React.ReactNode;
};

export const LoadingButton: React.FC<LoadingButtonProps> = ({
    textColor = 'text-white',
    btnColor = 'bg-gradient-to-r from-pink-500 to-yellow-500',
    children,
    loading = false,
}) => {
    return (
        <button
            type='submit'
            className={twMerge(
                `w-full py-3 font-semibold rounded-lg outline-none 
                border-none flex justify-center`,
                `${btnColor} ${loading && 'bg-[#ccc]'}`
            )}
        >
            {loading ? (
                <div className='flex items-center gap-3'>
                    <Spinner />
                    <span className='text-gray-200 inline-block'>Loading...</span>
                </div>
            ) : (
                <span className={`${textColor}`}>{children}</span>
            )}
        </button>
    );
};

» Create forms

  1. Next we want to create forms; our application is going to have 2 forms. 1- for login and 2 - for registration.

  2. So let’s create a file name form-input.tsx components/form-input.tsxin the components directory.

import React from 'react';
import { useFormContext } from 'react-hook-form';

type FormInputProps = {
    label: string;
    name: string;
    type?: string;
};

const FormInput: React.FC<FormInputProps> = ({
    label,
    name,
    type = 'text',
}) => {
    const {
        register,
        formState: { errors },
    } = useFormContext();
    return (
        <div className=''>
            <label htmlFor={name} className='block text-gray-200 mb-3'>
                {label}
            </label>
            <input
                type={type}
                placeholder=' '
                className='block w-full rounded-2xl appearance-none 
                focus:outline-none py-2 px-4'
                {...register(name)}
            />
            {errors[name] && (
                <span className='text-red-500 text-xs pt-1 block'>
                    {errors[name]?.message as string}
                </span>
            )}
        </div>
    );
};

export default FormInput;

» Home Page

  1. Next we create our home page- the main landing page for our app. So let’s open the page.tsx file present in the app directory. (app/page.tsx)
import Header from '@/components/header';
import Image from 'next/image';

export default function Home() {
  return (
    <>
      <Header />
      <section className='bg-gray-950 min-h-screen pt-20 mt-auto'>
        <div className='mx-14'>
          <div className="text-center text-white">
            <h1 className="text-5xl font-bold mb-4">Welcome to 
            <span className='bg-gradient-to-r from-pink-500 to-yellow-500 bg-clip-text 
            text-transparent'>
            All Star!</span></h1>
            <p className="text-lg mb-8 px-20"> We are a dynamic software agency 
            dedicated to transforming ideas into cutting-edge digital solutions. 
            With a passion for pushing technological boundaries, our team of 
            skilled developers, designers, and strategists collaborates to 
            craft bespoke software that propels businesses to new heights.</p>
            <a href="#" className="bg-gradient-to-r from-pink-500 to-yellow-500 text-white 
            py-2 px-4 rounded-full 
            hover:bg-gradient-to-l transition duration-300">Create an account.</a>
          </div>

          <div className="mt-16 grid grid-cols-2 gap-4">
            <div className='cursor-pointer'>
              <Image src="/fotis-fotopoulos-DuHKoV44prg-unsplash.jpg" 
              alt="Software Image 1" width={300} height={300} 
              className="w-full h-auto rounded-lg" />
              <h1 className='text-center text-lg'>Unlocking the Power of AI</h1>
            </div>
            <div className='cursor-pointer'>
              <Image src="/hack-capital-uv5_bsypFUM-unsplash.jpg" 
              alt="Software Image 2" width={300} height={300} 
              className="w-full h-auto rounded-lg" />
              <h1 className='text-center text-lg'>Building the Future of Blockchain</h1>
            </div>
            <div className='cursor-pointer'>
              <Image src="/john-schnobrich-FlPc9_VocJ4-unsplash.jpg" 
              alt="Software Image 3" width={300} height={300} 
              className="w-full h-auto rounded-lg" />
              <h1 className='text-center text-lg'>Creating the Next Big Thing</h1>
            </div>
            <div className='cursor-pointer'>
              <Image src="/zan-X_JsI_9Hl7o-unsplash.jpg" 
              alt="Software Image 3" width={300} height={300} 
              className="w-full h-auto rounded-lg" />
              <h1 className='text-center text-lg'>Building the Future of Blockchain</h1>
            </div>
          </div>
        </div>
      </section>
    </>
  );
}

» Register form

Now we want to create our register form right? So the user can login into it. So for it what we will do is create a another folder named register insise the app directory and then inside it create a file named register-form.tsx (app/register/register-form.tsx)

'use client';

import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import Link from 'next/link';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { CreateUserInput, createUserSchema } from '@/lib/user-schema';
import { trpc } from '@/utils/trpc';
import FormInput from '@/components/form-input';
import { LoadingButton } from '@/components/loading-button';

export default function RegisterForm() {
    const router = useRouter();
    const [submitting, setSubmitting] = useState(false);

    const methods = useForm<CreateUserInput>({
        resolver: zodResolver(createUserSchema),
    });

    const { reset, handleSubmit } = methods;

    const { mutate: registerFn } = trpc.registerUser.useMutation({
        onMutate() {
            setSubmitting(true);
        },
        onSettled() {
            setSubmitting(false);
        },
        onError(error) {
            reset({ password: '', passwordConfirm: '' });
            toast.error(error.message);
            console.log('Error message:', error.message);
        },
        onSuccess() {
            toast.success('registered successfully');
            router.push('/login');
        },
    });

    const onSubmitHandler: SubmitHandler<CreateUserInput> = (values) => {
        registerFn(values);
    };

    return (
        <FormProvider {...methods}>
            <form
                onSubmit={handleSubmit(onSubmitHandler)}
                className='max-w-md w-full mx-auto overflow-hidden shadow-lg 
                bg-gray-900 text-black rounded-2xl p-8 space-y-5'
            >
                <FormInput label='Full Name' name='name' />
                <FormInput label='Email' name='email' type='email' />
                <FormInput label='Password' name='password' type='password' />
                <FormInput
                    label='Confirm Password'
                    name='passwordConfirm'
                    type='password'
                />
                <span className='block text-white'>
                    Already have an account?{' '}
                    <Link href='/login' className='bg-gradient-to-r from-pink-500 to-yellow-500 
                    bg-clip-text text-transparent fonse'>
                        Login Here
                    </Link>
                </span>
                <LoadingButton loading={submitting} textColor='text-white'>
                    Register
                </LoadingButton>
            </form>
        </FormProvider>
    );
}
  1. now we want to create a page component taht will render account registration form that we created just above. So in the app dir inside the regisetr folder create a file named page.tsx(app/register/page.tsx)
import Header from '@/components/header';
import RegisterForm from './register-form';

export default async function RegisterPage() {
    return (
        <>
            <Header />
            <section className='py-8 bg-gray-950 min-h-screen grid place-items-center'>
                <div className='w-full'>
                    <h1 className='text-5xl font-bold mb-4 xl:text-6xl 
                    text-center bg-gradient-to-r from-pink-500 to-yellow-500 bg-clip-text 
                    text-transparent'>
                        Welcome to All Star Register!
                    </h1>
                    <h2 className='mt-2 text-lg text-center mb-4 text-white'>
                        Sign Up To Get Started!
                    </h2>
                    <RegisterForm />
                </div>
            </section>
        </>
    );
}

» Update Layout

  1. By wrapping the trpc provoder around the children prop in layout folder we ensure that all client components within the component tree have access to both the tRPC and React Query clients.

  2. So open the layout file present in the app directory. (layout.tsx)

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { TrpcProvider } from '@/utils/trpc-provider';
import { Toaster } from 'react-hot-toast';

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <TrpcProvider>
          <div>
            {children}
            <Toaster />
          </div>
        </TrpcProvider>
      </body>
    </html>
  );
}

» Login Form

  1. Now as we also done with user regisrtarion let’s start with creating a login form now. Login form will have just two input field email and password.

So tp create a login form first let’s create a login folder into the app dir. (app/login/login-form.tsx)

'use client';

import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { LoginUserInput, loginUserSchema } from '@/lib/user-schema';
import FormInput from '@/components/form-input';
import { LoadingButton } from '@/components/loading-button';
import { trpc } from '@/utils/trpc';
import toast from 'react-hot-toast';

export default function LoginForm() {
    const [submitting, setSubmitting] = useState(false);
    const router = useRouter();

    const methods = useForm<LoginUserInput>({
        resolver: zodResolver(loginUserSchema),
    });

    const { reset, handleSubmit } = methods;

    const { mutate: loginFn } = trpc.loginUser.useMutation({
        onSettled() {
            setSubmitting(false);
        },
        onMutate() {
            setSubmitting(true);
        },
        onError(error) {
            toast.error(error.message);
            console.log('Error message:', error.message);
            reset({ password: '' });
        },
        onSuccess() {
            toast.success('login successfully');
            router.push('/');
        },
    });

    const onSubmitHandler: SubmitHandler<LoginUserInput> = (values) => {
        loginFn(values);
    };

    return (
        <FormProvider {...methods}>
            <form
                onSubmit={handleSubmit(onSubmitHandler)}
                className='max-w-md w-full mx-auto overflow-hidden shadow-lg 
                bg-gray-900 rounded-2xl p-8 space-y-5 text-black'
            >
                <FormInput label='Email' name='email' type='email' />
                <FormInput label='Password' name='password' type='password' />

                <div className='text-center text-gray-200'>
                    <Link href='#' className=''>
                        Forgot Password?
                    </Link>
                </div>
                <LoadingButton loading={submitting} textColor='text-white'>
                    Login
                </LoadingButton>
                <span className='block'>
                    Need an account?{' '}
                    <Link href='/register' className='bg-gradient-to-r 
                    from-pink-500 to-yellow-500 bg-clip-text text-transparent font-semibold'>
                        Sign Up Here
                    </Link>
                </span>
            </form>
        </FormProvider>
    );
}
  1. Now that we have login page created successufuly we want it to render to the client right? So create let’s create a file named page.tsx inside the login dir. (app/login/page.tsx)
import Header from '@/components/header';
import LoginForm from './login-form';

export default async function LoginPage() {
    return (
        <>
            <Header />
            <section className='bg-gray-950 min-h-screen grid place-items-center'>
                <div className='w-full'>
                    <h1 className='text-4xl lg:text-6xl text-center font-[600]
                     bg-gradient-to-r from-pink-500 to-yellow-500 
                     bg-clip-text text-transparent mb-4'>
                        Welcome Back
                    </h1>
                    <h2 className='text-lg text-center mb-4 text-ct-dark-200'>
                        Login to have access
                    </h2>
                    <LoginForm />
                </div>
            </section>
        </>
    );
}

» Server Component

Now, let’s create a server component that will utilize the getAuthUser function and will retrieve the authenticated user’s information and based on it it will display the page. If user is authenticated the propage will be shown and if the user is not authenticated it will redirect user to the login page.

  1. So create a propage directory inside the app folder and then create a page.tsx file. (app/profile/page.tsx)

  2. This component represents the Pro Page of your application.

import Header from '@/components/header';
import { getAuthUser } from '@/utils/get-auth-user';
import Image from 'next/image';
// blog component

export default async function ProPage() {
    const user = await getAuthUser();

    return (
        <>
            <Header />
            {/* Blog */}
            <section className='bg-gray-950  min-h-screen pt-20'>
                <div className='max-w-4xl mx-auto rounded-md h-auto flex 
                justify-center items-center'>
                    <div>
                        <p className='text-5xl text-center font-bold mt-2 
                        bg-gradient-to-r from-pink-500 to-yellow-500 
                        bg-clip-text text-transparent'>
                            Pro Page
                        </p>
                        <div className="my-16 grid grid-cols-2 gap-4">
                            <div className='cursor-pointer'>
                                <Image src="/fotis-fotopoulos-DuHKoV44prg-unsplash.jpg" 
                                alt="Software Image 1" width={300} height={300} 
                                className="w-full h-auto rounded-lg" />
                                <h1 className='text-center text-lg text-gray-200 
                                shadow-white hover:shadow-lg font-semibold mt-2'>
                                Navigating the Digital Landscape</h1>
                            </div>
                            <div className='cursor-pointer'>
                                <Image src="/hack-capital-uv5_bsypFUM-unsplash.jpg" 
                                alt="Software Image 2" width={300} height={300} 
                                className="w-full h-auto rounded-lg" />
                                <h1 className='text-center text-lg text-gray-200
                                 shadow-white hover:shadow-lg font-semibold 
                                 mt-2'>Building the Future of Blockchain</h1>
                            </div>
                            <div className='cursor-pointer'>
                                <Image src="/john-schnobrich-FlPc9_VocJ4-unsplash.jpg" 
                                alt="Software Image 3" width={300} height={300} 
                                className="w-full h-auto rounded-lg" />
                                <h1 className='text-center text-lg text-gray-200 
                                shadow-white hover:shadow-lg font-semibold 
                                mt-2'>Behind the Code</h1>
                            </div>
                            <div className='cursor-pointer'>
                                <Image src="/zan-X_JsI_9Hl7o-unsplash.jpg" 
                                alt="Software Image 3" width={300} height={300} 
                                className="w-full h-auto rounded-lg" />
                                <h1 className='text-center text-lg text-gray-200
                                 shadow-white hover:shadow-lg font-semibold 
                                 mt-2'>Innovation Unleashed</h1>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
        </>
    );
}
  1. So now let’s save the file and go to localhost 3000 and start by registering a user and then login and then you will have the propage shown.

So that’s all for this blog. I hope you enjoyed it and learned something new. Thank you for reading this blog and I’ll see you in the next one. Till then happy coding. ByeBye