First cut of log on page (#9)

- Add session support
- Refactor security handling to use db connection
- Fix db path issue
This commit is contained in:
2024-04-15 23:25:58 -04:00
parent 4d736b8f77
commit cab26db255
12 changed files with 185 additions and 101 deletions

View File

@@ -9,7 +9,7 @@ class Data {
* @return SQLite3 A new connection to the database
*/
public static function getConnection(): SQLite3 {
$db = new SQLite3('../data/' . DATABASE_NAME);
$db = new SQLite3(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'data', DATABASE_NAME]));
$db->exec('PRAGMA foreign_keys = ON;');
return $db;
}
@@ -65,47 +65,6 @@ class Data {
$db->close();
}
/**
* Find a user by their ID
*
* @param string $email The e-mail address of the user to retrieve
* @return array|null The user information, or null if the user is not found
*/
public static function findUserByEmail(string $email): ?array {
$db = self::getConnection();
try {
$query = $db->prepare('SELECT * FROM frc_user WHERE email = :email');
$query->bindValue(':email', $email);
$result = $query->execute();
if ($result) {
$user = $result->fetchArray(SQLITE3_ASSOC);
if ($user) return $user;
return null;
}
return null;
} finally {
$db->close();
}
}
/**
* Add a user
*
* @param string $email The e-mail address for the user
* @param string $password The user's password
*/
public static function addUser(string $email, string $password): void {
$db = self::getConnection();
try {
$query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
$query->bindValue(':email', $email);
$query->bindValue(':password', password_hash($password, PASSWORD_DEFAULT));
$query->execute();
} finally {
$db->close();
}
}
/**
* Parse/format a date/time from a string
*
@@ -132,7 +91,7 @@ class Data {
try {
$query = $db->prepare('SELECT * FROM feed WHERE id = :id AND user_id = :user');
$query->bindValue(':id', $feedId);
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
$result = $query->execute();
return $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
} finally {

View File

@@ -229,7 +229,7 @@ class Feed {
private static function refreshFeed(string $url, SQLite3 $db): array {
$feedQuery = $db->prepare('SELECT id FROM feed WHERE url = :url AND user_id = :user');
$feedQuery->bindValue(':url', $url);
$feedQuery->bindValue(':user', $_REQUEST[Key::USER_ID]);
$feedQuery->bindValue(':user', $_SESSION[Key::USER_ID]);
$feedResult = $feedQuery->execute();
$feedId = $feedResult ? $feedResult->fetchArray(SQLITE3_NUM)[0] : -1;
if ($feedId < 0) return ['error' => "No feed for URL $url found"];
@@ -273,7 +273,7 @@ class Feed {
INSERT INTO feed (user_id, url, title, updated_on, checked_on)
VALUES (:user, :url, :title, :updated, :checked)
SQL);
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
$query->bindValue(':url', $feed['url']);
$query->bindValue(':title', self::eltValue($feed['channel'], 'title'));
$query->bindValue(':updated', $feed['updated']);
@@ -300,7 +300,7 @@ class Feed {
$query = $db->prepare('UPDATE feed SET url = :url WHERE id = :id AND user_id = :user');
$query->bindValue(':url', $url);
$query->bindValue(':id', $existing['id']);
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
$query->execute();
return self::refreshFeed($url, $db);
@@ -312,7 +312,7 @@ class Feed {
*/
public static function refreshAll(SQLite3 $db): array {
$query = $db->prepare('SELECT url FROM feed WHERE user_id = :user');
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
$result = $query->execute();
$url = $result ? $result->fetchArray(SQLITE3_NUM) : false;
if ($url) {

View File

@@ -2,10 +2,10 @@
class Key {
/** @var string The $_REQUEST key for teh current user's e-mail address */
/** @var string The $_SESSION key for the current user's e-mail address */
public const string USER_EMAIL = 'FRC_USER_EMAIL';
/** @var string The $_REQUEST key for the current user's ID */
/** @var string The $_SESSION key for the current user's ID */
public const string USER_ID = 'FRC_USER_ID';
/** @var string The $_REQUEST key for the array of user messages to display */

View File

@@ -14,38 +14,108 @@ class Security {
/** @var int Require users to provide e-mail address and password */
public const int MULTI_USER = 2;
/** @var string The e-mail address for the single user */
public const string SINGLE_USER_EMAIL = 'solouser@example.com';
/** @var string The password for the single user with no password */
private const string SINGLE_USER_PASSWORD = 'no-password-required';
/**
* Verify that user is logged on
* @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on
* Find a user by their ID
*
* @param string $email The e-mail address of the user to retrieve
* @param SQLite3 $db The data connection to use to retrieve the user
* @return array|false The user information, or null if the user is not found
*/
public static function verifyUser(bool $redirectIfAnonymous = true): void {
switch (SECURITY_MODEL) {
case self::SINGLE_USER:
$user = self::retrieveSingleUser();
break;
case self::SINGLE_USER_WITH_PASSWORD:
die('Single User w/ Password has not yet been implemented');
case self::MULTI_USER:
die('Multi-User Mode has not yet been implemented');
default:
die('Unrecognized security model (' . SECURITY_MODEL . ')');
}
if (!$user && $redirectIfAnonymous) {
header('/logon?returnTo=' . $_SERVER['REQUEST_URI'], true, HTTP_REDIRECT_TEMP);
die();
}
$_REQUEST[Key::USER_ID] = $user['id'];
$_REQUEST[Key::USER_EMAIL] = $user['email'];
private static function findUserByEmail(string $email, SQLite3 $db): array|false {
$query = $db->prepare('SELECT * FROM frc_user WHERE email = :email');
$query->bindValue(':email', $email);
$result = $query->execute();
return $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
}
/**
* Retrieve the single user
* @return array The user information for the single user
* Add a user
*
* @param string $email The e-mail address for the user
* @param string $password The user's password
* @param SQLite3 $db The data connection to use to add the user
*/
private static function retrieveSingleUser(): array {
$user = Data::findUserByEmail('solouser@example.com');
if ($user) return $user;
Data::addUser('solouser@example.com', 'no-password-required');
return Data::findUserByEmail('solouser@example.com');
public static function addUser(string $email, string $password, SQLite3 $db): void {
$query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
$query->bindValue(':email', $email);
$query->bindValue(':password', password_hash($password, PASSWORD_DEFAULT));
$query->execute();
}
/**
* Verify a user's password
*
* @param array $user The user information retrieved from the database
* @param string $password The password provided by the user
* @param string|null $returnTo The URL to which the user should be redirected
* @param SQLite3 $db The database connection to use to verify the user's credentials
*/
private static function verifyPassword(array $user, string $password, ?string $returnTo, SQLite3 $db): void {
if (password_verify($password, $user['password'])) {
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
$rehash = $db->prepare('UPDATE frc_user SET password = :hash WHERE id = :id');
$rehash->bindValue(':hash', password_hash($password, PASSWORD_DEFAULT));
$rehash->bindValue(':id', $user['id']);
$rehash->execute();
}
$_SESSION[Key::USER_ID] = $user['id'];
$_SESSION[Key::USER_EMAIL] = $user['email'];
frc_redirect($returnTo ?? '/');
}
}
/**
* Log on a user with e-mail address and password
*
* @param string $email The e-mail address for the user
* @param string $password The password provided by the user
* @param string|null $returnTo The URL to which the user should be redirected
* @param SQLite3 $db The database connection to use to verify the user's credentials
*/
public static function logOnUser(string $email, string $password, ?string $returnTo, SQLite3 $db): void {
$user = self::findUserByEmail($email, $db);
if ($user) self::verifyPassword($user, $password, $returnTo, $db);
add_error('Invalid credentials; log on unsuccessful');
}
/**
* Log on the single user
*
* @param SQLite3 $db The data connection to use to retrieve the user
*/
private static function logOnSingleUser(SQLite3 $db): void {
$user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db);
if (!$user) {
self::addUser(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db);
$user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db);
}
self::verifyPassword($user, self::SINGLE_USER_PASSWORD, $_GET['returnTo'], $db);
}
/**
* Verify that user is logged on
*
* @param SQLite3 $db The data connection to use if required
* @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on
*/
public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void {
if (array_key_exists(Key::USER_ID, $_SESSION)) return;
if (SECURITY_MODEL == self::SINGLE_USER) self::logOnSingleUser($db);
if (SECURITY_MODEL != self::SINGLE_USER_WITH_PASSWORD && SECURITY_MODEL != self::MULTI_USER) {
die('Unrecognized security model (' . SECURITY_MODEL . ')');
}
if ($redirectIfAnonymous) {
header("Location: /user/log-on?returnTo={$_SERVER['REQUEST_URI']}", true, 307);
die();
}
}
}