DEV Community

Seif Ahmed
Seif Ahmed

Posted on

Building a password-protected link sharing workflow

Ever wondered how to build a clean, secure system for sharing user content via unique links? It gets interesting when you have to separate public access from password-protected private shares.

Here is a breakdown of how the frontend and backend interact to extract IDs, enforce security via bcrypt, and protect against brute-force attacks with rate-limiting:

How It Works

  • URL Activation: A user opens the link http://localhost:3000/?id=POST_ID.
  • ID Extraction: The frontend parses the URL and extracts the ID using URLSearchParams.
  • Request Initiation: If an ID is present, the frontend requests data from /api/v1/get/save-from-link/:id.
  • Backend Verification: The backend checks if the ID exists in the database.
  • Public Access: If the link is public, the backend returns the content (link.for.content).
  • Private Access: If the link is private, the backend responds with a success signal and need_password: true without sending any content.
  • Frontend Evaluation: The frontend checks the need_password flag.
  • Content Display: If false, it renders the public content.
  • Password Prompt: If true, a popup prompts the user to enter a password.
  • Rate Limiting: Users get 10 password attempts per 7 minutes across all private saves globally.
  • Password Validation: The frontend sends the ID and password to /api/v1/validate-save-password/:id.
  • Security Match: The backend queries the ID ensuring status: "private", then verifies the password using await bcrypt.compare(password, link.password).

The Code

// In server.js
app.get("/api/v1/get/save-from-link/:id", checkValidID, async (req, res) => {
  try {
    const id = req.params.id;
    const link = await schemas.Links.findOne({ for: id }).populate("for").lean(); // Do you have permissions to see this doc?
    if (!link) return res.status(400).json({ error: "Invalid link!" });
    if (link.status === "private") return res.status(400).json({ success: true, need_password: true }); // Send a success singal, but warns that a password must be entered
    return res.status(200).json({ success: true, content: link.for.content });
  } catch (e) {
    console.log("Error: " + e.message);
    return res.status(500).json({ error: "Server error" });
  }
});

// In actions.js
router.post("/api/v1/validate-save-password/:id", validatePasswordRateLimiter, checkAuth, checkValidID, async (req, res) => {
  try {
    const id = req.params.id;
    const { password } = req.body;
    if (typeof password !== "string") return res.status(400).json({ error: "Password must be a type of string!" });
    if (!password) return res.status(400).json({ error: "You didn't enter a password!" }); // Find link
    const link = await schemas.Links.findOne({ for: id, status: "private" }).populate("for").lean(); // Check password match
    const isMatch = await bcrypt.compare(password, link.password);
    if (!isMatch) return res.status(400).json({ error: "Invalid password. Do you want to try again?", invalid_password: true });
    return res.status(200).json({ success: true, content: link.for.content });
  } catch (e) {
    console.log("Error: " + e.message);
    return res.status(400).json({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Liked the project? Please consider giving it a star on GitHub at https://github.com/Hfs2024/Markdown-Previewer

Top comments (0)