diff --git a/database/auth.db b/database/auth.db index c511977..c3fe620 100644 Binary files a/database/auth.db and b/database/auth.db differ diff --git a/public/index.php b/public/index.php index ec034dd..c446f88 100644 --- a/public/index.php +++ b/public/index.php @@ -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']); }); diff --git a/src/auth.php b/src/auth.php index a880a99..b39ee14 100644 --- a/src/auth.php +++ b/src/auth.php @@ -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; diff --git a/src/components.php b/src/components.php index 8ac2afa..5f7d052 100644 --- a/src/components.php +++ b/src/components.php @@ -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()]); } diff --git a/src/controllers/auth.php b/src/controllers/auth.php index 3031628..2633d32 100644 --- a/src/controllers/auth.php +++ b/src/controllers/auth.php @@ -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('/'); } diff --git a/src/controllers/char.php b/src/controllers/char.php index afdcd92..2dc951d 100644 --- a/src/controllers/char.php +++ b/src/controllers/char.php @@ -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('/'); } diff --git a/src/env.php b/src/env.php index e941417..49496f7 100644 --- a/src/env.php +++ b/src/env.php @@ -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) { diff --git a/src/helpers.php b/src/helpers.php index 671b2a5..b42b10f 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -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); } diff --git a/src/models/char.php b/src/models/char.php index f797541..76502b0 100644 --- a/src/models/char.php +++ b/src/models/char.php @@ -1,14 +1,14 @@ ":$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; +} diff --git a/src/router.php b/src/router.php index 5af9985..1e50e22 100644 --- a/src/router.php +++ b/src/router.php @@ -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; diff --git a/templates/layouts/basic.php b/templates/layouts/basic.php index 8f5c5bc..3244259 100644 --- a/templates/layouts/basic.php +++ b/templates/layouts/basic.php @@ -23,9 +23,7 @@ - +