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

Nitish Kumar Singh

Mar 14, 2024

Hello developers! In this blog post, we are going to write endpoints for handling Sign In, Sign Up, and other requests to manage a user's login sessions using Next.js Route Handlers.

This blog post is the second part of the "Creating Sign In and Sign Up pages series". In this post, we will write code for the server-side. In the previous post, we had seen that and wrote code in Next.js for handling Sign In, Sign Up, and the rest of all codes to manage a user's login state. If you have not read it yet, please read this blog post first from here.

Here, I have defined API route paths according to and used in the previous post, but you can customize it as you want. I have created /up, /in, /out, and /get for sign-up, sign-in, sign-out, and get user info based on session cookies with the prefix /api/users under the app directory of the Next.js project.

Sign Up Endpoint

So below is the code of the sign-up endpoint with the full path /api/users/up and POST method:

export const dynamic = 'force-dynamic';

export async function POST (req) {
    let userInfo = await req.json();
    const result = await signUp(userInfo);
    return Response.json(result.data,{status:result.status});
}

const signUp = async (userInfo)=>{
    const {endPoint,dbId,usersClId,projectId,secretKey} = process.env;
    const head = { "X-Appwrite-Project": projectId,"X-appwrite-key": secretKey,"Content-Type": "application/json","X-Appwrite-Response-Format": "1.4.0"};

    let check = await fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents?`+
    `queries[0]=equal("email",+["${userInfo.email}"])`,{headers:head});
    if (check.ok) {
        const data = await check.json();
        if(data.total > 0){
            return {data:{message:"Email already exists."},status:403};
        }
    }else return {data:{message:"An error occurred, please try again."},status:500};
    const data = {provider:"Email",name:userInfo.name,email:userInfo.email,password:userInfo.password,pic:"",emailVerified:false,prefs:"{}"};
    const create = await fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents`,
    {method:"POST",headers:head,body:JSON.stringify({documentId:userInfo.userId,data:data})});
    if (create.ok) {
        const user = await create.json();
        return {data:{email:user.email,userId:user.$id},status:201};
    }else return {data:{message:"An error occurred, please try again."},status:500};
}

In the above code, I have disabled caching to check if the email already exists with real-time user data and get all IDs and secret key from environment variables, making a head object for headers according to Appwrite database docs.

Before signing up, we will first check if a user already exists with this email or not by querying the database with this email, and on any failure, return a message and codes accordingly and proceed further on success.

Make a data object with properties based on attributes defined in the Appwrite database and make sure to correctly add values to the data object; otherwise, Appwrite gives errors.

Then finally create a document in Users collection of Appwrite database with the user-provided information and return messages according to the result.

This is a simple sign-up endpoint without user's email verification and password hashing or encryption, but it is good to hash or encrypt the password before storing and we can slightly change the way of sign up by sending an email to the user and after email verification we ask, store and create a user.

Sign In Endpoint

Similar to the sign-up endpoint, below is the code of the sign-in endpoint with the full path /api/users/in and POST method:

export const dynamic = 'force-dynamic';

export async function POST(req) {
    let userInfo = await req.json();
    const result = await signIn(userInfo);
    return Response.json(result.data,{status:result.status});
}

const signIn = async (userInfo)=>{
    const {endPoint,dbId,usersClId,projectId,secretKey} = process.env;
    const head = { "X-Appwrite-Project": projectId,"X-appwrite-key": secretKey,"Content-Type": "application/json","X-Appwrite-Response-Format": "1.4.0"};

    let check = await fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents?`+
    `queries[0]=equal("email",+["${userInfo.email}"])`,{headers:head});
    if (check.ok) {
        const data = await check.json();
        if(data.total <= 0) return {data:{message:"No any user found with provided email."},status:404};
        const user = data.documents[0];
        if(user.password !== userInfo.password) return {data:{message:"Password is incorrect."},status:403};
        const session = {userId:user.$id,tocken:generateToken(100),expiry:Date.now()+7776000000};
        user.sessions.push(JSON.stringify(session));
        const create = await fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents/${user.$id}`,
        {method:"PATCH",headers:head,body:JSON.stringify({data:{sessions:user.sessions}})});
        if (create.ok) {
            return {data:{user:{userId:user.$id,name:user.name,email:user.email,pic:user.pic,emailVerified:user.emailVerified,provider:user.provider},session:session},status:201};
        }else return {data:{message:"An error occurred, please try again."},status:500};
    }else return {data:{message:"An error occurred, please try again."},status:500};
}

Similarly, here also, first check user existence, match provided password, generate a session token, save this user session, and after successfully passing all processes return user information to display to the user and session object to save login state, and on any failure return messages accordingly.

I have used the below function to generate a unique 100-character token including A-Z, a-z, and 0-9:

const generateToken = (length)=> {
    const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let token = '';
    for (let i = 0; i < length; i++) {
        const randomIndex = Math.floor(Math.random() * characters.length);
        token += characters[randomIndex];
    }
    return token;
}

Get Sign In State

First of all, we check our stored cookies on the client side. If cookies are stored and not expired, it means the user is logged in and then fetch user information based on these session cookies. Otherwise, the user is not logged in.

So, below is the code for the endpoint for fetching user information with the full path /api/users/get and POST method:

export const dynamic = 'force-dynamic';

export async function POST(req) {
    let session = await req.json();
    const result = await getUser(session);
    return Response.json(result.data,{status:result.status});
}

const getUser = async (userInfo)=>{
    const {endPoint,dbId,usersClId,projectId,secretKey} = process.env;
    const head = { "X-Appwrite-Project": projectId,"X-appwrite-key": secretKey,"Content-Type": "application/json","X-Appwrite-Response-Format": "1.4.0"};

    let check = await fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents/${userInfo.userId}`,{headers:head});
    if (check.ok) {
        const data = await check.json();
        const sessions = data.sessions;
        let now = Date.now();
        let signedIn = false;
        let keep = [];
        for (let i = 0; i < sessions.length; i++) {
            const session = JSON.parse(sessions[i]);
            if (now <= session.expiry) {
                keep.push(sessions[i]);
                if(session.tocken === userInfo.tocken && !signedIn) signedIn = true;
            }
        }
        if (keep.length < sessions.length) {
            fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents/${user.$id}`,
            {method:"PATCH",headers:head,body:JSON.stringify({data:{sessions:keep}})});
        }
        if(signedIn) return {data:{userId:data.$id,name:data.name,email:data.email,pic:data.pic,emailVerified:data.emailVerified,provider:data.provider},status:200};
    }else if(check.status === 404) return {data:{message:"Not signed in."},status:404};
    return {data:{message:"Internal server error."},status:500};
}

Here, we will get a user by userId, validate expiry and token, remove any expired sessions, and return user information on successful verification and return messages for any failures.

Logout Endpoint

Logging out is so simple: just remove the stored session object from the database and the user's browser cookies, and the user will be logged out. So below is the code for removing the session from the database:

export const dynamic = 'force-dynamic';

export async function POST (req) {
    let params = new URL(req.url);
    const result = await logout(params.searchParams.get("userId"));
    return Response.json(result.data,{status:result.status});
}

const logout = async (userId)=>{
    try {
        const {endPoint,dbId,usersClId,projectId,secretKey} = process.env;
        const head = { "X-Appwrite-Project": projectId,"X-appwrite-key": secretKey,"Content-Type": "application/json","X-Appwrite-Response-Format": "1.4.0"};
        const out = await fetch(endPoint+`/databases/${dbId}/collections/${usersClId}/documents/${userId}`,
            {method:"PATCH",headers:head,body:JSON.stringify({data:{sessions:[]}})});
        if (out.ok) {
            return {data:{message:"Logged out succussfully."},status:200};
        }else return {data:{message:"Failed to logout."},status:500};
    } catch (error) {
        return {data:{message:"Failed to logout."},status:500};
    }
    
}

I have created separate API endpoints for all operations, but we can also do all operations with a single route by checking a URL search param like action=sign-in or action=sign-up.

So this is the whole process or concepts for developing a User Management system from scratch without any library or SDK. But in all these endpoints, we left an important aspect of development, and that is security. But still, we learn steps and logic behind a user management system and build it.

I hope by reading these two blog posts, you understand these concepts and find these posts helpful and informative. For any suggestions, ideas, and questions, feel free to comment below. Till now, Happy Coding!

Published on Mar 14, 2024
Comments (undefined)

Read More