DEV Community

Seif Ahmed
Seif Ahmed

Posted on

You Wished for It. Here It Is. ❤️

I know how tough it can be to build smooth, secure backend features, so I wanted to bring a little joy to your day. I made this polished gift redemption system as my little present to you! Inside it, you will find clean logic and a complete Node.js/MongoDB transaction.

The Logic:

1. Security & Validation

  • Authentication: The code checks if the user is logged in using the checkAuth middleware.
  • ID Validation: It ensures the gift ID in the URL is a valid database ID.

2. Character Limit Math

  • The maximum character limit allowed is 4000 characters.
  • It calculates how close the user currently is to that limit.
  • It attempts to grant the user a +100 character boost, capping it if they are already near the 4000-character.
  • Concurrency Handling: While the system may calculate the same inc value for concurrent requests, the atomic updateOne operation ensures that if applying both boosts exceeds the 4000-character limit, only the faster request succeeds.

3. Database Transaction (All or Nothing)

The system starts a database transaction. This guarantees that both the gift update and the user update must succeed together; otherwise, the entire operation rolls back.

  • The query looks for an active gift that the current logged-in user has not claimed yet. It increments the gift's usage count (usedCount), save the user's ID in the usedBy array so they cannot claim it again, and changes the status to "expired" if it just reached its max available uses.
  • Also, It adds the calculated character boost to the user's account.

If the gift is invalid, expired or already redeemed by the logged-in user, the code throws an error and aborts the entire transaction. Otherwise, it successfully returns a success: true response.

The Code:

app.post("/api/v1/redeem/gift-link/:id", checkAuth, [
    param("id").exists().isMongoId()
], validateResult, async (req, res) => {
    const id = req.cleanData.id;
    const remaining = Math.max(0, 4000 - req.currentUser.maxPostContentCharsLength);
    const inc = Math.min(100, remaining);
    if (inc <= 0) return res.status(400).json({ error: "Gift redeem failed!" }); // Specify the exact error if wanted!

    const session = await mongoose.startSession();
    await session.withTransaction(async () => {
        // Gift
        const giftResult = await schemas.Gifts.updateOne(
            { _id: id, status: "active", usedBy: { $ne: req.session.userId } },
            [
                {
                    $set: {
                        status: {
                            $cond: {
                                if: { $eq: [{ $subtract: ["$usesCount", "$usedCount"] }, 1] },
                                then: "expired",
                                else: "$status"
                            }
                        },
                        usedCount: {
                            $cond: {
                                if: { $eq: ["$usedCount", "$usesCount"] },
                                then: "$usedCount",
                                else: { $add: ["$usedCount", 1] }
                            }
                        },
                        usedBy: {
                            $setUnion: [
                                { $ifNull: ["$usedBy", []] },
                                [new mongoose.Types.ObjectId(req.session.userId)]
                            ]
                        }
                    }
                }
            ],
            { session }
        );
        if (giftResult.matchedCount === 0) throw new Error("GIFT_REDEEM_FAILED");

        // User
        const userResult = await schemas.Users.updateOne({
            _id: req.session.userId,
            maxPostContentCharsLength: { $lt: 4000 }
        }, {
            $inc: {
                maxPostContentCharsLength: inc
            }
        }, { session });
        if (userResult.matchedCount === 0) throw new Error("USER_UPDATE_FAILED");
    });

    await session.endSession();
    return res.status(200).json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

Note: A try/catch block is unnecessary here because I am using the express-async-errors.


If you want to view the entire system, check https://github.com/Hfs2024/Vlox.
Hope you liked your 🎁!

Top comments (3)

Collapse
 
codemaster_121482 profile image
Seif Ahmed

Who liked their 🎁 from me?

Collapse
 
kehinde_owolabi_e2e54567a profile image
Kehinde Owolabi

I love it. Thanks

Collapse
 
codemaster_121482 profile image
Seif Ahmed

You're welcome! 🎉