Implement session handling
This commit is contained in:
parent
28789a26e7
commit
5d185cf0a1
|
@ -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
76
server/app/auth.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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
32
server/models/Session.php
Normal 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;
|
||||
}
|
||||
}
|
0
server/modules/GateModule.php
Normal file
0
server/modules/GateModule.php
Normal 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]);
|
||||
|
|
Loading…
Reference in New Issue
Block a user