From dc427490a587243cd90fbc057d746b40b56e2bcd Mon Sep 17 00:00:00 2001 From: madgerm Date: Tue, 3 Feb 2026 09:18:15 +0100 Subject: [PATCH] uploads --- config/avatars.json | 10 + config/races.json | 2 + server/composer.lock | 3022 +++++++++++++++++ server/db/migrations/001_init.sql | 4 + server/db/seed.php | 15 +- server/src/Bootstrap.php | 2 + server/src/Config/ConfigLoader.php | 10 + server/src/Config/ConfigValidator.php | 27 + .../Module/Auth/Controller/AuthController.php | 475 +++ .../Module/Auth/Controller/MetaController.php | 101 + server/src/Module/Auth/Routes.php | 30 + .../src/Module/Auth/Service/AuthService.php | 32 + server/tests/Integration/AuthFlowTest.php | 182 + server/tests/Support/TestDatabase.php | 13 +- .../public/assets/avatars/avatar-01.svg | 14 + .../public/assets/avatars/avatar-02.svg | 15 + .../public/assets/avatars/avatar-03.svg | 14 + .../public/assets/avatars/avatar-04.svg | 14 + .../public/assets/avatars/avatar-05.svg | 15 + .../public/assets/avatars/avatar-06.svg | 14 + web/desktop/public/assets/style.css | 47 + web/desktop/public/assets/ui.js | 339 +- web/desktop/public/index.php | 18 +- web/desktop/src/partials/auth-login.php | 23 + .../src/partials/auth-register-step1.php | 16 + .../src/partials/auth-register-step2.php | 22 + .../src/partials/auth-register-step3.php | 30 + web/desktop/src/partials/ressourcen.php | 41 +- .../public/assets/avatars/avatar-01.svg | 14 + .../public/assets/avatars/avatar-02.svg | 15 + .../public/assets/avatars/avatar-03.svg | 14 + .../public/assets/avatars/avatar-04.svg | 14 + .../public/assets/avatars/avatar-05.svg | 15 + .../public/assets/avatars/avatar-06.svg | 14 + web/mobile/public/assets/style.css | 47 + web/mobile/public/assets/ui.js | 339 +- web/mobile/public/index.php | 18 +- web/mobile/src/partials/auth-login.php | 23 + .../src/partials/auth-register-step1.php | 16 + .../src/partials/auth-register-step2.php | 22 + .../src/partials/auth-register-step3.php | 30 + web/mobile/src/partials/ressourcen.php | 41 +- 42 files changed, 5104 insertions(+), 65 deletions(-) create mode 100644 config/avatars.json create mode 100644 server/composer.lock create mode 100644 server/src/Module/Auth/Controller/AuthController.php create mode 100644 server/src/Module/Auth/Controller/MetaController.php create mode 100644 server/src/Module/Auth/Routes.php create mode 100644 server/tests/Integration/AuthFlowTest.php create mode 100644 web/desktop/public/assets/avatars/avatar-01.svg create mode 100644 web/desktop/public/assets/avatars/avatar-02.svg create mode 100644 web/desktop/public/assets/avatars/avatar-03.svg create mode 100644 web/desktop/public/assets/avatars/avatar-04.svg create mode 100644 web/desktop/public/assets/avatars/avatar-05.svg create mode 100644 web/desktop/public/assets/avatars/avatar-06.svg create mode 100644 web/desktop/src/partials/auth-login.php create mode 100644 web/desktop/src/partials/auth-register-step1.php create mode 100644 web/desktop/src/partials/auth-register-step2.php create mode 100644 web/desktop/src/partials/auth-register-step3.php create mode 100644 web/mobile/public/assets/avatars/avatar-01.svg create mode 100644 web/mobile/public/assets/avatars/avatar-02.svg create mode 100644 web/mobile/public/assets/avatars/avatar-03.svg create mode 100644 web/mobile/public/assets/avatars/avatar-04.svg create mode 100644 web/mobile/public/assets/avatars/avatar-05.svg create mode 100644 web/mobile/public/assets/avatars/avatar-06.svg create mode 100644 web/mobile/src/partials/auth-login.php create mode 100644 web/mobile/src/partials/auth-register-step1.php create mode 100644 web/mobile/src/partials/auth-register-step2.php create mode 100644 web/mobile/src/partials/auth-register-step3.php diff --git a/config/avatars.json b/config/avatars.json new file mode 100644 index 0000000..f8b49d9 --- /dev/null +++ b/config/avatars.json @@ -0,0 +1,10 @@ +{ + "avatars": [ + {"key": "avatar-01", "label": "Nova", "image": "/assets/avatars/avatar-01.svg"}, + {"key": "avatar-02", "label": "Vektor", "image": "/assets/avatars/avatar-02.svg"}, + {"key": "avatar-03", "label": "Ion", "image": "/assets/avatars/avatar-03.svg"}, + {"key": "avatar-04", "label": "Astra", "image": "/assets/avatars/avatar-04.svg"}, + {"key": "avatar-05", "label": "Pulse", "image": "/assets/avatars/avatar-05.svg"}, + {"key": "avatar-06", "label": "Zenit", "image": "/assets/avatars/avatar-06.svg"} + ] +} diff --git a/config/races.json b/config/races.json index e082b6a..25bfdb0 100644 --- a/config/races.json +++ b/config/races.json @@ -2,6 +2,7 @@ "races": { "human": { "name": "Mensch", + "description": "Ausgewogene Spezies mit leichtem Kreditbonus.", "modifiers": { "produce": { "credits": 0.02 @@ -11,6 +12,7 @@ }, "robot": { "name": "Roboter", + "description": "Effiziente Maschinen mit Fokus auf Metallproduktion.", "modifiers": { "produce": { "metal": 0.05 diff --git a/server/composer.lock b/server/composer.lock new file mode 100644 index 0000000..eefb664 --- /dev/null +++ b/server/composer.lock @@ -0,0 +1,3022 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6d1f3562aec2f935ae70d1e64c15e043", + "packages": [ + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.8", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-01-08T16:22:46+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.3.7", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-08-30T10:22:22+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-08-16T11:10:48+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "slim/psr7", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Psr7.git", + "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/76e7e3b1cdfd583e9035c4c966c08e01e45ce959", + "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "php": "^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.0 || ^2.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.5|| ^2.0", + "ext-json": "*", + "http-interop/http-factory-tests": "^1.0 || ^2.0", + "php-http/psr7-integration-tests": "^1.5", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6 || ^10", + "squizlabs/php_codesniffer": "^3.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Psr7\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + } + ], + "description": "Strict PSR-7 implementation", + "homepage": "https://www.slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Psr7/issues", + "source": "https://github.com/slimphp/Slim-Psr7/tree/1.8.0" + }, + "time": "2025-11-02T17:51:19+00:00" + }, + { + "name": "slim/slim", + "version": "4.15.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "887893516557506f254d950425ce7f5387a26970" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/887893516557506f254d950425ce7f5387a26970", + "reference": "887893516557506f254d950425ce7f5387a26970", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4 || ^2", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.6", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17 || ^3", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.1", + "phpstan/phpstan": "^1 || ^2", + "phpunit/phpunit": "^9.6 || ^10 || ^11 || ^12", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.10", + "vimeo/psalm": "^5 || ^6" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2025-11-21T12:23:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/server/db/migrations/001_init.sql b/server/db/migrations/001_init.sql index 84c4fab..674c1f9 100644 --- a/server/db/migrations/001_init.sql +++ b/server/db/migrations/001_init.sql @@ -1,7 +1,11 @@ CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, race_key TEXT NOT NULL DEFAULT 'human', + title TEXT NOT NULL DEFAULT '', + avatar_key TEXT NOT NULL DEFAULT 'default', created_at TIMESTAMP NOT NULL DEFAULT NOW() ); diff --git a/server/db/seed.php b/server/db/seed.php index 0cb2d19..bf7cede 100644 --- a/server/db/seed.php +++ b/server/db/seed.php @@ -47,8 +47,19 @@ try { WHERE r.key = 'admin' ON CONFLICT DO NOTHING"); - $stmt = $pdo->prepare("INSERT INTO users (username, race_key) VALUES (:username, :race_key) ON CONFLICT (username) DO NOTHING"); - $stmt->execute(['username' => 'dev', 'race_key' => 'human']); + $stmt = $pdo->prepare( + "INSERT INTO users (username, email, password_hash, race_key, title, avatar_key) + VALUES (:username, :email, :password_hash, :race_key, :title, :avatar_key) + ON CONFLICT (username) DO NOTHING" + ); + $stmt->execute([ + 'username' => 'dev', + 'email' => 'dev@example.test', + 'password_hash' => password_hash('change-me', PASSWORD_DEFAULT), + 'race_key' => 'human', + 'title' => 'Pionier', + 'avatar_key' => 'avatar-01', + ]); $userId = (int)$pdo->query("SELECT id FROM users WHERE username = 'dev'")->fetchColumn(); diff --git a/server/src/Bootstrap.php b/server/src/Bootstrap.php index 6c43c60..92557f5 100644 --- a/server/src/Bootstrap.php +++ b/server/src/Bootstrap.php @@ -9,6 +9,7 @@ use App\Config\ConfigValidator; use App\Database\ConnectionFactory; use App\Module\Auth\Middleware\AuthContextMiddleware; use App\Module\Auth\Service\AuthService; +use App\Module\Auth\Routes as AuthRoutes; use App\Module\BuildQueue\Routes as BuildQueueRoutes; use App\Module\Economy\Routes as EconomyRoutes; use App\Module\PlanetGenerator\Routes as PlanetGeneratorRoutes; @@ -60,6 +61,7 @@ final class Bootstrap $app->addBodyParsingMiddleware(); $app->group('', function (RouteCollectorProxyInterface $group) use ($container) { + AuthRoutes::register($group, $container); EconomyRoutes::register($group, $container); BuildQueueRoutes::register($group, $container); PlanetGeneratorRoutes::register($group, $container); diff --git a/server/src/Config/ConfigLoader.php b/server/src/Config/ConfigLoader.php index 7f2f59c..78b3b99 100644 --- a/server/src/Config/ConfigLoader.php +++ b/server/src/Config/ConfigLoader.php @@ -48,6 +48,16 @@ final class ConfigLoader }); } + /** + * @return array + */ + public function avatars(): array + { + return $this->load('avatars.json', function (array $config): void { + $this->validator->validateAvatars($config, 'avatars.json'); + }); + } + /** * @param callable(array):void $validate * @return array diff --git a/server/src/Config/ConfigValidator.php b/server/src/Config/ConfigValidator.php index b9a9d49..f356381 100644 --- a/server/src/Config/ConfigValidator.php +++ b/server/src/Config/ConfigValidator.php @@ -177,6 +177,33 @@ final class ConfigValidator } } + /** + * @param array $config + */ + public function validateAvatars(array $config, string $file): void + { + $errors = []; + if (!isset($config['avatars']) || !is_array($config['avatars'])) { + $errors[] = "'avatars' muss ein Array sein"; + } + if (isset($config['avatars']) && is_array($config['avatars'])) { + foreach ($config['avatars'] as $idx => $avatar) { + if (!is_array($avatar)) { + $errors[] = "Avatar #{$idx} muss ein Objekt sein"; + continue; + } + foreach (['key', 'label', 'image'] as $req) { + if (empty($avatar[$req]) || !is_string($avatar[$req])) { + $errors[] = "Avatar #{$idx}: '{$req}' fehlt"; + } + } + } + } + if ($errors) { + throw new ConfigValidationException($file, $errors); + } + } + /** * @return string[] */ diff --git a/server/src/Module/Auth/Controller/AuthController.php b/server/src/Module/Auth/Controller/AuthController.php new file mode 100644 index 0000000..e679b96 --- /dev/null +++ b/server/src/Module/Auth/Controller/AuthController.php @@ -0,0 +1,475 @@ +parseBody($request); + $identifier = trim((string)($body['username_or_email'] ?? '')); + $password = (string)($body['password'] ?? ''); + + if ($identifier === '' || $password === '') { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_input', + 'message' => 'Username/E-Mail und Passwort sind Pflichtfelder.', + ], 400); + } + + $user = $this->findUserByIdentifier($identifier); + if (!$user || !password_verify($password, (string)($user['password_hash'] ?? ''))) { + return JsonResponder::withJson(new Response(), [ + 'error' => 'invalid_credentials', + 'message' => 'Login fehlgeschlagen.', + ], 401); + } + + $this->loginUser((int)$user['id']); + return JsonResponder::withJson($response, [ + 'user' => $this->buildUserSummary($user), + ]); + } + + public function logout(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $this->startSession(); + $_SESSION = []; + + if (ini_get('session.use_cookies')) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + session_destroy(); + + return JsonResponder::withJson($response, ['ok' => true]); + } + + public function me(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $user = $request->getAttribute('user'); + if (!is_array($user) || !isset($user['id'])) { + return JsonResponder::withJson(new Response(), [ + 'error' => 'auth_required', + 'message' => 'Authentifizierung erforderlich.', + ], 401); + } + + return JsonResponder::withJson($response, [ + 'user' => $this->buildUserSummary($user), + ]); + } + + public function registerStep1(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $body = $this->parseBody($request); + $raceKey = trim((string)($body['race_key'] ?? '')); + $races = $this->configLoader->races(); + $raceConfig = $races['races'][$raceKey] ?? null; + + if ($raceKey === '' || !is_array($raceConfig)) { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_race', + 'message' => 'Ungültige Rasse.', + ], 422); + } + + $draft = $this->getDraft(); + $draft['race_key'] = $raceKey; + $this->saveDraft($draft); + + return JsonResponder::withJson($response, [ + 'draft' => $this->sanitizeDraft($draft), + ]); + } + + public function registerStep2(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $draft = $this->getDraft(); + if (empty($draft['race_key'])) { + return JsonResponder::withJson($response, [ + 'error' => 'draft_missing', + 'message' => 'Bitte zuerst eine Rasse wählen.', + ], 409); + } + + $body = $this->parseBody($request); + $avatarKey = trim((string)($body['avatar_key'] ?? '')); + $title = $this->sanitizeTitle((string)($body['title'] ?? '')); + + if ($avatarKey === '' || !$this->avatarExists($avatarKey)) { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_avatar', + 'message' => 'Ungültiger Avatar.', + ], 422); + } + + if (!$this->isValidTitle($title)) { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_title', + 'message' => 'Titel muss zwischen 2 und 40 Zeichen lang sein.', + ], 422); + } + + $draft['avatar_key'] = $avatarKey; + $draft['title'] = $title; + $this->saveDraft($draft); + + return JsonResponder::withJson($response, [ + 'draft' => $this->sanitizeDraft($draft), + ]); + } + + public function registerStep3(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $draft = $this->getDraft(); + if (empty($draft['race_key']) || empty($draft['avatar_key']) || empty($draft['title'])) { + return JsonResponder::withJson($response, [ + 'error' => 'draft_incomplete', + 'message' => 'Bitte Registrierungsschritte 1 und 2 abschließen.', + ], 409); + } + + $body = $this->parseBody($request); + $username = trim((string)($body['username'] ?? '')); + $email = trim((string)($body['email'] ?? '')); + $password = (string)($body['password'] ?? ''); + + if (!$this->isValidUsername($username)) { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_username', + 'message' => 'Username muss zwischen 3 und 20 Zeichen lang sein.', + ], 422); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_email', + 'message' => 'Ungültige E-Mail-Adresse.', + ], 422); + } + + if ($this->length($password) < self::PASSWORD_MIN_LENGTH) { + return JsonResponder::withJson($response, [ + 'error' => 'invalid_password', + 'message' => 'Passwort zu kurz (mindestens 8 Zeichen).', + ], 422); + } + + $existing = $this->findUserByUsernameOrEmail($username, $email); + if ($existing['username'] ?? false) { + return JsonResponder::withJson($response, [ + 'error' => 'username_taken', + 'message' => 'Username bereits vergeben.', + ], 409); + } + if ($existing['email'] ?? false) { + return JsonResponder::withJson($response, [ + 'error' => 'email_taken', + 'message' => 'E-Mail bereits registriert.', + ], 409); + } + + try { + $this->pdo->beginTransaction(); + + $stmt = $this->pdo->prepare( + 'INSERT INTO users (username, email, password_hash, race_key, title, avatar_key) + VALUES (:username, :email, :password_hash, :race_key, :title, :avatar_key) + RETURNING *' + ); + $stmt->execute([ + 'username' => $username, + 'email' => $this->lower($email), + 'password_hash' => password_hash($password, PASSWORD_DEFAULT), + 'race_key' => $draft['race_key'], + 'title' => $draft['title'], + 'avatar_key' => $draft['avatar_key'], + ]); + $user = $stmt->fetch(); + + $userId = (int)($user['id'] ?? 0); + if ($userId <= 0) { + throw new \\RuntimeException('User-ID fehlt.'); + } + + $this->assignRole($userId, 'player'); + $this->createStarterPlanet($userId); + + $this->pdo->commit(); + } catch (PDOException $e) { + $this->pdo->rollBack(); + if ($e->getCode() === '23505') { + return JsonResponder::withJson($response, [ + 'error' => 'duplicate', + 'message' => 'Username oder E-Mail bereits registriert.', + ], 409); + } + return JsonResponder::withJson($response, [ + 'error' => 'registration_failed', + 'message' => 'Registrierung fehlgeschlagen.', + ], 500); + } catch (\\Throwable $e) { + $this->pdo->rollBack(); + return JsonResponder::withJson($response, [ + 'error' => 'registration_failed', + 'message' => 'Registrierung fehlgeschlagen.', + ], 500); + } + + $this->clearDraft(); + $this->loginUser((int)$user['id']); + + return JsonResponder::withJson($response, [ + 'user' => $this->buildUserSummary($user), + ], 201); + } + + /** + * @return array + */ + private function parseBody(ServerRequestInterface $request): array + { + $body = $request->getParsedBody(); + if (!is_array($body)) { + return []; + } + return $body; + } + + /** + * @param array $draft + * @return array + */ + private function sanitizeDraft(array $draft): array + { + return [ + 'race_key' => $draft['race_key'] ?? null, + 'avatar_key' => $draft['avatar_key'] ?? null, + 'title' => $draft['title'] ?? null, + ]; + } + + private function findUserByIdentifier(string $identifier): ?array + { + $stmt = $this->pdo->prepare('SELECT * FROM users WHERE username = :id OR LOWER(email) = :email'); + $stmt->execute([ + 'id' => $identifier, + 'email' => $this->lower($identifier), + ]); + $user = $stmt->fetch(); + return $user ?: null; + } + + /** + * @return array{username:bool,email:bool} + */ + private function findUserByUsernameOrEmail(string $username, string $email): array + { + $stmt = $this->pdo->prepare('SELECT username, email FROM users WHERE username = :username OR email = :email'); + $stmt->execute([ + 'username' => $username, + 'email' => $this->lower($email), + ]); + $rows = $stmt->fetchAll(); + $result = ['username' => false, 'email' => false]; + foreach ($rows as $row) { + if (isset($row['username']) && $row['username'] === $username) { + $result['username'] = true; + } + if (isset($row['email']) && $this->lower((string)$row['email']) === $this->lower($email)) { + $result['email'] = true; + } + } + return $result; + } + + /** + * @param array $user + * @return array + */ + private function buildUserSummary(array $user): array + { + return [ + 'id' => (int)($user['id'] ?? 0), + 'username' => (string)($user['username'] ?? ''), + 'email' => (string)($user['email'] ?? ''), + 'race_key' => (string)($user['race_key'] ?? ''), + 'title' => (string)($user['title'] ?? ''), + 'avatar_key' => (string)($user['avatar_key'] ?? ''), + ]; + } + + private function avatarExists(string $avatarKey): bool + { + $config = $this->configLoader->avatars(); + $avatars = $config['avatars'] ?? []; + foreach ($avatars as $avatar) { + if (is_array($avatar) && ($avatar['key'] ?? null) === $avatarKey) { + return true; + } + } + return false; + } + + private function isValidTitle(string $title): bool + { + $len = $this->length($title); + return $len >= self::TITLE_MIN_LENGTH && $len <= self::TITLE_MAX_LENGTH; + } + + private function sanitizeTitle(string $title): string + { + $clean = trim(strip_tags($title)); + $clean = preg_replace('/\s+/', ' ', $clean) ?? ''; + return $clean; + } + + private function isValidUsername(string $username): bool + { + $len = $this->length($username); + if ($len < self::USERNAME_MIN_LENGTH || $len > self::USERNAME_MAX_LENGTH) { + return false; + } + return (bool)preg_match('/^[A-Za-z0-9_\-]+$/', $username); + } + + private function length(string $value): int + { + if (function_exists('mb_strlen')) { + return mb_strlen($value); + } + return strlen($value); + } + + private function lower(string $value): string + { + if (function_exists('mb_strtolower')) { + return mb_strtolower($value); + } + return strtolower($value); + } + + private function startSession(): void + { + if (session_status() === PHP_SESSION_ACTIVE) { + return; + } + $secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; + session_start([ + 'cookie_httponly' => true, + 'cookie_samesite' => 'Lax', + 'cookie_secure' => $secure, + 'cookie_path' => '/', + ]); + } + + private function loginUser(int $userId): void + { + $this->startSession(); + session_regenerate_id(true); + $_SESSION['user_id'] = $userId; + } + + /** + * @return array + */ + private function getDraft(): array + { + $this->startSession(); + $draft = $_SESSION['reg_draft'] ?? []; + return is_array($draft) ? $draft : []; + } + + /** + * @param array $draft + */ + private function saveDraft(array $draft): void + { + $this->startSession(); + $_SESSION['reg_draft'] = $draft; + } + + private function clearDraft(): void + { + $this->startSession(); + unset($_SESSION['reg_draft']); + } + + private function assignRole(int $userId, string $roleKey): void + { + $stmt = $this->pdo->prepare( + 'INSERT INTO user_roles (user_id, role_id) + SELECT :user_id, r.id FROM roles r WHERE r.key = :role_key + ON CONFLICT DO NOTHING' + ); + $stmt->execute([ + 'user_id' => $userId, + 'role_key' => $roleKey, + ]); + } + + private function createStarterPlanet(int $userId): void + { + $config = $this->configLoader->planetClasses(); + $resources = []; + foreach (($config['resources'] ?? []) as $res) { + $resources[$res] = 500.0; + } + + $seed = random_int(10, 999999); + $generated = $this->planetGenerator->generate('temperate', 'normal', $seed); + + $stmt = $this->pdo->prepare( + 'INSERT INTO planets (user_id, name, class_key, planet_seed, temperature_c, modifiers, resources, last_resource_update_at) + VALUES (:user_id, :name, :class_key, :planet_seed, :temperature_c, :modifiers, :resources, :last_update) + RETURNING id' + ); + $stmt->execute([ + 'user_id' => $userId, + 'name' => 'Heimatwelt', + 'class_key' => $generated['class_key'], + 'planet_seed' => $seed, + 'temperature_c' => (int)$generated['temperature_c'], + 'modifiers' => json_encode($generated['modifiers'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'resources' => json_encode($resources, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'last_update' => (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s'), + ]); + $planetId = (int)$stmt->fetchColumn(); + + $stmt = $this->pdo->prepare( + 'INSERT INTO planet_buildings (planet_id, building_key, count, level) + VALUES (:planet_id, :building_key, :count, 0) + ON CONFLICT (planet_id, building_key) DO NOTHING' + ); + $stmt->execute([ + 'planet_id' => $planetId, + 'building_key' => 'build_center', + 'count' => 1, + ]); + } +} diff --git a/server/src/Module/Auth/Controller/MetaController.php b/server/src/Module/Auth/Controller/MetaController.php new file mode 100644 index 0000000..f9a9513 --- /dev/null +++ b/server/src/Module/Auth/Controller/MetaController.php @@ -0,0 +1,101 @@ +configLoader->races(); + $races = $config['races'] ?? []; + + $items = []; + foreach ($races as $key => $race) { + if (!is_array($race)) { + continue; + } + $items[] = [ + 'key' => (string)$key, + 'name' => (string)($race['name'] ?? $key), + 'description' => (string)($race['description'] ?? ''), + 'modifier_summary' => $this->buildModifierSummary($race['modifiers'] ?? []), + 'modifiers' => $race['modifiers'] ?? [], + ]; + } + + return JsonResponder::withJson($response, ['races' => $items]); + } + + public function avatars(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $config = $this->configLoader->avatars(); + $avatars = $config['avatars'] ?? []; + return JsonResponder::withJson($response, ['avatars' => $avatars]); + } + + /** + * @param array $modifiers + * @return string[] + */ + private function buildModifierSummary(array $modifiers): array + { + $summary = []; + $labels = [ + 'produce' => 'Produktion', + 'consume' => 'Verbrauch', + ]; + $resourceNames = [ + 'metal' => 'Metall', + 'alloy' => 'Legierung', + 'crystals' => 'Kristall', + 'energy' => 'Energie', + 'credits' => 'Credits', + 'population' => 'Bevölkerung', + 'water' => 'Wasser', + 'deuterium' => 'Deuterium', + 'food' => 'Nahrung', + ]; + + foreach ($labels as $section => $label) { + $entries = $modifiers[$section] ?? null; + if (!is_array($entries)) { + continue; + } + $parts = []; + foreach ($entries as $res => $val) { + if (!is_numeric($val) || (float)$val == 0.0) { + continue; + } + $percent = (float)$val * 100; + $sign = $percent >= 0 ? '+' : ''; + $name = $resourceNames[$res] ?? (string)$res; + $parts[] = sprintf('%s %s%s%%', $name, $sign, $this->formatPercent($percent)); + } + if ($parts) { + $summary[] = $label . ': ' . implode(', ', $parts); + } + } + + return $summary; + } + + private function formatPercent(float $value): string + { + $formatted = number_format($value, 1, ',', ''); + if (str_ends_with($formatted, ',0')) { + return substr($formatted, 0, -2); + } + return $formatted; + } +} diff --git a/server/src/Module/Auth/Routes.php b/server/src/Module/Auth/Routes.php new file mode 100644 index 0000000..3866c3d --- /dev/null +++ b/server/src/Module/Auth/Routes.php @@ -0,0 +1,30 @@ +get(AuthController::class); + $meta = $container->get(MetaController::class); + + $group->post('/auth/login', [$auth, 'login']); + $group->post('/auth/logout', [$auth, 'logout']); + $group->get('/me', [$auth, 'me']); + + $group->post('/auth/register/step1', [$auth, 'registerStep1']); + $group->post('/auth/register/step2', [$auth, 'registerStep2']); + $group->post('/auth/register/step3', [$auth, 'registerStep3']); + + $group->get('/meta/races', [$meta, 'races']); + $group->get('/meta/avatars', [$meta, 'avatars']); + } +} diff --git a/server/src/Module/Auth/Service/AuthService.php b/server/src/Module/Auth/Service/AuthService.php index ca24c74..a5e470c 100644 --- a/server/src/Module/Auth/Service/AuthService.php +++ b/server/src/Module/Auth/Service/AuthService.php @@ -18,11 +18,17 @@ final class AuthService */ public function resolveUser(ServerRequestInterface $request): ?array { + $this->ensureSession(); $id = $this->extractUserId($request); if ($id !== null) { return $this->findUserById($id); } + $sessionUserId = $this->getSessionUserId(); + if ($sessionUserId !== null) { + return $this->findUserById($sessionUserId); + } + if ((int)(getenv('DEV_MODE') ?: 0) === 1) { $devUserId = getenv('DEV_USER_ID'); if ($devUserId !== false && is_numeric($devUserId)) { @@ -54,6 +60,32 @@ final class AuthService return null; } + private function getSessionUserId(): ?int + { + if (!isset($_SESSION['user_id'])) { + return null; + } + $val = $_SESSION['user_id']; + if (is_numeric($val)) { + return (int)$val; + } + return null; + } + + private function ensureSession(): void + { + if (session_status() === PHP_SESSION_ACTIVE) { + return; + } + $secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; + session_start([ + 'cookie_httponly' => true, + 'cookie_samesite' => 'Lax', + 'cookie_secure' => $secure, + 'cookie_path' => '/', + ]); + } + /** * @return array|null */ diff --git a/server/tests/Integration/AuthFlowTest.php b/server/tests/Integration/AuthFlowTest.php new file mode 100644 index 0000000..dfe91a7 --- /dev/null +++ b/server/tests/Integration/AuthFlowTest.php @@ -0,0 +1,182 @@ +jsonRequest($factory, 'POST', '/auth/register/step1', [ + 'race_key' => 'human', + ], $cookie); + $response1 = $app->handle($step1); + self::assertSame(200, $response1->getStatusCode()); + $cookie = $this->extractCookie($response1, $cookie); + $body1 = json_decode((string)$response1->getBody(), true); + self::assertSame('human', $body1['draft']['race_key']); + + $step2 = $this->jsonRequest($factory, 'POST', '/auth/register/step2', [ + 'avatar_key' => 'avatar-01', + 'title' => 'Pionier', + ], $cookie); + $response2 = $app->handle($step2); + self::assertSame(200, $response2->getStatusCode()); + $cookie = $this->extractCookie($response2, $cookie); + $body2 = json_decode((string)$response2->getBody(), true); + self::assertSame('avatar-01', $body2['draft']['avatar_key']); + + $step3 = $this->jsonRequest($factory, 'POST', '/auth/register/step3', [ + 'username' => 'neo', + 'email' => 'neo@example.test', + 'password' => 'change-me', + ], $cookie); + $response3 = $app->handle($step3); + self::assertSame(201, $response3->getStatusCode()); + $cookie = $this->extractCookie($response3, $cookie); + + $meRequest = $factory->createServerRequest('GET', '/me'); + if ($cookie) { + $meRequest = $meRequest->withHeader('Cookie', $cookie); + } + $meResponse = $app->handle($meRequest); + self::assertSame(200, $meResponse->getStatusCode()); + $meBody = json_decode((string)$meResponse->getBody(), true); + self::assertSame('neo', $meBody['user']['username']); + self::assertSame('human', $meBody['user']['race_key']); + + $draftCheck = $this->jsonRequest($factory, 'POST', '/auth/register/step2', [ + 'avatar_key' => 'avatar-01', + 'title' => 'Test', + ], $cookie); + $draftResponse = $app->handle($draftCheck); + self::assertSame(409, $draftResponse->getStatusCode()); + } + + public function testLoginSuccessAndFailure(): void + { + $pdo = TestDatabase::create(); + TestDatabase::reset($pdo); + TestDatabase::seedMinimal($pdo); + + $time = new FixedTimeProvider(new \DateTimeImmutable('2026-02-03 00:00:00')); + $app = TestAppFactory::create($pdo, $time); + $factory = new ServerRequestFactory(); + + $badLogin = $this->jsonRequest($factory, 'POST', '/auth/login', [ + 'username_or_email' => 'tester', + 'password' => 'wrong-pass', + ]); + $badResponse = $app->handle($badLogin); + self::assertSame(401, $badResponse->getStatusCode()); + + $goodLogin = $this->jsonRequest($factory, 'POST', '/auth/login', [ + 'username_or_email' => 'tester', + 'password' => 'change-me', + ]); + $goodResponse = $app->handle($goodLogin); + self::assertSame(200, $goodResponse->getStatusCode()); + $cookie = $this->extractCookie($goodResponse, null); + + $meRequest = $factory->createServerRequest('GET', '/me'); + if ($cookie) { + $meRequest = $meRequest->withHeader('Cookie', $cookie); + } + $meResponse = $app->handle($meRequest); + self::assertSame(200, $meResponse->getStatusCode()); + } + + public function testRegistrationRejectsDuplicates(): void + { + $pdo = TestDatabase::create(); + TestDatabase::reset($pdo); + TestDatabase::seedMinimal($pdo); + + $time = new FixedTimeProvider(new \DateTimeImmutable('2026-02-03 00:00:00')); + $app = TestAppFactory::create($pdo, $time); + $factory = new ServerRequestFactory(); + + $cookie = null; + + $step1 = $this->jsonRequest($factory, 'POST', '/auth/register/step1', [ + 'race_key' => 'human', + ], $cookie); + $response1 = $app->handle($step1); + $cookie = $this->extractCookie($response1, $cookie); + + $step2 = $this->jsonRequest($factory, 'POST', '/auth/register/step2', [ + 'avatar_key' => 'avatar-01', + 'title' => 'Pilot', + ], $cookie); + $response2 = $app->handle($step2); + $cookie = $this->extractCookie($response2, $cookie); + + $dupUsername = $this->jsonRequest($factory, 'POST', '/auth/register/step3', [ + 'username' => 'tester', + 'email' => 'unique@example.test', + 'password' => 'change-me', + ], $cookie); + $dupResponse = $app->handle($dupUsername); + self::assertSame(409, $dupResponse->getStatusCode()); + + $dupEmail = $this->jsonRequest($factory, 'POST', '/auth/register/step3', [ + 'username' => 'unique-user', + 'email' => 'tester@example.test', + 'password' => 'change-me', + ], $cookie); + $dupEmailResponse = $app->handle($dupEmail); + self::assertSame(409, $dupEmailResponse->getStatusCode()); + } + + /** + * @param array $payload + */ + private function jsonRequest(ServerRequestFactory $factory, string $method, string $path, array $payload = [], ?string $cookie = null): \Psr\Http\Message\ServerRequestInterface + { + $request = $factory->createServerRequest($method, $path) + ->withHeader('Content-Type', 'application/json'); + if ($cookie) { + $request = $request->withHeader('Cookie', $cookie); + } + if ($payload !== []) { + $request->getBody()->write(json_encode($payload)); + $request->getBody()->rewind(); + } + return $request; + } + + private function extractCookie(\Psr\Http\Message\ResponseInterface $response, ?string $fallback): ?string + { + $setCookie = $response->getHeaderLine('Set-Cookie'); + if ($setCookie === '') { + return $fallback; + } + $parts = explode(';', $setCookie); + return trim($parts[0]); + } +} diff --git a/server/tests/Support/TestDatabase.php b/server/tests/Support/TestDatabase.php index 1c997fa..7fdac8a 100644 --- a/server/tests/Support/TestDatabase.php +++ b/server/tests/Support/TestDatabase.php @@ -51,7 +51,18 @@ final class TestDatabase $pdo->exec("INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON r.key = 'player' AND p.key = 'planet.public.view'"); - $pdo->exec("INSERT INTO users (username, race_key) VALUES ('tester', 'human')"); + $stmt = $pdo->prepare( + "INSERT INTO users (username, email, password_hash, race_key, title, avatar_key) + VALUES (:username, :email, :password_hash, :race_key, :title, :avatar_key)" + ); + $stmt->execute([ + 'username' => 'tester', + 'email' => 'tester@example.test', + 'password_hash' => password_hash('change-me', PASSWORD_DEFAULT), + 'race_key' => 'human', + 'title' => 'Tester', + 'avatar_key' => 'avatar-01', + ]); $userId = (int)$pdo->query("SELECT id FROM users WHERE username = 'tester'")->fetchColumn(); $pdo->exec("INSERT INTO user_roles (user_id, role_id) diff --git a/web/desktop/public/assets/avatars/avatar-01.svg b/web/desktop/public/assets/avatars/avatar-01.svg new file mode 100644 index 0000000..3bb80a3 --- /dev/null +++ b/web/desktop/public/assets/avatars/avatar-01.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/desktop/public/assets/avatars/avatar-02.svg b/web/desktop/public/assets/avatars/avatar-02.svg new file mode 100644 index 0000000..f8e725e --- /dev/null +++ b/web/desktop/public/assets/avatars/avatar-02.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/desktop/public/assets/avatars/avatar-03.svg b/web/desktop/public/assets/avatars/avatar-03.svg new file mode 100644 index 0000000..267abfd --- /dev/null +++ b/web/desktop/public/assets/avatars/avatar-03.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/desktop/public/assets/avatars/avatar-04.svg b/web/desktop/public/assets/avatars/avatar-04.svg new file mode 100644 index 0000000..f783eb0 --- /dev/null +++ b/web/desktop/public/assets/avatars/avatar-04.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/desktop/public/assets/avatars/avatar-05.svg b/web/desktop/public/assets/avatars/avatar-05.svg new file mode 100644 index 0000000..1e2e972 --- /dev/null +++ b/web/desktop/public/assets/avatars/avatar-05.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/desktop/public/assets/avatars/avatar-06.svg b/web/desktop/public/assets/avatars/avatar-06.svg new file mode 100644 index 0000000..4981e8f --- /dev/null +++ b/web/desktop/public/assets/avatars/avatar-06.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/desktop/public/assets/style.css b/web/desktop/public/assets/style.css index 3cd9b8a..fd89f09 100644 --- a/web/desktop/public/assets/style.css +++ b/web/desktop/public/assets/style.css @@ -416,6 +416,53 @@ button:active, .btn:active, input[type="submit"]:active{ transform: translateY(0 .actions{ display:flex; gap: 12px; margin-top: 14px; flex-wrap: wrap; } +/* Auth */ +.auth-view{ display:flex; flex-direction:column; gap: 16px; } +.auth-card{ max-width: 720px; margin: 0 auto; width: 100%; } +.form-row{ margin-top: 12px; } +.input{ + width: 100%; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(145,220,255,.22); + background: rgba(0,0,0,.28); + color: var(--text); +} +.input:focus{ outline: 2px solid rgba(66,245,255,.35); } +.auth-grid{ + display:grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + margin-top: 12px; +} +.auth-choice{ + border: 1px solid rgba(145,220,255,.18); + background: rgba(0,0,0,.18); + border-radius: 14px; + padding: 12px; + color: var(--text); + text-align: left; + display:flex; + flex-direction:column; + gap: 6px; + cursor:pointer; +} +.auth-choice:hover{ border-color: rgba(66,245,255,.45); box-shadow: var(--glow-cyan); transform: translateY(-1px); } +.auth-choice.is-selected{ + border-color: rgba(66,245,255,.55); + box-shadow: var(--glow-cyan); + background: linear-gradient(180deg, rgba(66,245,255,.16), rgba(91,124,255,.10)); +} +.auth-choice-title{ font-family: var(--font-sci); letter-spacing: .6px; } +.auth-choice-meta{ color: var(--muted); font-size: .85rem; } +.auth-message{ min-height: 18px; margin-top: 8px; } +.auth-avatar{ + width: 100%; + border-radius: 12px; + border: 1px solid rgba(145,220,255,.18); + background: rgba(0,0,0,.22); +} + /* Resourcebar sticky */ .resourcebar{ position: sticky; diff --git a/web/desktop/public/assets/ui.js b/web/desktop/public/assets/ui.js index 05b23cb..646e81c 100644 --- a/web/desktop/public/assets/ui.js +++ b/web/desktop/public/assets/ui.js @@ -119,28 +119,31 @@ }); }); + const authView = document.getElementById("authView"); + const gameView = document.getElementById("gameView"); + let stateTimer = null; + const authPanels = { + login: document.getElementById("authLogin"), + "register-step1": document.getElementById("authRegisterStep1"), + "register-step2": document.getElementById("authRegisterStep2"), + "register-step3": document.getElementById("authRegisterStep3") + }; + let racesCache = null; + let avatarsCache = null; + const regDraft = { race_key: null, avatar_key: null }; + function formatNumber(val){ const num = Number.isFinite(val) ? val : 0; return Math.round(num).toLocaleString("de-DE"); } - const resourceMap = { - "Metall": "metal", - "Kristall": "crystals", - "Deuterium": "deuterium", - "Energie": "energy" - }; - function updateResourceBar(state){ - const stats = document.querySelectorAll(".resource-row .stat"); - stats.forEach((stat)=>{ - const label = stat.querySelector(".stat-k")?.textContent?.trim(); - const key = resourceMap[label]; + const values = state?.resources || {}; + document.querySelectorAll("[data-resource-value]").forEach((valueEl)=>{ + const key = valueEl.dataset.resourceValue; if(!key) return; - const value = state?.resources?.[key]; + const value = values[key]; if(typeof value !== "number") return; - const valueEl = stat.querySelector(".stat-v"); - if(!valueEl) return; const dot = valueEl.querySelector(".dot"); const display = key === "energy" ? Math.round(value) : Math.floor(value); const prefix = key === "energy" && display >= 0 ? "+" : ""; @@ -150,19 +153,189 @@ }); } + function updateQueuePanel(state){ + const slotsEl = document.getElementById("queueSlots"); + if(slotsEl){ + const slots = Number.isFinite(state?.queue_slots) ? state.queue_slots : null; + slotsEl.textContent = slots === null ? "–" : String(slots); + } + const list = document.getElementById("queueList"); + if(!list) return; + list.textContent = ""; + const jobs = Array.isArray(state?.active_build_jobs) ? state.active_build_jobs : []; + if(jobs.length === 0){ + const li = document.createElement("li"); + li.className = "muted"; + li.textContent = "Keine aktiven Baujobs."; + list.appendChild(li); + return; + } + jobs.forEach((job)=>{ + const li = document.createElement("li"); + const slotIndex = Number.isFinite(Number(job?.slot_index)) ? Number(job.slot_index) + 1 : null; + const slotLabel = slotIndex ? `Slot ${slotIndex}: ` : ""; + const key = job?.building_key ? String(job.building_key) : "Unbekannt"; + const finish = job?.finish_at ? ` · fertig ${job.finish_at}` : ""; + li.textContent = `${slotLabel}${key}${finish}`; + list.appendChild(li); + }); + } + + function showAuthView(){ + if(authView) authView.removeAttribute("hidden"); + if(gameView) gameView.setAttribute("hidden", ""); + } + + function showGameView(){ + if(authView) authView.setAttribute("hidden", ""); + if(gameView) gameView.removeAttribute("hidden"); + } + + function showAuthPanel(key){ + Object.values(authPanels).forEach((panel)=>{ + if(panel) panel.setAttribute("hidden", ""); + }); + const panel = authPanels[key]; + if(panel) panel.removeAttribute("hidden"); + } + + function setMessage(el, message){ + if(!el) return; + el.textContent = message || ""; + } + + async function fetchJson(url, options){ + try{ + const res = await fetch(url, options); + const data = await res.json().catch(()=> ({})); + return { ok: res.ok, status: res.status, data }; + }catch(e){ + return { ok: false, status: 0, data: {} }; + } + } + + async function loadRaces(){ + if(racesCache) return racesCache; + const result = await fetchJson("/api/meta/races"); + if(result.ok){ + racesCache = result.data.races || []; + }else{ + racesCache = []; + } + return racesCache; + } + + async function loadAvatars(){ + if(avatarsCache) return avatarsCache; + const result = await fetchJson("/api/meta/avatars"); + if(result.ok){ + avatarsCache = result.data.avatars || []; + }else{ + avatarsCache = []; + } + return avatarsCache; + } + + function renderRaces(races){ + const list = document.getElementById("raceList"); + if(!list) return; + list.textContent = ""; + if(!Array.isArray(races) || races.length === 0){ + const empty = document.createElement("div"); + empty.className = "muted"; + empty.textContent = "Keine Rassen verfügbar."; + list.appendChild(empty); + return; + } + races.forEach((race)=>{ + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "auth-choice"; + btn.dataset.raceKey = race.key; + if(regDraft.race_key === race.key) btn.classList.add("is-selected"); + const summary = Array.isArray(race.modifier_summary) ? race.modifier_summary : []; + btn.innerHTML = ` +
${escapeHtml(race.name || race.key)}
+
${escapeHtml(race.description || "")}
+ ${summary.length ? `
${summary.map(s=>escapeHtml(s)).join("
")}
` : ""} + `; + btn.addEventListener("click", ()=>{ + regDraft.race_key = race.key; + list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected")); + btn.classList.add("is-selected"); + }); + list.appendChild(btn); + }); + } + + function renderAvatars(avatars){ + const list = document.getElementById("avatarList"); + if(!list) return; + list.textContent = ""; + if(!Array.isArray(avatars) || avatars.length === 0){ + const empty = document.createElement("div"); + empty.className = "muted"; + empty.textContent = "Keine Avatare verfügbar."; + list.appendChild(empty); + return; + } + avatars.forEach((avatar)=>{ + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "auth-choice"; + btn.dataset.avatarKey = avatar.key; + if(regDraft.avatar_key === avatar.key) btn.classList.add("is-selected"); + btn.innerHTML = ` + ${escapeHtml(avatar.label || avatar.key)} +
${escapeHtml(avatar.label || avatar.key)}
+ `; + btn.addEventListener("click", ()=>{ + regDraft.avatar_key = avatar.key; + list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected")); + btn.classList.add("is-selected"); + }); + list.appendChild(btn); + }); + } + + function startPolling(){ + if(stateTimer) return; + stateTimer = setInterval(refreshState, 15000); + } + + function stopPolling(){ + if(!stateTimer) return; + clearInterval(stateTimer); + stateTimer = null; + } + async function fetchState(){ try{ - const res = await fetch("/api/state"); - if(!res.ok) return null; - return await res.json(); + const res = await fetch("/api/state", { credentials: "same-origin" }); + if(res.status === 401) return { status: 401 }; + if(!res.ok) return { status: res.status }; + const data = await res.json(); + return { status: 200, data }; }catch(e){ - return null; + return { status: 0 }; } } async function refreshState(){ - const state = await fetchState(); - if(state) updateResourceBar(state); + const result = await fetchState(); + if(result.status === 200){ + showGameView(); + updateResourceBar(result.data); + updateQueuePanel(result.data); + ensureBuildButton(); + startPolling(); + return; + } + if(result.status === 401){ + showAuthView(); + showAuthPanel("login"); + stopPolling(); + } } function ensureBuildButton(){ @@ -198,8 +371,130 @@ } document.addEventListener("DOMContentLoaded", ()=>{ + document.querySelectorAll("[data-auth-switch]").forEach((btn)=>{ + btn.addEventListener("click", async ()=>{ + const target = btn.dataset.authSwitch; + if(!target) return; + showAuthView(); + showAuthPanel(target); + if(target === "register-step1"){ + const races = await loadRaces(); + renderRaces(races); + } + if(target === "register-step2"){ + const avatars = await loadAvatars(); + renderAvatars(avatars); + } + }); + }); + + const loginForm = document.getElementById("loginForm"); + if(loginForm){ + loginForm.addEventListener("submit", async (e)=>{ + e.preventDefault(); + const identifier = document.getElementById("loginIdentifier")?.value?.trim() || ""; + const password = document.getElementById("loginPassword")?.value || ""; + const message = document.getElementById("loginMessage"); + setMessage(message, ""); + const result = await fetchJson("/api/auth/login", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ username_or_email: identifier, password }) + }); + if(result.ok){ + showGameView(); + refreshState(); + }else{ + setMessage(message, result.data?.message || "Login fehlgeschlagen."); + } + }); + } + + const raceNext = document.getElementById("raceNext"); + if(raceNext){ + raceNext.addEventListener("click", async ()=>{ + const message = document.getElementById("raceMessage"); + setMessage(message, ""); + if(!regDraft.race_key){ + setMessage(message, "Bitte eine Rasse wählen."); + return; + } + const result = await fetchJson("/api/auth/register/step1", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ race_key: regDraft.race_key }) + }); + if(result.ok){ + showAuthPanel("register-step2"); + const avatars = await loadAvatars(); + renderAvatars(avatars); + }else{ + setMessage(message, result.data?.message || "Schritt 1 fehlgeschlagen."); + } + }); + } + + const avatarNext = document.getElementById("avatarNext"); + if(avatarNext){ + avatarNext.addEventListener("click", async ()=>{ + const title = document.getElementById("regTitle")?.value?.trim() || ""; + const message = document.getElementById("avatarMessage"); + setMessage(message, ""); + if(!regDraft.avatar_key){ + setMessage(message, "Bitte einen Avatar wählen."); + return; + } + if(title.length < 2){ + setMessage(message, "Titel ist zu kurz."); + return; + } + const result = await fetchJson("/api/auth/register/step2", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ avatar_key: regDraft.avatar_key, title }) + }); + if(result.ok){ + showAuthPanel("register-step3"); + }else{ + setMessage(message, result.data?.message || "Schritt 2 fehlgeschlagen."); + } + }); + } + + const registerForm = document.getElementById("registerForm"); + if(registerForm){ + registerForm.addEventListener("submit", async (e)=>{ + e.preventDefault(); + const username = document.getElementById("regUsername")?.value?.trim() || ""; + const email = document.getElementById("regEmail")?.value?.trim() || ""; + const password = document.getElementById("regPassword")?.value || ""; + const message = document.getElementById("registerMessage"); + setMessage(message, ""); + const result = await fetchJson("/api/auth/register/step3", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ username, email, password }) + }); + if(result.ok){ + showGameView(); + refreshState(); + }else{ + setMessage(message, result.data?.message || "Registrierung fehlgeschlagen."); + } + }); + } + + const logoutBtn = document.getElementById("logoutBtn"); + if(logoutBtn){ + logoutBtn.addEventListener("click", async ()=>{ + await fetchJson("/api/auth/logout", { method: "POST" }); + stopPolling(); + showAuthView(); + showAuthPanel("login"); + }); + } + + showAuthPanel("login"); refreshState(); - ensureBuildButton(); - setInterval(refreshState, 30000); }); })(); diff --git a/web/desktop/public/index.php b/web/desktop/public/index.php index 12d1368..3c14a5a 100644 --- a/web/desktop/public/index.php +++ b/web/desktop/public/index.php @@ -103,7 +103,14 @@ $partialsPath = __DIR__ . '/../src/partials';
-
+ + +
@@ -211,6 +219,14 @@ $partialsPath = __DIR__ . '/../src/partials';
+ +
+

Bauqueue (Live)

+
Slots: 0
+
    +
  • Keine aktiven Baujobs.
  • +
+
diff --git a/web/desktop/src/partials/auth-login.php b/web/desktop/src/partials/auth-login.php new file mode 100644 index 0000000..e7b7e2a --- /dev/null +++ b/web/desktop/src/partials/auth-login.php @@ -0,0 +1,23 @@ +
+
LOGIN
+

Melde dich an, um deine Kolonie zu laden.

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
diff --git a/web/desktop/src/partials/auth-register-step1.php b/web/desktop/src/partials/auth-register-step1.php new file mode 100644 index 0000000..1df43d0 --- /dev/null +++ b/web/desktop/src/partials/auth-register-step1.php @@ -0,0 +1,16 @@ + diff --git a/web/desktop/src/partials/auth-register-step2.php b/web/desktop/src/partials/auth-register-step2.php new file mode 100644 index 0000000..4e489b0 --- /dev/null +++ b/web/desktop/src/partials/auth-register-step2.php @@ -0,0 +1,22 @@ + diff --git a/web/desktop/src/partials/auth-register-step3.php b/web/desktop/src/partials/auth-register-step3.php new file mode 100644 index 0000000..a41c10b --- /dev/null +++ b/web/desktop/src/partials/auth-register-step3.php @@ -0,0 +1,30 @@ + diff --git a/web/desktop/src/partials/ressourcen.php b/web/desktop/src/partials/ressourcen.php index f171757..200f23e 100644 --- a/web/desktop/src/partials/ressourcen.php +++ b/web/desktop/src/partials/ressourcen.php @@ -5,24 +5,49 @@
-
+
Metall
-
12.340
+
12.340
-
+
Kristall
-
6.120
+
6.120
-
+
Deuterium
-
3.880
+
3.880
-
+
Energie
-
+120
+
+120
+
+
+
+
Legierung
+
0
+
+
+
+
Credits
+
0
+
+
+
+
Bevölkerung
+
0
+
+
+
+
Wasser
+
0
+
+
+
+
Nahrung
+
0
diff --git a/web/mobile/public/assets/avatars/avatar-01.svg b/web/mobile/public/assets/avatars/avatar-01.svg new file mode 100644 index 0000000..3bb80a3 --- /dev/null +++ b/web/mobile/public/assets/avatars/avatar-01.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/mobile/public/assets/avatars/avatar-02.svg b/web/mobile/public/assets/avatars/avatar-02.svg new file mode 100644 index 0000000..f8e725e --- /dev/null +++ b/web/mobile/public/assets/avatars/avatar-02.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/mobile/public/assets/avatars/avatar-03.svg b/web/mobile/public/assets/avatars/avatar-03.svg new file mode 100644 index 0000000..267abfd --- /dev/null +++ b/web/mobile/public/assets/avatars/avatar-03.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/mobile/public/assets/avatars/avatar-04.svg b/web/mobile/public/assets/avatars/avatar-04.svg new file mode 100644 index 0000000..f783eb0 --- /dev/null +++ b/web/mobile/public/assets/avatars/avatar-04.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/mobile/public/assets/avatars/avatar-05.svg b/web/mobile/public/assets/avatars/avatar-05.svg new file mode 100644 index 0000000..1e2e972 --- /dev/null +++ b/web/mobile/public/assets/avatars/avatar-05.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/mobile/public/assets/avatars/avatar-06.svg b/web/mobile/public/assets/avatars/avatar-06.svg new file mode 100644 index 0000000..4981e8f --- /dev/null +++ b/web/mobile/public/assets/avatars/avatar-06.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/web/mobile/public/assets/style.css b/web/mobile/public/assets/style.css index 3cd9b8a..fd89f09 100644 --- a/web/mobile/public/assets/style.css +++ b/web/mobile/public/assets/style.css @@ -416,6 +416,53 @@ button:active, .btn:active, input[type="submit"]:active{ transform: translateY(0 .actions{ display:flex; gap: 12px; margin-top: 14px; flex-wrap: wrap; } +/* Auth */ +.auth-view{ display:flex; flex-direction:column; gap: 16px; } +.auth-card{ max-width: 720px; margin: 0 auto; width: 100%; } +.form-row{ margin-top: 12px; } +.input{ + width: 100%; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(145,220,255,.22); + background: rgba(0,0,0,.28); + color: var(--text); +} +.input:focus{ outline: 2px solid rgba(66,245,255,.35); } +.auth-grid{ + display:grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + margin-top: 12px; +} +.auth-choice{ + border: 1px solid rgba(145,220,255,.18); + background: rgba(0,0,0,.18); + border-radius: 14px; + padding: 12px; + color: var(--text); + text-align: left; + display:flex; + flex-direction:column; + gap: 6px; + cursor:pointer; +} +.auth-choice:hover{ border-color: rgba(66,245,255,.45); box-shadow: var(--glow-cyan); transform: translateY(-1px); } +.auth-choice.is-selected{ + border-color: rgba(66,245,255,.55); + box-shadow: var(--glow-cyan); + background: linear-gradient(180deg, rgba(66,245,255,.16), rgba(91,124,255,.10)); +} +.auth-choice-title{ font-family: var(--font-sci); letter-spacing: .6px; } +.auth-choice-meta{ color: var(--muted); font-size: .85rem; } +.auth-message{ min-height: 18px; margin-top: 8px; } +.auth-avatar{ + width: 100%; + border-radius: 12px; + border: 1px solid rgba(145,220,255,.18); + background: rgba(0,0,0,.22); +} + /* Resourcebar sticky */ .resourcebar{ position: sticky; diff --git a/web/mobile/public/assets/ui.js b/web/mobile/public/assets/ui.js index 05b23cb..646e81c 100644 --- a/web/mobile/public/assets/ui.js +++ b/web/mobile/public/assets/ui.js @@ -119,28 +119,31 @@ }); }); + const authView = document.getElementById("authView"); + const gameView = document.getElementById("gameView"); + let stateTimer = null; + const authPanels = { + login: document.getElementById("authLogin"), + "register-step1": document.getElementById("authRegisterStep1"), + "register-step2": document.getElementById("authRegisterStep2"), + "register-step3": document.getElementById("authRegisterStep3") + }; + let racesCache = null; + let avatarsCache = null; + const regDraft = { race_key: null, avatar_key: null }; + function formatNumber(val){ const num = Number.isFinite(val) ? val : 0; return Math.round(num).toLocaleString("de-DE"); } - const resourceMap = { - "Metall": "metal", - "Kristall": "crystals", - "Deuterium": "deuterium", - "Energie": "energy" - }; - function updateResourceBar(state){ - const stats = document.querySelectorAll(".resource-row .stat"); - stats.forEach((stat)=>{ - const label = stat.querySelector(".stat-k")?.textContent?.trim(); - const key = resourceMap[label]; + const values = state?.resources || {}; + document.querySelectorAll("[data-resource-value]").forEach((valueEl)=>{ + const key = valueEl.dataset.resourceValue; if(!key) return; - const value = state?.resources?.[key]; + const value = values[key]; if(typeof value !== "number") return; - const valueEl = stat.querySelector(".stat-v"); - if(!valueEl) return; const dot = valueEl.querySelector(".dot"); const display = key === "energy" ? Math.round(value) : Math.floor(value); const prefix = key === "energy" && display >= 0 ? "+" : ""; @@ -150,19 +153,189 @@ }); } + function updateQueuePanel(state){ + const slotsEl = document.getElementById("queueSlots"); + if(slotsEl){ + const slots = Number.isFinite(state?.queue_slots) ? state.queue_slots : null; + slotsEl.textContent = slots === null ? "–" : String(slots); + } + const list = document.getElementById("queueList"); + if(!list) return; + list.textContent = ""; + const jobs = Array.isArray(state?.active_build_jobs) ? state.active_build_jobs : []; + if(jobs.length === 0){ + const li = document.createElement("li"); + li.className = "muted"; + li.textContent = "Keine aktiven Baujobs."; + list.appendChild(li); + return; + } + jobs.forEach((job)=>{ + const li = document.createElement("li"); + const slotIndex = Number.isFinite(Number(job?.slot_index)) ? Number(job.slot_index) + 1 : null; + const slotLabel = slotIndex ? `Slot ${slotIndex}: ` : ""; + const key = job?.building_key ? String(job.building_key) : "Unbekannt"; + const finish = job?.finish_at ? ` · fertig ${job.finish_at}` : ""; + li.textContent = `${slotLabel}${key}${finish}`; + list.appendChild(li); + }); + } + + function showAuthView(){ + if(authView) authView.removeAttribute("hidden"); + if(gameView) gameView.setAttribute("hidden", ""); + } + + function showGameView(){ + if(authView) authView.setAttribute("hidden", ""); + if(gameView) gameView.removeAttribute("hidden"); + } + + function showAuthPanel(key){ + Object.values(authPanels).forEach((panel)=>{ + if(panel) panel.setAttribute("hidden", ""); + }); + const panel = authPanels[key]; + if(panel) panel.removeAttribute("hidden"); + } + + function setMessage(el, message){ + if(!el) return; + el.textContent = message || ""; + } + + async function fetchJson(url, options){ + try{ + const res = await fetch(url, options); + const data = await res.json().catch(()=> ({})); + return { ok: res.ok, status: res.status, data }; + }catch(e){ + return { ok: false, status: 0, data: {} }; + } + } + + async function loadRaces(){ + if(racesCache) return racesCache; + const result = await fetchJson("/api/meta/races"); + if(result.ok){ + racesCache = result.data.races || []; + }else{ + racesCache = []; + } + return racesCache; + } + + async function loadAvatars(){ + if(avatarsCache) return avatarsCache; + const result = await fetchJson("/api/meta/avatars"); + if(result.ok){ + avatarsCache = result.data.avatars || []; + }else{ + avatarsCache = []; + } + return avatarsCache; + } + + function renderRaces(races){ + const list = document.getElementById("raceList"); + if(!list) return; + list.textContent = ""; + if(!Array.isArray(races) || races.length === 0){ + const empty = document.createElement("div"); + empty.className = "muted"; + empty.textContent = "Keine Rassen verfügbar."; + list.appendChild(empty); + return; + } + races.forEach((race)=>{ + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "auth-choice"; + btn.dataset.raceKey = race.key; + if(regDraft.race_key === race.key) btn.classList.add("is-selected"); + const summary = Array.isArray(race.modifier_summary) ? race.modifier_summary : []; + btn.innerHTML = ` +
${escapeHtml(race.name || race.key)}
+
${escapeHtml(race.description || "")}
+ ${summary.length ? `
${summary.map(s=>escapeHtml(s)).join("
")}
` : ""} + `; + btn.addEventListener("click", ()=>{ + regDraft.race_key = race.key; + list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected")); + btn.classList.add("is-selected"); + }); + list.appendChild(btn); + }); + } + + function renderAvatars(avatars){ + const list = document.getElementById("avatarList"); + if(!list) return; + list.textContent = ""; + if(!Array.isArray(avatars) || avatars.length === 0){ + const empty = document.createElement("div"); + empty.className = "muted"; + empty.textContent = "Keine Avatare verfügbar."; + list.appendChild(empty); + return; + } + avatars.forEach((avatar)=>{ + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "auth-choice"; + btn.dataset.avatarKey = avatar.key; + if(regDraft.avatar_key === avatar.key) btn.classList.add("is-selected"); + btn.innerHTML = ` + ${escapeHtml(avatar.label || avatar.key)} +
${escapeHtml(avatar.label || avatar.key)}
+ `; + btn.addEventListener("click", ()=>{ + regDraft.avatar_key = avatar.key; + list.querySelectorAll(".auth-choice").forEach(el=>el.classList.remove("is-selected")); + btn.classList.add("is-selected"); + }); + list.appendChild(btn); + }); + } + + function startPolling(){ + if(stateTimer) return; + stateTimer = setInterval(refreshState, 15000); + } + + function stopPolling(){ + if(!stateTimer) return; + clearInterval(stateTimer); + stateTimer = null; + } + async function fetchState(){ try{ - const res = await fetch("/api/state"); - if(!res.ok) return null; - return await res.json(); + const res = await fetch("/api/state", { credentials: "same-origin" }); + if(res.status === 401) return { status: 401 }; + if(!res.ok) return { status: res.status }; + const data = await res.json(); + return { status: 200, data }; }catch(e){ - return null; + return { status: 0 }; } } async function refreshState(){ - const state = await fetchState(); - if(state) updateResourceBar(state); + const result = await fetchState(); + if(result.status === 200){ + showGameView(); + updateResourceBar(result.data); + updateQueuePanel(result.data); + ensureBuildButton(); + startPolling(); + return; + } + if(result.status === 401){ + showAuthView(); + showAuthPanel("login"); + stopPolling(); + } } function ensureBuildButton(){ @@ -198,8 +371,130 @@ } document.addEventListener("DOMContentLoaded", ()=>{ + document.querySelectorAll("[data-auth-switch]").forEach((btn)=>{ + btn.addEventListener("click", async ()=>{ + const target = btn.dataset.authSwitch; + if(!target) return; + showAuthView(); + showAuthPanel(target); + if(target === "register-step1"){ + const races = await loadRaces(); + renderRaces(races); + } + if(target === "register-step2"){ + const avatars = await loadAvatars(); + renderAvatars(avatars); + } + }); + }); + + const loginForm = document.getElementById("loginForm"); + if(loginForm){ + loginForm.addEventListener("submit", async (e)=>{ + e.preventDefault(); + const identifier = document.getElementById("loginIdentifier")?.value?.trim() || ""; + const password = document.getElementById("loginPassword")?.value || ""; + const message = document.getElementById("loginMessage"); + setMessage(message, ""); + const result = await fetchJson("/api/auth/login", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ username_or_email: identifier, password }) + }); + if(result.ok){ + showGameView(); + refreshState(); + }else{ + setMessage(message, result.data?.message || "Login fehlgeschlagen."); + } + }); + } + + const raceNext = document.getElementById("raceNext"); + if(raceNext){ + raceNext.addEventListener("click", async ()=>{ + const message = document.getElementById("raceMessage"); + setMessage(message, ""); + if(!regDraft.race_key){ + setMessage(message, "Bitte eine Rasse wählen."); + return; + } + const result = await fetchJson("/api/auth/register/step1", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ race_key: regDraft.race_key }) + }); + if(result.ok){ + showAuthPanel("register-step2"); + const avatars = await loadAvatars(); + renderAvatars(avatars); + }else{ + setMessage(message, result.data?.message || "Schritt 1 fehlgeschlagen."); + } + }); + } + + const avatarNext = document.getElementById("avatarNext"); + if(avatarNext){ + avatarNext.addEventListener("click", async ()=>{ + const title = document.getElementById("regTitle")?.value?.trim() || ""; + const message = document.getElementById("avatarMessage"); + setMessage(message, ""); + if(!regDraft.avatar_key){ + setMessage(message, "Bitte einen Avatar wählen."); + return; + } + if(title.length < 2){ + setMessage(message, "Titel ist zu kurz."); + return; + } + const result = await fetchJson("/api/auth/register/step2", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ avatar_key: regDraft.avatar_key, title }) + }); + if(result.ok){ + showAuthPanel("register-step3"); + }else{ + setMessage(message, result.data?.message || "Schritt 2 fehlgeschlagen."); + } + }); + } + + const registerForm = document.getElementById("registerForm"); + if(registerForm){ + registerForm.addEventListener("submit", async (e)=>{ + e.preventDefault(); + const username = document.getElementById("regUsername")?.value?.trim() || ""; + const email = document.getElementById("regEmail")?.value?.trim() || ""; + const password = document.getElementById("regPassword")?.value || ""; + const message = document.getElementById("registerMessage"); + setMessage(message, ""); + const result = await fetchJson("/api/auth/register/step3", { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({ username, email, password }) + }); + if(result.ok){ + showGameView(); + refreshState(); + }else{ + setMessage(message, result.data?.message || "Registrierung fehlgeschlagen."); + } + }); + } + + const logoutBtn = document.getElementById("logoutBtn"); + if(logoutBtn){ + logoutBtn.addEventListener("click", async ()=>{ + await fetchJson("/api/auth/logout", { method: "POST" }); + stopPolling(); + showAuthView(); + showAuthPanel("login"); + }); + } + + showAuthPanel("login"); refreshState(); - ensureBuildButton(); - setInterval(refreshState, 30000); }); })(); diff --git a/web/mobile/public/index.php b/web/mobile/public/index.php index 12d1368..3c14a5a 100644 --- a/web/mobile/public/index.php +++ b/web/mobile/public/index.php @@ -103,7 +103,14 @@ $partialsPath = __DIR__ . '/../src/partials';
-
+ + +
@@ -211,6 +219,14 @@ $partialsPath = __DIR__ . '/../src/partials';
+ +
+

Bauqueue (Live)

+
Slots: 0
+
    +
  • Keine aktiven Baujobs.
  • +
+
diff --git a/web/mobile/src/partials/auth-login.php b/web/mobile/src/partials/auth-login.php new file mode 100644 index 0000000..e7b7e2a --- /dev/null +++ b/web/mobile/src/partials/auth-login.php @@ -0,0 +1,23 @@ +
+
LOGIN
+

Melde dich an, um deine Kolonie zu laden.

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
diff --git a/web/mobile/src/partials/auth-register-step1.php b/web/mobile/src/partials/auth-register-step1.php new file mode 100644 index 0000000..1df43d0 --- /dev/null +++ b/web/mobile/src/partials/auth-register-step1.php @@ -0,0 +1,16 @@ + diff --git a/web/mobile/src/partials/auth-register-step2.php b/web/mobile/src/partials/auth-register-step2.php new file mode 100644 index 0000000..4e489b0 --- /dev/null +++ b/web/mobile/src/partials/auth-register-step2.php @@ -0,0 +1,22 @@ + diff --git a/web/mobile/src/partials/auth-register-step3.php b/web/mobile/src/partials/auth-register-step3.php new file mode 100644 index 0000000..a41c10b --- /dev/null +++ b/web/mobile/src/partials/auth-register-step3.php @@ -0,0 +1,30 @@ + diff --git a/web/mobile/src/partials/ressourcen.php b/web/mobile/src/partials/ressourcen.php index f171757..200f23e 100644 --- a/web/mobile/src/partials/ressourcen.php +++ b/web/mobile/src/partials/ressourcen.php @@ -5,24 +5,49 @@
-
+
Metall
-
12.340
+
12.340
-
+
Kristall
-
6.120
+
6.120
-
+
Deuterium
-
3.880
+
3.880
-
+
Energie
-
+120
+
+120
+
+
+
+
Legierung
+
0
+
+
+
+
Credits
+
0
+
+
+
+
Bevölkerung
+
0
+
+
+
+
Wasser
+
0
+
+
+
+
Nahrung
+
0