In this guide, we will evolve our Node.js application to consume the ChainLetter.io API more efficiently and enable the handling of a user’s subscription status. We’ll refactor the initial example by abstracting API calls into a utility function and implementing a method to set the subscription status of users.
Step 1: Abstracting API Calls into a Utility Function
Originally, API calls are performed directly within functions like RegisterUser and GetUserToken. To improve this, we create a reusable fetchCLAPI function that encapsulates the repetitive aspects of making API calls, like setting headers and error handling.
// Example 2: Utility function to fetch from ChainLetter API
const fetchCLAPI = async ({ route, method, headers, body }) => {
const res = await fetch(`${process.env.CL_APIURL}/api/v1/server/${route}`, {
headers: {
...headers,
apikey: process.env.CL_APIKEY,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
method: method || "GET",
});
// Handle errors and response
// ...
};
// Then, call this utility function in RegisterUser and GetUserToken
Refactor your RegisterUser function as follows:
// Example 2: Register user with ChainLetter Server API
async function RegisterUser(user) {
if (!user.userRegistered) {
const regUser = await fetchCLAPI({
route: "register_user",
method: "POST",
body: {
remote_uuid: user.username,
remote_email: user.email,
},
});
return regUser.success;
}
return true;
}
Step 2: Enabling Subscription Status Updates
In the original example, there’s no handling of different user types. We introduce a new feature: handle a premiumUser property to set the user’s subscription status.
Refactor the GetUserToken function as shown below:
// Example 2: Get user token from ChainLetter Server API
async function GetUserToken(user) {
const { success, token, message } = await fetchCLAPI({
route: "get_user_token",
headers: {
remote_uuid: user.username,
autoenroll: user.premiumUser ? "true" : "false",
},
});
if (!success && message) {
console.error(message);
}
return success ? token : null;
}
Now, the GetUserToken function includes logic to pass the autoenroll property based on the user’s premiumUser status.
Tip: You can call the GetUserToken from your server to turn off autoenroll at any time, not just when the user is logging on. If you use it as a toggle just discard the returned token.
Step 3: Refactoring the Login Route
Update the login route to process the new premiumUser attribute correctly and use the utility function for API interactions.
Here’s how you can update your login route:
// Example 2: Login route
app.post("/login", async (req, res) => {
if (!req.body.username) {
res.status(400).send({ message: "Username required" });
return;
}
// Mock user login with janedoe123 as a premium user
if (req.body.username == "janedoe123" && req.body.password == "password") {
let currentUser = {
// ...other properties
userRegistered: false,
premiumUser: true,
};
// Register user and get a token using our utility function
if (!(await RegisterUser(currentUser))) {
return res.send({ message: "User Registration Failed." });
}
const token = await GetUserToken(currentUser);
if (!token) {
return res.send({ message: "Token Request Failed." });
}
// Include the token in a form to be submitted by the client
// ...
res.send({ /* form and login success message */ });
} else {
res.send({ message: "Incorrect Login or Password" });
}
});
By refactoring your code, you are improving the maintainability, readability, and extendibility of your application. Modularizing the API call logic and introducing a clearer handling of subscription statuses make your app more professional and reliable.
Make sure to test the updated code thoroughly to ensure that all the functionalities are working as expected. Remember to check for any environment-specific configurations, such as API keys and service URLs, before deploying your application.
Complete index.js File
const express = require("express");
require("dotenv").config({ path: ".env" });
const app = express();
const path = require("path");
const bodyParser = require("body-parser");
app.use(bodyParser.json({ limit: "2mb" }));
// Utility function to fetch from ChainLetter API
const fetchCLAPI = async ({ route, method, headers, body }) => {
const res = await fetch(`${process.env.CL_APIURL}/api/v1/server/${route}`, {
headers: {
...headers,
...{
apikey: process.env.CL_APIKEY,
"Content-Type": "application/json",
},
},
body: body ? JSON.stringify(body) : undefined,
method: method || "GET",
});
if (!res.ok) {
try {
const err = await res.json();
return { success: false, message: err.message };
} catch (ex) {
return { success: false, message: "API Error" };
}
}
return await res.json();
};
// Register user with ChainLetter Server API
async function RegisterUser(user) {
if (!user.userRegistered) {
const regUser = await fetchCLAPI({
route: "register_user",
method: "POST",
body: {
remote_uuid: user.username, // This must be unique on your server
remote_email: user.email,
},
});
// Store this in your DB so you don't need to keep incurring round trip latency to ChainLetter
return regUser.success;
}
return true;
}
// Get user token from ChainLetter Server API
async function GetUserToken(user) {
const { success, token, message } = await fetchCLAPI({
route: "get_user_token",
method: "GET",
headers: {
remote_uuid: user.username, // This must be unique in your application
autoenroll: user.premiumUser ? "true" : "false",
},
});
if (!success && message) {
console.log(message);
}
return success ? token : null;
}
// Login route
app.post("/login", async (req, res) => {
if (!req.body.username) {
res.status(400).send({ message: "Username required" });
return;
}
// Mock user login
if (req.body.username == "janedoe123" && req.body.password == "password") {
let currentUser = {
name: "Jane Doe",
email: "[email protected]",
username: "janedoe123",
userRegistered: false,
premiumUser: true, // Use this to trigger autoenroll and automatic subscription to a premium plan (or to remove the subscription)
};
// Register user with ChainLetter, get a token, then send the token to the client
// This doesn't need to be in the login route, it can be in any other server route
if ((await RegisterUser(currentUser)) === false) {
return res.send({ message: "User Registration Failed." });
}
const token = await GetUserToken(currentUser);
if (!token) {
return res.send({ message: "Token Request Failed." });
}
// By embedding the token in the form, the client can submit the form to ChainLetter via POST, allowing the cookie to be set
return res.send({
message: "Logged in",
form: `
<form action="${
process.env.CL_APIURL
}/api/v1/client/post_user_token" method="POST">
<div class="form-group">
<input name="authorization" type="hidden" value="AUTH ${token}" />
<input name="return_url" type="hidden" value="${
req.protocol
}://${req.get("host")}" />
<input type="submit" value="Access ChainLetter" />
</div>
</form>
`,
});
}
res.send({ message: "Incorrect Login or Password" });
});
// Serve static files
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "/index2.html"));
});
app.get("/style.css", (req, res) => {
res.sendFile(path.join(__dirname, "/style.css"));
});
// Start the server
app.listen(3111, "0.0.0.0", () => {
console.log(`Listening to requests on http://localhost:3111`);
});
You can use the same index.html and css from the previous example with this updated code.
