Implement session handling

This commit is contained in:
Sky Johnson 2024-07-15 18:05:35 -05:00
parent 28789a26e7
commit 5d185cf0a1
9 changed files with 157 additions and 4 deletions

View File

@ -19,7 +19,7 @@ The Update has not been merged into `master` yet, but it will be. In the meantim
- We're no longer using MySQL as the database! This was done for ease of install and operation; SQLite is plenty performant for Dragon Knight and makes it trivial to spin up new instances. The database is contained in `server/database/` as the file `dragon.db`. WAL mode is enabled, so you may see a couple extra files but this is expected.
- `lib.php` renamed to `server/library.php`
- The installer has been totally rewritten using the new database wrapper and a handful of new library functions.
- Classes have been totally reworked! Prior, they were hard-coded into the game's overall settings. This made them highly inflexible and allowed only three classes which all needed their own level rows to define. This sucked! Now, classes are their own rows in the `classes` table, with starting stats and stat growth per-level. They also now have a special syntax in the `spells` field to detail at what level what spells the player gets.
- Classes have been totally reworked! Prior, they were hard-coded into the game's overall settings. This made them highly inflexible and allowed only three classes which all needed their own level rows to define. This sucked! Now, classes are their own rows in the `classes` table, with starting stats and stat growth per-level. A class spells table now exists to allow admins to easily define what spells are awarded at what level per class.
- The help pages have been moved to the new structure and have been renamed to "Guide".
- Items now use a new special syntax similar to class spells to have multiple attributes; no more limits!

76
server/app/auth.php Normal file
View File

@ -0,0 +1,76 @@
<?php
/*
Security, and especially authentication, is not a simple matter.
There's a lot to learn here.
*/
class Auth
{
// name of the remember me cookie
private const COOKIE_NAME = 'dragon-of-memory';
// id of the player
public static int $id = 0;
public static function login(string $identifier, string $password, bool $remember = false): bool
{
// delete the old session
if (isset($_SESSION['player_id'])) self::logout();
// get the player by their username
$id = Player::validateCredentials($identifier, $password);
if ($id === false) return false;
// set the session
$_SESSION['player_id'] = $id;
self::$id = $id;
// set the remember me cookie
if ($remember) self::remember($id);
return true;
}
private static function remember(int $id): array|false
{
$data = ['player_id' => $id, 'token' => token()];
Session::createOrUpdate($data);
setcookie(self::COOKIE_NAME, implode('::', $data), strtotime('+30 days'), '/', '', true, true);
return $data;
}
private static function logout(): void
{
if (isset($_SESSION['player_id'])) unset($_SESSION['player_id']);
if (isset($_SESSION['remember'])) unset($_SESSION['remember']);
if (isset($_COOKIE[self::COOKIE_NAME])) setcookie(self::COOKIE_NAME, '', time() - 86400, '/', '', true, true);
}
public static function good(): bool
{
// if our player_id session still exists, carry on
if (isset($_SESSION['player_id'])) {
self::$id = $_SESSION['player_id'];
return true;
}
// if a remember me cookie exists, try to validate it
if (isset($_COOKIE[self::COOKIE_NAME])) {
$cookie = explode('::', $_COOKIE[self::COOKIE_NAME]); // player_id::token
// try to validate the token
if (!Session::validate($cookie[0], $cookie[1])) return false; // the token is invalid
// token is valid, refresh cookie and assign session
self::remember($cookie[0]);
$_SESSION['player_id'] = $cookie[0];
self::$id = $cookie[0];
return true;
}
return false;
}
}

View File

@ -6,7 +6,23 @@ error_reporting(E_ALL | E_STRICT);
define('START', microtime(true)); // start the timer for this execution
session_start(); // initialize the session engine
// adjust session settings
ini_set('session.gc_maxlifetime', 604800); // 1 week in seconds
ini_set('session.cookie_lifetime', 604800); // 1 week in seconds
// ensure secure session handling
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // only if using HTTPS
// start the session
session_start();
// regenerate session ID to prevent session fixation
if (!isset($_SESSION['initiated'])) {
session_regenerate_id(true);
$_SESSION['initiated'] = true;
}
// @todo move these to a settings config somewhere
const VERSION = '1.1.11';

View File

@ -2,7 +2,7 @@
This folder serves as the home for the game's database; `dragon.db` by default. This is a WAL- and foreign key-enabled SQLite database wrapped in a very thin class based on the PDO wrapper in PHP. In production, the `dragon.db` file will be created if it doesn't exist, and the installer should be used to populate the database. This file does not exist in the repo.
### Packs
New to Dragon Knight is the ability to upload "data packs" to the game! Using this feature, it is possible to upload and store `.zip` files that contain `.csv` files (spreadsheets) of data for the game. These spreadsheets must have a 1:1 structure to what's expected in Dragon Knight. This allows an admin to populate the game data quickly and easily with data they either make or get from someone else.
New to Dragon Knight is the ability to upload "data packs" to the game! Using this feature, it is possible to upload `.zip` files that contain `.csv` files (spreadsheets) of data for the game. These spreadsheets must have a 1:1 structure to what's expected in Dragon Knight. This allows an admin to populate the game data quickly and easily with data they either make or get from someone else.
The `Default` data pack is the default data used when doing a **Complete** install of Dragon Knight. You can edit this before running the installer to change the default data. You can also use it as a template for your own data packs!

View File

@ -76,6 +76,12 @@ function expToLevel(int $level): int
return $experience;
}
// Generate a 32 byte cryptographically secure random hex string.
function token(int $length = 32): string
{
return bin2hex(random_bytes($length));
}
function prettydate($uglydate) { // Change the MySQL date format (YYYY-MM-DD) into something friendlier.
return date("F j, Y", mktime(0,0,0,substr($uglydate, 5, 2),substr($uglydate, 8, 2),substr($uglydate, 0, 4)));

View File

@ -36,4 +36,20 @@ class Player
App::$db->do("INSERT INTO 'players' ($keys) VALUES ($placeholders);", array_values($data));
return App::$db->lastInsertID();
}
public static function validateCredentials(string $identifier, string $password, bool $fetch = false): int|false
{
// get the player from their username or email
$player = App::$db->do("SELECT " . $fetch ? '*' : 'id, password' . " FROM players WHERE username = :i OR email = :i LIMIT 1;", ['i' => $identifier]);
if ($player == false) return false;
$player = $player->fetch();
// check password, return the player data if good
if (password_verify($password, $player['password'])) {
unset($player['password']);
return $fetch ? $player : $player['id'];
}
return false;
}
}

32
server/models/Session.php Normal file
View File

@ -0,0 +1,32 @@
<?php
class Session
{
public static function createOrUpdate(array $data): void
{
App::$db->do("INSERT OR REPLACE INTO sessions (player_id, token, expires) VALUES (?, ?, DATETIME(CURRENT_TIMESTAMP, '+30 days'));", $data);
}
public static function get(int $id): array|false
{
$session = App::$db->do("SELECT * FROM sessions WHERE player_id = ? LIMIT 1;", [$id]);
return $session->fetch() ?: false;
}
public static function delete(int $id): void
{
App::$db->do("DELETE FROM sessions WHERE player_id = ?;", [$id]);
}
public static function validate(int $id, string $token): bool
{
$session = App::$db->do("SELECT * FROM sessions WHERE player_id = ? AND token = ? LIMIT 1;", [$id, $token]);
if ($session === false) return false;
$session = $session->fetch();
// if the current time is after the expires column, the token is invalid
if (strtotime($session['expires']) < time()) return false;
return true;
}
}

View File

View File

@ -244,7 +244,14 @@ class InstallModule
'p_maxmp' INTEGER DEFAULT 0,
'm_hp' INTEGER DEFAULT 0,
'm_maxhp' INTEGER DEFAULT 0,
'condi' TEXT DEFAULT ''
'effects' TEXT DEFAULT ''
);");
// @Sessions
App::$db->q("CREATE TABLE IF NOT EXISTS 'sessions' (
'player_id' INTEGER NOT NULL UNIQUE,
'token' TEXT NOT NULL,
'expires' DATETIME NOT NULL
);");
echo render('install/layout', ['title' => 'Database Setup', 'step' => 'second', 'complete' => $complete, 'start' => $istart]);