Refactor, update gate methods

This commit is contained in:
Sky Johnson 2024-10-03 12:36:49 -05:00
parent a61ac11f60
commit e32803b7f9
11 changed files with 109 additions and 100 deletions

Binary file not shown.

View File

@ -12,7 +12,7 @@ $r = [];
Home
*/
router_get($r, '/', function () {
if (user()) auth_char_ensure();
if (user()) must_have_character();
echo render('layouts/basic', ['view' => 'pages/home']);
});

View File

@ -3,7 +3,7 @@
/**
* Checks if the given username already exists.
*/
function auth_usernameExists(string $username): bool
function auth_username_exists(string $username): bool
{
return db_exists(db_auth(), 'users', 'username', $username);
}
@ -11,27 +11,11 @@ function auth_usernameExists(string $username): bool
/**
* Checks if the given email already exists.
*/
function auth_emailExists(string $email): bool
function auth_email_exists(string $email): bool
{
return db_exists(db_auth(), 'users', 'email', $email);
}
/**
* Create a long-lived session for the user.
*/
function auth_rememberMe()
{
$token = token();
$expires = strtotime('+30 days');
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
':t' => $token,
':u' => $_SESSION['user']['id'],
':e' => $expires
]);
if (!$result) router_error(400);
set_cookie('remember_me', $token, $expires);
}
/**
* Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me
* cookie. If a remember me cookie exists, validate the session and set $_SESSION['user'].
@ -58,7 +42,7 @@ function auth_check(): bool
* Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and
* populate the $_SESSION['user'] array.
*/
function auth_ensure(): void
function auth_only(): void
{
if (!auth_check()) redirect('/auth/login');
}
@ -66,7 +50,7 @@ function auth_ensure(): void
/**
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
*/
function auth_guest(): void
function guest_only(): void
{
if (auth_check()) redirect('/');
}
@ -75,7 +59,7 @@ function auth_guest(): void
* Ensure the user has a character selected. If they have no character, redirect to the character creation page. Otherwise,
* select the first character attached to the user.
*/
function auth_char_ensure(): void
function must_have_character(): void
{
// If there is a character selected, and the data exists, return early.
if ($_SESSION['user']['char_id'] !== 0 && !empty($_SESSION['char'])) return;

View File

@ -9,11 +9,11 @@ function c_logout_button(): string
}
/**
* Render the character bar; pass either the character data as an array or the character ID as an int.
* Render the character bar. Relies on there being a character in the session. Without one, this will return an empty
* string.
*/
function c_char_bar(array|int $char): string
function c_char_bar(): string
{
if (is_int($char)) $char = char_find($char);
if ($char === false) throw new Exception('Character not found for char bar.');
return render('components/char_bar', ['char' => $char]);
if (!char()) return '';
return render('components/char_bar', ['char' => char()]);
}

View File

@ -5,7 +5,7 @@
*/
function auth_controller_register_get(): void
{
gate(false, false);
guest_only();
echo render('layouts/basic', ['view' => 'pages/auth/register']);
}
@ -14,7 +14,7 @@ function auth_controller_register_get(): void
*/
function auth_controller_register_post(): void
{
gate(false, false);
guest_only();
csrf_ensure();
$errors = [];
@ -63,14 +63,14 @@ function auth_controller_register_post(): void
/*
A username must be unique.
*/
if (auth_usernameExists($u)) {
if (auth_username_exists($u)) {
$errors['u'][] = 'Username is already taken.';
}
/*
An email must be unique.
*/
if (auth_emailExists($e)) {
if (auth_email_exists($e)) {
$errors['e'][] = 'Email is already taken.';
}
@ -92,7 +92,7 @@ function auth_controller_register_post(): void
*/
function auth_controller_login_get(): void
{
gate(false, false);
guest_only();
echo render('layouts/basic', ['view' => 'pages/auth/login']);
}
@ -101,7 +101,7 @@ function auth_controller_login_get(): void
*/
function auth_controller_login_post(): void
{
gate(false, false);
guest_only();
csrf_ensure();
$errors = [];
@ -140,8 +140,20 @@ function auth_controller_login_post(): void
}
$_SESSION['user'] = $user;
$_SESSION['char'] = char_find($user['char_id']);
if ($_POST['remember'] ?? false) auth_rememberMe();
change_user_character($user['char_id']);
if ($_POST['remember'] ?? false) {
$token = token();
$expires = strtotime('+30 days');
$result = db_query(db_auth(), "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", [
':t' => $token,
':u' => $_SESSION['user']['id'],
':e' => $expires
]);
if (!$result) router_error(400);
set_cookie('remember_me', $token, $expires);
}
redirect('/');
}
@ -163,19 +175,22 @@ function auth_controller_logout_post(): void
*/
function auth_controller_change_character_post(): void
{
gate(true);
auth_only();
must_have_character();
csrf_ensure();
$char_id = (int) ($_POST['char_id'] ?? 0);
if (char_exists($char_id) === false) router_error(400);
$_SESSION['user']['char_id'] = $char_id;
if (db_query(db_auth(), 'UPDATE users SET char_id = :c WHERE id = :u', [
':c' => $char_id,
':u' => $_SESSION['user']['id']
]) === false) router_error(400);
// If the character ID is the current character, do nothing.
if ($char_id === $_SESSION['user']['char_id']) redirect('/');
$_SESSION['char'] = char_find($char_id);
// Make sure the character ID is valid.
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acccp)');
// Make sure the user owns the character.
if (char_belongs_to_user($char_id, $_SESSION['user']['id']) === false) router_error(999);
change_user_character($char_id);
redirect('/');
}

View File

@ -5,7 +5,7 @@
*/
function char_controller_create_first_get(): void
{
gate(false);
auth_only();
// If the user already has a character, redirect them to the main page.
if (char_count(user('id')) > 0) redirect('/');
@ -14,11 +14,11 @@ function char_controller_create_first_get(): void
}
/**
* Create a player for the currently logged in user.
* Create a character for the currently logged in user.
*/
function char_controller_create_post(): void
{
gate(false);
auth_only();
csrf_ensure();
$errors = [];
@ -38,9 +38,9 @@ function char_controller_create_post(): void
}
/*
A player's name must be unique.
A character's name must be unique.
*/
if (char_nameExists($name)) $errors['name'][] = 'Name is already taken.';
if (char_name_exists($name)) $errors['name'][] = 'Name is already taken.';
// If there are errors at this point, send them to the page with errors flashed.
if (!empty($errors)) {
@ -48,14 +48,17 @@ function char_controller_create_post(): void
redirect('/');
}
// Create the player
$player = char_create(user('id'), $name);
if ($player === false) router_error(400);
// Create the character
$char = char_create(user('id'), $name);
if ($char === false) router_error(400);
// Create the auxiliary tables
char_location_create($player);
char_wallet_create($player);
char_gear_create($player);
char_location_create($char);
char_wallet_create($char);
char_gear_create($char);
// Set the character as the user's selected character
change_user_character($char);
redirect('/');
}

View File

@ -5,7 +5,7 @@
*/
function env_load(string $filePath): void
{
if (!file_exists($filePath)) throw new Exception("The .env file does not exist.");
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {

View File

@ -139,11 +139,11 @@ function char(string $field = ''): mixed
}
/**
* Perform an authentication and optionally a character check. Failing user auth will redirect to the login page. Failing
* the character check will redirect to the character creation page.
* Shorthand to update the user's selected character.
*/
function gate(bool $char = false, bool $user = true): void
function change_user_character(int $char_id): void
{
if ($user && !auth_check()) redirect('/auth/login');
if ($char) auth_char_ensure();
$_SESSION['user']['char_id'] = $char_id;
db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]);
$_SESSION['char'] = char_find($char_id);
}

View File

@ -1,14 +1,14 @@
<?php
/*
Players are the living, breathing entities that interact with the game world. They are inextricably linked to their
accounts, and are the primary means by which the player interacts with the game world. Separating the player from
the account allows for multiple players to be associated with a single account, and to prevent concurrency issues
characters are the living, breathing entities that interact with the game world. They are inextricably linked to their
accounts, and are the primary means by which the character interacts with the game world. Separating the character from
the account allows for multiple characters to be associated with a single account, and to prevent concurrency issues
when performing auth checks on the database.
When creating a player, we want to init all of the related data tables; wallets, inventory, bank, etc.
When creating a character, we want to init all of the related data tables; wallets, inventory, bank, etc.
When retrieving a player, we will get the tables as-needed, to prevent allocating more memory than we need.
When retrieving a character, we will get the tables as-needed, to prevent allocating more memory than we need.
*/
const currently = [
@ -20,9 +20,9 @@ const currently = [
];
/**
* Create a player. Only a user ID and a name are required. All other fields are optional. Pass a key-value array
* of overrides to set additional fields. A player's name must be unique, but this function does not check for
* that. Returns the created player's ID.
* Create a character. Only a user ID and a name are required. All other fields are optional. Pass a key-value array
* of overrides to set additional fields. A character's name must be unique, but this function does not check for
* that. Returns the created character's ID.
*/
function char_create(int $user_id, string $name, array $overrides = []): int
{
@ -35,19 +35,19 @@ function char_create(int $user_id, string $name, array $overrides = []): int
$f = implode(', ', $k);
$v = implode(', ', array_map(fn($x) => ":$x", $k));
// Create the player!
// Create the character!
if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
// @TODO: Log this error
throw new Exception('Failed to create player.');
throw new Exception('Failed to create character. (cc)');
}
// Get the player ID
// Get the character ID
return db_live()->lastInsertRowID();
}
/**
* Create a player's location record. A player's location is where they are in the game world. A player can only be
* in one location at a time. Can define a starting location for the player. Default state is 'Exploring'.
* Create a character's location record. A character's location is where they are in the game world. A character can only be
* in one location at a time. Can define a starting location for the character. Default state is 'Exploring'.
*/
function char_location_create(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void
{
@ -57,12 +57,12 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current
':y' => $y,
':c' => $currently
]) === false) {
throw new Exception('Failed to create player location.');
throw new Exception('Failed to create character location. (clc)');
}
}
/**
* Creates a player's wallet. A player's wallet is where they store their currencies. Can optionally specify the
* Creates a character's wallet. A character's wallet is where they store their currencies. Can optionally specify the
* starting balances of the wallet. Returns the created wallet's ID. If a currency is set to -1, the starting_silver
* or starting_star_gems fields from the env will be used.
*/
@ -73,23 +73,23 @@ function char_wallet_create(int $char_id, int $silver = -1, int $starGems = -1):
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
]) === false) {
throw new Exception('Failed to create player wallet.');
throw new Exception('Failed to create character wallet. (cwc)');
}
}
/**
* Create the player's gear table. A player's gear is where they store their equipped items.
* Create the character's gear table. A character's gear is where they store their equipped items.
* @TODO: implement initial gear
*/
function char_gear_create(int $char_id, array $initialGear = []): void
{
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
throw new Exception('Failed to create player gear.');
throw new Exception('Failed to create character gear. (cgc)');
}
}
/**
* Create the player's bank account. The bank stores items and currency, with an interest rate based on
* Create the character's bank account. The bank stores items and currency, with an interest rate based on
* the bank account's tier. The bank account has a limited number of slots, which can be increased by upgrading
* the bank account. The bank account starts with 0 silver and 5 slots.
*/
@ -101,7 +101,7 @@ function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $ti
':si' => $silver,
':t' => $tier
]) === false) {
throw new Exception('Failed to create player bank.');
throw new Exception('Failed to create character bank. (cbc)');
}
}
@ -111,65 +111,63 @@ function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $ti
function char_find(int $char_id): array
{
$char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($char === false) throw new Exception('Character not found.');
if ($char === false) throw new Exception('Character not found. (cf)');
return $char;
}
/**
* Count the number of players associated with an account ID.
* Count the number of characters associated with an account ID.
*/
function char_count(int $user_id): int
{
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
if ($count === false) throw new Exception('Failed to count players.');
if ($count === false) throw new Exception('Failed to count characters. (cc)');
return (int) $count[0];
}
/**
* Get a an array of id => [name, level] for all players associated with an account ID.
* Get a an array of id => [name, level] for all characters associated with an account ID.
*/
function char_list(int $user_id): array
{
$stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]);
if ($stmt === false) throw new Exception('Failed to list players.');
if ($stmt === false) throw new Exception('Failed to list characters. (cl)');
$players = [];
$characters = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$players[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
$characters[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
}
return $players;
return $characters;
}
/**
* Get a player's location info by their player ID. Returns the location's data as an associative array.
* Get a character's location info by their character ID. Returns the location's data as an associative array.
*/
function char_get_location(int $char_id): array
{
// Get the location
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($location === false) throw new Exception('Location not found.');
if ($location === false) throw new Exception('Location not found. (cgl)');
return $location;
}
/**
* Get a player's wallet by their player ID. Returns the wallet's data as an associative array.
* Get a character's wallet by their character ID. Returns the wallet's data as an associative array.
*/
function char_get_wallet(int $char_id): array
{
$wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($wallet === false) throw new Exception('Wallet not found.');
if ($wallet === false) throw new Exception('Wallet not found. (cgw)');
return $wallet;
}
/**
* See if a player name exists.
* See if a character name exists.
*/
function char_nameExists(string $name): bool
function char_name_exists(string $name): bool
{
$exists = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE name = :n", [':n' => $name])->fetchArray(SQLITE3_NUM);
if ($exists === false) throw new Exception('Failed to check for player name.');
return (int) $exists[0] > 0;
return db_exists(db_live(), 'characters', 'name', $name);
}
/**
@ -179,3 +177,13 @@ function char_exists(int $char_id): bool
{
return db_exists(db_live(), 'characters', 'id', $char_id);
}
/**
* See if the given character belongs to the given user.
*/
function char_belongs_to_user(int $char_id, int $user_id): bool
{
$char = db_query(db_live(), "SELECT user_id FROM characters WHERE id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
if ($char === false) throw new Exception('Character not found. (cbtu)');
return $char['user_id'] === $user_id;
}

View File

@ -124,6 +124,7 @@ function router_error(int $code): void
404 => 'Not Found',
405 => 'Method Not Allowed',
418 => 'I\'m a teapot',
999 => 'Cheating attempt detected',
default => 'Unknown Error',
};
exit;

View File

@ -23,9 +23,7 @@
</div>
</header>
<?php if (char()) {
echo c_char_bar(user('char_id'));
} ?>
<?= c_char_bar(user('char_id')) ?>
<main>
<?= render($view, $data) ?>