diff --git a/server/SHServ/App.php b/server/SHServ/App.php index a4d89a5..e9f2ced 100644 --- a/server/SHServ/App.php +++ b/server/SHServ/App.php @@ -18,7 +18,6 @@ // CUSTOM public $utils; - public $sessions; public $factory; public $devtools; @@ -52,72 +51,9 @@ // CUSTOM $this -> utils = new Utils(); - $this -> sessions = new Sessions(); $this -> factory = new Factory(); } - /** - * Проверить авторизацию и rate limit для API запроса. - * - * @return array|null Null если запрос проходит, массив ['code' => int, 'body' => array] если нужно отклонить. - */ - public function check_api_auth(): ?array { - $uri = $_SERVER['REQUEST_URI'] ?? ''; - if (strpos($uri, '/api/v1/') !== 0) { - return null; - } - - // Rate limiting: 60 req/min per IP - $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; - $rate_limiter = new \SHServ\Tools\RateLimiter(60, 60); - if (!$rate_limiter -> check($ip)) { - return [ - 'code' => 429, - 'body' => [ - 'status' => false, - 'error_alias' => 'rate_limit_exceeded', - 'msg' => 'Too many requests' - ] - ]; - } - - $token = null; - - $auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; - if (strpos($auth_header, 'Bearer ') === 0) { - $token = substr($auth_header, 7); - } - - if (!$token && isset($_COOKIE['auth_token'])) { - $token = $_COOKIE['auth_token']; - } - - if (!$token || !app() -> sessions -> get_session_by_token($token)) { - return [ - 'code' => 401, - 'body' => [ - 'status' => false, - 'error_alias' => 'unauthorized', - 'msg' => 'Authentication required' - ] - ]; - } - - return null; - } - - public function api_auth_guard(): bool { - $response = $this -> check_api_auth(); - if ($response === null) { - return true; - } - - header('Content-Type: application/json'); - http_response_code($response['code']); - echo json_encode($response['body']); - return false; - } - public function root_folder(): String { return dirname(__DIR__, 2); } diff --git a/server/SHServ/EventsHandlers.php b/server/SHServ/EventsHandlers.php index e57332f..a9acd6f 100644 --- a/server/SHServ/EventsHandlers.php +++ b/server/SHServ/EventsHandlers.php @@ -7,9 +7,7 @@ events() -> handler('kernel:Bootstrap.ready_app', function(Array $params) { app() -> routes -> routes_init(); if(!app() -> console_flag) { - if(app() -> api_auth_guard()) { - app() -> router -> start_routing(); - } + app() -> router -> start_routing(); } }); diff --git a/server/SHServ/Migrations/Migration.php b/server/SHServ/Migrations/Migration.php deleted file mode 100644 index 6fad52a..0000000 --- a/server/SHServ/Migrations/Migration.php +++ /dev/null @@ -1,64 +0,0 @@ - thin_builder; - } - - /** - * Execute a raw SQL query. - * - * @param string $sql - * @return \PDOStatement - */ - protected function query(string $sql): \PDOStatement { - return $this -> tb() -> query($sql); - } - - /** - * Check if a table exists. - * - * @param string $tablename - * @return bool - */ - protected function table_exists(string $tablename): bool { - $result = $this -> tb() -> query( - "SHOW TABLES LIKE '{$tablename}'", - 'fetch', - \PDO::FETCH_NUM - ); - return (bool) $result; - } - - /** - * Drop a table if it exists. - * - * @param string $tablename - * @return void - */ - protected function drop_table_if_exists(string $tablename): void { - $this -> query("DROP TABLE IF EXISTS `{$tablename}`"); - } -} diff --git a/server/SHServ/Migrations/MigrationsManager.php b/server/SHServ/Migrations/MigrationsManager.php deleted file mode 100644 index c1cb00e..0000000 --- a/server/SHServ/Migrations/MigrationsManager.php +++ /dev/null @@ -1,308 +0,0 @@ - migrations_dir = dirname(__DIR__, 2) . '/migrations'; - $this -> tb = app() -> thin_builder; - $this -> ensure_migrations_table(); - } - - /** - * Create the migrations tracking table if it doesn't exist. - */ - protected function ensure_migrations_table(): void { - if ($this -> table_exists($this -> table_name)) { - return; - } - - $this -> tb -> create_table($this -> table_name, [ - 'id' => [ - 'type' => 'INT', - 'length' => 11, - 'auto_increment' => true, - 'can_be_null' => false, - ], - 'migration' => [ - 'type' => 'VARCHAR', - 'length' => 255, - 'can_be_null' => false, - ], - 'batch' => [ - 'type' => 'INT', - 'length' => 11, - 'default' => 0, - 'can_be_null' => false, - ], - 'applied_at' => [ - 'type' => 'TIMESTAMP', - 'default' => 'CURRENT_TIMESTAMP', - 'can_be_null' => false, - ], - ], 'id', 'InnoDB'); - } - - /** - * Run all pending migrations. - * - * @return array Applied migration filenames - */ - public function migrate(): array { - $pending = $this -> get_pending_migrations(); - if (empty($pending)) { - return []; - } - - $batch = $this -> get_next_batch_number(); - $applied = []; - - foreach ($pending as $file) { - $migration = $this -> load_migration($file); - $migration -> up(); - - $this -> tb -> insert($this -> table_name, [ - 'migration' => $file, - 'batch' => $batch, - 'applied_at' => date('Y-m-d H:i:s'), - ]); - - $applied[] = $file; - } - - return $applied; - } - - /** - * Rollback the last batch of migrations. - * - * @param int $steps Number of batches to rollback (default 1) - * @return array Rolled back migration filenames - */ - public function rollback(int $steps = 1): array { - $batches = $this -> tb -> select( - $this -> table_name, - ['batch'], - [], - ['batch'], - 'DESC', - [0, $steps] - ); - - if (empty($batches)) { - return []; - } - - $batch_numbers = array_map(fn($b) => (int) $b['batch'], $batches); - $batch_numbers = array_unique($batch_numbers); - - $migrations_to_rollback = $this -> tb -> select( - $this -> table_name, - [], - ['batch', 'IN', $batch_numbers], - ['id'], - 'DESC', - [] - ); - - $rolled_back = []; - foreach ($migrations_to_rollback as $row) { - $file = $row['migration']; - $migration = $this -> load_migration($file); - $migration -> down(); - - $this -> tb -> delete($this -> table_name, ['id', '=', $row['id']]); - $rolled_back[] = $file; - } - - return $rolled_back; - } - - /** - * Show migration status: applied and pending. - * - * @return array ['applied' => [...], 'pending' => [...]] - */ - public function status(): array { - $all_files = $this -> get_migration_files(); - $applied_rows = $this -> tb -> select($this -> table_name, [], [], ['id'], 'ASC', []); - $applied_map = array_column($applied_rows, 'batch', 'migration'); - - $applied = []; - $pending = []; - - foreach ($all_files as $file) { - if (isset($applied_map[$file])) { - $applied[] = ['file' => $file, 'batch' => $applied_map[$file]]; - } else { - $pending[] = $file; - } - } - - return compact('applied', 'pending'); - } - - /** - * Generate a new migration file template. - * - * @param string $name Snake_case migration name - * @return string Generated filename - */ - public function create(string $name): string { - $timestamp = date('Ymd_His'); - $filename = "{$timestamp}_{$name}.php"; - $filepath = $this -> migrations_dir . '/' . $filename; - - $class_name = $this -> migration_class_name($name); - - $template = << migrations_dir)) { - return []; - } - - $files = array_filter(scandir($this -> migrations_dir), function ($f) { - return substr($f, -4) === '.php'; - }); - - sort($files); - return array_values($files); - } - - /** - * Get migrations that have not been applied yet. - * - * @return array - */ - protected function get_pending_migrations(): array { - $all = $this -> get_migration_files(); - $applied = $this -> tb -> select($this -> table_name, ['migration'], [], ['id'], 'ASC', []); - $applied_names = array_column($applied, 'migration'); - - return array_values(array_diff($all, $applied_names)); - } - - /** - * Determine the next batch number. - * - * @return int - */ - protected function get_next_batch_number(): int { - $result = $this -> tb -> select( - $this -> table_name, - ['batch'], - [], - ['batch'], - 'DESC', - [0, 1] - ); - - if (empty($result)) { - return 1; - } - return (int) $result[0]['batch'] + 1; - } - - /** - * Load and instantiate a migration class from file. - * - * @param string $filename - * @return Migration - */ - protected function load_migration(string $filename): Migration { - $filepath = $this -> migrations_dir . '/' . $filename; - if (!file_exists($filepath)) { - throw new \Exception("Migration file not found: {$filepath}"); - } - - require_once $filepath; - - // Extract class name from filename: Ymd_His_name.php => MigrationName - $base = substr($filename, 0, -4); - $parts = explode('_', $base, 3); - if (count($parts) < 3) { - throw new \Exception("Invalid migration filename format: {$filename}"); - } - - $class_name = $this -> migration_class_name($parts[2]); - - if (!class_exists($class_name)) { - throw new \Exception("Migration class {$class_name} not found in {$filename}"); - } - - $instance = new $class_name(); - if (!($instance instanceof Migration)) { - throw new \Exception("Class {$class_name} must extend SHServ\\Migrations\\Migration"); - } - - return $instance; - } - - /** - * Convert snake_case migration name to CamelCase class name. - * - * @param string $name - * @return string - */ - protected function migration_class_name(string $name): string { - return str_replace(' ', '', ucwords(str_replace('_', ' ', $name))); - } - - /** - * Check if a table exists. - * - * @param string $tablename - * @return bool - */ - protected function table_exists(string $tablename): bool { - $result = $this -> tb -> query( - "SHOW TABLES LIKE '{$tablename}'", - 'fetch', - \PDO::FETCH_NUM - ); - return (bool) $result; - } -} diff --git a/server/SHServ/Sessions.php b/server/SHServ/Sessions.php deleted file mode 100644 index 268462c..0000000 --- a/server/SHServ/Sessions.php +++ /dev/null @@ -1,115 +0,0 @@ - thin_builder -> insert($this -> table_name, [ - "uid" => $uid, - "token" => $token, - "create_at" => date("Y-m-d H:i:s") - ]); - - return $result ? $token : false; - } - - public function close($token) { - $session = $this -> get_session_by_token($token); - if(!$session) { - return false; - } - - $session -> set("status", 2); - return $session -> update(); - } - - public function close_current_session() { - return $this -> close($this -> get_auth_token()); - } - - public function set_session(String $token) { - setcookie("auth_token", $token, [ - 'expires' => time() + 3600 * 24 * 30, - 'path' => '/', - 'httponly' => true, - 'secure' => true, - 'samesite' => 'Strict' - ]); - } - - public function init_session(Int $uid) { - $token = $this -> create($uid); - - if($token){ - $this -> set_session($token); - } - - return $token; - } - - public function is_auth() { - return $this -> get_current_session() ? true : false; - } - - public function get_auth_token() { - return isset($_COOKIE["auth_token"]) ? $_COOKIE["auth_token"] : null; - } - - public function auth_user() { - if(!$this -> get_auth_token()) { - return null; - } - - if(!$this -> auth_user_instance) { - $session = $this -> get_current_session(); - if(!$session){ - return null; - } - - $this -> auth_user_instance = new User($session -> get("uid")); - } - - return $this -> auth_user_instance; - } - - public function get_current_session() { - if(!$this -> current_session_instance){ - $token = $this -> get_auth_token(); - if(!$token) { - return null; - } - - $this -> current_session_instance = $this -> get_session_by_token($token); - if($this -> current_session_instance){ - $this -> current_session_instance -> set("last_using_at", date("Y-m-d H:i:s")) -> update(); - } - } - - return $this -> current_session_instance; - } - - public function get_session_by_token(String $token) { - $result = app() -> thin_builder -> select( - Session::$table_name, - Session::get_fields(), - [ - ["token", "=", $token], - "AND", - ["status", "=", 1] - ] - ); - - if(!$result) { - return null; - } - - return new Session(intval($result[0]["id"]), $result[0]); - } -} \ No newline at end of file diff --git a/server/console.php b/server/console.php index 2ed7a97..44c0d06 100644 --- a/server/console.php +++ b/server/console.php @@ -1,113 +1,19 @@ [args]\n"; - echo "\nCommands:\n"; - echo " get.config Show server config (without DB credentials)\n"; - echo " run-regular-script Run a regular control script by alias\n"; - echo " migrate Run all pending migrations\n"; - echo " migrate:rollback [steps] Rollback last batch of migrations\n"; - echo " migrate:status Show applied and pending migrations\n"; - echo " migrate:create Generate a new migration file\n"; - echo "\n"; - return; - } - - $command = $argv[1]; - - switch($command) { + switch($argv[1]) { case "get.config": - $config = FCONF; - unset($config['db']['password']); - unset($config['db']['user']); - echo json_encode($config); + echo json_encode(FCONF); break; - - case "run-regular-script": - if(!isset($argv[2])) { - echo "No alias provided\n"; - exit(1); - } - $alias = $argv[2]; - $result = \SHServ\Middleware\ControlScripts::run_regular_script($alias); - if(!$result) { - exit(1); - } - break; - - case "migrate": - $manager = new \SHServ\Migrations\MigrationsManager(); - $applied = $manager -> migrate(); - if(empty($applied)) { - echo "Nothing to migrate.\n"; - } else { - echo "Applied " . count($applied) . " migration(s):\n"; - foreach($applied as $file) { - echo " ✓ {$file}\n"; - } - } - break; - - case "migrate:rollback": - $steps = isset($argv[2]) ? (int) $argv[2] : 1; - $manager = new \SHServ\Migrations\MigrationsManager(); - $rolled = $manager -> rollback($steps); - if(empty($rolled)) { - echo "Nothing to rollback.\n"; - } else { - echo "Rolled back " . count($rolled) . " migration(s):\n"; - foreach($rolled as $file) { - echo " ↶ {$file}\n"; - } - } - break; - - case "migrate:status": - $manager = new \SHServ\Migrations\MigrationsManager(); - $status = $manager -> status(); - echo "Applied migrations:\n"; - if(empty($status['applied'])) { - echo " (none)\n"; - } else { - foreach($status['applied'] as $row) { - echo " [batch {$row['batch']}] {$row['file']}\n"; - } - } - echo "\nPending migrations:\n"; - if(empty($status['pending'])) { - echo " (none)\n"; - } else { - foreach($status['pending'] as $file) { - echo " … {$file}\n"; - } - } - break; - - case "migrate:create": - if(!isset($argv[2])) { - echo "Migration name required.\n"; - echo "Usage: php console.php migrate:create \n"; - exit(1); - } - $name = $argv[2]; - $manager = new \SHServ\Migrations\MigrationsManager(); - $filename = $manager -> create($name); - echo "Created migration: {$filename}\n"; - break; - - default: - echo "Unknown command: {$command}\n"; - exit(1); + default: echo "\nNo command"; } echo "\n"; } -console(); +console(); \ No newline at end of file diff --git a/server/migrations/20250602_000000_create_sessions_table.php b/server/migrations/20250602_000000_create_sessions_table.php deleted file mode 100644 index b537a45..0000000 --- a/server/migrations/20250602_000000_create_sessions_table.php +++ /dev/null @@ -1,56 +0,0 @@ -tb()->create_table('sessions', [ - 'id' => [ - 'type' => 'INT', - 'length' => 11, - 'auto_increment' => true, - 'can_be_null' => false, - ], - 'uid' => [ - 'type' => 'INT', - 'length' => 11, - 'default' => 0, - 'can_be_null' => false, - ], - 'status' => [ - 'type' => 'VARCHAR', - 'length' => 50, - 'default' => 'active', - 'can_be_null' => false, - ], - 'token' => [ - 'type' => 'VARCHAR', - 'length' => 255, - 'can_be_null' => false, - ], - 'last_using_at' => [ - 'type' => 'DATETIME', - 'default' => 'NULL', - 'can_be_null' => true, - ], - 'update_at' => [ - 'type' => 'DATETIME', - 'default' => 'NULL', - 'can_be_null' => true, - ], - 'create_at' => [ - 'type' => 'DATETIME', - 'default' => 'CURRENT_TIMESTAMP', - 'can_be_null' => false, - ], - ], 'id', 'InnoDB'); - } - - public function down(): void { - $this->drop_table_if_exists('sessions'); - } -} diff --git a/server/tests/AppAuthGuardTest.php b/server/tests/AppAuthGuardTest.php deleted file mode 100644 index 8bf3adb..0000000 --- a/server/tests/AppAuthGuardTest.php +++ /dev/null @@ -1,134 +0,0 @@ - originalApp = app(); - $this -> testApp = new TestableAppForGuard(); - \Fury\Kernel\AppContainer::set_app($this -> testApp); - $this -> create_sessions_table(); - } - - protected function tearDown(): void { - \Fury\Kernel\AppContainer::set_app($this -> originalApp); - $_SERVER = []; - $_COOKIE = []; - app() -> thin_builder -> query("DROP TABLE IF EXISTS sessions"); - } - - private function create_sessions_table(): void { - app() -> thin_builder -> query("CREATE TABLE sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - uid INTEGER, - status INTEGER DEFAULT 1, - token TEXT, - last_using_at TEXT, - update_at TEXT, - create_at TEXT - )"); - } - - public function test_non_api_uri_returns_null(): void { - $_SERVER['REQUEST_URI'] = '/index.php'; - $result = $this -> testApp -> check_api_auth(); - $this -> assertNull($result); - } - - public function test_valid_bearer_token_passes(): void { - $token = app() -> sessions -> create(1); // create session for user 1 - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $token; - - $result = $this -> testApp -> check_api_auth(); - $this -> assertNull($result); - } - - public function test_valid_cookie_token_passes(): void { - $token = app() -> sessions -> create(1); - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - $_COOKIE['auth_token'] = $token; - - $result = $this -> testApp -> check_api_auth(); - $this -> assertNull($result); - } - - public function test_missing_token_returns_401(): void { - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - - $result = $this -> testApp -> check_api_auth(); - $this -> assertNotNull($result); - $this -> assertSame(401, $result['code']); - $this -> assertSame('unauthorized', $result['body']['error_alias']); - } - - public function test_invalid_token_returns_401(): void { - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer invalid_token_123'; - - $result = $this -> testApp -> check_api_auth(); - $this -> assertNotNull($result); - $this -> assertSame(401, $result['code']); - } - - public function test_rate_limit_exceeded_returns_429(): void { - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - $_SERVER['REMOTE_ADDR'] = '1.2.3.4'; - - // Exhaust rate limit (60 requests in 1 minute window) - for ($i = 0; $i < 60; $i++) { - $this -> testApp -> check_api_auth(); - } - - $result = $this -> testApp -> check_api_auth(); - $this -> assertNotNull($result); - $this -> assertSame(429, $result['code']); - $this -> assertSame('rate_limit_exceeded', $result['body']['error_alias']); - } - - public function test_rate_limit_resets_after_window(): void { - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - $_SERVER['REMOTE_ADDR'] = '1.2.3.5'; - - // Exhaust limit - for ($i = 0; $i < 60; $i++) { - $this -> testApp -> check_api_auth(); - } - $result = $this -> testApp -> check_api_auth(); - $this -> assertSame(429, $result['code']); - - // Reset rate limiter storage to simulate window passing - $limiter = new \SHServ\Tools\RateLimiter(60, 60); - $limiter -> clear(); - - $token = app() -> sessions -> create(1); - $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $token; - - $result = $this -> testApp -> check_api_auth(); - $this -> assertNull($result); - } - - public function test_api_auth_guard_outputs_error_and_returns_false(): void { - $_SERVER['REQUEST_URI'] = '/api/v1/devices/list'; - $_SERVER['REMOTE_ADDR'] = '1.2.3.6'; - - ob_start(); - $passed = $this -> testApp -> api_auth_guard(); - $output = ob_get_clean(); - - $this -> assertFalse($passed); - $this -> assertSame(401, http_response_code()); - $data = json_decode($output, true); - $this -> assertSame('unauthorized', $data['error_alias']); - } -} - -class TestableAppForGuard extends \SHServ\App { - public function app_init(): void { - $this -> utils = new \SHServ\Utils(); - $this -> sessions = new \SHServ\Sessions(); - } -}