/**
* Generate random password
* @param {boolean} lowerLetter Enable lowercased letter
* @param {boolean} capitalLetter Enable capital letter
* @param {boolean} number Enable number
* @param {boolean} symbol Enable symbol
* @param {number} length Password length
*/
function generatePassword(lowerLetter, capitalLetter, number, symbol, length = 8) {
const lowerCase = "abcdefghijklmnopqrstuvwxyz";
const upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const numbers = "0123456789";
const symbols = "!@#$%^&*_-+=";
// Password ratio, from 0 to 1, total all of them will be 1
let lowerRatio = 0.4;
let upperRatio = 0.2;
let numberRatio = 0.2;
let symbolRatio = 0.2;
// Change ratio amount if any of them is disabled
if (!lowerLetter) {
lowerRatio = 0;
}
if (!capitalLetter) {
upperRatio = 0;
}
if (!number) {
numberRatio = 0;
}
if (!symbol) {
symbolRatio = 0;
}
// Calculate total ratio
const totalRatio = lowerRatio + upperRatio + numberRatio + symbolRatio;
if (totalRatio === 0) {
return "";
}
// Calculate ratio for each type
lowerRatio /= totalRatio;
upperRatio /= totalRatio;
numberRatio /= totalRatio;
symbolRatio /= totalRatio;
let password = "";
const lowerLength = Math.floor(length * lowerRatio);
const upperLength = Math.floor(length * upperRatio);
const numberLength = Math.floor(length * numberRatio);
const symbolLength = Math.floor(length * symbolRatio);
const randomGet = (str, len) => {
if (len < 1) return "";
let result = "";
for (let i = 0; i < len; i++) {
result += str.charAt(Math.floor(Math.random() * str.length));
}
return result;
}
const shuffle = (str) => {
return str.split('').sort(function () {
return 0.5 - Math.random();
}).join('');
}
// Generate password
password += randomGet(lowerCase, lowerLength);
password += randomGet(upperCase, upperLength);
password += randomGet(numbers, numberLength);
password += randomGet(symbols, symbolLength);
return shuffle(password);
} // <-- the reason it is not in the below safeguard is because I want to test it from the browser console itself.
// safeguard, make sure the js content and function cannot be acessed by the browser
(function() {
/**
* Our custom state object, simulating React state object.
* This state basically holds the data that we will use to share between functions.
*/
const DashboardState = {
/** @type {number | null} */
deleted: null,
/** @type {string | null} */
editing: null,
// allow us to lock the modal so it cannot be closed.
modalLock: {
modalError: false,
modalDelete: false,
modalAdd: false,
modalEdit: false,
}
}
/**
* Check if value is undefined or null
* @param {any} value
* @returns result
*/
function isNone(value) {
return value === undefined || value === null;
}
/**
* Check if string is empty
* @param {string} str
*/
function isEmpty(str) {
if (isNone(str)) return true;
return str.replace(/\s/g, '').length < 1;
}
/**
* Fetch data from our backend server in JSON format
* @param {url} url The URL to be fetched from
* @returns Fetched data, in Promise format
*/
function GETJson(url) {
return fetch(url, {
method: "GET",
headers: {
"Accept": "application/json"
},
}).then((resp) => resp.json());
}
/**
* Send data to an API
* @param {"GET" | "POST" | "DELETE" | "PATCH"} method HTTP Method
* @param {string} url The URL to send data to
* @param {any | null} data The data to be sent
* @returns
*/
function SENDJson(method, url, data = null) {
const fetchStats = {
method: method,
headers: {
"Content-Type": "application/json"
},
}
if (!isNone(data)) {
fetchStats.body = JSON.stringify(data);
}
return fetch(url, fetchStats);
}
/**
* Send POST data to the API
* @param {string} url The URL to send data to
* @param {any | null} data The data to be sent
*/
const POSTJson = (url, data = null) => SENDJson("POST", url, data);
/**
* Send DELETE data to the API
* @param {string} url The URL to send data to
* @param {any | null} data The data to be sent
*/
const DELETEJson = (url, data = null) => SENDJson("DELETE", url, data);
/**
* Send PATCH data to the API
* @param {string} url The URL to send data to
* @param {any | null} data The data to be sent
*/
const PATCHJson = (url, data = null) => SENDJson("PATCH", url, data);
/**
* Handle password deletion
* @param {number} passwordId the password id
*/
function handlePasswordDelete(passwordId) {
const $tr = document.querySelector(`tr[data-password-id="${passwordId}"]`);
const $btnDelete = $tr.querySelector("button[data-button-type='delete']");
$btnDelete.setAttribute("disabled", "disabled");
console.info("[Delete]", passwordId);
// Send deletion request to the backend server
DELETEJson("/api/passwords", {id: passwordId}).then((resp) => resp.json()).then((data) => {
$btnDelete.removeAttribute("disabled");
DashboardState.deleted = null;
if (data.success) {
$tr.remove();
} else {
displayError(data.error);
}
})
}
/**
* Show specific modal to the user
* @param {string} modalId Modal ID to be displayed
*/
function showModal(modalId) {
const $modal = document.getElementById(modalId);
$modal.classList.remove("hidden");
}
/**
* Hide/close specific modal to the user
* @param {string} modalId Modal ID to be closed
*/
function hideModal(modalId) {
const $modal = document.getElementById(modalId);
$modal.classList.add("hidden");
}
/**
* Generate a table row for our password list!
* @param {number} passwordId Password ID
* @param {string} email The email
* @param {string} password plaintext password
*/
function generateTableRow(passwordId, email, password) {
// Create our base
element
const $tr = document.createElement("tr");
$tr.classList.add("border-b-[1px]", "border-gray-500");
// Create our email element
const $tdEmail = document.createElement("td");
$tdEmail.classList.add("text-center", "p-2");
$tdEmail.innerText = email;
// Create our password element
const $tdPassword = document.createElement("td");
$tdPassword.classList.add("p-2", "text-center");
// Create our inner password, so it only select the password itself instead of the whole div object
const $innerPassword = document.createElement("span");
$innerPassword.classList.add("blur-md", "hover:blur-none", "transition", "select-all");
$innerPassword.innerText = password;
$tdPassword.appendChild($innerPassword);
// Create our actions element wrapper
const $tdActions = document.createElement("td");
$tdActions.classList.add("p-2", "py-4");
const $divActionsInner = document.createElement("div");
$divActionsInner.classList.add("flex", "flex-col", "justify-center", "gap-2");
// Create our edit button
const $buttonEdit = document.createElement("button");
$buttonEdit.classList.add("bg-teal-400", "hover:bg-teal-500", "transition", "mx-auto", "px-4", "py-1", "rounded-md", "text-sm", "uppercase", "font-semibold", "select-none");
$buttonEdit.innerText = "Edit";
$buttonEdit.addEventListener("click", () => {
// Add our edit button event handler here
// First we add the password ID to our editing state (simulating React)
DashboardState.editing = passwordId;
document.getElementById("genPasswordValueEdit").value = password;
// Now, let's check our old password
// See if it contains lowercase letters
const $checkLower = document.querySelector(`#modalEdit input[name='genPassLower']`);
// See if it contains uppercase letters
const $checkUpper = document.querySelector(`#modalEdit input[name='genPassUpper']`);
// See if it contains any numbers
const $checkNum = document.querySelector(`#modalEdit input[name='genPassNum']`);
// See if it contains any symbols
const $checkSym = document.querySelector(`#modalEdit input[name='genPassSym']`);
// Check our password length
const $checkLength = document.querySelector(`#modalEdit input[name='genPassLength']`);
$checkLower.checked = hasAnyLowercase(password);
$checkUpper.checked = hasAnyUppercase(password);
$checkNum.checked = hasAnyNumber(password);
$checkSym.checked = hasAnySymbol(password);
$checkLength.value = password.length.toString();
document.getElementById("emailBoxEdit").value = email;
// After checking, now show the modal
showModal("modalEdit");
});
// Add custom attribute as helper when we will use querySelector to replace element or delete it
$buttonEdit.setAttribute("data-password-id", passwordId.toString());
$buttonEdit.setAttribute("data-button-type", "edit");
// Create our delete button
const $buttonDelete = document.createElement("button");
$buttonDelete.classList.add("bg-red-400", "hover:bg-red-500", "disabled:bg-red-500", "disabled:cursor-not-allowed", "transition", "mx-auto", "px-4", "py-1", "rounded-md", "text-sm", "uppercase", "font-semibold", "select-none");
$buttonDelete.innerText = "Delete";
$buttonDelete.addEventListener("click", () => {
// Handle our deletion, first set the state like the edit button and show the modal
DashboardState.deleted = passwordId;
showModal("modalDelete");
});
// Add custom attribute as helper when we will use querySelector to replace element or delete it
$buttonDelete.setAttribute("data-password-id", passwordId.toString());
$buttonDelete.setAttribute("data-button-type", "delete");
// Add all of them to our base wrapper
$divActionsInner.appendChild($buttonEdit);
$divActionsInner.appendChild($buttonDelete);
$tdActions.appendChild($divActionsInner);
$tr.appendChild($tdEmail);
$tr.appendChild($tdPassword);
$tr.appendChild($tdActions);
// Set password-id to our element
$tr.setAttribute("data-password-id", passwordId.toString());
// Return the result
return $tr;
}
/**
* Check if all of our password generator modifier is unchecked
* @param {"modalAdd" | "modalEdit"} prefix the modal we're editing
* @returns {boolean} true if all modifier is unchecked, false otherwise
*/
function isAllGenModifierOff(prefix) {
const $checkLower = document.querySelector(`#${prefix} input[name='genPassLower']`);
const $checkUpper = document.querySelector(`#${prefix} input[name='genPassUpper']`);
const $checkNum = document.querySelector(`#${prefix} input[name='genPassNum']`);
const $checkSym = document.querySelector(`#${prefix} input[name='genPassSym']`);
return !$checkLower.checked && !$checkUpper.checked && !$checkNum.checked && !$checkSym.checked;
}
/**
* Disable our modifier button so user cannot check/uncheck them
* @param {"modalAdd" | "modalEdit"} prefix the modal we're editing
*/
function disablePasswordModifierCheckbox(prefix) {
const $checkLower = document.querySelector(`#${prefix} input[name='genPassLower']`);
const $checkUpper = document.querySelector(`#${prefix} input[name='genPassUpper']`);
const $checkNum = document.querySelector(`#${prefix} input[name='genPassNum']`);
const $checkSym = document.querySelector(`#${prefix} input[name='genPassSym']`);
const $checkLength = document.querySelector(`#${prefix} input[name='genPassLength']`);
$checkLower.setAttribute("disabled", "true");
$checkUpper.setAttribute("disabled", "true");
$checkNum.setAttribute("disabled", "true");
$checkSym.setAttribute("disabled", "true");
$checkLength.setAttribute("disabled", "true");
}
/**
* Enable our modifier button so user cannot check/uncheck them
* @param {"modalAdd" | "modalEdit"} prefix the modal we're editing
*/
function enablePasswordModifierCheckbox(prefix) {
const $checkLower = document.querySelector(`#${prefix} input[name='genPassLower']`);
const $checkUpper = document.querySelector(`#${prefix} input[name='genPassUpper']`);
const $checkNum = document.querySelector(`#${prefix} input[name='genPassNum']`);
const $checkSym = document.querySelector(`#${prefix} input[name='genPassSym']`);
const $checkLength = document.querySelector(`#${prefix} input[name='genPassLength']`);
$checkLower.removeAttribute("disabled");
$checkUpper.removeAttribute("disabled");
$checkNum.removeAttribute("disabled");
$checkSym.removeAttribute("disabled");
$checkLength.removeAttribute("disabled");
}
/**
* Should we close our modal or not!
* @param {string} modalId Modal ID
* @param {PointerEvent} ev Event
*/
function shouldCloseOrNot(modalId, ev) {
if (DashboardState.modalLock[modalId]) {
return;
}
let shouldCloseModal = true;
ev.composedPath().forEach((el) => {
const modalView = modalId + "View";
if (el.id === modalView) {
shouldCloseModal = false;
}
});
if (shouldCloseModal) {
hideModal(modalId);
}
}
/**
* Display error message to the user using the modal
* @param {string} message Error message to be displayed
*/
function displayError(message) {
const $modalErrorTextView = document.getElementById("modalErrorTextView");
$modalErrorTextView.innerText = message;
showModal("modalError");
}
/**
* Check if our password contain any lowercase letter or not
* @param {string} str String to be checked
* @returns {boolean} true if contain lowercase letter, false otherwise
*/
function hasAnyUppercase(str) {
// Check by lowercasing everything and see if it's the same
// If yes, that means that all are lowercase and no uppercase
return str.toLowerCase() !== str;
}
/**
* Check if our password contain any uppercase letter or not
* @param {string} str String to be checked
* @returns {boolean} true if contain uppercase letter, false otherwise
*/
function hasAnyLowercase(str) {
// Check by uppercase everything and see if it's the same
// If yes, that means that all are uppercase and no lowercase
return str.toUpperCase() !== str;
}
/**
* Check if our password contain any numbers or not
* @param {string} str String to be checked
* @returns {boolean} true if contain numbers, false otherwise
*/
function hasAnyNumber(str) {
// Use regex to check if we have any number
return /\d/.test(str);
}
/**
* Check if our password contain any symbols or not
* @param {string} str String to be checked
* @returns {boolean} true if contain symbols, false otherwise
*/
function hasAnySymbol(str) {
// Regex, basically invert checking anything outside capital, lowercase, and numbers
return /[^a-zA-Z\d]/.test(str);
}
/**
* Regenerate password and put it in the modal
* @param {"modalAdd" | "modalEdit"} prefix the modal we're editing
*/
function cycleGeneratedPassword(prefix) {
// Check prefix first
const extraText = prefix.includes("Edit") ? "Edit" : "";
const $btnRegenPass = document.getElementById("btnRegenPass" + extraText);
const $checkLength = document.querySelector(`#${prefix} input[name='genPassLength']`);
const isAllOff = isAllGenModifierOff(prefix);
// Check the modifier, if all off disable everything and return.
if (isAllOff) {
$btnRegenPass.setAttribute("disabled", "true");
$checkLength.setAttribute("disabled", "true");
return;
} else {
$btnRegenPass.removeAttribute("disabled");
$checkLength.removeAttribute("disabled");
}
// Check our modifier, and length to generate password
const $checkLower = document.querySelector(`#${prefix} input[name='genPassLower']`);
const $checkUpper = document.querySelector(`#${prefix} input[name='genPassUpper']`);
const $checkNum = document.querySelector(`#${prefix} input[name='genPassNum']`);
const $checkSym = document.querySelector(`#${prefix} input[name='genPassSym']`);
const passLength = parseInt($checkLength.value) || 8;
const password = generatePassword($checkLower.checked, $checkUpper.checked, $checkNum.checked, $checkSym.checked, passLength);
const $passValue = document.getElementById("genPasswordValue" + extraText);
// check if node type is
if ($passValue.tagName === "INPUT") {
$passValue.value = password;
} else {
$passValue.innerText = password;
}
}
// Our main function
function main() {
const $tableBody = document.getElementById("passwordView");
// Get our password list from the API, then append to our table
GETJson("/api/passwords")
.then(
(data) => {
/**
* @type {import ("../../../src/types").IPassword[]}
*/
const results = data.data;
results.forEach((pwd) => {
const $tr = generateTableRow(pwd.id, pwd.email, pwd.password);
$tableBody.appendChild($tr);
})
}
)
// Add event listener to our modal
const $deleteModal = document.getElementById("modalDelete");
$deleteModal.onclick = (ev) => {
shouldCloseOrNot("modalDelete", ev);
}
const $modalDeleteBtn = document.querySelector(".modal-delete-btn");
const $modalDeleteCancelBtn = document.querySelector(".modal-delete-cancel-btn");
$modalDeleteBtn.addEventListener("click", (ev) => {
ev.preventDefault();
console.info("delete", DashboardState.deleted);
hideModal("modalDelete");
if (isNone(DashboardState.deleted)) {
displayError("No password selected to delete!");
return;
}
handlePasswordDelete(DashboardState.deleted);
});
$modalDeleteCancelBtn.addEventListener("click", (ev) => {
ev.preventDefault();
hideModal("modalDelete");
});
const $errorModal = document.getElementById("modalError");
$errorModal.onclick = (ev) => {
shouldCloseOrNot("modalError", ev);
}
const $modalErrorBtn = document.querySelector(".modal-error-btn");
$modalErrorBtn.addEventListener("click", (ev) => {
ev.preventDefault();
hideModal("modalError");
});
const $addPasswordBtn = document.getElementById("generateNewPass");
$addPasswordBtn.addEventListener("click", (ev) => {
ev.preventDefault();
cycleGeneratedPassword("modalAdd");
showModal("modalAdd");
});
const $btnRegenPass = document.getElementById("btnRegenPass");
$btnRegenPass.addEventListener("click", (ev) => {
ev.preventDefault();
cycleGeneratedPassword("modalAdd");
});
const $checkLower = document.querySelector("#modalAdd input[name='genPassLower']");
const $checkUpper = document.querySelector("#modalAdd input[name='genPassUpper']");
const $checkNum = document.querySelector("#modalAdd input[name='genPassNum']");
const $checkSym = document.querySelector("#modalAdd input[name='genPassSym']");
const $checkLength = document.querySelector("#modalAdd input[name='genPassLength']");
const cyclePass = (ev) => {
ev.preventDefault();
cycleGeneratedPassword("modalAdd");
}
$checkLower.addEventListener("change", cyclePass);
$checkUpper.addEventListener("change", cyclePass);
$checkNum.addEventListener("change", cyclePass);
$checkSym.addEventListener("change", cyclePass);
$checkLength.addEventListener("change", cyclePass);
const $btnRegenPassEdit = document.getElementById("btnRegenPassEdit");
$btnRegenPassEdit.addEventListener("click", (ev) => {
ev.preventDefault();
cycleGeneratedPassword("modalEdit");
});
const $checkLowerEd = document.querySelector("#modalEdit input[name='genPassLower']");
const $checkUpperEd = document.querySelector("#modalEdit input[name='genPassUpper']");
const $checkNumEd = document.querySelector("#modalEdit input[name='genPassNum']");
const $checkSymEd = document.querySelector("#modalEdit input[name='genPassSym']");
const $checkLengthEd = document.querySelector("#modalEdit input[name='genPassLength']");
const cyclePassEdit = (ev) => {
ev.preventDefault();
cycleGeneratedPassword("modalEdit");
}
$checkLowerEd.addEventListener("change", cyclePassEdit);
$checkUpperEd.addEventListener("change", cyclePassEdit);
$checkNumEd.addEventListener("change", cyclePassEdit);
$checkSymEd.addEventListener("change", cyclePassEdit);
$checkLengthEd.addEventListener("change", cyclePassEdit);
const $addModal = document.getElementById("modalAdd");
$addModal.onclick = (ev) => {
shouldCloseOrNot("modalAdd", ev);
}
const $modalAddBtn = document.querySelector(".modal-add-btn");
$modalAddBtn.addEventListener("click", (ev) => {
ev.preventDefault();
DashboardState.modalLock.modalAdd = true;
const $email = document.getElementById("emailBoxAdd");
const $password = document.getElementById("genPasswordValue");
const email = $email.value;
const innerPass = $password.innerText;
if (isEmpty(email)) {
alert("Email cannot be empty!");
DashboardState.modalLock.modalAdd = false;
return;
};
if (isEmpty(innerPass)) {
alert("Password cannot be empty!");
DashboardState.modalLock.modalAdd = false;
return;
}
$modalAddBtn.setAttribute("disabled", "disabled");
$btnRegenPass.setAttribute("disabled", "disabled");
$email.setAttribute("disabled", "disabled");
$password.setAttribute("disabled", "disabled");
disablePasswordModifierCheckbox("modalAdd");
POSTJson("/api/passwords", {
email: email,
password: innerPass
}).then((resp) => resp.json()).then((data) => {
DashboardState.modalLock.modalAdd = false;
$modalAddBtn.removeAttribute("disabled");
$btnRegenPass.removeAttribute("disabled");
$email.removeAttribute("disabled");
$password.removeAttribute("disabled");
enablePasswordModifierCheckbox("modalAdd");
if (data.success) {
const $tr = generateTableRow(data.data.id, email, innerPass);
$tableBody.appendChild($tr);
hideModal("modalAdd");
} else {
hideModal("modalAdd");
displayError(data.error);
}
});
});
const $modalAddBtnCancel = document.querySelector(".modal-add-cancel-btn");
$modalAddBtnCancel.addEventListener("click", (ev) => {
ev.preventDefault();
if (!DashboardState.modalLock.modalAdd) {
hideModal("modalAdd");
}
});
const $editModal = document.getElementById("modalEdit");
$editModal.onclick = (ev) => {
shouldCloseOrNot("modalEdit", ev);
}
const $modalEditBtnCancel = document.querySelector(".modal-edit-cancel-btn");
$modalEditBtnCancel.addEventListener("click", (ev) => {
ev.preventDefault();
if (!DashboardState.modalLock.modalEdit) {
hideModal("modalEdit");
}
});
const $modalEditBtn = document.querySelector(".modal-edit-btn");
$modalEditBtn.addEventListener("click", (ev) => {
ev.preventDefault();
DashboardState.modalLock.modalEdit = true;
const $email = document.getElementById("emailBoxEdit");
const $password = document.getElementById("genPasswordValueEdit");
const email = $email.value;
const innerPass = $password.value;
if (isEmpty(email)) {
alert("Email cannot be empty!");
DashboardState.modalLock.modalEdit = false;
return;
};
if (isEmpty(innerPass)) {
alert("Password cannot be empty!");
DashboardState.modalLock.modalEdit = false;
return;
}
$modalEditBtn.setAttribute("disabled", "disabled");
$email.setAttribute("disabled", "disabled");
$password.setAttribute("disabled", "disabled");
$btnRegenPassEdit.setAttribute("disabled", "disabled");
disablePasswordModifierCheckbox("modalEdit");
PATCHJson("/api/passwords", {
id: DashboardState.editing,
email: email,
password: innerPass
}).then((resp) => resp.json()).then((data) => {
DashboardState.modalLock.modalEdit = false;
$modalEditBtn.removeAttribute("disabled");
$btnRegenPassEdit.removeAttribute("disabled");
$email.removeAttribute("disabled");
$password.removeAttribute("disabled");
enablePasswordModifierCheckbox("modalEdit");
DashboardState.editing = null;
if (data.success) {
const $tr = generateTableRow(data.data.id, email, innerPass);
$tableBody.replaceChild($tr, document.querySelector(`tr[data-password-id="${data.data.id}"]`));
hideModal("modalEdit");
} else {
hideModal("modalEdit");
displayError(data.error);
}
});
});
document.getElementById("logOutBtn").addEventListener("click", (ev) => {
ev.preventDefault();
POSTJson("/api/logout").then((resp) => resp.json()).then((d) => {
if (d.success) {
window.location.href = "/";
} else {
displayError("Unable to log out from server!");
}
})
});
}
/**
* Check if our document is ready, if not wait until ready and call our main function
*/
if (document.readyState === "complete") {
main();
} else {
document.addEventListener("DOMContentLoaded", main);
}
})();