Massively clean up JS and PHP

This commit is contained in:
Florine W. Dekker 2022-08-19 19:07:39 +02:00
parent 29de649fda
commit e6def72c2e
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
10 changed files with 206 additions and 101 deletions

View File

@ -1,7 +1,7 @@
{
"name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.",
"version": "0.0.13",
"version": "0.0.14",
"type": "project",
"license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier",

BIN
composer.lock generated

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.0.13",
"version": "0.0.14",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

View File

@ -145,11 +145,10 @@ if (isset($_POST["action"])) {
case "login":
$response = validate_csrf()
?? validate_logged_out()
?? validate_has_arguments($_POST["email"], $_POST["password"]);
if ($response !== null) break;
?? validate_has_arguments($_POST["email"], $_POST["password"])
?? $user_manager->check_login($_POST["email"], $_POST["password"]);
[$response, $uuid] = $user_manager->check_login($_POST["email"], $_POST["password"]);
if ($uuid !== null) $_SESSION["uuid"] = $uuid;
if ($response->satisfied) $_SESSION["uuid"] = $response->payload["uuid"];
break;
case "logout":
$response = validate_csrf() ?? validate_logged_in();
@ -206,14 +205,31 @@ if (isset($_POST["action"])) {
}
} elseif (isset($_GET["action"])) {
// GET requests; do not alter state
$response = match ($_GET["action"]) {
"get-user-data" => validate_logged_in() ?? $user_manager->get_user_data($_SESSION["uuid"]),
"list-trackings" => validate_logged_in() ?? $tracking_manager->list_trackings($_SESSION["uuid"]),
default => new Response(
payload: ["target" => null, "message" => "Unknown GET action '" . $_GET["action"] . "'."],
satisfied: false
),
};
switch ($_GET["action"]) {
case "start-session":
if (!isset($_SESSION["uuid"])) {
$response = new Response(payload: null, satisfied: true);
break;
}
$response = $user_manager->get_user_data($_SESSION["uuid"]);
if (!$response->satisfied) {
session_destroy();
session_start();
}
break;
case "get-user-data":
$response = validate_logged_in() ?? $user_manager->get_user_data($_SESSION["uuid"]);
break;
case "list-trackings":
$response = validate_logged_in() ?? $tracking_manager->list_trackings($_SESSION["uuid"]);
break;
default:
$response = new Response(
payload: ["target" => null, "message" => "Unknown GET action '" . $_GET["action"] . "'."],
satisfied: false
);
}
} elseif ($argc > 1) {
// CLI
if (hash_equals($config["admin"]["update_secret"], "REPLACE THIS WITH A SECRET VALUE"))

View File

@ -4,11 +4,21 @@
}
table form, table button {
/* Trackings table */
#trackings form, #trackings button, #addTrackingPersonName {
margin-bottom: unset;
}
#addTrackingPersonName, #addTrackingButton {
display: inline;
}
#addTrackingPersonName {
width: unset;
}
/* Input validation elements */
.validationInfo {
display: block;
font-weight: normal;

View File

@ -34,9 +34,12 @@
<section class="container">
<div class="row">
<p id="validationInfoShared">
<span class="validationInfo"></span>
</p>
<div class="column">
<!-- Move form-specific messages closer to the form! -->
<p id="validationInfoShared">
<span class="validationInfo"></span>
</p>
</div>
</div>
<div class="row hidden" id="loginRow">
@ -54,7 +57,7 @@
<input id="loginPassword" type="password" name="password" />
<span class="validationInfo"></span>
</label>
<button>Log in</button>
<button id="loginButton">Log in</button>
</form>
</div>
<div class="column">
@ -81,7 +84,7 @@
<input id="registerPasswordConfirm" type="password" name="passwordConfirm" />
<span class="validationInfo"></span>
</label>
<button>Create account</button>
<button id="registerButton">Create account</button>
</form>
</div>
</div>
@ -99,18 +102,16 @@
</thead>
<tbody>
<tr>
<td>
<td colspan="3">
<form id="addTrackingForm" novalidate>
<label for="addTrackingPersonName">
<input id="addTrackingPersonName" type="text" name="personName"
autocomplete="on" />
<button id="addTrackingButton">Add</button>
<span class="validationInfo"></span>
</label>
<button>Add</button>
</form>
</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
@ -122,25 +123,27 @@
<h2>Manage account</h2>
<!-- TODO: Add way to delete account -->
<form id="logoutForm" novalidate>
<button>Log out</button>
<button id="logoutButton">Log out</button>
</form>
</div>
<div class="column">
<h3>Change email</h3>
Current email: TODO<br />
Verified? TODO
<p>
<b>Current email:</b> <span id="emailCurrent">TODO</span><br />
<b>Validated:</b> <span id="emailValidated">TODO</span>
</p>
<form id="updateEmailForm" novalidate>
<label for="updateEmailEmail">
Email
<input id="updateEmailEmail" type="email" name="email" autocomplete="on" />
<span class="validationInfo"></span>
</label>
<button>Change email</button>
<button id="updateEmailButton">Change email</button>
</form>
</div>
<div class="column">
<h3>Change password</h3>
Last changed: TODO
<p>Last changed: <span id="passwordLastChanged">TODO</span></p>
<form id="updatePasswordForm" novalidate>
<label for="updatePasswordPasswordOld">
Old password
@ -157,7 +160,7 @@
<input id="updatePasswordPasswordConfirm" type="password" name="passwordConfirm" />
<span class="validationInfo"></span>
</label>
<button>Change password</button>
<button id="updatePasswordButton">Change password</button>
</form>
</div>
</div>

View File

@ -82,6 +82,15 @@ function showError(element: HTMLElement, message?: string) {
}
/**
* Sends a GET request to the API.
*
* @param params the GET parameters to send
* @param form the form to display validation info in
* @param onSatisfied the callback to execute if the request returns successfully
* @param onUnsatisfied the callback to execute if the request returns unsuccessfully
* @param onError the callback to execute if there was an HTTP error
*/
function getApi(
params: Record<string, string>,
form: HTMLFormElement,
@ -92,6 +101,15 @@ function getApi(
interactWithApi("api.php?" + new URLSearchParams(params), undefined, form, onSatisfied, onUnsatisfied, onError);
}
/**
* Sends a POST request to the API.
*
* @param params the POST parameters to send
* @param form the form to display validation info in
* @param onSatisfied the callback to execute if the request returns successfully
* @param onUnsatisfied the callback to execute if the request returns unsuccessfully
* @param onError the callback to execute if there was an HTTP error
*/
function postApi(
params: object,
form: HTMLFormElement,
@ -115,6 +133,16 @@ function postApi(
);
}
/**
* Sends a request to the API.
*
* @param url the URL to send the request to
* @param options the options to send with the request
* @param form the form to display validation info in
* @param onSatisfied the callback to execute if the request returns successfully
* @param onUnsatisfied the callback to execute if the request returns unsuccessfully
* @param onError the callback to execute if there was an HTTP error
*/
function interactWithApi(
url: string,
options: object | undefined,
@ -152,10 +180,11 @@ function interactWithApi(
});
}
/**
* Refreshes the list of trackings in the table.
*/
function refreshTrackings() {
function refreshTrackings(): void {
getApi(
{action: "list-trackings"},
sharedMessageElement,
@ -182,9 +211,7 @@ function refreshTrackings() {
const deleteCell = document.createElement("td");
const deleteForm = document.createElement("form");
const deleteButton = document.createElement("button");
deleteButton.innerText = "remove";
deleteButton.addEventListener("submit", (event: SubmitEvent) => {
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
@ -193,22 +220,36 @@ function refreshTrackings() {
() => refreshTrackings()
)
});
const deleteButton = document.createElement("button");
deleteButton.innerText = "remove";
deleteForm.append(deleteButton);
deleteCell.append(deleteForm);
row.append(deleteCell);
tableBody.insertBefore(row, lastRow);
});
},
() => {
// TODO
},
() => {
// TODO
}
);
}
/**
* Refreshes displays of the user's data.
*
* @param userData the most up-to-date information on the user
*/
function refreshUserData(userData: any): void {
// Email
$("#emailCurrent").innerText = userData.email;
// Password update time
const today = new Date();
today.setHours(0, 0, 0, 0)
const updateTime = new Date(userData.password_update_time * 1000);
updateTime.setHours(0, 0, 0, 0);
const diff = (+today - +updateTime) / 86400000;
$("#passwordLastChanged").innerText = diff === 0 ? "today" : diff + " days ago";
}
// Initialize template
doAfterLoad(() => {
@ -226,10 +267,12 @@ doAfterLoad(() => {
// Event handlers and so on
doAfterLoad(() => {
// Find rows
const loginRow = $("#loginRow");
const trackingRow = $("#trackingRow");
const accountRow = $("#accountRow");
// Find forms
const loginForm = $("#loginForm");
const registerForm = $("#registerForm");
const logoutForm = $("#logoutForm");
@ -237,6 +280,35 @@ doAfterLoad(() => {
const updatePasswordForm = $("#updatePasswordForm");
const addTrackingForm = $("#addTrackingForm");
// Add common event code
function onLogin() {
loginRow.classList.add("hidden")
trackingRow.classList.remove("hidden");
accountRow.classList.remove("hidden");
refreshTrackings();
loginForm.reset();
clearMessages(loginForm);
registerForm.reset();
clearMessages(registerForm);
}
function onLogout() {
loginRow.classList.remove("hidden")
trackingRow.classList.add("hidden");
accountRow.classList.add("hidden");
addTrackingForm.reset();
clearMessages(addTrackingForm);
updateEmailForm.reset();
clearMessages(updateEmailForm);
updatePasswordForm.reset();
clearMessages(updatePasswordForm);
addTrackingForm.reset();
clearMessages(addTrackingForm);
}
// Add event handlers
loginForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -248,11 +320,10 @@ doAfterLoad(() => {
password: $("#loginPassword").value,
},
loginForm,
() => {
loginRow.classList.add("hidden");
trackingRow.classList.remove("hidden");
accountRow.classList.remove("hidden");
refreshTrackings();
(response) => {
$("#emailCurrent").innerText = response.payload.email;
$("#passwordLastChanged").innerText = response.payload.password_last_update;
onLogin();
}
);
});
@ -285,11 +356,7 @@ doAfterLoad(() => {
token: csrfToken,
},
logoutForm,
() => {
loginRow.classList.remove("hidden");
trackingRow.classList.add("hidden");
accountRow.classList.add("hidden");
}
() => onLogout()
);
});
@ -303,7 +370,11 @@ doAfterLoad(() => {
email: $("#updateEmailEmail").value,
},
updateEmailForm,
() => sharedMessageElement.innerText = "Email updated successfully!"
() => {
$("#emailCurrent").innerText = $("#updateEmailEmail").value;
updateEmailForm.reset();
showSuccess(sharedMessageElement, "Email updated successfully!");
}
);
});
@ -314,12 +385,16 @@ doAfterLoad(() => {
{
action: "update-password",
token: csrfToken,
password_old: $("#updatePasswordPasswordOld"),
password_new: $("#updatePasswordPasswordNew"),
password_confirm: $("#updatePasswordPasswordConfirm"),
password_old: $("#updatePasswordPasswordOld").value,
password_new: $("#updatePasswordPasswordNew").value,
password_confirm: $("#updatePasswordPasswordConfirm").value,
},
updatePasswordForm,
() => sharedMessageElement.innerText = "Password updated successfully!"
() => {
$("#passwordLastChanged").innerText = "today";
updatePasswordForm.reset();
showSuccess(sharedMessageElement, "Password updated successfully!");
}
);
});
@ -340,21 +415,17 @@ doAfterLoad(() => {
);
});
// TODO: Add appropriate message when session is expired
// TODO: Log out if session is expired
// Show content depending on whether user is logged in
getApi(
// TODO: Rename to `start-session` for semantic reasons?
{action: "get-user-data"},
{action: "start-session"},
sharedMessageElement,
() => {
trackingRow.classList.remove("hidden");
accountRow.classList.remove("hidden");
refreshTrackings();
},
() => {
clearMessage(sharedMessageElement);
loginRow.classList.remove("hidden")
(response: ServerResponse) => {
if (response.payload === null) {
onLogout();
} else {
onLogin();
refreshUserData(response.payload);
}
}
);
});

View File

@ -64,6 +64,11 @@ class TrackingManager
*/
public function add_tracking(string $user_uuid, string $person_name): Response
{
if (trim($person_name) === "")
return new Response(
payload: ["target" => "personName", "message" => "Invalid page name: empty."],
satisfied: false
);
if (strlen($person_name) > self::MAX_TITLE_LENGTH)
return new Response(
payload: ["target" => "personName", "message" => "Invalid page name: too long."],

View File

@ -57,7 +57,8 @@ class UserManager
public function install(): void
{
$conn = Database::connect($this->db_filename);
$conn->exec("CREATE TABLE users(uuid text primary key not null, email text not null, password text not null);");
$conn->exec("CREATE TABLE users(uuid text primary key not null, email text not null,
password text not null, password_update_time int not null);");
}
/**
@ -117,7 +118,8 @@ class UserManager
}
// Register user
$stmt = $conn->prepare("INSERT INTO users (uuid, email, password) VALUES (:uuid, :email, :password);");
$stmt = $conn->prepare("INSERT INTO users (uuid, email, password, password_update_time)
VALUES (:uuid, :email, :password, unixepoch());");
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":email", $email);
$stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT));
@ -151,44 +153,38 @@ class UserManager
*
* @param string $email the email address of the user whose password should be checked
* @param string $password the password to check against the specified user
* @return array{Response, ?string} the first element is a response with message `null` if the login was successful,
* or a response with a message explaining what went wrong otherwise; the second element is the UUID of the user
* that was logged in as, or `null` if the login should not be performed
* @return Response a response with user data if the login was successful, or a response with a message explaining
* what went wrong otherwise
*/
public function check_login(string $email, string $password): array
public function check_login(string $email, string $password): Response
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
return [
new Response(
payload: ["target" => "email", "message" => "Invalid email address."],
satisfied: false
),
null
];
return new Response(
payload: ["target" => "email", "message" => "Invalid email address."],
satisfied: false
);
if (strlen($password) > self::MAX_PASSWORD_LENGTH)
return [
new Response(
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
satisfied: false
),
null
];
return new Response(
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
satisfied: false
);
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("SELECT uuid, password FROM users WHERE email=:email;");
$stmt = $conn->prepare("SELECT uuid, email, password, password_update_time FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return (sizeof($results) === 0 || !password_verify($password, $results[0]["password"]))
? [
new Response(
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
satisfied: false
),
null
]
: [new Response(payload: null, satisfied: true), $results[0]["uuid"]];
if (sizeof($results) === 0 || !password_verify($password, $results[0]["password"])) {
return new Response(
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
satisfied: false
);
}
$user = $results[0];
unset($user["password"]);
return new Response(payload: $user, satisfied: true);
}
/**
@ -200,12 +196,15 @@ class UserManager
public function get_user_data(string $uuid): Response
{
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("SELECT * FROM users WHERE uuid=:uuid;");
$stmt = $conn->prepare("SELECT uuid, email, password_update_time FROM users WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false)
return new Response(payload: ["target" => "uuid", "message" => "Invalid user."], satisfied: false);
return new Response(
payload: ["target" => "uuid", "message" => "Something went wrong. Please try logging in again."],
satisfied: false
);
return new Response(payload: $user, satisfied: true);
}
@ -305,7 +304,8 @@ class UserManager
}
// Update password
$stmt = $conn->prepare("UPDATE users SET password=:password WHERE uuid=:uuid;");
$stmt = $conn->prepare("UPDATE users SET password=:password, password_update_time=unixepoch()
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT));
$stmt->execute();