Let's Create Sign In and Sign Up Page with Next.js and Appwrite | Part 1

Nitish Kumar Singh

Mar 14, 2024

In the digital world, Sign In and Sign Up pages serve as the gateway to a personalized and secure online experience. These pages are essential components of any website, serving the dual purpose of securing user access while also safeguarding sensitive information of both users and/or the website.

These pages enhance the level of trust users have in a website, as both the user and service make an agreement regarding data privacy and security, therefore adding these pages to a website instills positive trust in the brand or service. So, Hello developers! In this blog post, we are going to create properly working and production-ready Sign In and Sign Up pages using Next.js and the Appwrite Database.

To create these pages, we will use Next.js Server and Client Components for UI creation, Route Handler for API endpoints, and the Appwrite database for storing user information and session data. Appwrite provides a built-in User Management system with almost all sign-in and sign-up options, including Email, Direct Link, Google, Facebook, and more, and you can start from this by visiting the Appwrite Docs. But in this post, we will not be using this and build everything from scratch using the database.

So let's start coding! Create a file user/[action]/page.js under the app directory and paste the code below into it:

import SignIn from '@/app/user/SignIn.js';
import SignUp from '@/app/user/SignUp.js';
import React from 'react';

export async function generateMetadata ({params}) {
  return {title:params.action === "sign-in"?"Sign In":"Sign Up"};
}

export default function UserAction({params}) {
    const action = params.action;
    if(action === "sign-in") return <SignIn/>
    else if(action === "sign-up") return <SignUp/>
    else return "";
}

Here, I have created paths like /user/sign-in and /user/sign-up using dynamic routing of Next.js and display SignIn, SignUp client components and generate metadata's title based on the action param, but you can create paths as per your needs.

Sign Up Page

I have created a simple Sign-up page based on my website that is shown in the above image, but you can fully customize it according to your needs. The code of this SignUp component is below:

"use client";
import Link from 'next/link';
import React, { Component, createRef } from 'react';
import showToast from '@/app/Toast';
import { inProgress } from '../ProgressBar';
import { router } from '../Navbar';

export default class SignUp extends Component {
    j = createRef();
    data = {Name:"",Email:"",Password:"","Confirm password":""}; i = "";
    change = (event)=> this.data[event.target.placeholder] = event.target.value;
    show = (event)=>{
      let f = event.target.parentElement.firstElementChild;
      let l = event.target.parentElement.lastElementChild;
      f.type = f.type === "password"?"text":"password";
      l.type = l.type === "password"?"text":"password";
    }

  render() {
    return (
      <div style={{padding:"1px"}}>
        <div className='signBox'>
          <div style={{padding:"45px",display:"flex",flexDirection:"column",alignItems:"center"}}>
            <h2 style={{fontSize:"35px",margin:"0"}}>Sign up</h2>
            <p style={{fontSize:"18px",margin:"10px 0 20px 0"}}>Welcome! Let's Create Your Account</p>
            <input type="text" placeholder='Name' onChange={this.change}/>
            <input type="email" placeholder='Email' onChange={this.change}/>
            <div style={{width:"100%", maxWidth:"324px", position:"relative"}}>
                <input type="password" placeholder='Password' onChange={this.change}/>
                <svg onClick={this.show} style={{position: "absolute",inset: "0 5px 0 auto",width: "30px",left: "auto",marginTop: "12px",cursor: "pointer"}} xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
                    <path style={{pointerEvents:"none"}} d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/>
                </svg>
                <input type="password" placeholder='Confirm password' onChange={this.change}/>
            </div>
            <button onClick={this.signUp} className='signBtn'>Sign up</button>

            <p style={{fontSize:"17px",fontFamily:"arial"}}>Already have an account? 
                <Link href={"/user/sign-in"} replace={true} style={{textDecoration:"none",cursor:"pointer",fontSize:"inherit",color:"green",border:"none",background:"none",padding:"0"}}><b>Sign in</b></Link>
            </p>
          </div>
          <div ref={this.j} style={{display:"none",position:"absolute",inset:'0',background:"#ffffffc7"}}></div>
        </div>
      </div>
    );
  }
}

Here, I have used a simple data variable to store user-entered information that works fine, but it can be stored using state, and the show function done works for showing and hiding passwords. And the j element is used to completely hide and disable this page during processing, and the inProgress function shows a progress bar during this time.

The showToast is a JS function solely written in JavaScript or you can consider it as a Toast-Messenger that shows messages with controls of message type using different colors, position on the screen, and duration of the message. To use or see the code of this Toast Messenger, you can read the blog post How to create Toast or Snackbar using CSS and JavaScript.

The router variable is a next.js router object that gets by useRouter hook from next/navigation and signUp function of this component used to perform sign-up process and it is called when clicked on the sign button whose code is below:

    signUp = async ()=>{
      try {
        if(this.i !== ""){
          showToast("Now "+this.i);
          return;
        }
        let v = validateData(this.data);
        if (v !== true) {
          showToast(v);
          return;
        }
        this.i = "Signing up";
        inProgress(true);
        this.j.current.style.display = "block";
        const pay = JSON.stringify({userId:"unique()",name:this.data.Name,email:this.data.Email,password:this.data.Password});
        const res = await fetch(window.origin+"/api/users/up",{method:"POST",body:pay});
        const result = await res.json();
        if (res.ok) {
          // userData.ev = result;
          router.replace("sign-in");
          showToast("Signed up succussfully");
        }else showToast(result.message);
        this.i = "";
        this.j.current.style.display = "none";
        inProgress(false);
      } catch (error) {
        showToast(error.message)
      }
    }

This function first validates user information and then uses this to sign up the user and show messages accordingly. The validate function returns true on successful validation, otherwise, a message.

const validateData = (user)=> {
    if (user.Name.trim() === "" || user.Email.trim() ==="" || user.Password.trim() === "") {
        return "Name, Email and Password are required.";
    }
  
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(user.Email)) {
        return "Invalid email format.";
    }
  
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$!%*?&])[A-Za-z\d@#$!%*?&]+$/;
    if (!passwordRegex.test(user.Password)) {
        return "Password must contain at least one a-z, A-Z, 0-9, [@#$!%*?&], and be 8+ characters long.";
    }
  
    if (user.Name && user.Name.length > 128) {
        return "Name cannot exceed 128 characters.";
    }
    if (user.Password !== user["Confirm password"]) {
        return "Passwords do not match.";
    }
  
    return true;
}

Sign In Page

The Sign In form is the same as shown in the above image and its code is similar to the SignUp component with little change. Here after successful sign-in, we save session information that comes from the server in cookies.

"use client";
import Link from 'next/link';
import React, { Component, createRef } from 'react';
import showToast from '@/app/Toast';
import { inProgress } from '../ProgressBar';
import { userData } from '../UserBox';
import { router } from '../Navbar';

export default class SignIn extends Component {
    j = createRef();
    data = {Email:"",Password:""}; performing = "";
    change = (event)=> {this.data[event.target.placeholder] = event.target.value;}
    signIn = async ()=>{
      try {
        if(this.performing !== ""){
          showToast("Now "+this.performing);
          return;
        }
        let v = validateData(this.data);
        if (v !== true) {
          showToast(v);
          return;
        }
        this.performing = "Signing in";
        this.j.current.style.display = "block";
        inProgress(true);
        const res = await fetch(window.origin+"/api/users/in",{method:"POST",body:JSON.stringify({email:this.data.Email,password:this.data.Password})});
        const user = await res.json();
        // console.log(user);
        if (res.ok) {
          userData.user = user.user;
          setSession(user.session);
          router.back();
          showToast("Signed in succussfully.");
        }else showToast(user.message);
        this.performing = "";
        inProgress(false);
        this.j.current.style.display = "none"; 
      } catch (error) {
        showToast(error.message);
      }
    }
  render() {
    return (
      <div style={{padding:"1px"}}>
        <div className='signBox'>
          <div style={{padding:"45px",display:"flex",flexDirection:"column",alignItems:"center"}}>
            <h2 style={{fontSize:"35px",margin:"0"}}>Sign in</h2>
            <p style={{fontSize:"18px",margin:"10px 0 20px 0"}}>Welcome Back! Sign In to Your Account</p>
            <input type="email" placeholder='Email' onChange={this.change}/>
            <input type="password" placeholder='Password' onChange={this.change}/>
            <button onClick={this.signIn} className='signBtn'>Sign in</button>
            
            <p style={{fontSize:"17px",fontFamily:"arial"}}>No account? 
              <Link href={"/user/sign-up"} replace={true} style={{textDecoration:"none",cursor:"pointer",fontSize:"inherit",color:"green",border:"none",background:"none",padding:"0"}}><b> Create one</b></Link>
            </p>
          </div>
          <div ref={this.j} style={{display:"none",position:"absolute",inset:'0',background:"#ffffffc7"}}></div>
        </div>
      </div>
    );
  }
}
const validateData = (user)=> {
    if (user.Email.trim() ==="" || user.Password.trim() === "") {
      return "Email and Password are required.";
    }
  
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(user.Email)) {
        return "Invalid email format.";
    }
  
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$!%*?&])[A-Za-z\d@#$!%*?&]+$/;
    if (!passwordRegex.test(user.Password)) {
        return "Password must contain at least one a-z, A-Z, 0-9, [@#$!%*?&], and be 8+ characters long.";
    }
  
    return true;
}

Here userData is an object that stores user information after sign-in and it is rendered at the top-right corner as a profile picture and shows user pic and more information on click. And below is the code of settings session of the user:

const setSession = (session)=>{
    const date = new Date(session.expiry).toUTCString();
    setCookie("userId", session.userId, date, "/", window.location.hostname);
    setCookie("tocken", session.tocken, date, "/", window.location.hostname);
}
const setCookie = (key, value, expires, path, domain)=> {
    let cookieString = key + "=" + encodeURIComponent(value);
    if (expires) {
        cookieString += "; expires=" + expires;
    }
    if (path) {
        cookieString += "; path=" + path;
    }
    if (domain) {
        cookieString += "; domain=" + domain;
    }
    cookieString += "; samesite=strict";
    document.cookie = cookieString;
}

Now the question is how to manage and show the login state of the user. So, For showing the login state and sending the user to the login page if it is not logged in are all done by the UserBox component and the code of it is below:

'use client';
import React, { Component } from 'react';                 
import ShowUserInfo from './ShowUserInfo';
import showToast from './Toast';
export const userData = {user:null};
export const Router = {get:undefined};

export default class UserBox extends Component {
    what = "userPic";
    constructor(){
      super();
      userData.updateUserBox = this.updateUserBox;
    }
    componentDidMount(){
      this.getUser();
    }
    updateUserBox = ()=>{
      this.setState({});
    }
    getUser = async ()=>{
      const session = parseCookie(document.cookie);
      if (session.userId && session.tocken) {
        try {
          const res = await fetch(window.origin+"/api/users/get",{method:"POST",body:JSON.stringify({userId:session.userId,tocken:session.tocken})});
          const data = await res.json();
          if(res.ok){
            userData.user = data;
            // console.log(data);
            this.what === "userPic";
            this.setState({});
          }else {
            showToast(data.message);
          }
        } catch (error) {
          showToast(error.message);
        }
      }
      if(userData.waiting) userData.waiting();
    }
    userPicClick = ()=>{
        if (this.what === "userPic") {
          if (userData.user) {
            this.what = "showInfo";
            this.setState({});
            document.body.addEventListener("click",this.close); 
          }else{
            Router.get().push("/user/sign-in");
          }
        }
    }
    close = (r)=>{
        this.what = "userPic";
        if(r instanceof Event) document.body.removeEventListener("click",this.close);
        this.setState({});
    }
  render() {
    return (
      <div>
        <button onClick={this.userPicClick} className="userPic">{this.getUserPic()}</button>
        {this.what === "showInfo" ? <ShowUserInfo/> : ""}
      </div>
    )
  }
  getUserPic = ()=>{
    if(userData.user){
      let trimmedString = userData.user.pic.trim();
      if (trimmedString !== "") return <img src={userData.user.pic} crossorigin="anonymous" style={{position:"absolute",inset:"0",width:"100%",borderRadius:"25px",objectFit:"cover",height:"100%",pointerEvents:"none"}}/>;
      const words = userData.user.name.trim().split(/\s+/);
      let g = "";
      words.forEach(word => {if(g.length < 2) g = g + word.charAt(0).toUpperCase();});
      return (<svg xmlns="http://www.w3.org/2000/svg" style={{position:"absolute",inset:"0",width:"100%",height:'100%',pointerEvents:"none"}}>
        <text style={{fontSize:"15px",fontWeight:"bold"}} x="50%" y="50%" textAnchor="middle" dominantBaseline="middle" fill="black">{g}</text>
      </svg>);
    }else return (<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" style={{position:"absolute",inset:"0",margin:"auto",pointerEvents:"none"}}>
      <path d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"/>
    </svg>);
  }
}
const  parseCookie = (cookieString)=> {
  try {
    const cookies = cookieString.split(';');
    const cookieObject = {};
    cookies.forEach(cookie => {
    const parts = cookie.split('=');
    const name = parts[0].trim();
    const value = parts[1].trim();
    if (name === 'userId' || name === 'tocken') {
      cookieObject[name] = value;
    }
    });
    return cookieObject;
  } catch (error) {
    return {};
  }
}

This component takes care of all things. It shows a person svg on logout, user pic or svg of the first letter of the user name and surname in the logged-in state, shows more user info like name, email, or link to the settings page, or sends the user to the login page when clicked on it.

To logout a user, we first remove session information from the database and if successfully removed from the database then remove cookies on the user's browser. Below is the code of logout a user when clicked on a button and logout function is called.

// don't confuse it is code of a React class component, so modify it as need    
logout = async ()=>{
      try {
        if (this.userInfo) {
          this.j.current.style.display = "block";
          inProgress(true);
          const res =  await fetch(window.location.origin+"/api/users/out?userId="+this.userInfo.userId,{method:"POST"});
          if (res.ok) {
            const u = await res.json();
            removeSession({userId:"",tocken:"",expiry:Date.now()-360000000});
            this.userInfo = undefined;
            this.setState({});
            router.replace("/user/sign-in");
            showToast(u.message);
          }else {
            let r = await res.json();
            showToast(r.message);
          }
          this.j.current.style.display = "none";
          inProgress(false);
        }
      } catch (error) {
        showToast(error.message);
      }
    }

// code out of react component 
const removeSession = (session)=>{
  const date = new Date(session.expiry).toUTCString();
  // console.log(session);
  setCookie("userId", session.userId, date, "/", window.location.hostname);
  setCookie("tocken", session.tocken, date, "/", window.location.hostname);
}
const setCookie = (key, value, expires, path, domain)=> {
  let cookieString = key + "=" + encodeURIComponent(value);
  if (expires) {
      cookieString += "; expires=" + expires;
  }
  if (path) {
      cookieString += "; path=" + path;
  }
  if (domain) {
      cookieString += "; domain=" + domain;
  }
  cookieString += "; samesite=strict";
  document.cookie = cookieString;
}

So these are the codes and components on the client-side and in the next blog, we will see code on the server-side. Till now you can check this live at NKS CODING LEARNINGS.

I hope you get some informative and helpful knowledge and experience by reading this post. For any suggestions and questions, comment below. Happy Coding!

Published on Mar 12, 2024
Comments (undefined)

Read More