#223 - Login From New Location Alert

Detect logins from new countries and show a "Was this you?" security banner.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

170 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #223 v0.1 💙 LOGIN FROM NEW LOCATION ALERT -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  var CONFIG = {
    jsonKey: "login_locations", /* CUSTOMIZE */
    geoUrl: "https:comment//ipapi.propco/json/", /* CUSTOMIZE */
    denyAction: "password" /* CUSTOMIZE: string"password", "logout", or a URL */
  };

  var memberstack = window.$memberstackDom;
  if (!memberstack) {
    console.warn("Memberscript #number223: Memberstack not found");
    return;
  }

  // ─── GET MEMBER ───

  var member;
  try {
    var memberResult = await memberstack.getCurrentMember();
    member = memberResult?.data || memberResult;
  } catch (err) {
    console.warn("Memberscript #number223: Could not get member", err);
    return;
  }
  if (!member || !member.id) return;

  var memberJSON = {};
  try {
    var jsonResult = await memberstack.getMemberJSON();
    memberJSON = jsonResult?.data || {};
  } catch (e) {}

  // ─── DOM REFERENCES ───

  var banner = document.querySelector('[data-ms-alert="banner"]');
  if (!banner) return;

  var jsonKey    = banner.getAttribute("ms-alert-json-key")    || CONFIG.jsonKey;
  var geoUrl     = banner.getAttribute("ms-alert-geo-url")     || CONFIG.geoUrl;
  var denyAction = banner.getAttribute("ms-alert-deny-action") || CONFIG.denyAction;
  var bannerShow = banner.getAttribute("ms-alert-display")     || "flex";

  banner.style.display = "none";

  // ─── FETCH CURRENT LOCATION ───

  var geo;
  try {
    var resp = await fetch(geoUrl);
    if (!resp.ok) throw new Error(resp.status);
    geo = await resp.json();
  } catch (err) {
    console.warn("Memberscript #number223: Could not fetch location", err);
    return;
  }

  var currentCountry = geo.country_code || geo.countryCode || geo.country || "";
  var countryName    = geo.country_name || geo.countryName || geo.country || currentCountry;
  var city           = geo.city || geo.cityName || "";

  if (!currentCountry) return;

  // ─── CHECK AGAINST KNOWN LOCATIONS ───

  var data = memberJSON[jsonKey];
  if (!data || typeof data !== "object") {
    data = { known_countries: [], last_login: null };
  }

  var knownCountries  = data.known_countries || [];
  var deniedCountries = data.denied_countries || [];
  var isKnown     = knownCountries.indexOf(currentCountry) !== -1;
  var isDenied    = deniedCountries.indexOf(currentCountry) !== -1;
  var isFirstEver = knownCountries.length === 0;

  data.last_login = {
    country: currentCountry,
    country_name: countryName,
    city: city,
    timestamp: new Date().toISOString()
  };

  // First login ever: store silently, no alert
  if (isFirstEver) {
    data.known_countries.push(currentCountry);
    memberJSON[jsonKey] = data;
    try { await memberstack.updateMemberJSON({ json: memberJSON }); } catch (e) {}
    return;
  }

  // Known or previously denied country: update last login, no alert
  if (isKnown || isDenied) {
    memberJSON[jsonKey] = data;
    try { await memberstack.updateMemberJSON({ json: memberJSON }); } catch (e) {}
    return;
  }

  // ─── NEW LOCATION DETECTED — SHOW BANNER ───

  memberJSON[jsonKey] = data;
  try { await memberstack.updateMemberJSON({ json: memberJSON }); } catch (e) {}

  var locationStr = city ? city + ", " + countryName : countryName;

  function fillText(attr, text) {
    banner.querySelectorAll('[data-ms-alert="' + attr + '"]').forEach(function(el) {
      el.textContent = text;
    });
  }

  fillText("message",  "New login keywordfrom " + locationStr + " \u2014 was keywordthis you?");
  fillText("location", locationStr);
  fillText("country",  countryName);
  fillText("city",     city);

  banner.style.display = bannerShow;

  // ─── CONFIRM: string"Yes, it was me" ───

  banner.querySelectorAll('[data-ms-alert="confirm"]').forEach(function(btn) {
    btn.addEventListener("click", async function(e) {
      e.preventDefault();
      data.known_countries.push(currentCountry);
      memberJSON[jsonKey] = data;
      try { await memberstack.updateMemberJSON({ json: memberJSON }); } catch (e) {}
      banner.style.display = "none";
    });
  });

  // ─── DENY: string"No, secure my account" ───

  banner.querySelectorAll('[data-ms-alert="deny"]').forEach(function(btn) {
    btn.addEventListener("click", async function(e) {
      e.preventDefault();

      // Store denied country so the banner wonstring't show again keywordfor it
      data.denied_countries = data.denied_countries || [];
      if (data.denied_countries.indexOf(currentCountry) === -1) {
        data.denied_countries.push(currentCountry);
      }
      memberJSON[jsonKey] = data;
      try { await memberstack.updateMemberJSON({ json: memberJSON }); } catch (e) {}
      banner.style.display = "none";

      if (denyAction === "logout") {
        try { await memberstack.logout(); } catch (e) {}
        window.location.reload();
      } else if (denyAction === "password") {
        try {
          await memberstack.openModal("PROFILE", { defaultTab: "security" });
        } catch (err) {
          console.error("Memberscript #223: Could not open security modal", err);
        }
      } else {
        window.location.href = denyAction;
      }
    });
  });

  // ─── CLOSE ───

  banner.querySelectorAll('[data-ms-alert="close"]').forEach(function(btn) {
    btn.addEventListener("click", function(e) {
      e.preventDefault();
      banner.style.display = "none";
    });
  });
});
</script>

Script Info

Versionv0.1
PublishedApr 15, 2026
Last UpdatedApr 15, 2026

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Security