Part 128: Building a User Registration Page with Next.js and Prisma

[App] Authentication User Database

[App] Authentication User Database

In our previous discussions, we set the groundwork for user authentication by defining a User model in our Prisma schema and implementing a function to create new users in the database. Today, we will take the next step by adding a "Sign Up" page that allows new users to register on our website. This will involve creating a new page and form in our Next.js application, and integrating it with the database logic we have already established.

Setting Up the Development Environment

Before we start coding, let's run the Next.js development server. This will allow us to preview changes in real-time as we build our "Sign Up" page. You can start the server using the following command:

npm run dev

Creating the "Sign Up" Page

Defining the Route

The first task is to create a new route for the "Sign Up" page. We'll add this under the /sign-up path. This page will be structured similarly to our existing "Sign In" page, but with some modifications to accommodate user registration.

Building the "Sign Up" Form

We will create a SignUpForm component, which will include fields for the user's email, name, and password. This form will handle the user input and trigger the registration process.

// File path: components/SignUpForm.jsx

'use client';

import { signUpAction } from '@/app/sign-up/actions';
import { useFormState } from '@/lib/hooks';

export default function SignUpForm() {
  const [state, handleSubmit] = useFormState(signUpAction);

  return (
    <form onSubmit={handleSubmit}
      className="border bg-white flex flex-col gap-2
                 max-w-screen-sm mt-3 px-3 py-3 rounded">
      <div className="flex">
        <label htmlFor="emailField" className="shrink-0 w-32">
          Email
        </label>
        <input id="emailField" name="email" type="email" required
          className="border px-2 py-1 rounded w-full"
        />
      </div>
      <div className="flex">
        <label htmlFor="nameField" className="shrink-0 w-32">
          Name
        </label>
        <input id="nameField" name="name" type="text" required
          className="border px-2 py-1 rounded w-full"
        />
      </div>
      <div className="flex">
        <label htmlFor="passwordField" className="shrink-0 w-32">
          Password
        </label>
        <input id="passwordField" name="password" type="password" required
          className="border px-2 py-1 rounded w-full"
        />
      </div>
      {Boolean(state.error) && (
        <p className="text-red-700">{state.error.message}</p>
      )}
      <button type="submit" disabled={state.loading}
        className="bg-orange-800 rounded px-2 py-1 self-center
                   text-slate-50 w-32 hover:bg-orange-700
                   disabled:bg-slate-500 disabled:cursor-not-allowed">
        Submit
      </button>
    </form>
  );
}

Implementing the "Sign Up" Action

The signUpAction function is responsible for handling form submissions. It extracts user data from the form, calls the createUser function to save the data in the database, and sets a session cookie.

// File path: app/sign-up/actions.js

'use server';

import { redirect } from 'next/navigation';
import { setSessionCookie } from '@/lib/auth';
import { createUser } from '@/lib/users';

export async function signUpAction(formData) {
  console.log('[signUpAction]', formData);
  const data = {
    email: formData.get('email'),
    name: formData.get('name'),
    password: formData.get('password'),
  };
  // TODO validate data
  const user = await createUser(data);
  console.log('[signUpAction] user:', user);
  await setSessionCookie(user);
  redirect('/');
}

To enhance user experience, we add navigation links between the "Sign In" and "Sign Up" pages. This makes it easy for users to switch between signing in and registering.

// File path: app/sign-up/page.jsx

import Link from 'next/link';
import Heading from '@/components/Heading';
import SignUpForm from '@/components/SignUpForm';

export const metadata = {
  title: 'Sign Up',
};

export default function SignUpPage() {
  return (
    <>
      <Heading>Sign Up</Heading>
      <SignUpForm />
      <div className="py-3">
        Already registered?{' '}
        <Link href="/sign-in" className="text-orange-800 hover:underline">
          Sign in
        </Link> instead
      </div>
    </>
  );
}

The lib/auth.js file is crucial for handling authentication, specifically the creation of session cookies using JSON Web Tokens (JWT). The changes made here ensure that sensitive information, such as the user's password, is not included in the JWT payload.

// File path: lib/auth.js

export async function setSessionCookie({ id, email, name }) {
  const expirationTime = new Date(Date.now() + JWT_DURATION);
  const sessionToken = await new SignJWT({ id, email, name })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime(expirationTime)
    .sign(JWT_SECRET);

  // Set the cookie in the response header to establish a session
  // Implementation details for setting the cookie will depend on your server setup
}

Here, we specifically destructure the user object to include only the id, email, and name fields when creating the JWT. This prevents the password from being encoded into the token, which is a critical security measure.

Updates in app/sign-in/page.jsx

The SignInPage component now includes a link to the "Sign Up" page, providing a seamless navigation experience for users who have not yet registered. This is a simple addition but an important one for user experience.

// File path: app/sign-in/page.jsx

import Link from 'next/link';
import Heading from '@/components/Heading';
import SignInForm from '@/components/SignInForm';

export default function SignInPage() {
  return (
    <>
      <Heading>Sign In</Heading>
      <SignInForm />
      <div className="py-3">
        Not yet registered?{' '}
        <Link href="/sign-up" className="text-orange-800 hover:underline">
          Sign up
        </Link> instead
      </div>
    </>
  );
}

Explanation

  1. Session Cookie Setup (lib/auth.js):

    • The setSessionCookie function creates a JWT with the user data, excluding sensitive information like the password. Only the necessary details (id, email, name) are included in the token payload, which is then signed and used to set a session cookie. This cookie establishes a session for the authenticated user.

  2. Navigation Links (app/sign-in/page.jsx):

    • Adding a link to the "Sign Up" page in the SignInPage component helps guide users who might be visiting the sign-in page for the first time but are not registered yet. This small addition significantly enhances usability by providing clear pathways for users to navigate between registration and login processes.

Testing the Registration Process

To ensure everything works as expected, let's test the registration process by signing up a new user. We will use sample credentials to see if the data is correctly inserted into the database and if a session is established.

Verifying the Database Entry

After submitting the form, you can verify that the new user has been added to the database by running:

sqlite3 dev.db "SELECT * FROM User"

This command will display all the users in the User table, confirming that the registration process works as intended.

Conclusion

With the "Sign Up" page in place, users can now register on our website, and their data is securely stored in the database. This marks a significant milestone in building a comprehensive authentication system. In future posts, we will explore enhancing security by validating user input and securely handling passwords. Stay tuned for more updates!

Last updated