Specify pass algorithm, remove some duplication

This commit is contained in:
Florine W. Dekker 2022-11-14 11:46:07 +01:00
parent 65e25c608f
commit 634cb477ef
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
7 changed files with 43 additions and 55 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "fwdekker/death-notifier", "name": "fwdekker/death-notifier",
"description": "Get notified when a famous person dies.", "description": "Get notified when a famous person dies.",
"version": "0.0.30", "_comment_version": "Also update version in `package.json`!", "version": "0.0.31", "_comment_version": "Also update version in `package.json`!",
"type": "project", "type": "project",
"license": "MIT", "license": "MIT",
"homepage": "https://git.fwdekker.com/tools/death-notifier", "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", "name": "death-notifier",
"version": "0.0.30", "_comment_version": "Also update version in `composer.json`!", "version": "0.0.31", "_comment_version": "Also update version in `composer.json`!",
"description": "Get notified when a famous person dies.", "description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker", "author": "Florine W. Dekker",
"browser": "dist/bundle.js", "browser": "dist/bundle.js",

View File

@ -1,5 +1,8 @@
;<?php exit(); ?> ;<?php exit(); ?>
# TODO: Add feature for global site message
# TODO: Add i18n
[admin] [admin]
# Password to use the CLI of `api.php`. Until this value is changed from its default, the feature is disabled # Password to use the CLI of `api.php`. Until this value is changed from its default, the feature is disabled
cli_secret = REPLACE THIS WITH A SECRET VALUE cli_secret = REPLACE THIS WITH A SECRET VALUE

View File

@ -173,6 +173,7 @@
<div class="row hidden" id="accountRow"> <div class="row hidden" id="accountRow">
<div class="column"> <div class="column">
<h2>Manage account</h2> <h2>Manage account</h2>
<!-- TODO: Position these buttons more nicely -->
<form id="logoutForm" novalidate> <form id="logoutForm" novalidate>
<p class="formValidationInfo"><span class="validationInfo"></span></p> <p class="formValidationInfo"><span class="validationInfo"></span></p>
<button id="logoutButton">Log out</button> <button id="logoutButton">Log out</button>
@ -228,6 +229,7 @@
</label> </label>
<button id="updatePasswordButton">Change password</button> <button id="updatePasswordButton">Change password</button>
</form> </form>
<!-- TODO: Add forgot password button after logging in -->
</div> </div>
</div> </div>
</section> </section>

View File

@ -78,6 +78,22 @@ class UserManager
} }
/**
* Returns `true` if there is a user with the given email address.
*
* @param string $email the email address to check
* @return bool `true` if there is a user with the given email address
*/
private function email_is_used(string $email): bool
{
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE email=:email);");
$stmt->bindValue(":email", $email);
$stmt->execute();
$result = $stmt->fetch();
return $result[0] === 1;
}
/** /**
* Registers a new user. * Registers a new user.
* *
@ -99,11 +115,7 @@ class UserManager
$this->conn->beginTransaction(); $this->conn->beginTransaction();
// Check if email address is already in use // Check if email address is already in use
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE email=:email);"); if ($this->email_is_used($email)) {
$stmt->bindValue(":email", $email);
$stmt->execute();
$result = $stmt->fetch();
if ($result[0] === 1) {
$this->conn->rollBack(); $this->conn->rollBack();
return new Response( return new Response(
payload: ["target" => "email", "message" => "Email address already in use."], payload: ["target" => "email", "message" => "Email address already in use."],
@ -116,8 +128,7 @@ class UserManager
VALUES (:email, :password) VALUES (:email, :password)
RETURNING email_verification_token;"); RETURNING email_verification_token;");
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
// TODO: Specify password hash function, for forwards compatibility $stmt->bindValue(":password", password_hash($password, PASSWORD_BCRYPT));
$stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT));
$stmt->execute(); $stmt->execute();
$email_verification_token = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"]; $email_verification_token = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"];
$this->mailer->queue_register_password($email, $email_verification_token); $this->mailer->queue_register_password($email, $email_verification_token);
@ -239,14 +250,8 @@ class UserManager
); );
} }
// TODO: Reject update if email address was changed too recently (within, say, 5 minutes)
// Check if email address is already in use // Check if email address is already in use
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE email=:email);"); if ($this->email_is_used($email)) {
$stmt->bindValue(":email", $email);
$stmt->execute();
$result = $stmt->fetch();
if ($result[0] === 1) {
$this->conn->rollBack(); $this->conn->rollBack();
return new Response( return new Response(
payload: ["target" => "email", "message" => "Email address already in use."], payload: ["target" => "email", "message" => "Email address already in use."],
@ -283,6 +288,7 @@ class UserManager
{ {
$this->conn->beginTransaction(); $this->conn->beginTransaction();
// Check if email is verified
$stmt = $this->conn->prepare("SELECT email, email_verification_token, email_verification_token_timestamp $stmt = $this->conn->prepare("SELECT email, email_verification_token, email_verification_token_timestamp
FROM users FROM users
WHERE uuid=:uuid;"); WHERE uuid=:uuid;");
@ -297,6 +303,7 @@ class UserManager
); );
} }
// Check if new verification can be sent
$minutes_since_last_verify_email = $minutes_since_last_verify_email =
(new DateTime("@{$user["email_verification_token_timestamp"]}"))->diff(new DateTime(), absolute: true)->i; (new DateTime("@{$user["email_verification_token_timestamp"]}"))->diff(new DateTime(), absolute: true)->i;
if ($minutes_since_last_verify_email < self::MINUTES_BETWEEN_VERIFICATION_EMAILS) { if ($minutes_since_last_verify_email < self::MINUTES_BETWEEN_VERIFICATION_EMAILS) {
@ -313,6 +320,7 @@ class UserManager
); );
} }
// Queue verification email
$stmt = $this->conn->prepare("UPDATE users $stmt = $this->conn->prepare("UPDATE users
SET email_verification_token_timestamp=unixepoch() SET email_verification_token_timestamp=unixepoch()
WHERE uuid=:uuid;"); WHERE uuid=:uuid;");
@ -336,7 +344,7 @@ class UserManager
{ {
$this->conn->beginTransaction(); $this->conn->beginTransaction();
// Check if token is correct for email
$stmt = $this->conn->prepare("SELECT email_verification_token_timestamp $stmt = $this->conn->prepare("SELECT email_verification_token_timestamp
FROM users FROM users
WHERE email=:email AND email_verification_token=:token;"); WHERE email=:email AND email_verification_token=:token;");
@ -357,6 +365,7 @@ class UserManager
); );
} }
// Check if token is still valid
$token_timestamp = $results[0]["email_verification_token_timestamp"]; $token_timestamp = $results[0]["email_verification_token_timestamp"];
$minutes_since_creation = (new DateTime("@$token_timestamp"))->diff(new DateTime(), absolute: true)->i; $minutes_since_creation = (new DateTime("@$token_timestamp"))->diff(new DateTime(), absolute: true)->i;
if ($minutes_since_creation > self::MINUTES_VALID_VERIFICATION) { if ($minutes_since_creation > self::MINUTES_VALID_VERIFICATION) {
@ -370,6 +379,7 @@ class UserManager
); );
} }
// Set as verified
$stmt = $this->conn->prepare("UPDATE users SET email_verification_token=null WHERE email=:email;"); $stmt = $this->conn->prepare("UPDATE users SET email_verification_token=null WHERE email=:email;");
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$stmt->execute(); $stmt->execute();
@ -419,7 +429,7 @@ class UserManager
password_reset_token=null password_reset_token=null
WHERE uuid=:uuid;"); WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT)); $stmt->bindValue(":password", password_hash($password_new, PASSWORD_BCRYPT));
$stmt->execute(); $stmt->execute();
// Respond // Respond
@ -491,9 +501,9 @@ class UserManager
* @return Response a satisfied `Response` with payload `null` if the password reset token is currently valid, or an * @return Response a satisfied `Response` with payload `null` if the password reset token is currently valid, or an
* unsatisfied `Response` otherwise * unsatisfied `Response` otherwise
*/ */
public function validate_password_reset_token(string $email, string $token): Response public function validate_password_reset_token(string $email, string $token, bool $use_transaction = true): Response
{ {
$this->conn->beginTransaction(); if ($use_transaction) $this->conn->beginTransaction();
$stmt = $this->conn->prepare("SELECT password_reset_token_timestamp $stmt = $this->conn->prepare("SELECT password_reset_token_timestamp
FROM users WHERE email=:email AND password_reset_token=:token;"); FROM users WHERE email=:email AND password_reset_token=:token;");
@ -502,7 +512,7 @@ class UserManager
$stmt->execute(); $stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC); $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (sizeof($results) === 0) { if (sizeof($results) === 0) {
$this->conn->rollBack(); if ($use_transaction) $this->conn->rollBack();
return new Response( return new Response(
payload: [ payload: [
"target" => null, "target" => null,
@ -515,7 +525,7 @@ class UserManager
$token_timestamp = $results[0]["password_reset_token_timestamp"]; $token_timestamp = $results[0]["password_reset_token_timestamp"];
$minutes_since_creation = (new DateTime("@$token_timestamp"))->diff(new DateTime(), absolute: true)->i; $minutes_since_creation = (new DateTime("@$token_timestamp"))->diff(new DateTime(), absolute: true)->i;
if ($minutes_since_creation > self::MINUTES_VALID_PASSWORD_RESET) { if ($minutes_since_creation > self::MINUTES_VALID_PASSWORD_RESET) {
$this->conn->rollBack(); if ($use_transaction) $this->conn->rollBack();
return new Response( return new Response(
payload: [ payload: [
"target" => null, "target" => null,
@ -525,7 +535,7 @@ class UserManager
); );
} }
$this->conn->commit(); if ($use_transaction) $this->conn->commit();
return new Response(payload: null, satisfied: true); return new Response(payload: null, satisfied: true);
} }
@ -544,35 +554,9 @@ class UserManager
{ {
$this->conn->beginTransaction(); $this->conn->beginTransaction();
$stmt = $this->conn->prepare("SELECT password_reset_token_timestamp $token_is_valid = $this->validate_password_reset_token($email, $token, false);
FROM users WHERE email=:email AND password_reset_token=:token;"); if (!$token_is_valid->satisfied)
$stmt->bindValue(":email", $email); return $token_is_valid;
$stmt->bindValue(":token", $token);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (sizeof($results) === 0) {
$this->conn->rollBack();
return new Response(
payload: [
"target" => null,
"message" => "This password reset link is invalid. Maybe you already reset your password?"
],
satisfied: false
);
}
$token_timestamp = $results[0]["password_reset_token_timestamp"];
$minutes_since_creation = (new DateTime("@$token_timestamp"))->diff(new DateTime(), absolute: true)->i;
if ($minutes_since_creation > self::MINUTES_VALID_PASSWORD_RESET) {
$this->conn->rollBack();
return new Response(
payload: [
"target" => null,
"message" => "This password reset link has expired."
],
satisfied: false
);
}
if ($password_new !== $password_confirm) { if ($password_new !== $password_confirm) {
$this->conn->rollBack(); $this->conn->rollBack();
@ -583,10 +567,9 @@ class UserManager
} }
$stmt = $this->conn->prepare("UPDATE users $stmt = $this->conn->prepare("UPDATE users
SET password=:password, SET password=:password, password_reset_token=null
password_reset_token=null
WHERE email=:email;"); WHERE email=:email;");
$stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT)); $stmt->bindValue(":password", password_hash($password_new, PASSWORD_BCRYPT));
$stmt->bindValue(":email", $email); $stmt->bindValue(":email", $email);
$stmt->execute(); $stmt->execute();