death-notifier/src/main/js/Main.ts

603 lines
20 KiB
TypeScript

// @ts-ignore
const {$, $a, doAfterLoad, footer, header, nav} = window.fwdekker;
import {csrfToken, emptyFunction, getApi, postApi, ServerResponse, sharedMessageElement} from "./API";
import {CustomEventHandler} from "./CustomEventHandler";
import {clearMessage, clearMessages, showError, showInfo, showSuccess, showWarning} from "./Message";
/**
* Handles listeners to be invoked when the user logs in.
*/
const loginHandler = new CustomEventHandler();
/**
* Handles listeners to be invoked when the user logs out.
*/
const logoutHandler = new CustomEventHandler();
/**
* Refreshes the list of trackings in the table.
*/
function refreshTrackings(): void {
getApi(
{action: "list-trackings", token: csrfToken ?? ""},
sharedMessageElement,
(response) => {
// Remove old rows
$a("#trackings tbody tr:not(:last-child)").forEach((it: HTMLTableRowElement) => it.remove());
// Insert new rows
const tableBody = $("#trackings tbody");
const lastRow = $("#trackings tbody tr:last-child");
response.payload.forEach((tracking: any) => {
const row = document.createElement("tr");
const nameCell = document.createElement("td");
const nameLink = document.createElement("a");
nameLink.href = "https://en.wikipedia.org/wiki/" + tracking.name;
nameLink.innerText = tracking.name;
if (tracking.deleted)
nameLink.classList.add("redLink");
nameCell.append(nameLink);
row.append(nameCell);
const statusCell = document.createElement("td");
let statusText;
if (tracking.deleted) {
statusText = "article not found";
} else {
switch (tracking.name) {
case "Adolf Hitler":
statusText = "dead 🥳";
break;
case "Vladimir Putin":
statusText = tracking.status === "alive" ? "alive ☹️" : "dead 🥳";
break;
default:
statusText = tracking.status;
break;
}
}
statusCell.innerText = statusText;
row.append(statusCell);
const deleteCell = document.createElement("td");
const deleteForm = document.createElement("form");
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "remove-tracking", token: csrfToken, person_name: tracking.name},
deleteForm,
() => refreshTrackings()
)
});
const deleteButton = document.createElement("button");
deleteButton.innerText = "remove";
deleteForm.append(deleteButton);
deleteCell.append(deleteForm);
row.append(deleteCell);
tableBody.insertBefore(row, lastRow);
});
}
);
}
/**
* Refreshes displays of the user's data.
*/
function refreshUserData(): void {
getApi(
{action: "get-user-data", token: csrfToken ?? ""},
sharedMessageElement,
(response) => {
const userData = response.payload;
// Account deletion
$("#deleteEmail").value = userData.email;
// Email
$("#emailCurrent").innerText = userData.email;
$("#emailVerified").innerText = userData.email_verified ? "yes" : "no";
if (!userData.email_verified)
showWarning(
sharedMessageElement,
"You will not receive any email notifications until you verify your email address. " +
"Check your inbox for further instructions."
);
else
clearMessage(sharedMessageElement);
$("#resendEmailVerificationButton").classList.toggle("hidden", userData.email_verified);
// Notifications
const toggleButton = $("#toggleNotificationsButton");
const notificationsEnabled = userData.email_verified && userData.email_notifications_enabled;
$("#notificationsEnabled").innerText = notificationsEnabled ? "enabled" : "disabled";
toggleButton.innerText = notificationsEnabled ? "disable" : "enable";
toggleButton.classList.toggle("hidden", !userData.email_verified);
// Password update time
const today = new Date();
today.setHours(0, 0, 0, 0)
const updateTime = new Date(userData.password_last_change * 1000);
updateTime.setHours(0, 0, 0, 0);
const diff = (+today - +updateTime) / 86400000;
$("#passwordLastChanged").innerText = diff === 0 ? "today" : diff + " days ago";
}
)
}
/**
* Handles the action specified in the URL's get parameters.
*
* @param params the URL's get parameters
*/
function handleAction(params: URLSearchParams): void {
const sharedHomeLink = $("#sharedHomeLink");
let params_are_valid = true;
switch (params.get("action")) {
case "verify-email":
if (!params.has("email") || !params.has("token")) {
params_are_valid = false;
break;
}
postApi(
{
action: "verify-email",
token: csrfToken,
email: params.get("email"),
verify_token: params.get("token"),
},
sharedMessageElement,
() => {
sharedHomeLink.classList.remove("hidden");
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showSuccess(
sharedMessageElement,
`Your email address has been verified. ` +
`You will be redirected after ${secondsLeft} second(s).`
);
});
},
() => sharedHomeLink.classList.remove("hidden")
);
break;
case "reset-password":
if (!params.has("email") || !params.has("token")) {
params_are_valid = false;
break;
}
getApi(
{
action: "validate-password-reset-token",
token: csrfToken ?? "",
email: params.get("email") ?? "",
reset_token: params.get("token") ?? "",
},
sharedMessageElement,
() => {
$("#resetPasswordToken").value = params.get("token");
$("#resetPasswordEmail").value = params.get("email");
$("#resetPasswordRow").classList.remove("hidden");
$("#resetPasswordPassword").focus();
},
() => sharedHomeLink.classList.remove("hidden")
);
break;
case null:
break;
default:
params_are_valid = false;
break;
}
if (!params_are_valid) {
sharedHomeLink.classList.remove("hidden");
showError(
sharedMessageElement,
`Invalid URL.`
);
}
}
/**
* Redirects the user to `target` after `seconds` seconds, calling `doEachSecond` after every second.
*
* @param target the location to redirect the user to after the timeout
* @param seconds the number of seconds before redirecting the user
* @param doEachSecond the function to invoke each second; the only argument is the number of seconds left at that time
*/
function redirectWithTimeout(target: string, seconds: number, doEachSecond: (secondsLeft: number) => void): void {
let secondsLeft = seconds;
const update = () => {
doEachSecond(secondsLeft);
secondsLeft--;
setTimeout(update, 1000);
};
update();
setTimeout(() => window.location.href = target, seconds * 1000);
}
// Initialize template
doAfterLoad(() => {
$("#nav").appendChild(nav("/Tools/Death-Notifier/"));
$("#header").appendChild(header({
title: "Death Notifier",
description: "Get notified when a famous person dies"
}));
$("#footer").appendChild(footer({
vcsURL: "https://git.fwdekker.com/tools/death-notifier/",
version: "v%%VERSION_NUMBER%%"
}));
$("main").classList.remove("hidden");
});
// Register event handlers
doAfterLoad(() => {
// Switch between logged-out and logged-in views
loginHandler.addListener(() => {
clearMessage(sharedMessageElement);
refreshUserData();
refreshTrackings();
$("#loginRow").classList.add("hidden")
$("#trackingRow").classList.remove("hidden");
$("#accountRow").classList.remove("hidden");
$("#addTrackingPersonName").focus();
});
logoutHandler.addListener(() => {
clearMessage(sharedMessageElement);
$("#loginRow").classList.remove("hidden")
$("#trackingRow").classList.add("hidden");
$("#accountRow").classList.add("hidden");
$("#loginEmail").focus();
});
// Password visibility toggling
$a(".passwordToggle").forEach((toggle: HTMLElement) => {
const passwordField = $(`#${toggle.dataset.toggles}`);
const setState = (showPassword: boolean) => {
toggle.innerText = showPassword ? "Hide" : "Show";
passwordField.type = showPassword ? "text" : "password";
};
passwordField.form.addEventListener("reset", () => setState(false));
toggle.addEventListener("click", () => setState(passwordField.type === "password"));
});
// Message closing
$a(".formValidationInfo .closeButton").forEach((button: HTMLElement) => {
const parent = button.parentElement;
if (parent == null) return;
button.addEventListener("click", () => clearMessage(parent));
});
// Login
const loginForm = $("#loginForm");
loginForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "login",
token: csrfToken,
email: $("#loginEmail").value,
password: $("#loginPassword").value,
},
event.target as HTMLFormElement,
() => loginHandler.invokeListeners()
);
});
loginHandler.addListener(() => {
loginForm.reset();
clearMessages(loginForm);
});
const registerForm = $("#registerForm");
registerForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "register",
token: csrfToken,
email: $("#registerEmail").value,
password: $("#registerPassword").value
},
registerForm,
() => {
// TODO: Add client-side form validation
registerForm.reset();
showSuccess(
$("#registerFormValidationInfo"),
"Account created successfully! You may now log in."
);
$("#loginEmail").focus();
}
);
});
loginHandler.addListener(() => {
registerForm.reset();
clearMessages(registerForm);
});
const logoutForm = $("#logoutForm");
logoutForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "logout",
token: csrfToken,
},
logoutForm,
() => {
logoutForm.reset();
logoutHandler.invokeListeners();
}
);
});
// Forgot password
const sendPasswordResetForm = $("#sendPasswordResetForm");
sendPasswordResetForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "send-password-reset",
token: csrfToken,
email: $("#sendPasswordResetEmail").value,
},
sendPasswordResetForm,
() => {
sendPasswordResetForm.reset();
showSuccess(
$("#sendPasswordResetFormValidationInfo"),
"Password reset email sent successfully!"
);
}
);
});
loginHandler.addListener(() => {
sendPasswordResetForm.reset();
clearMessages(sendPasswordResetForm);
});
const resetPasswordForm = $("#resetPasswordForm");
resetPasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "reset-password",
token: csrfToken,
email: $("#resetPasswordEmail").value,
reset_token: $("#resetPasswordToken").value,
password: $("#resetPasswordPassword").value,
},
resetPasswordForm,
() => {
$("#resetPasswordForm").reset();
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showSuccess(
$("#resetPasswordFormValidationInfo"),
`Your password has been updated. You will be redirected after ${secondsLeft} second(s).`
);
}
)
}
);
});
$("#forgotPasswordGoTo").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
$("#loginRow").classList.add("hidden");
$("#sendForgotPasswordRow").classList.remove("hidden");
const resetEmail = $("#sendPasswordResetEmail");
resetEmail.value = $("#loginEmail").value;
resetEmail.focus();
});
$("#forgotPasswordGoBack").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
$("#sendPasswordResetForm").reset();
$("#sendForgotPasswordRow").classList.add("hidden");
$("#loginRow").classList.remove("hidden");
const loginEmail = $("#loginEmail");
loginEmail.value = $("#sendPasswordResetEmail").value;
loginEmail.focus();
});
// Account management
const deleteForm = $("#deleteForm");
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
const actual_email = $("#deleteEmail").value;
const entered_email = window.prompt(
`Are you sure you want to delete your account? ` +
`This action cannot be undone. ` +
`Enter ${actual_email} to confirm.`
);
if (entered_email === null) {
return;
} else if (entered_email !== actual_email) {
showError($("#deleteFormValidationInfo"), "Incorrect email address.");
return;
}
postApi(
{action: "user-delete", token: csrfToken},
deleteForm,
() => {
logoutHandler.invokeListeners();
showSuccess(sharedMessageElement, "Your account has been deleted.");
}
);
});
logoutHandler.addListener(() => clearMessages(deleteForm));
const resendEmailVerificationForm = $("#resendEmailVerificationForm");
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "resend-verify-email", token: csrfToken},
resendEmailVerificationForm,
() => {
refreshUserData();
showSuccess(
$("#resetEmailVerificationFormValidationInfo"),
"Email verification resent successfully!"
);
}
);
});
logoutHandler.addListener(() => clearMessages(resendEmailVerificationForm));
const toggleNotificationsForm = $("#toggleNotificationsForm");
toggleNotificationsForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "toggle-notifications", token: csrfToken},
toggleNotificationsForm,
() => refreshUserData()
);
});
logoutHandler.addListener(() => clearMessages(toggleNotificationsForm));
const updateEmailForm = $("#updateEmailForm");
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-email",
token: csrfToken,
email: $("#updateEmailEmail").value,
},
updateEmailForm,
() => {
updateEmailForm.reset();
refreshUserData();
showSuccess(
$("#updateEmailFormValidationInfo"),
"Email address updated successfully! " +
"Check your inbox for the verification email. " +
"You will not receive notifications until you verify your email address."
);
}
);
});
logoutHandler.addListener(() => {
updateEmailForm.reset();
clearMessages(updateEmailForm);
});
const updatePasswordForm = $("#updatePasswordForm");
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-password",
token: csrfToken,
password_old: $("#updatePasswordPasswordOld").value,
password_new: $("#updatePasswordPasswordNew").value,
},
event.target as HTMLFormElement,
() => {
updatePasswordForm.reset();
refreshUserData();
showSuccess($("#updatePasswordFormValidationInfo"), "Password updated successfully!");
}
);
});
logoutHandler.addListener(() => {
updatePasswordForm.reset();
clearMessages(updatePasswordForm);
});
// Tracking management
const addTrackingForm = $("#addTrackingForm");
addTrackingForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "add-tracking",
token: csrfToken,
person_name: $("#addTrackingPersonName").value,
},
addTrackingForm,
(response: ServerResponse) => {
addTrackingForm.reset();
refreshTrackings();
showSuccess(
$("#addTrackingFormValidationInfo"),
response.payload["renamed"]
? (
`Successfully added <b>${response.payload["input"]}</b> as ` +
`<b>${response.payload["name"]}</b>!`
)
: `Successfully added <b>${response.payload["name"]}</b>!`
);
}
);
});
logoutHandler.addListener(() => {
addTrackingForm.reset();
clearMessages(addTrackingForm);
});
});
// Run initialization code
doAfterLoad(() => {
const params = new URLSearchParams(window.location.search);
getApi(
{action: "start-session"},
sharedMessageElement,
(response: ServerResponse) => {
if (!params.has("action")) {
if (response.payload["logged_in"] === true)
loginHandler.invokeListeners();
else
logoutHandler.invokeListeners();
}
},
emptyFunction,
emptyFunction,
(response) => {
// Always execute the following
const message = (response?.payload ?? {})["global_message"];
if (message != null)
showInfo($("#globalMessage"), message);
handleAction(params);
}
);
});