From d6d4be762a0211974180ddbda4b7091e67fd40b0 Mon Sep 17 00:00:00 2001 From: Elias Kleppinger Date: Sat, 9 Nov 2024 19:09:39 +0100 Subject: [PATCH] Vielll --- README.md | 9 + composer.json | 13 +- dashboard-module/src/DashboardModule.php | 15 ++ example-app/config/server.json | 4 + src/Auth/PermissionNode.php | 84 ++++++++ src/Auth/Session.php | 31 +++ src/Auth/SessionManager.php | 31 +++ src/Auth/User.php | 181 ++++++++++++++++++ src/Auth/UserGroup.php | 67 +++++++ .../Commands/Server/StartServerCommand.php | 8 +- .../Commands/User/CreateUserCommand.php | 63 ++++++ src/Console/VersaCLI.php | 33 ++-- src/Database/Model/BaseModel.php | 49 +++++ src/Database/VersaBase.php | 52 +++++ src/Kernel/Attributes/Route.php | 14 ++ src/Kernel/Config/PlatformConfig.php | 8 + src/Kernel/Controller.php | 8 + src/Kernel/Http/Request.php | 8 + src/Kernel/Http/Response.php | 8 + .../Controller/InternalAuthController.php | 19 ++ src/Kernel/InternalModule/InternalModule.php | 20 ++ src/Kernel/VersaKernel.php | 124 ++++++++++++ src/Module/AbstractModule.php | 10 + src/Module/Module.php | 21 ++ src/Module/ModuleManager.php | 32 ++++ src/Server/VersaServer.php | 32 +++- versa.json | 3 + 27 files changed, 922 insertions(+), 25 deletions(-) create mode 100644 README.md create mode 100644 dashboard-module/src/DashboardModule.php create mode 100644 example-app/config/server.json create mode 100644 src/Auth/PermissionNode.php create mode 100644 src/Auth/Session.php create mode 100644 src/Auth/SessionManager.php create mode 100644 src/Auth/User.php create mode 100644 src/Auth/UserGroup.php create mode 100644 src/Console/Commands/User/CreateUserCommand.php create mode 100644 src/Database/Model/BaseModel.php create mode 100644 src/Database/VersaBase.php create mode 100644 src/Kernel/Attributes/Route.php create mode 100644 src/Kernel/Config/PlatformConfig.php create mode 100644 src/Kernel/Controller.php create mode 100644 src/Kernel/Http/Request.php create mode 100644 src/Kernel/Http/Response.php create mode 100644 src/Kernel/InternalModule/Controller/InternalAuthController.php create mode 100644 src/Kernel/InternalModule/InternalModule.php create mode 100644 src/Kernel/VersaKernel.php create mode 100644 src/Module/AbstractModule.php create mode 100644 src/Module/Module.php create mode 100644 src/Module/ModuleManager.php create mode 100644 versa.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1421a2 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# versa digitale + +*versa digitale* is an modern high-performance application platform, with an modular development approach. + +The platform has the following features: +- HTTP Server written with ReactPHP +- high-performant async handling of requests +- Caching +- \ No newline at end of file diff --git a/composer.json b/composer.json index df3ac1f..d260b47 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,6 @@ "description": "Anwendungsserver der modularen versa digitale Anwendungsplatform", "type": "platform", "license": "MIT", - "version": "1.0", "autoload": { "psr-4": { "VersaDigitale\\Platform\\": "src/" @@ -19,6 +18,16 @@ "symfony/console": "^7.1", "react/http": "^3@dev", "react/async": "^3@dev", - "secondtruth/phar-compiler": "^1.2" + "secondtruth/phar-compiler": "^1.2", + "symfony/http-kernel": "^7.1", + "symfony/http-foundation": "^7.1", + "doctrine/orm": "^3.3", + "doctrine/dbal": "^4.2", + "symfony/cache": "^7.1", + "nilportugues/sql-query-builder": "^1.8", + "bramus/router": "^1.6", + "symfony/routing": "^7.1", + "symfony/psr-http-message-bridge": "^7.1", + "nyholm/psr7": "^1.8" } } diff --git a/dashboard-module/src/DashboardModule.php b/dashboard-module/src/DashboardModule.php new file mode 100644 index 0000000..9449094 --- /dev/null +++ b/dashboard-module/src/DashboardModule.php @@ -0,0 +1,15 @@ +name = $name; + $this->parent = $parent; + $this->children = new ArrayCollection(); + $this->userGroups = new ArrayCollection(); + $this->users = new ArrayCollection(); + parent::__construct(); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function getChildren(): Collection + { + return $this->children; + } + + public function addChild(PermissionNode $child): void + { + if (!$this->children->contains($child)) { + $this->children->add($child); + $child->parent = $this; + } + } + + public function removeChild(PermissionNode $child): void + { + if ($this->children->removeElement($child)) { + $child->parent = null; + } + } + + public function getUserGroups(): Collection + { + return $this->userGroups; + } + + public function getUsers(): Collection + { + return $this->users; + } +} diff --git a/src/Auth/Session.php b/src/Auth/Session.php new file mode 100644 index 0000000..5cad5f3 --- /dev/null +++ b/src/Auth/Session.php @@ -0,0 +1,31 @@ +user = $user; + $authToken = md5(uniqid(rand(), true)); + } + + public function getAuthToken(): string + { + return $this->authToken; + } + + /** + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + + +} \ No newline at end of file diff --git a/src/Auth/SessionManager.php b/src/Auth/SessionManager.php new file mode 100644 index 0000000..b5d14b0 --- /dev/null +++ b/src/Auth/SessionManager.php @@ -0,0 +1,31 @@ +getAuthToken() === $authToken) { + return $session; + } + } + } + + public static function createSession(User $user): Session + { + $session = new Session($user); + self::$sessions[] = $session; + return $session; + } + + + +} \ No newline at end of file diff --git a/src/Auth/User.php b/src/Auth/User.php new file mode 100644 index 0000000..227e69f --- /dev/null +++ b/src/Auth/User.php @@ -0,0 +1,181 @@ +username = $username; + $this->email = $email; + $this->password = $this->hashPassword($password); + $this->userGroups = new ArrayCollection(); + $this->permissions = new ArrayCollection(); + parent::__construct(); + } + + public function getUsername(): string + { + return $this->username; + } + + public function setUsername(string $username): void + { + $this->username = $username; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): void + { + $this->email = $email; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): void + { + $this->password = $this->hashPassword($password); + } + + public function isActive(): bool + { + return $this->isActive; + } + + public function activate(): void + { + $this->isActive = true; + } + + public function deactivate(): void + { + $this->isActive = false; + } + + public function getLastLoginAt(): ?DateTimeImmutable + { + return $this->lastLoginAt; + } + + public function setLastLoginAt(DateTimeImmutable $lastLoginAt): void + { + $this->lastLoginAt = $lastLoginAt; + } + + public function login(): void + { + $this->lastLoginAt = new DateTimeImmutable(); + $this->updateTimestamps(); + } + + public function getUserGroups(): Collection + { + return $this->userGroups; + } + + public function addUserGroup(UserGroup $group): void + { + if (!$this->userGroups->contains($group)) { + $this->userGroups->add($group); + } + } + + public function removeUserGroup(UserGroup $group): void + { + $this->userGroups->removeElement($group); + } + + public function getPermissions(): Collection + { + return $this->permissions; + } + + public function addPermission(PermissionNode $permission): void + { + if (!$this->permissions->contains($permission)) { + $this->permissions->add($permission); + } + } + + public function removePermission(PermissionNode $permission): void + { + $this->permissions->removeElement($permission); + } + + public function getEffectivePermissions(): Collection + { + $effectivePermissions = new ArrayCollection(); + + // Direct permissions + foreach ($this->permissions as $permission) { + if (!$effectivePermissions->contains($permission)) { + $effectivePermissions->add($permission); + } + } + + // Group permissions + foreach ($this->userGroups as $group) { + foreach ($group->getPermissions() as $permission) { + if (!$effectivePermissions->contains($permission)) { + $effectivePermissions->add($permission); + } + } + } + + return $effectivePermissions; + } + + private function hashPassword(string $password): string + { + if (strlen($password) < 8) { + throw new Exception('Password must be at least 8 characters long.'); + } + return password_hash($password, PASSWORD_BCRYPT); + } + + public function verifyPassword(string $password): bool + { + return password_verify($password, $this->password); + } +} \ No newline at end of file diff --git a/src/Auth/UserGroup.php b/src/Auth/UserGroup.php new file mode 100644 index 0000000..69b521d --- /dev/null +++ b/src/Auth/UserGroup.php @@ -0,0 +1,67 @@ +name = $name; + $this->permissions = new ArrayCollection(); + $this->users = new ArrayCollection(); + parent::__construct(); + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getPermissions(): Collection + { + return $this->permissions; + } + + public function addPermission(PermissionNode $permission): void + { + if (!$this->permissions->contains($permission)) { + $this->permissions->add($permission); + } + } + + public function removePermission(PermissionNode $permission): void + { + $this->permissions->removeElement($permission); + } + + public function getUsers(): Collection + { + return $this->users; + } + +} \ No newline at end of file diff --git a/src/Console/Commands/Server/StartServerCommand.php b/src/Console/Commands/Server/StartServerCommand.php index a3e844a..8114ebc 100644 --- a/src/Console/Commands/Server/StartServerCommand.php +++ b/src/Console/Commands/Server/StartServerCommand.php @@ -6,6 +6,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use VersaDigitale\Platform\Kernel\VersaKernel; use VersaDigitale\Platform\Logging\Log; use VersaDigitale\Platform\Server\VersaServer; @@ -23,12 +24,7 @@ class StartServerCommand extends Command // Deine Logik für den Compiler $ver = phpversion(); Log::info("Starte den versa digitale Platformserver - powered by ReactPHP"); - $server = new VersaServer( - host: "0.0.0.0", - port: 8080 - ); - - $server->start(); + VersaKernel::__startServer(); return Command::SUCCESS; } diff --git a/src/Console/Commands/User/CreateUserCommand.php b/src/Console/Commands/User/CreateUserCommand.php new file mode 100644 index 0000000..ccf38cb --- /dev/null +++ b/src/Console/Commands/User/CreateUserCommand.php @@ -0,0 +1,63 @@ +entityManager = VersaBase::$em; + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Creates a new user') + ->setHelp('This command allows you to create a user...') + ->addArgument('username', InputArgument::REQUIRED, 'The username of the user') + ->addArgument('email', InputArgument::REQUIRED, 'The email of the user') + ->addArgument('password', InputArgument::REQUIRED, 'The password of the user'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + // Retrieve input arguments + $username = $input->getArgument('username'); + $email = $input->getArgument('email'); + $password = $input->getArgument('password'); + + try { + // Create new user instance + $user = new User($username, $email, $password); + + // Persist the user to the database + $this->entityManager->persist($user); + $this->entityManager->flush(); + + $io->success('User created successfully.'); + + return Command::SUCCESS; + } catch (\Exception $e) { + $io->error('Error creating user: ' . $e->getMessage()); + + return Command::FAILURE; + } + } +} \ No newline at end of file diff --git a/src/Console/VersaCLI.php b/src/Console/VersaCLI.php index fd996d5..2999608 100644 --- a/src/Console/VersaCLI.php +++ b/src/Console/VersaCLI.php @@ -7,6 +7,8 @@ use Symfony\Component\Console\Command\ListCommand; use VersaDigitale\Platform\Console\Commands\Development\CompilePlatformCommand; use VersaDigitale\Platform\Console\Commands\Module\CompileModuleCommand; use VersaDigitale\Platform\Console\Commands\Server\StartServerCommand; +use VersaDigitale\Platform\Console\Commands\User\CreateUserCommand; +use VersaDigitale\Platform\Kernel\VersaKernel; /** * Class VersaCLI @@ -22,19 +24,6 @@ class VersaCLI { public ?ConsoleApplication $application = null ) { - $this->application = new ConsoleApplication(); - - $this->application->addCommands([ - new CompilePlatformCommand(), - new CompileModuleCommand(), - new StartServerCommand() - ]); - } - - public function run(): void - { - - // Output pink text to stdout using fwrite $logo = <<application = new ConsoleApplication(); + + $this->application->addCommands([ + new CompilePlatformCommand(), + new CompileModuleCommand(), + new StartServerCommand(), + new CreateUserCommand() + ]); + } + + public function run(): void + { + + // Output pink text to stdout using fwrite + + + $this->application->run(); } diff --git a/src/Database/Model/BaseModel.php b/src/Database/Model/BaseModel.php new file mode 100644 index 0000000..a1005bf --- /dev/null +++ b/src/Database/Model/BaseModel.php @@ -0,0 +1,49 @@ +createdAt = new DateTimeImmutable(); + $this->updatedAt = new DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCreatedAt(): DateTimeInterface + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeInterface + { + return $this->updatedAt; + } + + public function updateTimestamps(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} \ No newline at end of file diff --git a/src/Database/VersaBase.php b/src/Database/VersaBase.php new file mode 100644 index 0000000..e158482 --- /dev/null +++ b/src/Database/VersaBase.php @@ -0,0 +1,52 @@ + 'pdo_sqlite', + 'path' => __DIR__ . '/../../db.sqlite', + ], $config); + +// obtaining the entity manager + self::$em = new EntityManager($connection, $config); + Log::info("Doctrine ORM geladen!"); + self::checkModels(); + } + + public static function checkModels() { + $sm = self::$em->getConnection()->createSchemaManager(); + $models = self::$em->getMetadataFactory()->getAllMetadata(); + $schemaTool = new SchemaTool(self::$em); + Log::info(count($models) . " Modelle gefunden"); + foreach($models as $model) { + if(!$sm->tableExists($model->getTableName()) and !$model->isMappedSuperclass) { + + $schemaTool->createSchema([$model]); + Log::info("Modell \"" . $model->getTableName() . "\" erstellt!"); + } + } + + } + +} \ No newline at end of file diff --git a/src/Kernel/Attributes/Route.php b/src/Kernel/Attributes/Route.php new file mode 100644 index 0000000..a42d409 --- /dev/null +++ b/src/Kernel/Attributes/Route.php @@ -0,0 +1,14 @@ +registerController(InternalAuthController::class); + } +} \ No newline at end of file diff --git a/src/Kernel/VersaKernel.php b/src/Kernel/VersaKernel.php new file mode 100644 index 0000000..cb74606 --- /dev/null +++ b/src/Kernel/VersaKernel.php @@ -0,0 +1,124 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $attributes = $method->getAttributes(Route::class); + + foreach ($attributes as $attribute) { + /** @var Route $route */ + $route = $attribute->newInstance(); + + // Create a Symfony route + $symfonyRoute = new \Symfony\Component\Routing\Route( + $route->path, + ['_controller' => [$controllerClass, $method->getName()]] + ); + $symfonyRoute->setMethods([$route->method]); + + // Add the route to the RouteCollection + self::$routes->add($route->path, $symfonyRoute); + } + } + } catch(\Exception $e) { + Log::error("Error while registering controller routes: ".$e->getMessage()); + } + } + + public static function __handleRequest(Request $request): Response + { + try { + $context = new RequestContext(); + $context->fromRequest($request); + $matcher = new UrlMatcher(self::$routes, $context); + Log::info("Incoming " . $request->getMethod() . " request for path " . $request->getPathInfo()); + try { + $routeInfo = $matcher->match($request->getPathInfo()); + [$controllerClass, $method] = $routeInfo['_controller']; + unset($routeInfo['_controller']); // Remove the controller key from params + + // Call the controller and get the response + $controller = new $controllerClass(); + Log::info("Found controller " . $controllerClass . " with method " . $method); + return $controller->$method($request); + } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) { + return new Response('404 Not Found', 404); + } catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) { + return new Response('405 Method Not Allowed', 405); + } + } catch(\Exception $e) { + return new Response($e->getTraceAsString(), 500); + } + } + + + public static function __startServer(string $host = "0.0.0.0", int $port = 8080) { + if(!self::$isInitialized) { + self::__initKernel(); + } + + self::$server = new VersaServer( + host: $host, + port: $port, + ); + + self::$server->start(); + } + + +} \ No newline at end of file diff --git a/src/Module/AbstractModule.php b/src/Module/AbstractModule.php new file mode 100644 index 0000000..b3de62b --- /dev/null +++ b/src/Module/AbstractModule.php @@ -0,0 +1,10 @@ +name . "\" geladen."); + } + + public static function initModules(): void + { + foreach (self::$modules as $module) { + $module->init(); + Log::info("Modul \"" . $module->name . "\" initialisiert."); + } + } + +} \ No newline at end of file diff --git a/src/Server/VersaServer.php b/src/Server/VersaServer.php index 1cbef46..31af68a 100644 --- a/src/Server/VersaServer.php +++ b/src/Server/VersaServer.php @@ -2,10 +2,15 @@ namespace VersaDigitale\Platform\Server; +use Nyholm\Psr7\Factory\Psr17Factory; use Psr\Http\Message\ServerRequestInterface; use React\Http\HttpServer; use React\Http\Message\Response; use React\Socket\SocketServer; +use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Symfony\Component\HttpFoundation\Request; +use VersaDigitale\Platform\Kernel\VersaKernel; use VersaDigitale\Platform\Logging\Log; class VersaServer @@ -21,12 +26,31 @@ class VersaServer } public function start(): void { - $this->http = new HttpServer(function (ServerRequestInterface $request) { - return Response::plaintext( - "Hello World!\n" - ); + $psr17Factory = new Psr17Factory(); + $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); + $this->http = new HttpServer(function (ServerRequestInterface $request) use($psrHttpFactory) { + $httpFoundationFactory = new HttpFoundationFactory(); + Log::info("Test"); + try { + $symfonyRequest = $httpFoundationFactory->createRequest($request); + $versaRequest = new \VersaDigitale\Platform\Kernel\Http\Request($symfonyRequest->query->all(), $symfonyRequest->request->all(), $symfonyRequest->attributes->all(), $symfonyRequest->cookies->all(), $symfonyRequest->files->all(), $symfonyRequest->server->all(), $symfonyRequest->getContent()); + $versaResponse = VersaKernel::__handleRequest($versaRequest); + + return $psrHttpFactory->createResponse($versaResponse); + } catch(\Exception $e) { + return Response::plaintext($e->getTraceAsString())->withStatus(500); + } + }); + $this->http->on("error", function(\Throwable $e) { + Log::error($e->getTraceAsString()); + }); + $this->socket = new SocketServer("{$this->host}:{$this->port}"); + $this->socket->on("error", function(\Throwable $e) { + Log::error($e->getTraceAsString()); + }); + $this->http->listen($this->socket); Log::info("versa digitale Platform erreichbar unter {$this->host}:{$this->port}"); } diff --git a/versa.json b/versa.json new file mode 100644 index 0000000..20c5360 --- /dev/null +++ b/versa.json @@ -0,0 +1,3 @@ +{ + "base_dir": "example-app" +} \ No newline at end of file