In this module, you use your application that you’ve deployed. Before you do that, let’s do a quick recap of the AWS components you’re using in your application:

Let’s see how each of these pieces tie together. In the following steps, you walk through your different application endpoints using these components.

First, you start with a Registration endpoint, where a new user signs up and creates their account.

Second, you use a Login endpoint where a user can use a client (such as a web application or a mobile app) to authenticate and receive an ID token.

Third, you use a CreateGame endpoint to create a new game between two users.

Fourth, you use the FetchGame endpoint to retrieve the current state of a game for display in a browser.

Finally, you use the PerformMove endpoint to simulate users makig moves and to trigger SMS messages.

Time to Complete Module: 20 Minutes


  • Step 1. Register a new user

    The first workflow you review is the Registration endpoint. At this endpoint, a new user signs up by providing login information, like username and password.

    You create two users in this step so that you can simulate the two users alternating moves in a game of Nim. 

    As you look at these endpoints, the relevant snippets of code that are invoked by the endpoint are shown in this guide.

    Your registration endpoint is available by making a POST request to the /users endpoint. You can see the logic for this endpoint in the application/app.js file about halfway down:

    // Create user
    app.post("/users", wrapAsync(async (req, res) => {
      const validated = validateCreateUser(req.body);
      if (!validated.valid) {
        throw new Error(validated.message);
      }
      const user = await createCognitoUser(
        req.body.username,
        req.body.password,
        req.body.email,
        req.body.phoneNumber
      );
      res.json(user);
    }));

    Your handler takes the request payload and validates that it has the required properties. If all required properties are present, it creates a new user in Amazon Cognito.

    You already reviewed the createCognitoUser function in the Amazon Cognito module, so we won’t recap it here.

    Try invoking your Registration endpoint to create a new user. Run the following command in your terminal:

    curl -X POST ${BASE_URL}/users \
      -H 'Content-Type: application/json' \
      -d '{
    	"username": "myfirstuser",
    	"password": "Password1",
    	"phoneNumber": "'${PHONE_NUMBER}'",
    	"email": "test@email.com"
    }'

    You should see the following output in your terminal:

    {"username":"myfirstuser","email":"test@email.com","phoneNumber":"+15555555555"}

    Great! You’ve successfully created your user. Now, create a second user so that you can simulate playing a game.

    Run the following command in your terminal:

    curl -X POST ${BASE_URL}/users \
      -H 'Content-Type: application/json' \
      -d '{
    	"username": "theseconduser",
    	"password": "Password1",
    	"phoneNumber": "'${PHONE_NUMBER}'",
    	"email": "test@email.com"
    }'
    

    You should see the following output in your terminal:

    {"username":"theseconduser","email":"test@email.com","phoneNumber":"+15555555555"}

    Now, let’s log in and receive an ID token for each of the two users.

  • Step 2. Login to fetch user credentials

    Your second endpoint is the Login endpoint. Users submit their username and password to this endpoint to receive an ID token, which is used to authenticate them on subsequent requests.

    To handle this authentication and token dispensing, your application has a /login endpoint. The handler is in application/app.js as follows:

    // Login
    app.post('/login', wrapAsync(async (req, res) => {
      const idToken = await login(req.body.username, req.body.password)
      res.json({ idToken })
    }))

    It expects the payload body to have username and password properties. It then calls your helper login function from your auth.js file. If the login is successful, it returns the ID token for the user.

    Let’s test it out with your first created user. Run the following command in your terminal:

    curl -X POST ${BASE_URL}/login \
      -H 'Content-Type: application/json' \
      -d '{
    	"username": "myfirstuser",
    	"password": "Password1"
    }'
    

    You should see a response with an idToken property in your terminal:

    {"idToken":"eyJraWQiO…."}

    This ID Token is used in subsequent requests to authorize a user. Save the value of the ID token in your shell by copying the value of the token between the quotations, then running the following command:

    export FIRST_ID_TOKEN=<idToken>

    Do the same steps to login with your second user. Execute the following command in your terminal:

    curl -X POST ${BASE_URL}/login \
      -H 'Content-Type: application/json' \
      -d '{
    	"username": "theseconduser",
    	"password": "Password1"
    }'

    You should see a response with an idToken property in your terminal:

    {"idToken":"eyJraWQiOiI...."}

    Again, copy the quoted value of the ID token and save it as an environment variable with the following command:

    export SECOND_ID_TOKEN=<idToken>

    Note: These identity tokens are temporary credentials that will expire over time. If you return to this tutorial after a period of time, you may need to re-run the steps above to generate new tokens.

  • Step 3. Create a new game

    Now that your user has a way to authenticate, let’s create a new game. This happens in the browser for your application, as the user chooses to create a new game and enters the username of their opponent.

    The endpoint for creating a new game is POST /games, and the handler code in application/app.js is as follows:

    // Create new game
    app.post("/games", wrapAsync(async (req, res) => {
      const validated = validateCreateGame(req.body);
      if (!validated.valid) {
        throw new Error(validated.message);
      }
      const token = await verifyToken(req.header("Authorization"));
      const opponent = await fetchUserByUsername(req.body.opponent);
      const game = await createGame({
        creator: token["cognito:username"],
        opponent: opponent
      });
      res.json(game);
    }));

    This code is doing a few things. First, it validates that the incoming payload is in the proper format and contains all required fields. Then, it verifies the ID token that is sent over in the Authorization header. If this token is valid, it then fetches details about the second user using the fetchUserByUsername function in the auth.js script that you reviewed in a previous module. Finally, it creates a new game by passing the username of the creator and the opponent details into the createGame function.

    The createGame function is in application/data/createGame.js and looks as follows:

    const AWS = require("aws-sdk");
    const documentClient = new AWS.DynamoDB.DocumentClient();
    const uuidv4 = require("uuid/v4");
    const sendMessage = require("./sendMessage");
    
    const createGame = async ({ creator, opponent }) => {
      const params = {
        TableName: "turn-based-game",
        Item: {
          gameId: uuidv4().split('-')[0],
          user1: creator,
          user2: opponent.username,
          heap1: 5,
          heap2: 4,
          heap3: 5,
          lastMoveBy: creator
        }
      };
    
      try {
        await documentClient.put(params).promise();
      } catch (error) {
        console.log("Error creating game: ", error.message);
        throw new Error("Could not create game");
      }
    
      const message = `Hi ${opponent.username}. Your friend ${creator} has invited you to a new game! Your game ID is ${params.Item.gameId}`;
      try {
        await sendMessage({ phoneNumber: opponent.phoneNumber, message });
      } catch (error) {
        console.log("Error sending message: ", error.message);
        throw new Error("Could not send message to user");
      }
    
      return params.Item;
    };
    
    module.exports = createGame;

    The createGame function generates a unique game id and saves the basic game details into DynamoDB. Then, it sends a message to the opponent to alert them that a new game has started and it is their turn to play.

    Let’s try using this endpoint. Run the following command in your terminal:

    curl -X POST ${BASE_URL}/games \
     -H 'Content-Type: application/json' \
      -H "Authorization: ${FIRST_ID_TOKEN}" \
      -d '{
    	"opponent": "theseconduser"
    }'

    Note that you’re passing the ID token of the first user in the Authorization header with your request.

    You should see the following output in your terminal:

    {"gameId":"433334d0","user1":"myfirstuser","user2":"theseconduser","heap1":5,"heap2":4,"heap3":5,"lastMoveBy":"myfirstuser"}

    A new game has been created! You should receive an SMS message on the phone number you registered for the second user.

    turn-based-game-new-game

    (Click to enlarge)

    turn-based-game-new-game

    Save the value of the game Id to your terminal using the following command:

    (Make sure you substitute the value of your game Id for <yourGameId>.

    export GAME_ID=<yourGameId>

    Try running this endpoint again but without an ID token. Run the following command in your terminal: 

    curl -X POST ${BASE_URL}/games \
     -H 'Content-Type: application/json' \
      -d '{
    	"opponent": "theseconduser"
    }'

    Because your request does not include a token, you should see the following output:

    {"message":"jwt must be provided"}

    You have used your Amazon Cognito tokens to protect an endpoint.

  • Step 4. Fetch the status of a game

    In this step, you can fetch the details for a particular game. This can be used in your application when a user checks their in-progress games or wants to make a move in a game.

    The endpoint to fetch a game is GET /games/:gameId, and the handler code is contained in application/app.js as follows:

    // Fetch game
    app.get("/games/:gameId", wrapAsync(async (req, res) => {
      const game = await fetchGame(req.params.gameId);
      res.json(game);
    }));

    This endpoint is not an authenticated endpoint as each user can see in-progress games if desired.

    Invoke the endpoint and fetch the game by entering the following command in your terminal:

    curl -X GET ${BASE_URL}/games/${GAME_ID}

    You should see the following output in your terminal:

    {"heap2":4,"heap1":5,"heap3":5,"gameId":"32597bd8","user2":"theseconduser","user1":"myfirstuser","lastMoveBy":"myfirstuser"}

    Success! You retrieved the game, and it has the details such as the number of objects in each heap and the users involved in the game.

    In the next step, you perform moves in the game.

  • Step 5. Perform moves in a game

    In this final step, you simulate users performing moves in a game. This happens from your application client, such as a web browser, where users select and submit a potential move.

    The endpoint to fetch a user is POST /games/:gameId. The handler code is contained in application/app.js as follows:

    // Perform move
    app.post("/games/:gameId", wrapAsync(async (req, res) => {
      const validated = validatePerformMove(req.body);
      if (!validated.valid) {
        throw new Error(validated.message);
      }
      const token = await verifyToken(req.header("Authorization"));
      const game = await performMove({
        gameId: req.params.gameId,
        user: token["cognito:username"],
        changedHeap: req.body.changedHeap,
        changedHeapValue: req.body.changedHeapValue
      });
      let opponentUsername
      if (game.user1 !== game.lastMoveBy) {
        opponentUsername = game.user1
      } else {
        opponentUsername = game.user2
      }
      const opponent = await fetchUserByUsername(opponentUsername);
      const mover = {
        username: token['cognito:username'],
        phoneNumber: token['phone_number']
      }
      await handlePostMoveNotification({ game, mover, opponent })
      res.json(game);
    }));
    

    The handler does a number of things. First, it validates that the request payload matches the required shape. If the payload shape passes validation, it then verifies the given Authorization header to identify the user requesting the game. After verification, it then attempts to perform a move using the performMove function. This function is similar to the performMove function in the DynamoDB module, as it performs an UpdateItem API call with all of the conditions needed.

    After the move is successfully performed, your application then needs to send any post-move notifications. It first fetches the opponent by username to get the phone number for the user. Then, it passes the game, mover, and opponent into the handlePostMoveNotification method. That method is shown below:

    const sendMessage = require('./sendMessage')
    
    const handlePostMoveNotification = async ({ game, mover, opponent }) => {
      // Handle when game is finished
      if (game.heap1 == 0 && game.heap2 == 0 && game.heap3 == 0) {
        const winnerMessage = `You beat ${mover.username} in a game of Nim!`
        const loserMessage = `Ahh, you lost to ${opponent.username} in Nim.`
        await Promise.all([
          sendMessage({ phoneNumber: opponent.phoneNumber, message: winnerMessage }),
          sendMessage({ phoneNumber: mover.phoneNumber, message: loserMessage })
        ])
    
        return
      }
    
      const message = `${mover.username} has moved. It's your turn next in Game ID ${game.gameId}!`
      await sendMessage({ phoneNumber: opponent.phoneNumber, message })
    };
    
    module.exports = handlePostMoveNotification;

    The handlePostMoveNotification first checks to see if the game is over because all heaps are empty. If so, it sends winning and losing notifications to the respective users. If the game is not over, it then notifies the opponent that it is their turn to play.

    Try performing a move with the following command:

    curl -X POST ${BASE_URL}/games/${GAME_ID} \
      -H "Authorization: ${SECOND_ID_TOKEN}" \
      -H 'Content-Type: application/json' \
      -d '{
    	"changedHeap": "heap1",
    	"changedHeapValue": 0
    }'

    Note that it is the second user making this request -- as indicated by usage of the SECOND_ID_TOKEN for authorization -- and that the user is setting the value of heap1 to 0.

    You should see the following results in your terminal:

    {"heap2":4,"heap1":0,"heap3":5,"gameId":"32597bd8","user2":"theseconduser","user1":"myfirstuser","lastMoveBy":"theseconduser"}

    The response includes the current state of the game and indicates that heap1 now has 0 elements, and that the last move was by theseconduser.

    You should have also received a notification for the first user that it’s their turn to move.

    turn-based-game-second-user

    (Click to enlarge)

    turn-based-game-second-user

    Let’s simulate the end of a game by making two more moves.

    First, have the first user remove all the items in heap 2 by running the following command:

    curl -X POST ${BASE_URL}/games/${GAME_ID} \
      -H "Authorization: ${FIRST_ID_TOKEN}" \
      -H 'Content-Type: application/json' \
      -d '{
    	"changedHeap": "heap2",
    	"changedHeapValue": 0
    }'

    You should see the following output in your terminal:

    {"heap2":0,"heap1":0,"heap3":5,"gameId":"32597bd8","user2":"theseconduser","user1":"myfirstuser","lastMoveBy":"myfirstuser"}

    Finally, have the second user remove all the objects from the third pile, which ends the game:

    curl -X POST ${BASE_URL}/games/${GAME_ID} \
      -H "Authorization: ${SECOND_ID_TOKEN}" \
      -H 'Content-Type: application/json' \
      -d '{
    	"changedHeap": "heap3",
    	"changedHeapValue": 0
    }'

    You should see the following output in your terminal:

    {"heap2":0,"heap1":0,"heap3":0,"gameId":"32597bd8","user2":"theseconduser","user1":"myfirstuser","lastMoveBy":"theseconduser"}

    This ends the game, which triggers the end-of-game notifications for both users.

    This screen capture shows the notification for both users, as both users were configured to use the same phone number.

    turn-based-game-end

    (Click to enlarge)

    turn-based-game-end

In this module, you exercised your working endpoints to see how your components worked together. First, you registered two new users that would simulate playing your game. Second, you exercised the login endpoint to fetch ID tokens for your users that can be used by the client to authenticate the user. Third, you used this ID token to create a new game with a user. Fourth, you fetched the current status of the game. Finally, you performed some moves with your users to simulate playing the game.

In the next module, you clean up the resources you created.