DEV Community

Cover image for Exploding Kittens Card Game - React, Nodejs and Redis (Part 2)
NabajitS
NabajitS

Posted on

Exploding Kittens Card Game - React, Nodejs and Redis (Part 2)

This is the second part of the series. In this part I'll be going over the backend implementation, the frontend code along with its implementation has been given in the first part, so you can check it out.

Prerequisites πŸ–₯️

  • React (Basics)
  • Nodejs (Basics)
  • Typescript (Basics)

Also, having Redis-insight installed is recommended. You can get it here

For this project we are going to be using Redis Cloud, just go here and signup to create an account (you can also install and setup a Redis database locally).

I choose the free plan, where we get 30mb free storage, although it might feel less, but it's enough to get familiar with redis.

After you signup inside subscriptions click on your database name. It's configuration page will open up, you will get the database public endpoint, username and password from here. We will be using these values to connect our application as well as redis-insight to our cloud database.

Configuration Page

Security


Let's get started

First let us install the required dependencies for this project -

npm install express jsonwebtoken nodemon dotenv cors redis-om
Enter fullscreen mode Exit fullscreen mode

You might be wondering, what is this redis-om?
What redis-om basically does is provide a level of abstraction so that we can work with data without worrying about how it is actually stored inside of Redis.
For example redis-om provides us the functionality of storing and retrieving data in json format, however under the hood redis actually stores data in the form of key-value pairs.

Here's the projects folder structure

-
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ controllers
β”‚   β”œβ”€β”€ middleware
β”‚   β”œβ”€β”€ routes
β”‚   β”œβ”€β”€ schema
β”‚   └── redisClient.ts
β”œβ”€β”€ server.ts
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ package.json
└── package-lock.json
Enter fullscreen mode Exit fullscreen mode

We will initiate the server inside server.ts

//server.ts
import express from 'express';
import cors from 'cors';
import { authRouter } from './src/routes/userRoutes';

const app = express();
app.use(cors());

app.use(express.json());
const port = process.env.PORT ?? 5000;

app.listen(port, () => {
  console.log(`Server running on: http://localhost:${port}`);
});

app.use('/users', authRouter);
Enter fullscreen mode Exit fullscreen mode

Then inside the userRoutes.ts file

import express from 'express';
import { createUser, updateScores, getHighScores, signInUser } from '../controllers/userController';
import { checkAuth } from '../middleware/checkAuth';

const authRouter = express.Router();

authRouter.post('/signup', createUser);
authRouter.post('/login', signInUser);

authRouter.use(checkAuth)

authRouter.get('/updatescore', updateScores);
authRouter.get('/highest', getHighScores);

export { authRouter }
Enter fullscreen mode Exit fullscreen mode

We set up the various routes and their respective controllers. On /signup and /login we will call the createUser and signInUser controllers, which we will create later. We we will add checkAuth middleware which checks the user authentication and saves current user's validated token to browser's local storage so that it can be sent to the server on subsequent requests.

Now let's get to the Redis bit
First we will connect to our Redis cloud database from our app.
The connection string will have the following format

redis://<username>:<password>@<host:port or Public endpoint>
Enter fullscreen mode Exit fullscreen mode

So inside redisClient.ts add the following code.

//redisClient.ts
import { Client } from 'redis-om';
import { config } from 'dotenv';
config()

const redisClient = new Client();

(async () => {
    await redisClient.open(`redis://${process.env.USER}:${process.env.PASS}@${process.env.URL}`)
})();
;

export { redisClient }
Enter fullscreen mode Exit fullscreen mode

We import the Client class from redis-om and connect to the database by calling its open method, and then export the client.
Also, if the code block looks unfamiliar to you, then it's called IIFE and what it basically does is call the function as soon as it is defined. And as to why we need to use it, we use it to make typescript happyπŸ˜…

Inside userSchema.ts add the following code.

//userSchema.ts
import { Entity, Schema } from "redis-om"

interface User {
    password: string,
    email: string,
    score: number
}

class User extends Entity { }

export const userSchema = new Schema(User, {
    password: { type: 'string' },
    email: { type: 'string' },
    score: {
        type: 'number', sortable: true
    }
}, {
    dataStructure: "JSON"
})
Enter fullscreen mode Exit fullscreen mode

Our userSchema consists of an id, email, password and a score field. Ideally we would have a seperate schema(i.e objects) such as scoreSchema for score and a seperate schema for the user's email, password and id. Then add a createdBy field to scoreSchema to denote to which user that score belongs to.Then filter the information based on userID.
But here we going to everything in one single object.
We also add an extra property sortable to score, this basically helps us in getting the sorted results faster.

By default entities map to JSON documents using RedisJSON but we can still define their type by.

{
    dataStructure: "JSON"
}

Enter fullscreen mode Exit fullscreen mode

There are other options too, such as HASH

Then let us create a .env file for storing the environment variables

PORT=
#If your using redis cloud, user should be default
USER=default
#URL is the public endpoint, which you can find on your database configuration page, along with the password
URL=
PASS=
# jwt secret, you can write any string here
SECRET=
Enter fullscreen mode Exit fullscreen mode

Now, we will create the middleware checkAuth, which checks and verifies user authentication using jwt.

//checkAuth.ts
import jwt, { JwtPayload } from "jsonwebtoken"
import { Request, Response } from "express";

interface CustomRequest extends Request {
    currUserId?: any;
}

const checkAuth = async (req: CustomRequest, res: Response, next: () => void) => {
    const { authorization } = req.headers;

    if (!authorization) {
        return res.status(401).json({ error: "Authorization token not present" })
    }
    const token = authorization.split(' ')[1]

    try {
        const { id } = jwt.verify(token, process.env.SECRET as string) as JwtPayload

        req.currUserId = id  // add a new property to the req object to store current authenticated user's id.
        next();
    }
    catch (err) {
        res.status(401).json({ error: "Token not verified" })
    }
}

export { checkAuth }
Enter fullscreen mode Exit fullscreen mode

We create CustomRequest interface because we are going to store the authenticated user's id(or entity Id) by adding a new property called currUserId on the req object so that the server can keep track of which user is making the requests.
This id is returned by the jwt.verify method after it verifies the token

Now lets create the controllers

//userControllers.ts
import { Request, Response } from 'express';
import { redisClient } from '../redisClient';
import { userSchema } from '../schema/userschema';
import jwt from "jsonwebtoken"

const userRepository = redisClient.fetchRepository(userSchema);
(async () => {
    await userRepository.createIndex();
})();

const createToken = (id: string) => {
    const token = jwt.sign({ id: id }, "amanandacateatfoodtogether", { expiresIn: '3d' });
    return token;
}

interface CustomRequest extends Request {
    currUserId?: any;
}

const createUser = async (req: CustomRequest, res: Response) => {
    try {
        const { email, password } = req.body

        let user = userRepository.createEntity({
            password: password,
            email: email,
            score: 0,
        })
        const id = await userRepository.save(user)
        const token = createToken(id)

        res.status(200).json({
            email: email,
            token: token
        })
    }
    catch (err: any) {
        res.status(401).json({ err: err.message })
    }
}

const signInUser = async (req: CustomRequest, res: Response) => {
    try {
        const { email, password } = req.body

        const userSearch: any = await userRepository.search() 
           .where('email').eq(email).where('password')
            .eq(password).return.first()

        if (!userSearch) {
            return res.status(401).json({
                msg: "invalid email or password"
            })
        }
        const token = createToken(userSearch?.entityId)

        res.status(200).json({
            email: userSearch.email,
            token: token
        })
    }
    catch (err: any) {
        res.status(401).json({ err: err.message })
    }
}

const updateScores= async (req: CustomRequest, res: Response) => {
    const user = await userRepository.fetch(req.currUserId);
    user.score = user.score + 1;
    await userRepository.save(user);

    res.send(user)
}

const getHighScores = async (req: CustomRequest, res: Response) => {
    const users = await userRepository.search()
        .sortDescending('score')
        .return.page(0, 10);  //page(offset, count) i.e start from 0 and go till 2 i.e returns 2 user highscores

    res.send(users);
}

export { createUser, updateScores, signInUser, getHighScores, userRepository }


Enter fullscreen mode Exit fullscreen mode

First we create a repository called userRepository because inorder to work with entitites i.e to create, read entities we need access to that repository. Then inside the IIFE we create an Index on the repository, to get search functionality on a repository we first need to create an index on it.
Inside creatUser the createEntity method creates an entity but remember it doesn't save it to Redis, we do that by calling save. This will return the entityId of the recently created entity(or object), which we then send to the createToken function to create a jwt token.
The updateScores function is called when a user wins a game and all it does is update the user's score in the database with 1.
The other controllers are quite simple.

With this we are done.
You can the source code for the whole project here

Top comments (0)