From 86528602662977c953752cd2aee3c443dc1e5db6 Mon Sep 17 00:00:00 2001 From: Alf Drollinger <40421928+adrolli@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:59:11 +0200 Subject: [PATCH] Login-Link, User Session and some improvements to User Device (#516) Co-authored-by: adrolli --- .env.example | 9 +- .github/workflows/monorepo-split-packages.yml | 1 + app/Providers/Filament/AdminPanelProvider.php | 4 + composer.json | 2 + config/login-link.php | 9 + ..._04_25_131401_create_login_links_table.php | 34 ++++ packages/login-link/.github/FUNDING.yml | 13 ++ packages/login-link/.gitignore | 49 +++++ packages/login-link/CHANGELOG.md | 1 + packages/login-link/FUNDING.yml | 13 ++ packages/login-link/LICENSE.md | 21 +++ packages/login-link/README.md | 53 ++++++ packages/login-link/SECURITY.md | 13 ++ packages/login-link/composer.json | 36 ++++ packages/login-link/config/login-link.php | 10 ++ .../create_login_links_table.php.stub | 35 ++++ .../resources/lang/de/translations.php | 20 +++ .../resources/lang/en/translations.php | 21 +++ .../resources/lang/es/translations.php | 16 ++ .../resources/lang/hr/translations.php | 16 ++ .../resources/lang/nb_NO/translations.php | 16 ++ .../resources/lang/ru/translations.php | 16 ++ .../views/emails/loginlink.blade.php | 14 ++ .../resources/views/request.blade.php | 6 + packages/login-link/routes/web.php | 10 ++ .../src/Commands/InstallCommand.php | 163 +++++++++++++++++ .../Http/Controllers/LoginLinkController.php | 98 ++++++++++ packages/login-link/src/LoginLinkPlugin.php | 35 ++++ .../src/LoginLinkServiceProvider.php | 24 +++ .../login-link/src/Mail/LoginLinkEmail.php | 29 +++ packages/login-link/src/Models/LoginLink.php | 51 ++++++ .../src/Resources/LoginLinkResource.php | 170 ++++++++++++++++++ .../LoginLinkResource/Pages/ListPage.php | 41 +++++ .../Widgets/LoginLinkWidgets.php | 30 ++++ .../resources/lang/en/translations.php | 2 +- .../src/Listeners/StoreUserDevice.php | 4 - .../user-device/src/Models/UserDevice.php | 35 +++- .../src/Resources/UserDeviceResource.php | 2 +- packages/user-session/.github/FUNDING.yml | 13 ++ packages/user-session/.gitignore | 49 +++++ packages/user-session/CHANGELOG.md | 1 + packages/user-session/FUNDING.yml | 13 ++ packages/user-session/LICENSE.md | 21 +++ packages/user-session/README.md | 53 ++++++ packages/user-session/SECURITY.md | 13 ++ packages/user-session/composer.json | 36 ++++ packages/user-session/config/user-session.php | 14 ++ .../create_user_sessions_table.php.stub | 32 ++++ .../resources/lang/de/translations.php | 16 ++ .../resources/lang/en/translations.php | 19 ++ .../resources/lang/es/translations.php | 16 ++ .../resources/lang/hr/translations.php | 16 ++ .../resources/lang/nb_NO/translations.php | 16 ++ .../resources/lang/ru/translations.php | 16 ++ .../src/Commands/InstallCommand.php | 163 +++++++++++++++++ .../user-session/src/Models/UserSession.php | 19 ++ .../src/Resources/UserSessionResource.php | 124 +++++++++++++ .../UserSessionResource/Pages/ListPage.php | 41 +++++ .../Widgets/UserSessionWidgets.php | 30 ++++ .../user-session/src/UserSessionPlugin.php | 35 ++++ .../src/UserSessionServiceProvider.php | 23 +++ 61 files changed, 1884 insertions(+), 17 deletions(-) create mode 100644 config/login-link.php create mode 100644 database/migrations/2024_04_25_131401_create_login_links_table.php create mode 100644 packages/login-link/.github/FUNDING.yml create mode 100644 packages/login-link/.gitignore create mode 100644 packages/login-link/CHANGELOG.md create mode 100644 packages/login-link/FUNDING.yml create mode 100644 packages/login-link/LICENSE.md create mode 100644 packages/login-link/README.md create mode 100644 packages/login-link/SECURITY.md create mode 100644 packages/login-link/composer.json create mode 100644 packages/login-link/config/login-link.php create mode 100644 packages/login-link/database/migrations/create_login_links_table.php.stub create mode 100644 packages/login-link/resources/lang/de/translations.php create mode 100644 packages/login-link/resources/lang/en/translations.php create mode 100644 packages/login-link/resources/lang/es/translations.php create mode 100644 packages/login-link/resources/lang/hr/translations.php create mode 100644 packages/login-link/resources/lang/nb_NO/translations.php create mode 100644 packages/login-link/resources/lang/ru/translations.php create mode 100644 packages/login-link/resources/views/emails/loginlink.blade.php create mode 100644 packages/login-link/resources/views/request.blade.php create mode 100644 packages/login-link/routes/web.php create mode 100644 packages/login-link/src/Commands/InstallCommand.php create mode 100644 packages/login-link/src/Http/Controllers/LoginLinkController.php create mode 100644 packages/login-link/src/LoginLinkPlugin.php create mode 100644 packages/login-link/src/LoginLinkServiceProvider.php create mode 100644 packages/login-link/src/Mail/LoginLinkEmail.php create mode 100644 packages/login-link/src/Models/LoginLink.php create mode 100644 packages/login-link/src/Resources/LoginLinkResource.php create mode 100644 packages/login-link/src/Resources/LoginLinkResource/Pages/ListPage.php create mode 100644 packages/login-link/src/Resources/LoginLinkResource/Widgets/LoginLinkWidgets.php create mode 100644 packages/user-session/.github/FUNDING.yml create mode 100644 packages/user-session/.gitignore create mode 100644 packages/user-session/CHANGELOG.md create mode 100644 packages/user-session/FUNDING.yml create mode 100644 packages/user-session/LICENSE.md create mode 100644 packages/user-session/README.md create mode 100644 packages/user-session/SECURITY.md create mode 100644 packages/user-session/composer.json create mode 100644 packages/user-session/config/user-session.php create mode 100644 packages/user-session/database/migrations/create_user_sessions_table.php.stub create mode 100644 packages/user-session/resources/lang/de/translations.php create mode 100644 packages/user-session/resources/lang/en/translations.php create mode 100644 packages/user-session/resources/lang/es/translations.php create mode 100644 packages/user-session/resources/lang/hr/translations.php create mode 100644 packages/user-session/resources/lang/nb_NO/translations.php create mode 100644 packages/user-session/resources/lang/ru/translations.php create mode 100644 packages/user-session/src/Commands/InstallCommand.php create mode 100644 packages/user-session/src/Models/UserSession.php create mode 100644 packages/user-session/src/Resources/UserSessionResource.php create mode 100644 packages/user-session/src/Resources/UserSessionResource/Pages/ListPage.php create mode 100644 packages/user-session/src/Resources/UserSessionResource/Widgets/UserSessionWidgets.php create mode 100644 packages/user-session/src/UserSessionPlugin.php create mode 100644 packages/user-session/src/UserSessionServiceProvider.php diff --git a/.env.example b/.env.example index 08f49bbe4..05c2fe681 100644 --- a/.env.example +++ b/.env.example @@ -32,12 +32,9 @@ REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_MAILER=smtp -MAIL_HOST=mailhog -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS="hello@example.com" +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_FROM_ADDRESS="hello@moox.org" MAIL_FROM_NAME="${APP_NAME}" AWS_ACCESS_KEY_ID= diff --git a/.github/workflows/monorepo-split-packages.yml b/.github/workflows/monorepo-split-packages.yml index 68e6c2ac6..86f04a4dc 100644 --- a/.github/workflows/monorepo-split-packages.yml +++ b/.github/workflows/monorepo-split-packages.yml @@ -21,6 +21,7 @@ jobs: - jobs - user - user-device + - user-session - sync - audit diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 9a48fec90..315da7cc3 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -82,6 +82,10 @@ public function panel(Panel $panel): Panel \Moox\UserDevice\UserDevicePlugin::make(), + \Moox\LoginLink\LoginLinkPlugin::make(), + + \Moox\UserSession\UserSessionPlugin::make(), + ]); } } diff --git a/composer.json b/composer.json index 189538f92..e2559ff9e 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,8 @@ "moox/sync": "*", "moox/user": "*", "moox/user-device": "*", + "moox/login-link": "*", + "moox/user-session": "*", "wikimedia/composer-merge-plugin": "^2.1" }, "require-dev": { diff --git a/config/login-link.php b/config/login-link.php new file mode 100644 index 000000000..38a8c3c98 --- /dev/null +++ b/config/login-link.php @@ -0,0 +1,9 @@ + 2001, + 'user_models' => [ + 'App Users' => \App\Models\User::class, + 'Moox Users' => \Moox\User\Models\User::class, + ], +]; diff --git a/database/migrations/2024_04_25_131401_create_login_links_table.php b/database/migrations/2024_04_25_131401_create_login_links_table.php new file mode 100644 index 000000000..c4fd93200 --- /dev/null +++ b/database/migrations/2024_04_25_131401_create_login_links_table.php @@ -0,0 +1,34 @@ +id(); + $table->unsignedBigInteger('user_id')->nullable; + $table->string('user_type')->nullable; + $table->string('email')->index(); + $table->string('token')->index()->nullable; + $table->dateTime('expires_at'); + $table->string('user_agent')->nullable(); + $table->ipAddress('ip_address')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('login-link'); + } +}; diff --git a/packages/login-link/.github/FUNDING.yml b/packages/login-link/.github/FUNDING.yml new file mode 100644 index 000000000..0446fa42d --- /dev/null +++ b/packages/login-link/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mooxphp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/login-link/.gitignore b/packages/login-link/.gitignore new file mode 100644 index 000000000..6a81cbb00 --- /dev/null +++ b/packages/login-link/.gitignore @@ -0,0 +1,49 @@ +# Environment +.env +.env.backup + +# Composer +/vendor +composer.lock +auth.json + +# NPM / Node +/node_modules +npm-debug.log +package-lock.json + +# Laravel +/public/hot +/public/storage +/storage/*.key + +# PHPUnit +.phpunit.result.cache +phpunit.xml + +# Yarn +yarn-error.log + +# PHPStan +/build +phpstan.neon + +# Testbench +testbench.yaml + +# PHP CS Fixer +.php-cs-fixer.cache + +# Homestead +Homestead.json +Homestead.yaml + +# IDEs +/.idea +/.vscode + +# MacOS +.DS_Store + +# Windows +Thumbs.db diff --git a/packages/login-link/CHANGELOG.md b/packages/login-link/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/packages/login-link/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/login-link/FUNDING.yml b/packages/login-link/FUNDING.yml new file mode 100644 index 000000000..0446fa42d --- /dev/null +++ b/packages/login-link/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mooxphp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/login-link/LICENSE.md b/packages/login-link/LICENSE.md new file mode 100644 index 000000000..7dfc5ad0b --- /dev/null +++ b/packages/login-link/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Moox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/login-link/README.md b/packages/login-link/README.md new file mode 100644 index 000000000..6b62b5c58 --- /dev/null +++ b/packages/login-link/README.md @@ -0,0 +1,53 @@ +![Moox LoginLink](https://github.com/mooxphp/moox/raw/main/art/banner/login-link.jpg) + +# Moox LoginLink + +This is my package login-link + +## Quick Installation + +These two commmands are all you need to install the package: + +```bash +composer require moox/login-link +php artisan mooxlogin-link:install +``` + +Curious what the install command does? See manual installation below. + +## What it does + + + +Here are some things missing, like an overview with screenshots about this package, or simply a link to the package's docs. + + + +## Manual Installation + +Instead of using the install-command `php artisan mooxlogin-link:install` you are able to install this package manually step by step: + +```bash +// Publish and run the migrations: +php artisan vendor:publish --tag="login-link-migrations" +php artisan migrate + +// Publish the config file with: +php artisan vendor:publish --tag="login-link-config" +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Security Vulnerabilities + +Please review [our security policy](https://github.com/mooxphp/moox/security/policy) on how to report security vulnerabilities. + +## Credits + +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/packages/login-link/SECURITY.md b/packages/login-link/SECURITY.md new file mode 100644 index 000000000..5e4eb6307 --- /dev/null +++ b/packages/login-link/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +We maintain the current version of `Moox LoginLink` actively. + +Do not expect security fixes for older versions. + +## Reporting a Vulnerability + +If you find any security-related bug, please report it to security@moox.org. + +Please do not use Github issues, to give us enough time to review and fix the issue, before others can use it, to do stupid things. diff --git a/packages/login-link/composer.json b/packages/login-link/composer.json new file mode 100644 index 000000000..fa9af9f0e --- /dev/null +++ b/packages/login-link/composer.json @@ -0,0 +1,36 @@ +{ + "name": "moox/login-link", + "description": "This is my package login-link", + "keywords": [ + "Laravel", + "Filament", + "Filament plugin", + "Laravel package" + ], + "homepage": "https://moox.org/", + "license": "MIT", + "authors": [ + { + "name": "Moox Developer", + "email": "dev@moox.org", + "role": "Developer" + } + ], + "require": { + "moox/core": "*" + }, + "autoload": { + "psr-4": { + "Moox\\LoginLink\\": "src" + } + }, + "extra": { + "laravel": { + "providers": [ + "Moox\\LoginLink\\LoginLinkServiceProvider" + ] + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/packages/login-link/config/login-link.php b/packages/login-link/config/login-link.php new file mode 100644 index 000000000..dc69b2022 --- /dev/null +++ b/packages/login-link/config/login-link.php @@ -0,0 +1,10 @@ + 2001, + 'expiration_time' => 24, // hours + 'user_models' => [ + 'App Users' => \App\Models\User::class, + 'Moox Users' => \Moox\User\Models\User::class, + ], +]; diff --git a/packages/login-link/database/migrations/create_login_links_table.php.stub b/packages/login-link/database/migrations/create_login_links_table.php.stub new file mode 100644 index 000000000..d2fa844f3 --- /dev/null +++ b/packages/login-link/database/migrations/create_login_links_table.php.stub @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('user_id')->nullable; + $table->string('user_type'); + $table->string('email')->index(); + $table->string('token')->index()->nullable; + $table->dateTime('expires_at'); + $table->string('user_agent')->nullable(); + $table->ipAddress('ip_address')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + + { + Schema::dropIfExists('login-link'); + } +}; diff --git a/packages/login-link/resources/lang/de/translations.php b/packages/login-link/resources/lang/de/translations.php new file mode 100644 index 000000000..dc9f22c09 --- /dev/null +++ b/packages/login-link/resources/lang/de/translations.php @@ -0,0 +1,20 @@ + 'Login Link', + 'plural' => 'Login Links', + 'breadcrumb' => 'Login Link', + 'title' => 'Login Link', + 'navigation_label' => 'Login Link', + 'navigation_group' => 'Moox User', + 'totalone' => 'Login Links Pending', + 'totaltwo' => 'Login Links Used', + 'totalthree' => 'Login Links Invalid', + 'token' => 'Token', + 'expires_at' => 'Expires at', + 'email' => 'E-Mail', + 'user_agent' => 'User Agent', + 'ip_address' => 'IP-Adresse', + 'user_type' => 'User Model', + 'user_id' => 'User ID', +]; diff --git a/packages/login-link/resources/lang/en/translations.php b/packages/login-link/resources/lang/en/translations.php new file mode 100644 index 000000000..6228491a9 --- /dev/null +++ b/packages/login-link/resources/lang/en/translations.php @@ -0,0 +1,21 @@ + 'Login Link', + 'plural' => 'Login Links', + 'breadcrumb' => 'Login Link', + 'title' => 'Login Link', + 'navigation_label' => 'Login Link', + 'navigation_group' => 'Moox User', + 'totalone' => 'Login Links Pending', + 'totaltwo' => 'Login Links Used', + 'totalthree' => 'Login Links Invalid', + 'token' => 'Token', + 'expires_at' => 'Expires at', + 'email' => 'E-Mail', + 'user_agent' => 'User Agent', + 'ip_address' => 'IP-Adresse', + 'user_id' => 'User ID', + 'user_type' => 'User Model', + 'username' => 'Username', +]; diff --git a/packages/login-link/resources/lang/es/translations.php b/packages/login-link/resources/lang/es/translations.php new file mode 100644 index 000000000..e7e6673f7 --- /dev/null +++ b/packages/login-link/resources/lang/es/translations.php @@ -0,0 +1,16 @@ + 'Desarrollador', + 'plural' => 'Desarrolladores', + 'breadcrumb' => 'Desarrollador', + 'title' => 'Desarrollador', + 'navigation_label' => 'Desarrollador', + 'navigation_group' => 'Desarrollador de Moox', + 'totalone' => 'Desarrollador uno', + 'totaltwo' => 'Desarrollador dos', + 'totalthree' => 'Desarrollador tres', + 'name' => 'Nombre', + 'started_at' => 'Empezó a las', + 'failed' => 'failed', +]; diff --git a/packages/login-link/resources/lang/hr/translations.php b/packages/login-link/resources/lang/hr/translations.php new file mode 100644 index 000000000..a145e7504 --- /dev/null +++ b/packages/login-link/resources/lang/hr/translations.php @@ -0,0 +1,16 @@ + '', + 'plural' => '', + 'breadcrumb' => '', + 'title' => '', + 'navigation_label' => '', + 'navigation_group' => '', + 'totalone' => '', + 'totaltwo' => '', + 'totalthree' => '', + 'name' => 'Ime', + 'started_at' => 'Pokrenuto', + 'failed' => 'neuspjelo', +]; diff --git a/packages/login-link/resources/lang/nb_NO/translations.php b/packages/login-link/resources/lang/nb_NO/translations.php new file mode 100644 index 000000000..4fb4f41af --- /dev/null +++ b/packages/login-link/resources/lang/nb_NO/translations.php @@ -0,0 +1,16 @@ + 'Bygger', + 'plural' => 'Byggers', + 'breadcrumb' => 'Bygger', + 'title' => 'Bygger', + 'navigation_label' => 'Bygger', + 'navigation_group' => 'Moox LoginLink', + 'totalone' => 'Bygger én', + 'totaltwo' => 'Bygger to', + 'totalthree' => 'Bygger tre', + 'name' => 'Navn', + 'started_at' => 'Startet', + 'failed' => 'mislyktes', +]; diff --git a/packages/login-link/resources/lang/ru/translations.php b/packages/login-link/resources/lang/ru/translations.php new file mode 100644 index 000000000..38cdbd13b --- /dev/null +++ b/packages/login-link/resources/lang/ru/translations.php @@ -0,0 +1,16 @@ + 'Сборщик', + 'plural' => 'Сборщики', + 'breadcrumb' => 'Сборщик', + 'title' => 'Сборщик', + 'navigation_label' => 'Сборщик', + 'navigation_group' => 'Сборщик Moox', + 'totalone' => 'Первый сборщик', + 'totaltwo' => 'Второй Сборщик', + 'totalthree' => 'Третий Сборщик', + 'name' => 'Имя', + 'started_at' => 'Начато в', + 'failed' => 'Неудача', +]; diff --git a/packages/login-link/resources/views/emails/loginlink.blade.php b/packages/login-link/resources/views/emails/loginlink.blade.php new file mode 100644 index 000000000..2445a6443 --- /dev/null +++ b/packages/login-link/resources/views/emails/loginlink.blade.php @@ -0,0 +1,14 @@ +@component('mail::message') +# Login Link + +Click the button below to log in to your account: + +@component('mail::button', ['url' => $url]) +Login +@endcomponent + +This link will expire in {{ config('loginlink.expiration_time') }} hours. + +Thanks,
+{{ config('app.name') }} +@endcomponent diff --git a/packages/login-link/resources/views/request.blade.php b/packages/login-link/resources/views/request.blade.php new file mode 100644 index 000000000..12a786780 --- /dev/null +++ b/packages/login-link/resources/views/request.blade.php @@ -0,0 +1,6 @@ +
+ @csrf + + + +
diff --git a/packages/login-link/routes/web.php b/packages/login-link/routes/web.php new file mode 100644 index 000000000..dd8366315 --- /dev/null +++ b/packages/login-link/routes/web.php @@ -0,0 +1,10 @@ + ['web']], function () { + Route::get('/login-link', [LoginLinkController::class, 'requestForm'])->name('login-link.request'); + Route::post('/login-link', [LoginLinkController::class, 'sendLink'])->name('login-link.send'); + Route::get('/login-link/{userId}-{token}', [LoginLinkController::class, 'authenticate'])->name('login-link.authenticate'); +}); diff --git a/packages/login-link/src/Commands/InstallCommand.php b/packages/login-link/src/Commands/InstallCommand.php new file mode 100644 index 000000000..c5bd84559 --- /dev/null +++ b/packages/login-link/src/Commands/InstallCommand.php @@ -0,0 +1,163 @@ +art(); + $this->welcome(); + $this->publishConfiguration(); + $this->publishMigrations(); + $this->runMigrations(); + $this->registerPlugins(); + $this->finish(); + } + + public function art(): void + { + info(' + + ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ + ▓▓▒░░▒▓▓▒▒░░░░░░▒▒▓▓▓▒░░░░░░░▒▓▓ ▓▓▓▓▒░░░░░░░▒▓▓▓▓ ▓▓▓▓▓▒░░░░░░░▒▒▓▓▓▓▓▒▒▒▒▓▓ ▓▓▓▒▒▒▒▓▓ + ▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓░░░░░▒▓▓ ▓▓▒░░░░░▓▓ + ▓▒░░░░░░▒▓▓▓▓▒░░░░░░░▒▓▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓▓▓▒░░░░░░░▓▓▓▓░░░░░░▒▓▓▓▓▓░░░░░░▒▓▓░░░░░▒▓▓▓▓▓░░░░░▒▓▓ + ▓▒░░░░▓▓▓▓ ▓▓░░░░░▓▓▓ ▓▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▓░░░░░▓░░░░░░▓▓▓▓ ▓▓▓▒░░░░▓▓▓▒░░░░░▓▓▓░░░░░▓▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▓░░▒░░░░░▓▓▓ ▓▓░░░░▒▓▓▓▓░░░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓ ▓▓▓░░░░░▒▓▓ ▓▓▒░░░░▓ ▓▓▓░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▒░░░░░▒░░▒▓▓ ▓▓░░░░▒▓▓▓▒░░░░░▒░░░░░▒▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▒░░░░░▒▒░░░░░▒▓▓▓ ▓▓▓░░░░░▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓░░░░░░▒▒▓▓▒░░░░░░▒▓▓▓▓░░░░░░░▒▒▓▓▒░░░░░░▓▓▓░░░░░▒▓▓▓▓▓▒░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▒░░░░░▓▓▓ ▓▓▒░░░░░▒▓ + ▓▓░░░▒▓▓ ▓▓▒░░░▒▓▓ ▓▓░░░░▓▓ ▓▓▓▓▒░░░░░░▒▒▓▓▓▓ ▓▓▓▓▓▒▒░░░░░▒▒▓▓▓▓▓░░░░▒▓▓ ▓▓▓░░░░▒▓ + ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ + + '); + } + + public function welcome(): void + { + info('Welcome to Moox LoginLink Installer'); + } + + public function publishConfiguration(): void + { + if (confirm('Do you wish to publish the configuration?', true)) { + if (! File::exists('config/login-link.php')) { + info('Publishing LoginLink Configuration...'); + $this->callSilent('vendor:publish', ['--tag' => 'login-link-config']); + + return; + } + warning('The LoginLink config already exist. The config will not be published.'); + } + } + + public function publishMigrations(): void + { + if (confirm('Do you wish to publish the migrations?', true)) { + if (Schema::hasTable('login_links')) { + warning('The login_links table already exists. The migrations will not be published.'); + + return; + } + info('Publishing LoginLinks Migrations...'); + $this->callSilent('vendor:publish', ['--tag' => 'login-link-migrations']); + } + } + + public function runMigrations(): void + { + if (confirm('Do you wish to run the migrations?', true)) { + info('Running LoginLink Migrations...'); + $this->callSilent('migrate'); + } + } + + public function registerPlugins(): void + { + $providerPath = app_path('Providers/Filament/AdminPanelProvider.php'); + + if (File::exists($providerPath)) { + $content = File::get($providerPath); + + $intend = ' '; + + $namespace = "\Moox\LoginLink"; + + $pluginsToAdd = multiselect( + label: 'These plugins will be installed:', + options: ['LoginLinkPlugin'], + default: ['LoginLinkPlugin'], + ); + + $function = '::make(),'; + + $pattern = '/->plugins\(\[([\s\S]*?)\]\);/'; + $newPlugins = ''; + + foreach ($pluginsToAdd as $plugin) { + $searchPlugin = '/'.$plugin.'/'; + if (preg_match($searchPlugin, $content)) { + warning("$plugin already registered."); + } else { + $newPlugins .= $intend.$namespace.'\\'.$plugin.$function."\n"; + } + } + + if ($newPlugins) { + if (preg_match($pattern, $content)) { + info('Plugins section found. Adding new plugins...'); + + $replacement = "->plugins([$1\n$newPlugins\n ]);"; + $newContent = preg_replace($pattern, $replacement, $content); + } else { + info('Plugins section created. Adding new plugins...'); + + $pluginsSection = " ->plugins([\n$newPlugins\n ]);"; + $placeholderPattern = '/(\->authMiddleware\(\[.*?\]\))\s*\;/s'; + $replacement = "$1\n".$pluginsSection; + $newContent = preg_replace($placeholderPattern, $replacement, $content, 1); + } + + File::put($providerPath, $newContent); + } + } else { + alert('AdminPanelProvider not found. You need to add the plugins manually.'); + } + } + + public function finish(): void + { + note('Moox LoginLink installed successfully. Enjoy!'); + } +} diff --git a/packages/login-link/src/Http/Controllers/LoginLinkController.php b/packages/login-link/src/Http/Controllers/LoginLinkController.php new file mode 100644 index 000000000..180ac9004 --- /dev/null +++ b/packages/login-link/src/Http/Controllers/LoginLinkController.php @@ -0,0 +1,98 @@ +validate(['email' => 'required|email']); + + $result = $this->findUserByEmail($request->email); + if (! $result) { + return back()->withErrors(['email' => 'No user found with this email.']); + } + + $user = $result->user; + + $this->sendLinkInternal($user); + + return back()->with('message', 'Login link has been sent!'); + } + + public function sendLinkInternal($user) + { + $token = Str::random(40); + + $loginLink = LoginLink::create([ + 'user_id' => $user->id, + 'user_type' => get_class($user), + 'email' => $user->email, + 'token' => $token, + 'expires_at' => now()->addHours(config('login-link.expiration_time')), + ]); + + Mail::to($user->email)->send(new LoginLinkEmail($loginLink)); + } + + public function authenticate($userId, $token) + { + try { + $userId = urldecode(decrypt($userId)); + } catch (DecryptException $e) { + return redirect()->route('login')->withErrors(['invalid' => 'Invalid or corrupted link.']); + } + + $loginLink = LoginLink::where('token', $token) + ->where('expires_at', '>', now()) + ->where('user_id', $userId) + ->firstOrFail(); + + if (isset($loginLink->user_type)) { + $userType = $loginLink->user_type; + } else { + $userType = 'App\Models\User'; + } + + $loginLink->update(['token' => 'used']); + + $userModel = Config::get('login-link.user_models.'.$userType, User::class); + $user = $userModel::findOrFail($userId); + Auth::login($user); + + return redirect('/moox')->with('message', 'You have been successfully logged in!'); + } + + private function findUserByEmail($email) + { + $userModels = config('login-link.user_models', []); + + foreach ($userModels as $key => $model) { + $user = $model::where('email', $email)->first(); + if ($user) { + return (object) [ + 'user' => $user, + 'type' => $key, + ]; + } + } + + return null; + } +} diff --git a/packages/login-link/src/LoginLinkPlugin.php b/packages/login-link/src/LoginLinkPlugin.php new file mode 100644 index 000000000..4a7148fa6 --- /dev/null +++ b/packages/login-link/src/LoginLinkPlugin.php @@ -0,0 +1,35 @@ +resources([ + LoginLinkResource::class, + ]); + } + + public function boot(Panel $panel): void + { + // + } + + public static function make(): static + { + return app(static::class); + } +} diff --git a/packages/login-link/src/LoginLinkServiceProvider.php b/packages/login-link/src/LoginLinkServiceProvider.php new file mode 100644 index 000000000..79c40ad8e --- /dev/null +++ b/packages/login-link/src/LoginLinkServiceProvider.php @@ -0,0 +1,24 @@ +name('login-link') + ->hasConfigFile() + ->hasViews() + ->hasTranslations() + ->hasRoute('web') + ->hasMigrations(['create_login_links_table']) + ->hasCommand(InstallCommand::class); + } +} diff --git a/packages/login-link/src/Mail/LoginLinkEmail.php b/packages/login-link/src/Mail/LoginLinkEmail.php new file mode 100644 index 000000000..daa854a81 --- /dev/null +++ b/packages/login-link/src/Mail/LoginLinkEmail.php @@ -0,0 +1,29 @@ +loginLink = $loginLink; + } + + public function build() + { + $userId = urlencode(encrypt($this->loginLink->user_id)); + $url = url("/login-link/{$userId}-{$this->loginLink->token}"); + + return $this->subject('Your Login Link') + ->markdown('login-link::emails.loginlink', ['url' => $url]); + } +} diff --git a/packages/login-link/src/Models/LoginLink.php b/packages/login-link/src/Models/LoginLink.php new file mode 100644 index 000000000..8f464c3fb --- /dev/null +++ b/packages/login-link/src/Models/LoginLink.php @@ -0,0 +1,51 @@ + 'datetime', + ]; + + /** + * The "booting" method of the model. + */ + protected static function boot() + { + parent::boot(); + + LoginLink::creating(function ($item) { + if (empty($item->token)) { + $item->token = Str::random(40); + } + }); + } + + /** + * Get the owning user model. + */ + public function user() + { + if (isset($this->user_type)) { + return $this->belongsTo($this->user_type, 'user_id'); + } else { + return $this->belongsTo('App\Models\User', 'user_id'); + } + } +} diff --git a/packages/login-link/src/Resources/LoginLinkResource.php b/packages/login-link/src/Resources/LoginLinkResource.php new file mode 100644 index 000000000..d3e2c01d2 --- /dev/null +++ b/packages/login-link/src/Resources/LoginLinkResource.php @@ -0,0 +1,170 @@ +schema([ + TextInput::make('email') + ->label(__('login-link::translations.email')) + ->maxLength(255), + TextInput::make('ip_address') + ->label(__('login-link::translations.ip_address')) + ->maxLength(255), + TextInput::make('user_agent') + ->label(__('login-link::translations.user_agent')) + ->maxLength(255) + ->columnSpan(2), + TextInput::make('token') + ->label(__('login-link::translations.token')) + ->maxLength(255), + DateTimePicker::make('expires_at'), + Select::make('user_type') + ->options(function () { + $models = Config::get('login-link.user_models', []); + + return array_flip($models); + }) + ->reactive() + ->afterStateUpdated(function (Set $set, $state) { + $set('user_id', null); + }) + ->required(), + + Select::make('user_id') + ->options(function ($get) { + $userType = $get('user_type'); + if (! $userType) { + return []; + } + + return $userType::query()->pluck('name', 'id')->toArray(); + }) + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('email') + ->label(__('login-link::translations.email')) + ->sortable(), + TextColumn::make('token') + ->label(__('login-link::translations.token')) + ->sortable(), + TextColumn::make('expires_at') + ->label(__('login-link::translations.expires_at')) + ->sortable(), + TextColumn::make('user_agent') + ->label(__('login-link::translations.user_agent')) + ->sortable(), + TextColumn::make('expires_at') + ->label(__('login-link::translations.expires_at')) + ->since() + ->sortable(), + TextColumn::make('ip_address') + ->label(__('login-link::translations.ip_address')) + ->sortable(), + TextColumn::make('user_type') + ->label(__('login-link::translations.user_type')) + ->sortable(), + TextColumn::make('user_id') + ->label(__('login-link::translations.username')) + ->getStateUsing(function ($record) { + return optional($record->user)->name ?? 'unknown'; + }) + ->sortable(), + ]) + ->actions([ + EditAction::make(), + ]) + ->bulkActions([ + DeleteBulkAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListPage::route('/'), + ]; + } + + public static function getWidgets(): array + { + return [ + LoginLinkWidgets::class, + ]; + } + + public static function getModelLabel(): string + { + return __('login-link::translations.single'); + } + + public static function getPluralModelLabel(): string + { + return __('login-link::translations.plural'); + } + + public static function getNavigationLabel(): string + { + return __('login-link::translations.navigation_label'); + } + + public static function getBreadcrumb(): string + { + return __('login-link::translations.breadcrumb'); + } + + public static function shouldRegisterNavigation(): bool + { + return true; + } + + public static function getNavigationBadge(): ?string + { + return number_format(static::getModel()::count()); + } + + public static function getNavigationGroup(): ?string + { + return __('login-link::translations.navigation_group'); + } + + public static function getNavigationSort(): ?int + { + return config('login-link.navigation_sort'); + } +} diff --git a/packages/login-link/src/Resources/LoginLinkResource/Pages/ListPage.php b/packages/login-link/src/Resources/LoginLinkResource/Pages/ListPage.php new file mode 100644 index 000000000..42728c301 --- /dev/null +++ b/packages/login-link/src/Resources/LoginLinkResource/Pages/ListPage.php @@ -0,0 +1,41 @@ +using(function (array $data, string $model): LoginLink { + return $model::create($data); + }), + ]; + } +} diff --git a/packages/login-link/src/Resources/LoginLinkResource/Widgets/LoginLinkWidgets.php b/packages/login-link/src/Resources/LoginLinkResource/Widgets/LoginLinkWidgets.php new file mode 100644 index 000000000..2864fe648 --- /dev/null +++ b/packages/login-link/src/Resources/LoginLinkResource/Widgets/LoginLinkWidgets.php @@ -0,0 +1,30 @@ +select($aggregationColumns) + ->first(); + + return [ + Stat::make(__('login-link::translations.totalone'), $aggregatedInfo->count ?? 0), + Stat::make(__('login-link::translations.totaltwo'), $aggregatedInfo->count ?? 0), + Stat::make(__('login-link::translations.totalthree'), $aggregatedInfo->count ?? 0), + ]; + } +} diff --git a/packages/user-device/resources/lang/en/translations.php b/packages/user-device/resources/lang/en/translations.php index 3c5c82750..1a8bcd917 100644 --- a/packages/user-device/resources/lang/en/translations.php +++ b/packages/user-device/resources/lang/en/translations.php @@ -13,7 +13,7 @@ 'created_at' => 'Created at', 'updated_at' => 'Last login', 'active' => 'Active', - 'user_type' => 'Model', + 'user_type' => 'User Model', 'username' => 'Username', 'slug' => 'Slug', ]; diff --git a/packages/user-device/src/Listeners/StoreUserDevice.php b/packages/user-device/src/Listeners/StoreUserDevice.php index 0b7a0a3e5..ddf9a525e 100644 --- a/packages/user-device/src/Listeners/StoreUserDevice.php +++ b/packages/user-device/src/Listeners/StoreUserDevice.php @@ -5,7 +5,6 @@ use GeoIp2\Database\Reader; use Illuminate\Auth\Events\Login; use Illuminate\Http\Request; -use Illuminate\Support\Str; use Jenssegers\Agent\Agent; use Moox\UserDevice\Models\UserDevice; use Moox\UserDevice\Services\LocationService; @@ -42,8 +41,6 @@ public function handle(Login $event) $title = $platform.' '.$browser.' on '.$os.' in '.($location['city'] ?? '- Unknown').' - '.($location['country'] ?? null); - $slug = Str::slug($title); - $device = UserDevice::updateOrCreate([ 'user_id' => $user_id, 'user_type' => get_class($user), @@ -51,7 +48,6 @@ public function handle(Login $event) 'user_agent' => $userAgent, ], [ 'title' => $title, - 'slug' => $slug, 'active' => true, 'os' => $os, 'platform' => $platform, diff --git a/packages/user-device/src/Models/UserDevice.php b/packages/user-device/src/Models/UserDevice.php index 3e9a766b5..527904ede 100644 --- a/packages/user-device/src/Models/UserDevice.php +++ b/packages/user-device/src/Models/UserDevice.php @@ -10,8 +10,25 @@ class UserDevice extends Model protected $table = 'user_devices'; protected $fillable = [ - 'title', 'slug', 'user_id', 'user_type', 'user_agent', - 'platform', 'os', 'browser', 'city', 'country', 'location', 'whitelisted', 'active', 'ip_address', + 'title', + 'slug', + 'user_id', + 'user_type', + 'user_agent', + 'platform', + 'os', + 'browser', + 'city', + 'country', + 'location', + 'whitelisted', + 'active', + 'ip_address', + ]; + + protected $casts = [ + 'active' => 'bool', + 'whitelisted' => 'bool', ]; /** @@ -21,9 +38,17 @@ protected static function boot() { parent::boot(); - // Automatically generate a slug when creating a new device. - static::creating(function ($device) { - $device->slug = Str::slug($device->title); + UserDevice::creating(function ($item) { + $baseSlug = Str::slug($item->title); + $slug = $baseSlug; + $counter = 1; + + while (UserDevice::where('slug', $slug)->exists()) { + $slug = "{$baseSlug}-{$counter}"; + $counter++; + } + + $item->slug = $slug; }); } diff --git a/packages/user-device/src/Resources/UserDeviceResource.php b/packages/user-device/src/Resources/UserDeviceResource.php index eb58be33d..cce132d54 100644 --- a/packages/user-device/src/Resources/UserDeviceResource.php +++ b/packages/user-device/src/Resources/UserDeviceResource.php @@ -24,7 +24,7 @@ class UserDeviceResource extends Resource { protected static ?string $model = UserDevice::class; - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationIcon = 'heroicon-o-computer-desktop'; public static function form(Form $form): Form { diff --git a/packages/user-session/.github/FUNDING.yml b/packages/user-session/.github/FUNDING.yml new file mode 100644 index 000000000..0446fa42d --- /dev/null +++ b/packages/user-session/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mooxphp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/user-session/.gitignore b/packages/user-session/.gitignore new file mode 100644 index 000000000..6a81cbb00 --- /dev/null +++ b/packages/user-session/.gitignore @@ -0,0 +1,49 @@ +# Environment +.env +.env.backup + +# Composer +/vendor +composer.lock +auth.json + +# NPM / Node +/node_modules +npm-debug.log +package-lock.json + +# Laravel +/public/hot +/public/storage +/storage/*.key + +# PHPUnit +.phpunit.result.cache +phpunit.xml + +# Yarn +yarn-error.log + +# PHPStan +/build +phpstan.neon + +# Testbench +testbench.yaml + +# PHP CS Fixer +.php-cs-fixer.cache + +# Homestead +Homestead.json +Homestead.yaml + +# IDEs +/.idea +/.vscode + +# MacOS +.DS_Store + +# Windows +Thumbs.db diff --git a/packages/user-session/CHANGELOG.md b/packages/user-session/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/packages/user-session/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/user-session/FUNDING.yml b/packages/user-session/FUNDING.yml new file mode 100644 index 000000000..0446fa42d --- /dev/null +++ b/packages/user-session/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [mooxphp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/user-session/LICENSE.md b/packages/user-session/LICENSE.md new file mode 100644 index 000000000..7dfc5ad0b --- /dev/null +++ b/packages/user-session/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Moox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/user-session/README.md b/packages/user-session/README.md new file mode 100644 index 000000000..2da4263e5 --- /dev/null +++ b/packages/user-session/README.md @@ -0,0 +1,53 @@ +![Moox UserSession](https://github.com/mooxphp/moox/raw/main/art/banner/user-session.jpg) + +# Moox UserSession + +This is my package user-session + +## Quick Installation + +These two commmands are all you need to install the package: + +```bash +composer require moox/user-session +php artisan mooxuser-session:install +``` + +Curious what the install command does? See manual installation below. + +## What it does + + + +Here are some things missing, like an overview with screenshots about this package, or simply a link to the package's docs. + + + +## Manual Installation + +Instead of using the install-command `php artisan mooxuser-session:install` you are able to install this package manually step by step: + +```bash +// Publish and run the migrations: +php artisan vendor:publish --tag="user-session-migrations" +php artisan migrate + +// Publish the config file with: +php artisan vendor:publish --tag="user-session-config" +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Security Vulnerabilities + +Please review [our security policy](https://github.com/mooxphp/moox/security/policy) on how to report security vulnerabilities. + +## Credits + +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/packages/user-session/SECURITY.md b/packages/user-session/SECURITY.md new file mode 100644 index 000000000..82d9f794d --- /dev/null +++ b/packages/user-session/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +We maintain the current version of `Moox UserSession` actively. + +Do not expect security fixes for older versions. + +## Reporting a Vulnerability + +If you find any security-related bug, please report it to security@moox.org. + +Please do not use Github issues, to give us enough time to review and fix the issue, before others can use it, to do stupid things. diff --git a/packages/user-session/composer.json b/packages/user-session/composer.json new file mode 100644 index 000000000..dfd34ff9d --- /dev/null +++ b/packages/user-session/composer.json @@ -0,0 +1,36 @@ +{ + "name": "moox/user-session", + "description": "This is my package user-session", + "keywords": [ + "Laravel", + "Filament", + "Filament plugin", + "Laravel package" + ], + "homepage": "https://moox.org/", + "license": "MIT", + "authors": [ + { + "name": "Moox Developer", + "email": "dev@moox.org", + "role": "Developer" + } + ], + "require": { + "moox/core": "*" + }, + "autoload": { + "psr-4": { + "Moox\\UserSession\\": "src" + } + }, + "extra": { + "laravel": { + "providers": [ + "Moox\\UserSession\\UserSessionServiceProvider" + ] + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/packages/user-session/config/user-session.php b/packages/user-session/config/user-session.php new file mode 100644 index 000000000..7bd167c52 --- /dev/null +++ b/packages/user-session/config/user-session.php @@ -0,0 +1,14 @@ + 2001, + 'user_models' => [ + 'App Users' => \App\Models\User::class, + 'Moox Users' => \Moox\User\Models\User::class, + ], + 'device_model' => \Moox\UserDevice\Models\UserDevice::class, + 'session-expiry' => [ + 'Default' => 1, // day + 'Whitelisted' => 365, // days + ], +]; diff --git a/packages/user-session/database/migrations/create_user_sessions_table.php.stub b/packages/user-session/database/migrations/create_user_sessions_table.php.stub new file mode 100644 index 000000000..92238b842 --- /dev/null +++ b/packages/user-session/database/migrations/create_user_sessions_table.php.stub @@ -0,0 +1,32 @@ +id(); + $table->string('name')->nullable(); + $table->timestamp('started_at')->nullable()->index(); + $table->timestamp('finished_at')->nullable(); + $table->boolean('failed')->default(false)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + + { + Schema::dropIfExists('user-session'); + } +}; diff --git a/packages/user-session/resources/lang/de/translations.php b/packages/user-session/resources/lang/de/translations.php new file mode 100644 index 000000000..263a0962d --- /dev/null +++ b/packages/user-session/resources/lang/de/translations.php @@ -0,0 +1,16 @@ + 'Session', + 'plural' => 'Sessions', + 'breadcrumb' => 'Session', + 'title' => 'Sessions', + 'navigation_label' => 'Sessions', + 'navigation_group' => 'Moox User', + 'totalone' => 'Active Sessions', + 'totaltwo' => 'Unused Sessions', + 'totalthree' => 'Expired Sessions', + 'name' => 'Name', + 'started_at' => 'Gestartet am', + 'failed' => 'failed', +]; diff --git a/packages/user-session/resources/lang/en/translations.php b/packages/user-session/resources/lang/en/translations.php new file mode 100644 index 000000000..b52200813 --- /dev/null +++ b/packages/user-session/resources/lang/en/translations.php @@ -0,0 +1,19 @@ + 'Session', + 'plural' => 'Sessions', + 'breadcrumb' => 'Session', + 'title' => 'Sessions', + 'navigation_label' => 'Sessions', + 'navigation_group' => 'Moox User', + 'totalone' => 'Active Sessions', + 'totaltwo' => 'Unused Sessions', + 'totalthree' => 'Expired Sessions', + 'name' => 'Name', + 'started_at' => 'Started at', + 'failed' => 'failed', + 'user_id' => 'User ID', + 'id' => 'Session', + 'ip_address' => 'IP-Address', +]; diff --git a/packages/user-session/resources/lang/es/translations.php b/packages/user-session/resources/lang/es/translations.php new file mode 100644 index 000000000..e7e6673f7 --- /dev/null +++ b/packages/user-session/resources/lang/es/translations.php @@ -0,0 +1,16 @@ + 'Desarrollador', + 'plural' => 'Desarrolladores', + 'breadcrumb' => 'Desarrollador', + 'title' => 'Desarrollador', + 'navigation_label' => 'Desarrollador', + 'navigation_group' => 'Desarrollador de Moox', + 'totalone' => 'Desarrollador uno', + 'totaltwo' => 'Desarrollador dos', + 'totalthree' => 'Desarrollador tres', + 'name' => 'Nombre', + 'started_at' => 'Empezó a las', + 'failed' => 'failed', +]; diff --git a/packages/user-session/resources/lang/hr/translations.php b/packages/user-session/resources/lang/hr/translations.php new file mode 100644 index 000000000..a145e7504 --- /dev/null +++ b/packages/user-session/resources/lang/hr/translations.php @@ -0,0 +1,16 @@ + '', + 'plural' => '', + 'breadcrumb' => '', + 'title' => '', + 'navigation_label' => '', + 'navigation_group' => '', + 'totalone' => '', + 'totaltwo' => '', + 'totalthree' => '', + 'name' => 'Ime', + 'started_at' => 'Pokrenuto', + 'failed' => 'neuspjelo', +]; diff --git a/packages/user-session/resources/lang/nb_NO/translations.php b/packages/user-session/resources/lang/nb_NO/translations.php new file mode 100644 index 000000000..3ca53f2aa --- /dev/null +++ b/packages/user-session/resources/lang/nb_NO/translations.php @@ -0,0 +1,16 @@ + 'Bygger', + 'plural' => 'Byggers', + 'breadcrumb' => 'Bygger', + 'title' => 'Bygger', + 'navigation_label' => 'Bygger', + 'navigation_group' => 'Moox UserSession', + 'totalone' => 'Bygger én', + 'totaltwo' => 'Bygger to', + 'totalthree' => 'Bygger tre', + 'name' => 'Navn', + 'started_at' => 'Startet', + 'failed' => 'mislyktes', +]; diff --git a/packages/user-session/resources/lang/ru/translations.php b/packages/user-session/resources/lang/ru/translations.php new file mode 100644 index 000000000..38cdbd13b --- /dev/null +++ b/packages/user-session/resources/lang/ru/translations.php @@ -0,0 +1,16 @@ + 'Сборщик', + 'plural' => 'Сборщики', + 'breadcrumb' => 'Сборщик', + 'title' => 'Сборщик', + 'navigation_label' => 'Сборщик', + 'navigation_group' => 'Сборщик Moox', + 'totalone' => 'Первый сборщик', + 'totaltwo' => 'Второй Сборщик', + 'totalthree' => 'Третий Сборщик', + 'name' => 'Имя', + 'started_at' => 'Начато в', + 'failed' => 'Неудача', +]; diff --git a/packages/user-session/src/Commands/InstallCommand.php b/packages/user-session/src/Commands/InstallCommand.php new file mode 100644 index 000000000..8a12455bd --- /dev/null +++ b/packages/user-session/src/Commands/InstallCommand.php @@ -0,0 +1,163 @@ +art(); + $this->welcome(); + $this->publishConfiguration(); + $this->publishMigrations(); + $this->runMigrations(); + $this->registerPlugins(); + $this->finish(); + } + + public function art(): void + { + info(' + + ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ + ▓▓▒░░▒▓▓▒▒░░░░░░▒▒▓▓▓▒░░░░░░░▒▓▓ ▓▓▓▓▒░░░░░░░▒▓▓▓▓ ▓▓▓▓▓▒░░░░░░░▒▒▓▓▓▓▓▒▒▒▒▓▓ ▓▓▓▒▒▒▒▓▓ + ▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓░░░░░▒▓▓ ▓▓▒░░░░░▓▓ + ▓▒░░░░░░▒▓▓▓▓▒░░░░░░░▒▓▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓▓▓▒░░░░░░░▓▓▓▓░░░░░░▒▓▓▓▓▓░░░░░░▒▓▓░░░░░▒▓▓▓▓▓░░░░░▒▓▓ + ▓▒░░░░▓▓▓▓ ▓▓░░░░░▓▓▓ ▓▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▓░░░░░▓░░░░░░▓▓▓▓ ▓▓▓▒░░░░▓▓▓▒░░░░░▓▓▓░░░░░▓▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▓░░▒░░░░░▓▓▓ ▓▓░░░░▒▓▓▓▓░░░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓ ▓▓▓░░░░░▒▓▓ ▓▓▒░░░░▓ ▓▓▓░░░░░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓░░░░▒▓▓ ▓▓▒░░░░░▒░░▒▓▓ ▓▓░░░░▒▓▓▓▒░░░░░▒░░░░░▒▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓░░░░▒▓▓▓ ▓▓▓▒░░░░░▒▒░░░░░▒▓▓▓ ▓▓▓░░░░░▓▓▓░░░░░▒▓▓▓░░░░░▒▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓░░░░░░▒▒▓▓▒░░░░░░▒▓▓▓▓░░░░░░░▒▒▓▓▒░░░░░░▓▓▓░░░░░▒▓▓▓▓▓▒░░░░░▓▓ + ▓▒░░░░▒▓ ▓▓░░░░░▓▓ ▓▓░░░░▒▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▓ ▓▓▓▓▒░░░░░░░░░░░░░▒▓▓▒░░░░░▓▓▓ ▓▓▒░░░░░▒▓ + ▓▓░░░▒▓▓ ▓▓▒░░░▒▓▓ ▓▓░░░░▓▓ ▓▓▓▓▒░░░░░░▒▒▓▓▓▓ ▓▓▓▓▓▒▒░░░░░▒▒▓▓▓▓▓░░░░▒▓▓ ▓▓▓░░░░▒▓ + ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ + + '); + } + + public function welcome(): void + { + info('Welcome to Moox UserSession Installer'); + } + + public function publishConfiguration(): void + { + if (confirm('Do you wish to publish the configuration?', true)) { + if (! File::exists('config/user-session.php')) { + info('Publishing UserSession Configuration...'); + $this->callSilent('vendor:publish', ['--tag' => 'user-session-config']); + + return; + } + warning('The UserSession config already exist. The config will not be published.'); + } + } + + public function publishMigrations(): void + { + if (confirm('Do you wish to publish the migrations?', true)) { + if (Schema::hasTable('user_sessions')) { + warning('The user_sessions table already exists. The migrations will not be published.'); + + return; + } + info('Publishing UserSessions Migrations...'); + $this->callSilent('vendor:publish', ['--tag' => 'user-session-migrations']); + } + } + + public function runMigrations(): void + { + if (confirm('Do you wish to run the migrations?', true)) { + info('Running UserSession Migrations...'); + $this->callSilent('migrate'); + } + } + + public function registerPlugins(): void + { + $providerPath = app_path('Providers/Filament/AdminPanelProvider.php'); + + if (File::exists($providerPath)) { + $content = File::get($providerPath); + + $intend = ' '; + + $namespace = "\Moox\UserSession"; + + $pluginsToAdd = multiselect( + label: 'These plugins will be installed:', + options: ['UserSessionPlugin'], + default: ['UserSessionPlugin'], + ); + + $function = '::make(),'; + + $pattern = '/->plugins\(\[([\s\S]*?)\]\);/'; + $newPlugins = ''; + + foreach ($pluginsToAdd as $plugin) { + $searchPlugin = '/'.$plugin.'/'; + if (preg_match($searchPlugin, $content)) { + warning("$plugin already registered."); + } else { + $newPlugins .= $intend.$namespace.'\\'.$plugin.$function."\n"; + } + } + + if ($newPlugins) { + if (preg_match($pattern, $content)) { + info('Plugins section found. Adding new plugins...'); + + $replacement = "->plugins([$1\n$newPlugins\n ]);"; + $newContent = preg_replace($pattern, $replacement, $content); + } else { + info('Plugins section created. Adding new plugins...'); + + $pluginsSection = " ->plugins([\n$newPlugins\n ]);"; + $placeholderPattern = '/(\->authMiddleware\(\[.*?\]\))\s*\;/s'; + $replacement = "$1\n".$pluginsSection; + $newContent = preg_replace($placeholderPattern, $replacement, $content, 1); + } + + File::put($providerPath, $newContent); + } + } else { + alert('AdminPanelProvider not found. You need to add the plugins manually.'); + } + } + + public function finish(): void + { + note('Moox UserSession installed successfully. Enjoy!'); + } +} diff --git a/packages/user-session/src/Models/UserSession.php b/packages/user-session/src/Models/UserSession.php new file mode 100644 index 000000000..73a9cf433 --- /dev/null +++ b/packages/user-session/src/Models/UserSession.php @@ -0,0 +1,19 @@ +schema([ + //TextInput::make('id') + // ->maxLength(255), + TextInput::make('user_id') + ->maxLength(255), + TextInput::make('ip_address') + ->maxLength(255), + TextInput::make('user_agent') + ->maxLength(255), + //Textarea::make('payload'), + //TextInput::make('last_activity') + // ->maxLength(255), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('id') + ->label(__('user-session::translations.id')) + ->sortable(), + TextColumn::make('user_id') + ->label(__('user-session::translations.user_id')) + ->sortable(), + TextColumn::make('ip_address') + ->label(__('user-session::translations.ip_address')) + ->sortable(), + ]) + ->defaultSort('id', 'desc') + ->actions([ + EditAction::make(), + ]) + ->bulkActions([ + DeleteBulkAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListPage::route('/'), + ]; + } + + public static function getWidgets(): array + { + return [ + UserSessionWidgets::class, + ]; + } + + public static function getModelLabel(): string + { + return __('user-session::translations.single'); + } + + public static function getPluralModelLabel(): string + { + return __('user-session::translations.plural'); + } + + public static function getNavigationLabel(): string + { + return __('user-session::translations.navigation_label'); + } + + public static function getBreadcrumb(): string + { + return __('user-session::translations.breadcrumb'); + } + + public static function shouldRegisterNavigation(): bool + { + return true; + } + + public static function getNavigationBadge(): ?string + { + return number_format(static::getModel()::count()); + } + + public static function getNavigationGroup(): ?string + { + return __('user-session::translations.navigation_group'); + } + + public static function getNavigationSort(): ?int + { + return config('user-session.navigation_sort'); + } +} diff --git a/packages/user-session/src/Resources/UserSessionResource/Pages/ListPage.php b/packages/user-session/src/Resources/UserSessionResource/Pages/ListPage.php new file mode 100644 index 000000000..0b9b080e0 --- /dev/null +++ b/packages/user-session/src/Resources/UserSessionResource/Pages/ListPage.php @@ -0,0 +1,41 @@ +using(function (array $data, string $model): UserSession { + return $model::create($data); + }), + ]; + } +} diff --git a/packages/user-session/src/Resources/UserSessionResource/Widgets/UserSessionWidgets.php b/packages/user-session/src/Resources/UserSessionResource/Widgets/UserSessionWidgets.php new file mode 100644 index 000000000..f1c82c6cd --- /dev/null +++ b/packages/user-session/src/Resources/UserSessionResource/Widgets/UserSessionWidgets.php @@ -0,0 +1,30 @@ +select($aggregationColumns) + ->first(); + + return [ + Stat::make(__('user-session::translations.totalone'), $aggregatedInfo->count ?? 0), + Stat::make(__('user-session::translations.totaltwo'), $aggregatedInfo->count ?? 0), + Stat::make(__('user-session::translations.totalthree'), $aggregatedInfo->count ?? 0), + ]; + } +} diff --git a/packages/user-session/src/UserSessionPlugin.php b/packages/user-session/src/UserSessionPlugin.php new file mode 100644 index 000000000..471bb88b7 --- /dev/null +++ b/packages/user-session/src/UserSessionPlugin.php @@ -0,0 +1,35 @@ +resources([ + UserSessionResource::class, + ]); + } + + public function boot(Panel $panel): void + { + // + } + + public static function make(): static + { + return app(static::class); + } +} diff --git a/packages/user-session/src/UserSessionServiceProvider.php b/packages/user-session/src/UserSessionServiceProvider.php new file mode 100644 index 000000000..4a55bfb3e --- /dev/null +++ b/packages/user-session/src/UserSessionServiceProvider.php @@ -0,0 +1,23 @@ +name('user-session') + ->hasConfigFile() + ->hasViews() + ->hasTranslations() + ->hasMigrations(['create_user_sessions_table']) + ->hasCommand(InstallCommand::class); + } +}