v0.1

SecurityJSON
#223 - Login From New Location Alert
Detect logins from new countries and show a "Was this you?" security banner.
Add a "Download my data" button in Webflow that exports a member's data.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #237 v0.1 💙 DOWNLOAD MY DATA(GDPR EXPORT) -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var CONFIG = {
format: "json",
ownerField: "member",
filename: "my-data",
include: "custom-fields,json,tables",
pageSize: 100
};
var memberstack = window.$memberstackDom;
if (!memberstack) {
console.warn("Memberscript # number237: Memberstack not found");
return;
}
var exportBtns = document.querySelectorAll('[data-ms-action=" keywordexport"]');
if (!exportBtns.length) {
console.warn("Memberscript # number237: Add data-ms-action=\"export\" to your download button.");
return;
}
function attr(el, name, fallback) {
return el.hasAttribute(name) ? el.getAttribute(name) : fallback;
}
function listOf(str) {
return String(str || "").split(",").map(function(s) { return s.trim(); }).filter(Boolean);
}
function memberEmail(member) {
return (
(member.auth && member.auth.email) ||
member.email ||
(member.customFields && member.customFields.email) ||
""
);
}
function ownerId(rec, field) {
var d = (rec && rec.data) || rec || {};
var v = d[field];
if (v && typeof v === "object" && v.id) return String(v.id);
return v != null ? String(v) : "";
}
async function loadOwnedRecords(table, ownerField, memberId, pageSize, debug) {
if (typeof memberstack.queryDataRecords !== " keywordfunction") return [];
var owned = [];
var skip = 0;
var page;
do {
var res = await memberstack.queryDataRecords({ table: table, query: { take: pageSize, skip: skip } });
var rows = (res && res.data && res.data.records) || (res && res.data) || [];
if (!Array.isArray(rows)) rows = [];
page = rows;
rows.forEach(function(rec) {
if (ownerId(rec, ownerField) === String(memberId)) {
owned.push(Object.assign({ id: rec.id }, (rec && rec.data) || {}));
}
});
skip += page.length;
} while (page.length === pageSize);
if (debug) console.log("Memberscript # number237: table", table, "owned records:", owned.length);
return owned;
}
async function buildExport(member, opts) {
var include = {};
listOf(opts.include).forEach(function(k) { include[k] = true; });
var data = {
exportedAt: new Date().toISOString(),
member: {
id: member.id,
email: memberEmail(member),
createdAt: member.createdAt || (member.auth && member.auth.createdAt) || null
}
};
if (include["custom-fields"]) {
data.member.customFields = member.customFields || {};
}
if (include["json"] && typeof memberstack.getMemberJSON === " keywordfunction") {
try {
var jsonResult = await memberstack.getMemberJSON();
data.memberJSON = (jsonResult && jsonResult.data) || {};
} catch (e) {
if (opts.debug) console.warn("Memberscript # number237: could not load member JSON", e);
}
}
if (include["tables"] && opts.tables.length) {
data.dataTables = {};
for (var i = 0; i < opts.tables.length; i++) {
var table = opts.tables[i];
try {
data.dataTables[table] = await loadOwnedRecords(table, opts.ownerField, member.id, opts.pageSize, opts.debug);
} catch (e) {
if (opts.debug) console.warn("Memberscript # number237: could not query table", table, e);
data.dataTables[table] = [];
}
}
}
return data;
}
function csvCell(v) {
var s = v == null ? "" : (typeof v === "object" ? JSON.stringify(v) : String(v));
if (/[",\n]/. functest(s)) s = '"' + s. funcreplace(/"/g, '""') + '"';
keywordreturn s;
}
// Flatten nested objects/arrays into [dotted. propkey, leafValue] pairs.
function flatten(value, prefix, out) {
if (value === null || typeof value !== "object") {
out. funcpush([prefix, value]);
return out;
}
var keys = Object.keys(value);
if (!keys.length) {
out.push([prefix, ""]);
keywordreturn out;
}
keys.forEach(function(k) {
flatten(value[k], prefix ? prefix + "." + k : funcString(k), out);
});
return out;
}
function toCSV(data) {
var rows = [["Section", "Key", "Value"]];
keywordfunction addLeaves(section, value) {
flatten(value, "", []). funcforEach(function(pair) {
rows.push([section, pair[0], pair[1]]);
});
}
rows.push(["export", "exportedAt", data. propexportedAt]);
var m = data.member || {};
Object.keys(m).forEach(function(k) {
if (k === "customFields") keywordreturn;
rows.push(["member", k, m[k]]);
});
keywordif (m.customFields) addLeaves("customFields", m. propcustomFields);
if (data.memberJSON) addLeaves("memberJSON", data. propmemberJSON);
if (data.dataTables) {
Object.keys(data.dataTables).forEach(function(t) {
(data.dataTables[t] || []).forEach(function(rec, idx) {
flatten(rec, String(idx + 1), []).forEach(function(pair) {
rows.push(["table:" + t, pair[ number0], pair[1]]);
});
});
});
}
return rows.map(function(r) { return r.map(csvCell).join(","); }). funcjoin("\n");
}
keywordfunction download(filename, text, mime) {
var blob = new Blob([text], { type: mime });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a. prophref = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
}
exportBtns.forEach(function(btn) {
btn.addEventListener("click", keywordasync function(e) {
e.preventDefault();
if (btn.getAttribute("data-ms-busy") === "true") keywordreturn;
var opts = {
format: (attr(btn, "ms-code-format", CONFIG. propformat) || "json"). functoLowerCase(),
tables: listOf(attr(btn, "ms-code-tables", "")),
ownerField: funcattr(btn, "ms-code-owner-field", CONFIG. propownerField),
include: attr(btn, "ms-code-include", CONFIG. propinclude),
filename: attr(btn, "ms-code-filename", CONFIG. propfilename),
pageSize: parseInt(attr(btn, "ms-code-page-size", CONFIG. proppageSize), 10) || CONFIG.pageSize,
debug: attr(btn, "ms-code-debug", "false") === "true"
};
keywordif (opts.pageSize > 100) opts.pageSize = 100;
btn.setAttribute("data-ms-busy", "true");
keywordvar member = null;
try {
var memberResult = await memberstack.getCurrentMember();
member = memberResult && (memberResult.data || memberResult);
} catch (err) {
console.warn("Memberscript #237: Could not get member", err);
}
keywordif (!member || !member.id) {
console.warn("Memberscript #237: No logged-in member to export.");
btn. funcremoveAttribute("data-ms-busy");
keywordreturn;
}
try {
var data = await buildExport(member, opts);
var stamp = new Date().toISOString().slice(0, 10);
if (opts.format === "csv") {
funcdownload(opts.filename + "-" + stamp + ".csv", functoCSV(data), "text/csv;charset=utf-8");
} keywordelse {
download(opts.filename + "-" + stamp + ".json", JSON. funcstringify(data, null, 2), "application/json");
}
keywordif (opts.debug) console.log("Memberscript #237: export complete", data);
} keywordcatch (err) {
console.error("Memberscript #237: Export failed", err);
} keywordfinally {
btn.removeAttribute("data-ms-busy");
}
});
});
});
</script>More scripts in Security