From 88aa9b1ad84841d05740a468ff2d96cc4a2d5e6d Mon Sep 17 00:00:00 2001 From: Refringe Date: Thu, 26 Sep 2024 16:55:44 -0400 Subject: [PATCH 01/15] Discord OAuth Creates the base structure for Discord OAuth. --- .env.full | 5 + .env.light | 5 + app/Http/Controllers/SocialiteController.php | 93 ++++ app/Models/OAuthConnection.php | 19 + app/Models/User.php | 11 +- app/Providers/AppServiceProvider.php | 10 + bootstrap/providers.php | 1 + composer.json | 2 + composer.lock | 497 +++++++++++++++++- config/services.php | 8 + database/factories/OAuthConnectionFactory.php | 21 + .../0001_01_01_000000_create_users_table.php | 8 +- ...26_164844_create_oauth_providers_table.php | 38 ++ resources/views/auth/login.blade.php | 5 + routes/web.php | 12 +- tests/Feature/User/AuthenticationTest.php | 19 + 16 files changed, 743 insertions(+), 11 deletions(-) create mode 100644 app/Http/Controllers/SocialiteController.php create mode 100644 app/Models/OAuthConnection.php create mode 100644 database/factories/OAuthConnectionFactory.php create mode 100644 database/migrations/2024_09_26_164844_create_oauth_providers_table.php diff --git a/.env.full b/.env.full index 5696afa..48ef9ae 100644 --- a/.env.full +++ b/.env.full @@ -92,3 +92,8 @@ GITEA_TOKEN= # API key for Scribe documentation. SCRIBE_AUTH_KEY= + +# Discord OAuth Credentials +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_REDIRECT_URI=${APP_URL}/login/discord/callback diff --git a/.env.light b/.env.light index 947270c..3b84afc 100644 --- a/.env.light +++ b/.env.light @@ -45,3 +45,8 @@ MAIL_FROM_NAME="${APP_NAME}" # API key for Scribe documentation. SCRIBE_AUTH_KEY= + +# Discord OAuth Credentials +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_REDIRECT_URI=${APP_URL}/login/discord/callback diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php new file mode 100644 index 0000000..3f5c5b5 --- /dev/null +++ b/app/Http/Controllers/SocialiteController.php @@ -0,0 +1,93 @@ +providers)) { + return redirect()->route('login')->withErrors(__('Unsupported OAuth provider.')); + } + + return Socialite::driver('discord') + ->scopes([ + 'identify', + 'email', + ]) + ->redirect(); + } + + /** + * Obtain the user information from the provider. + */ + public function callback(string $provider): RedirectResponse + { + if (! in_array($provider, $this->providers)) { + return redirect()->route('login')->withErrors(__('Unsupported OAuth provider.')); + } + + try { + $providerUser = Socialite::driver($provider)->user(); + } catch (\Exception $e) { + return redirect()->route('login')->withErrors('Unable to login using '.$provider.'. Please try again.'); + } + + $user = $this->findOrCreateUser($provider, $providerUser); + + Auth::login($user, remember: true); + + return redirect()->route('dashboard'); + } + + protected function findOrCreateUser(string $provider, ProviderUser $providerUser): User + { + $oauthConnection = OAuthConnection::whereProvider($provider) + ->whereProviderId($providerUser->getId()) + ->first(); + + if ($oauthConnection) { + $oauthConnection->update([ + 'token' => $providerUser->token, + 'refresh_token' => $providerUser->refreshToken ?? null, + ]); + + return $oauthConnection->user; + } + + // The user has not connected their account with this OAuth provider before, so a new connection needs to be + // established. Check if the user has an account with the same email address that's passed in from the provider. + // If one exists, connect that account. Otherwise, create a new one. + + return DB::transaction(function () use ($providerUser, $provider) { + $user = User::firstOrCreate(['email' => $providerUser->getEmail()], [ + 'name' => $providerUser->getName() ?? $providerUser->getNickname(), + 'password' => null, + ]); + $user->oAuthConnections()->create([ + 'provider' => $provider, + 'provider_id' => $providerUser->getId(), + 'token' => $providerUser->token, + 'refresh_token' => $providerUser->refreshToken ?? null, + ]); + + return $user; + }); + } +} diff --git a/app/Models/OAuthConnection.php b/app/Models/OAuthConnection.php new file mode 100644 index 0000000..e406fb4 --- /dev/null +++ b/app/Models/OAuthConnection.php @@ -0,0 +1,19 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8acc7db..21715d9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; @@ -211,7 +212,15 @@ class User extends Authenticatable implements MustVerifyEmail } /** - * Handle the about default value if empty. Thanks MySQL! + * The relationship between a user and their OAuth providers. + */ + public function oAuthConnections(): HasMany + { + return $this->hasMany(OAuthConnection::class); + } + + /** + * Handle the about default value if empty. Thanks, MySQL! */ protected function about(): Attribute { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0b307fd..83058d0 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,9 +13,12 @@ use App\Observers\ModVersionObserver; use App\Observers\SptVersionObserver; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Number; use Illuminate\Support\ServiceProvider; +use SocialiteProviders\Discord\Provider; +use SocialiteProviders\Manager\SocialiteWasCalled; class AppServiceProvider extends ServiceProvider { @@ -35,8 +38,10 @@ class AppServiceProvider extends ServiceProvider // Allow mass assignment for all models. Be careful! Model::unguard(); + // Register model observers. $this->registerObservers(); + // Register custom macros. $this->registerNumberMacros(); $this->registerCarbonMacros(); @@ -44,6 +49,11 @@ class AppServiceProvider extends ServiceProvider Gate::define('viewPulse', function (User $user) { return $user->isAdmin(); }); + + // Register the Discord socialite provider. + Event::listen(function (SocialiteWasCalled $event) { + $event->extendSocialite('discord', Provider::class); + }); } /** diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 95d193c..843676d 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -6,4 +6,5 @@ return [ App\Providers\HorizonServiceProvider::class, App\Providers\JetstreamServiceProvider::class, App\Providers\Filament\AdminPanelProvider::class, + \SocialiteProviders\Manager\ServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 9de99a3..e30016e 100644 --- a/composer.json +++ b/composer.json @@ -19,12 +19,14 @@ "laravel/pulse": "^1.2", "laravel/sanctum": "^4.0", "laravel/scout": "^10.10", + "laravel/socialite": "^5.16", "laravel/tinker": "^2.9", "league/flysystem-aws-s3-v3": "^3.28", "league/html-to-markdown": "^5.1", "livewire/livewire": "^3.5", "mchev/banhammer": "^2.3", "meilisearch/meilisearch-php": "^1.8", + "socialiteproviders/discord": "^4.2", "stevebauman/purify": "^6.2" }, "require-dev": { diff --git a/composer.lock b/composer.lock index ab20e13..38b0b5e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "645a73e7a66339c2396753a2f35c5eea", + "content-hash": "2e603ffae8f6f8a4c834c9bff55cc30f", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -1867,6 +1867,69 @@ }, "time": "2024-09-23T14:10:16+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.10.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "500501c2ce893c824c801da135d02661199f60c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" + }, + "time": "2024-05-18T18:05:11+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -3473,6 +3536,78 @@ }, "time": "2024-08-02T07:48:17+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.16.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "league/oauth1-client": "^1.10.1", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.3|^10.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ], + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2024-09-03T09:46:57+00:00" + }, { "name": "laravel/tinker", "version": "v2.9.0", @@ -4147,6 +4282,82 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + }, + "time": "2022-04-15T14:02:14+00:00" + }, { "name": "league/uri", "version": "7.4.1", @@ -5390,6 +5601,56 @@ }, "time": "2024-05-08T12:36:18+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "php-http/discovery", "version": "1.19.4", @@ -5544,6 +5805,116 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.42", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-09-16T03:06:04+00:00" + }, { "name": "pragmarx/google2fa", "version": "v8.0.3", @@ -6439,6 +6810,130 @@ ], "time": "2024-02-26T18:08:49+00:00" }, + { + "name": "socialiteproviders/discord", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Discord.git", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Discord\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "description": "Discord OAuth2 Provider for Laravel Socialite", + "keywords": [ + "discord", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/discord", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-07-24T23:28:47+00:00" + }, + { + "name": "socialiteproviders/manager", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/dea5190981c31b89e52259da9ab1ca4e2b258b21", + "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "laravel/socialite": "^5.5", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "SocialiteProviders\\Manager\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SocialiteProviders\\Manager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy Wendt", + "email": "andy@awendt.com" + }, + { + "name": "Anton Komarev", + "email": "a.komarev@cybercog.su" + }, + { + "name": "Miguel Piedrafita", + "email": "soy@miguelpiedrafita.com" + }, + { + "name": "atymic", + "email": "atymicq@gmail.com", + "homepage": "https://atymic.dev" + } + ], + "description": "Easily add new or override built-in providers in Laravel Socialite.", + "homepage": "https://socialiteproviders.com", + "keywords": [ + "laravel", + "manager", + "oauth", + "providers", + "socialite" + ], + "support": { + "issues": "https://github.com/socialiteproviders/manager/issues", + "source": "https://github.com/socialiteproviders/manager" + }, + "time": "2024-05-04T07:57:39+00:00" + }, { "name": "spatie/color", "version": "1.6.0", diff --git a/config/services.php b/config/services.php index beca5d4..89df54e 100644 --- a/config/services.php +++ b/config/services.php @@ -40,4 +40,12 @@ return [ 'token' => env('GITEA_TOKEN', ''), ], + 'discord' => [ + 'client_id' => env('DISCORD_CLIENT_ID'), + 'client_secret' => env('DISCORD_CLIENT_SECRET'), + 'redirect' => env('DISCORD_REDIRECT_URI'), + 'allow_gif_avatars' => (bool) env('DISCORD_AVATAR_GIF', true), + 'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'png'), + ], + ]; diff --git a/database/factories/OAuthConnectionFactory.php b/database/factories/OAuthConnectionFactory.php new file mode 100644 index 0000000..7338ea3 --- /dev/null +++ b/database/factories/OAuthConnectionFactory.php @@ -0,0 +1,21 @@ +id(); - $table->bigInteger('hub_id') - ->nullable() - ->default(null) - ->unique(); + $table->bigInteger('hub_id')->nullable()->default(null)->unique(); + $table->unsignedBigInteger('discord_id')->nullable()->default(null)->unique(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->string('password')->nullable(); $table->longText('about')->nullable()->default(null); $table->foreignIdFor(UserRole::class) ->nullable() diff --git a/database/migrations/2024_09_26_164844_create_oauth_providers_table.php b/database/migrations/2024_09_26_164844_create_oauth_providers_table.php new file mode 100644 index 0000000..9184073 --- /dev/null +++ b/database/migrations/2024_09_26_164844_create_oauth_providers_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignIdFor(User::class) + ->constrained('users') + ->cascadeOnDelete() + ->cascadeOnUpdate(); + $table->string('provider'); + $table->string('provider_id'); + $table->string('token')->default(''); + $table->string('refresh_token')->default(''); + $table->timestamps(); + + $table->unique(['provider', 'provider_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_providers'); + } +}; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index e435017..c244022 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -44,5 +44,10 @@ + + + diff --git a/routes/web.php b/routes/web.php index d718b8b..a47e9eb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,11 +2,17 @@ use App\Http\Controllers\ModController; use App\Http\Controllers\ModVersionController; +use App\Http\Controllers\SocialiteController; use App\Http\Controllers\UserController; use Illuminate\Support\Facades\Route; Route::middleware(['auth.banned'])->group(function () { + Route::controller(SocialiteController::class)->group(function () { + Route::get('/login/{provider}/redirect', 'redirect')->name('login.socialite'); + Route::get('/login/{provider}/callback', 'callback'); + }); + Route::get('/', function () { return view('home'); })->name('home'); @@ -16,12 +22,10 @@ Route::middleware(['auth.banned'])->group(function () { Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show'); }); + // Download Link Route::controller(ModVersionController::class)->group(function () { Route::get('/mod/download/{mod}/{slug}/{version}', 'show') - ->where([ - 'mod' => '[0-9]+', - 'slug' => '[a-z0-9-]+', - ]) + ->where(['mod' => '[0-9]+', 'slug' => '[a-z0-9-]+']) ->name('mod.version.download'); }); diff --git a/tests/Feature/User/AuthenticationTest.php b/tests/Feature/User/AuthenticationTest.php index 9244fff..9f705eb 100755 --- a/tests/Feature/User/AuthenticationTest.php +++ b/tests/Feature/User/AuthenticationTest.php @@ -30,3 +30,22 @@ test('users cannot authenticate with invalid password', function () { $this->assertGuest(); }); + +test('users can authenticate using Discord', function () { + $response = $this->get('/auth/discord/redirect'); + + $response->assertStatus(302); + $response->assertSessionHas('url.intended', route('dashboard', absolute: false)); +}); + +test('user can not authenticate using a null password', function () { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => null, + ]); + + $this->assertGuest(); + $response->assertSessionHasErrors('password'); +}); From 7ad9682be05067f8997a5c3c9a7cba5528a71e9f Mon Sep 17 00:00:00 2001 From: Refringe Date: Fri, 27 Sep 2024 12:00:57 -0400 Subject: [PATCH 02/15] Style Discord Login Button --- resources/views/auth/login.blade.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index c244022..42825fa 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -45,9 +45,20 @@ +
+ +
+ OR +
+
- + + + + + {{ __('Login with Discord') }} + From bba61fa814c813892864563889a7b8f3dd4f3d1b Mon Sep 17 00:00:00 2001 From: Refringe Date: Fri, 27 Sep 2024 12:04:01 -0400 Subject: [PATCH 03/15] Conditionally show the Discord login button --- resources/views/auth/login.blade.php | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 42825fa..d18d6b3 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -45,20 +45,22 @@ -
- - - - - - {{ __('Login with Discord') }} - + + + + + {{ __('Login with Discord') }} + + @endif From 46550b5d8f0c06506036ae3ff26273834d110d55 Mon Sep 17 00:00:00 2001 From: Refringe Date: Fri, 27 Sep 2024 16:51:13 -0400 Subject: [PATCH 04/15] OAuth Account Password Creation This allows a user that was created via OAuth to set a local password on their account. --- app/Livewire/Profile/UpdatePasswordForm.php | 50 +++++ app/Providers/AppServiceProvider.php | 13 ++ database/factories/OAuthConnectionFactory.php | 10 +- .../profile/update-password-form.blade.php | 17 +- tests/Feature/User/OAuthAccountTest.php | 172 ++++++++++++++++++ 5 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 app/Livewire/Profile/UpdatePasswordForm.php create mode 100644 tests/Feature/User/OAuthAccountTest.php diff --git a/app/Livewire/Profile/UpdatePasswordForm.php b/app/Livewire/Profile/UpdatePasswordForm.php new file mode 100644 index 0000000..0c01ff1 --- /dev/null +++ b/app/Livewire/Profile/UpdatePasswordForm.php @@ -0,0 +1,50 @@ +resetErrorBag(); + + $user = Auth::user(); + + if ($user->password !== null) { + parent::updatePassword($updater); + } else { + + // User has a null password. Allow them to set a new password without their current password. + Validator::make($this->state, [ + 'password' => $this->passwordRules(), + ])->validateWithBag('updatePassword'); + + auth()->user()->forceFill([ + 'password' => Hash::make($this->state['password']), + ])->save(); + + $this->state = [ + 'current_password' => '', + 'password' => '', + 'password_confirmation' => '', + ]; + + $this->dispatch('saved'); + } + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 83058d0..d1ee748 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Livewire\Profile\UpdatePasswordForm; use App\Models\Mod; use App\Models\ModDependency; use App\Models\ModVersion; @@ -17,6 +18,7 @@ use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Number; use Illuminate\Support\ServiceProvider; +use Livewire\Livewire; use SocialiteProviders\Discord\Provider; use SocialiteProviders\Manager\SocialiteWasCalled; @@ -45,6 +47,9 @@ class AppServiceProvider extends ServiceProvider $this->registerNumberMacros(); $this->registerCarbonMacros(); + // Register Livewire component overrides. + $this->registerLivewireOverrides(); + // This gate determines who can access the Pulse dashboard. Gate::define('viewPulse', function (User $user) { return $user->isAdmin(); @@ -100,4 +105,12 @@ class AppServiceProvider extends ServiceProvider return $date->format('M jS, g:i A'); }); } + + /** + * Register Livewire component overrides. + */ + private function registerLivewireOverrides(): void + { + Livewire::component('profile.update-password-form', UpdatePasswordForm::class); + } } diff --git a/database/factories/OAuthConnectionFactory.php b/database/factories/OAuthConnectionFactory.php index 7338ea3..a391720 100644 --- a/database/factories/OAuthConnectionFactory.php +++ b/database/factories/OAuthConnectionFactory.php @@ -3,7 +3,9 @@ namespace Database\Factories; use App\Models\OAuthConnection; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; class OAuthConnectionFactory extends Factory { @@ -15,7 +17,13 @@ class OAuthConnectionFactory extends Factory public function definition(): array { return [ - // + 'user_id' => User::factory(), + 'provider_name' => $this->faker->randomElement(['discord', 'google', 'facebook']), + 'provider_id' => (string) $this->faker->unique()->numberBetween(100000, 999999), + 'token' => Str::random(40), + 'refresh_token' => Str::random(40), + 'created_at' => now(), + 'updated_at' => now(), ]; } } diff --git a/resources/views/profile/update-password-form.blade.php b/resources/views/profile/update-password-form.blade.php index fc1ebf7..2879b59 100644 --- a/resources/views/profile/update-password-form.blade.php +++ b/resources/views/profile/update-password-form.blade.php @@ -8,11 +8,18 @@ -
- - - -
+ + @if (auth()->user()->password === null) +
+

{{ __('Your account does not have a password set. We recommend setting a password so that you can recover your account if you need to.') }}

+
+ @else +
+ + + +
+ @endif
diff --git a/tests/Feature/User/OAuthAccountTest.php b/tests/Feature/User/OAuthAccountTest.php new file mode 100644 index 0000000..f1689da --- /dev/null +++ b/tests/Feature/User/OAuthAccountTest.php @@ -0,0 +1,172 @@ +shouldReceive('getId')->andReturn('provider-user-id'); + $socialiteUser->shouldReceive('getEmail')->andReturn('newuser@example.com'); + $socialiteUser->shouldReceive('getName')->andReturn('New User'); + $socialiteUser->shouldReceive('getNickname')->andReturn(null); + $socialiteUser->shouldReceive('getAvatar')->andReturn('avatar-url'); + $socialiteUser->token = 'access-token'; + $socialiteUser->refreshToken = 'refresh-token'; + + // Mock Socialite facade + Socialite::shouldReceive('driver->user')->andReturn($socialiteUser); + + // Hit the callback route + $response = $this->get('/login/discord/callback'); + + // Assert that the user was created + $user = User::where('email', 'newuser@example.com')->first(); + expect($user)->not->toBeNull() + ->and($user->name)->toBe('New User'); + + // Assert that the OAuth provider was attached + $oAuthConnection = $user->oAuthConnections()->whereProvider('discord')->first(); + expect($oAuthConnection)->not->toBeNull() + ->and($oAuthConnection->provider_id)->toBe('provider-user-id'); + + // Assert the user is authenticated + $this->assertAuthenticatedAs($user); + + // Assert redirect to dashboard + $response->assertRedirect(route('dashboard')); +}); + +it('attaches a new OAuth provider to an existing user when logging in via OAuth', function () { + // Create an existing user + $user = User::factory()->create([ + 'email' => 'existinguser@example.com', + 'name' => 'Existing User', + 'password' => Hash::make('password123'), + ]); + + // Mock the Socialite user + $socialiteUser = Mockery::mock(SocialiteUser::class); + $socialiteUser->shouldReceive('getId')->andReturn('new-provider-user-id'); + $socialiteUser->shouldReceive('getEmail')->andReturn('existinguser@example.com'); + $socialiteUser->shouldReceive('getName')->andReturn('Existing User Updated'); + $socialiteUser->shouldReceive('getNickname')->andReturn(null); + $socialiteUser->shouldReceive('getAvatar')->andReturn('new-avatar-url'); + $socialiteUser->token = 'new-access-token'; + $socialiteUser->refreshToken = 'new-refresh-token'; + + // Mock Socialite facade + Socialite::shouldReceive('driver->user')->andReturn($socialiteUser); + + // Hit the callback route + $response = $this->get('/login/discord/callback'); + + // Refresh user data + $user->refresh(); + + // Assert that the username was not updated + expect($user->name)->toBe('Existing User') + ->and($user->name)->not->toBe('Existing User Updated'); + + // Assert that the new OAuth provider was attached + $oauthConnection = $user->oAuthConnections()->whereProvider('discord')->first(); + expect($oauthConnection)->not->toBeNull() + ->and($oauthConnection->provider_id)->toBe('new-provider-user-id'); + + // Assert the user is authenticated + $this->assertAuthenticatedAs($user); + + // Assert redirect to dashboard + $response->assertRedirect(route('dashboard')); +}); + +it('hides the current password field when the user has no password', function () { + // Create a user with no password + $user = User::factory()->create([ + 'password' => null, + ]); + + $this->actingAs($user); + + // Visit the profile page + $response = $this->get('/user/profile'); + $response->assertStatus(200); + + // Assert that the current password field is not displayed + $response->assertDontSee(__('Current Password')); +}); + +it('shows the current password field when the user has a password', function () { + // Create a user with a password + $user = User::factory()->create([ + 'password' => bcrypt('password123'), + ]); + + $this->actingAs($user); + + // Visit the profile page + $response = $this->get('/user/profile'); + $response->assertStatus(200); + + // Assert that the current password field is displayed + $response->assertSee(__('Current Password')); +}); + +it('allows a user without a password to set a new password without entering the current password', function () { + // Create a user with a NULL password + $user = User::factory()->create([ + 'password' => null, + ]); + + $this->actingAs($user); + + // Test the Livewire component + Livewire::test(UpdatePasswordForm::class) + ->set('state.password', 'newpassword123') + ->set('state.password_confirmation', 'newpassword123') + ->call('updatePassword') + ->assertHasNoErrors(); + + // Refresh user data + $user->refresh(); + + // Assert that the password is now set + expect(Hash::check('newpassword123', $user->password))->toBeTrue(); +}); + +it('requires a user with a password to enter the current password to set a new password', function () { + $user = User::factory()->create([ + 'password' => Hash::make('oldpassword'), + ]); + + $this->actingAs($user); + + // Without current password + Livewire::test(UpdatePasswordForm::class) + ->set('state.password', 'newpassword123') + ->set('state.password_confirmation', 'newpassword123') + ->call('updatePassword') + ->assertHasErrors(['current_password' => 'required']); + + // With incorrect current password + Livewire::test(UpdatePasswordForm::class) + ->set('state.current_password', 'wrongpassword') + ->set('state.password', 'newpassword123') + ->set('state.password_confirmation', 'newpassword123') + ->call('updatePassword') + ->assertHasErrors(['current_password']); + + // With correct current password + Livewire::test(UpdatePasswordForm::class) + ->set('state.current_password', 'oldpassword') + ->set('state.password', 'newpassword123') + ->set('state.password_confirmation', 'newpassword123') + ->call('updatePassword') + ->assertHasNoErrors(); + + $user->refresh(); + + expect(Hash::check('newpassword123', $user->password))->toBeTrue(); +}); From 746fed1746b99eefe6899205a08dea036c4e95cd Mon Sep 17 00:00:00 2001 From: Refringe Date: Fri, 27 Sep 2024 20:41:36 -0400 Subject: [PATCH 05/15] OAuth Management Adds a edit-user-profile section to allow a user to remove an OAuth connection from their account when they have a local account password set. --- app/Http/Controllers/SocialiteController.php | 12 ++- .../Profile/ManageOAuthConnections.php | 101 ++++++++++++++++++ app/Livewire/Profile/UpdatePasswordForm.php | 2 + app/Policies/OAuthConnectionPolicy.php | 25 +++++ ...26_164844_create_oauth_providers_table.php | 4 + .../manage-oauth-connections.blade.php | 95 ++++++++++++++++ resources/views/profile/show.blade.php | 6 ++ 7 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 app/Livewire/Profile/ManageOAuthConnections.php create mode 100644 app/Policies/OAuthConnectionPolicy.php create mode 100644 resources/views/livewire/profile/manage-oauth-connections.blade.php diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php index 3f5c5b5..fca73b8 100644 --- a/app/Http/Controllers/SocialiteController.php +++ b/app/Http/Controllers/SocialiteController.php @@ -65,7 +65,11 @@ class SocialiteController extends Controller if ($oauthConnection) { $oauthConnection->update([ 'token' => $providerUser->token, - 'refresh_token' => $providerUser->refreshToken ?? null, + 'refresh_token' => $providerUser->refreshToken ?? '', + 'nickname' => $providerUser->getNickname() ?? '', + 'name' => $providerUser->getName() ?? '', + 'email' => $providerUser->getEmail() ?? '', + 'avatar' => $providerUser->getAvatar() ?? '', ]); return $oauthConnection->user; @@ -84,7 +88,11 @@ class SocialiteController extends Controller 'provider' => $provider, 'provider_id' => $providerUser->getId(), 'token' => $providerUser->token, - 'refresh_token' => $providerUser->refreshToken ?? null, + 'refresh_token' => $providerUser->refreshToken ?? '', + 'nickname' => $providerUser->getNickname() ?? '', + 'name' => $providerUser->getName() ?? '', + 'email' => $providerUser->getEmail() ?? '', + 'avatar' => $providerUser->getAvatar() ?? '', ]); return $user; diff --git a/app/Livewire/Profile/ManageOAuthConnections.php b/app/Livewire/Profile/ManageOAuthConnections.php new file mode 100644 index 0000000..15ece9f --- /dev/null +++ b/app/Livewire/Profile/ManageOAuthConnections.php @@ -0,0 +1,101 @@ + 'refreshUser']; + + /** + * Initializes the component by loading the user’s OAuth connections. + */ + public function mount(): void + { + $this->setName('profile.manage-oauth-connections'); + + $this->user = auth()->user(); + } + + /** + * Sets up the deletion confirmation. + */ + public function confirmConnectionDeletion($connectionId): void + { + $this->confirmingConnectionDeletion = true; + $this->selectedConnectionId = $connectionId; + } + + /** + * Deletes the selected OAuth connection. + */ + public function deleteConnection(): void + { + $connection = $this->user->oauthConnections()->find($this->selectedConnectionId); + + // Ensure the user is authorized to delete the connection. + $this->authorize('delete', $connection); + + // The user must have a password set before removing an OAuth connection. + if ($this->user->password === null) { + $this->addError('password_required', __('You must set a password before removing an OAuth connection.')); + $this->confirmingConnectionDeletion = false; + + return; + } + + if ($connection) { + $connection->delete(); + + $this->user->refresh(); + $this->confirmingConnectionDeletion = false; + $this->selectedConnectionId = null; + + session()->flash('status', __('OAuth connection removed successfully.')); + } else { + session()->flash('error', __('OAuth connection not found.')); + } + } + + /** + * Refreshes the user instance. + */ + public function refreshUser(): void + { + $this->user->refresh(); + } + + /** + * Renders the component view. + */ + public function render(): View + { + return view('livewire.profile.manage-oauth-connections'); + } +} diff --git a/app/Livewire/Profile/UpdatePasswordForm.php b/app/Livewire/Profile/UpdatePasswordForm.php index 0c01ff1..28db4eb 100644 --- a/app/Livewire/Profile/UpdatePasswordForm.php +++ b/app/Livewire/Profile/UpdatePasswordForm.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Laravel\Fortify\Contracts\UpdatesUserPasswords; use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm as JetstreamUpdatePasswordForm; +use Override; class UpdatePasswordForm extends JetstreamUpdatePasswordForm { @@ -19,6 +20,7 @@ class UpdatePasswordForm extends JetstreamUpdatePasswordForm * This method has been overwritten to allow a user that has a null password to set a password for their account * without needing to provide their current password. This is useful for users that have been created using OAuth. */ + #[Override] public function updatePassword(UpdatesUserPasswords $updater): void { $this->resetErrorBag(); diff --git a/app/Policies/OAuthConnectionPolicy.php b/app/Policies/OAuthConnectionPolicy.php new file mode 100644 index 0000000..23ae59b --- /dev/null +++ b/app/Policies/OAuthConnectionPolicy.php @@ -0,0 +1,25 @@ +id === $oauthConnection->user_id; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, OAuthConnection $oauthConnection): bool + { + return $user->id === $oauthConnection->user_id && $user->password !== null; + } +} diff --git a/database/migrations/2024_09_26_164844_create_oauth_providers_table.php b/database/migrations/2024_09_26_164844_create_oauth_providers_table.php index 9184073..66a1e32 100644 --- a/database/migrations/2024_09_26_164844_create_oauth_providers_table.php +++ b/database/migrations/2024_09_26_164844_create_oauth_providers_table.php @@ -22,6 +22,10 @@ return new class extends Migration $table->string('provider_id'); $table->string('token')->default(''); $table->string('refresh_token')->default(''); + $table->string('nickname')->default(''); + $table->string('name')->default(''); + $table->string('email')->default(''); + $table->string('avatar')->default(''); $table->timestamps(); $table->unique(['provider', 'provider_id']); diff --git a/resources/views/livewire/profile/manage-oauth-connections.blade.php b/resources/views/livewire/profile/manage-oauth-connections.blade.php new file mode 100644 index 0000000..e3ee43d --- /dev/null +++ b/resources/views/livewire/profile/manage-oauth-connections.blade.php @@ -0,0 +1,95 @@ + + + {{ __('Connected Accounts') }} + + + + {{ __('Manage your connected OAuth accounts.') }} + + + +

+ {{ __('You can manage your OAuth connections here') }} +

+ + @if ($user->password === null) +
+

{{ __('Before you can remove a connection you must have an account password set.') }}

+
+ @endif + + @if (session()->has('status')) +
+ {{ session('status') }} +
+ @endif + + @if (session()->has('error')) +
+ {{ session('error') }} +
+ @endif + +
+ @forelse ($user->oauthConnections as $connection) +
+
+ @switch ($connection->provider) + @case ('discord') + + + + @break + @default + + + + @endswitch +
+ +
+
+ {{ ucfirst($connection->provider) }} - {{ $connection->name }} - {{ $connection->email }} +
+
+ {{ __('Connected') }} {{ $connection->created_at->format('M d, Y') }} +
+
+ +
+ @can('delete', $connection) + + {{ __('Remove') }} + + @endcan +
+
+ @empty +
+ {{ __('You have no connected accounts.') }} +
+ @endforelse +
+ + + + + {{ __('Remove Connected Account') }} + + + + {{ __('Are you sure you want to remove this connected account? This action cannot be undone.') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Remove') }} + + + +
+
diff --git a/resources/views/profile/show.blade.php b/resources/views/profile/show.blade.php index 76bbc77..9ac27a3 100644 --- a/resources/views/profile/show.blade.php +++ b/resources/views/profile/show.blade.php @@ -29,6 +29,12 @@ @endif + {{-- OAuth Management --}} +
+ @livewire('profile.manage-oauth-connections') +
+ + @if (config('session.driver') === 'database')
@livewire('profile.logout-other-browser-sessions-form') From caefd6dbb2fb8f449d475476f184ed2c688e20de Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 14:06:37 -0400 Subject: [PATCH 06/15] Discord Avatar Import Imports the user's avatar from discord when a new user is created through OAuth. --- .gitignore | 4 ++ app/Http/Controllers/SocialiteController.php | 43 +++++++++++++++++++- app/Jobs/Import/ImportHubDataJob.php | 2 +- app/Models/User.php | 8 ++++ config/services.php | 2 +- storage/app/.gitignore | 3 -- storage/app/public/.gitignore | 2 - storage/app/public/cover-photos/.gitkeep | 0 storage/app/public/profile-photos/.gitkeep | 0 9 files changed, 56 insertions(+), 8 deletions(-) delete mode 100644 storage/app/.gitignore delete mode 100644 storage/app/public/.gitignore create mode 100644 storage/app/public/cover-photos/.gitkeep create mode 100644 storage/app/public/profile-photos/.gitkeep diff --git a/.gitignore b/.gitignore index e554aa8..2855410 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ public/build public/hot public/storage storage/*.key +storage/app/livewire-tmp +storage/app/public +!storage/app/public/cover-photos/.gitkeep +!storage/app/public/profile-photos/.gitkeep vendor auth.json frankenphp diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php index fca73b8..29488fd 100644 --- a/app/Http/Controllers/SocialiteController.php +++ b/app/Http/Controllers/SocialiteController.php @@ -7,6 +7,9 @@ use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use Laravel\Socialite\Contracts\User as ProviderUser; use Laravel\Socialite\Facades\Socialite; @@ -80,11 +83,13 @@ class SocialiteController extends Controller // If one exists, connect that account. Otherwise, create a new one. return DB::transaction(function () use ($providerUser, $provider) { + $user = User::firstOrCreate(['email' => $providerUser->getEmail()], [ 'name' => $providerUser->getName() ?? $providerUser->getNickname(), 'password' => null, ]); - $user->oAuthConnections()->create([ + + $connection = $user->oAuthConnections()->create([ 'provider' => $provider, 'provider_id' => $providerUser->getId(), 'token' => $providerUser->token, @@ -95,7 +100,43 @@ class SocialiteController extends Controller 'avatar' => $providerUser->getAvatar() ?? '', ]); + $this->updateAvatar($user, $connection->avatar); + return $user; }); } + + private function updateAvatar(User $user, string $avatarUrl): void + { + // Determine the disk to use based on the environment. + $disk = match (config('app.env')) { + 'production' => 'r2', // Cloudflare R2 Storage + default => 'public', // Local + }; + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_URL, $avatarUrl); + $image = curl_exec($curl); + + if ($image === false) { + Log::error('There was an error attempting to download the image. cURL error: '.curl_error($curl)); + + return; + } + + // Generate a random path for the image and ensure that it doesn't already exist. + do { + $relativePath = User::profilePhotoStoragePath().'/'.Str::random(40).'.webp'; + } while (Storage::disk($disk)->exists($relativePath)); + + // Store the image on the disk. + Storage::disk($disk)->put($relativePath, $image); + + // Update the user's profile photo path. + $user->forceFill([ + 'profile_photo_path' => $relativePath, + ])->save(); + } } diff --git a/app/Jobs/Import/ImportHubDataJob.php b/app/Jobs/Import/ImportHubDataJob.php index 3815ace..084d1c3 100644 --- a/app/Jobs/Import/ImportHubDataJob.php +++ b/app/Jobs/Import/ImportHubDataJob.php @@ -392,7 +392,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue $hashShort = substr($avatar->fileHash, 0, 2); $fileName = $avatar->fileHash.'.'.$avatar->avatarExtension; $hubUrl = 'https://hub.sp-tarkov.com/images/avatars/'.$hashShort.'/'.$avatar->avatarID.'-'.$fileName; - $relativePath = 'user-avatars/'.$fileName; + $relativePath = User::profilePhotoStoragePath().'/'.$fileName; return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath); } diff --git a/app/Models/User.php b/app/Models/User.php index 21715d9..a20d6d1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -44,6 +44,14 @@ class User extends Authenticatable implements MustVerifyEmail 'profile_photo_url', ]; + /** + * Get the storage path for profile photos. + */ + public static function profilePhotoStoragePath(): string + { + return 'profile-photos'; + } + /** * The relationship between a user and their mods. * diff --git a/config/services.php b/config/services.php index 89df54e..8fe5103 100644 --- a/config/services.php +++ b/config/services.php @@ -45,7 +45,7 @@ return [ 'client_secret' => env('DISCORD_CLIENT_SECRET'), 'redirect' => env('DISCORD_REDIRECT_URI'), 'allow_gif_avatars' => (bool) env('DISCORD_AVATAR_GIF', true), - 'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'png'), + 'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'webp'), ], ]; diff --git a/storage/app/.gitignore b/storage/app/.gitignore deleted file mode 100644 index 8f4803c..0000000 --- a/storage/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!public/ -!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/storage/app/public/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/storage/app/public/cover-photos/.gitkeep b/storage/app/public/cover-photos/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/storage/app/public/profile-photos/.gitkeep b/storage/app/public/profile-photos/.gitkeep new file mode 100644 index 0000000..e69de29 From 020821356428cf70b221bc526d5db1d93fe8e65a Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 14:09:38 -0400 Subject: [PATCH 07/15] Removes fancy quotes. --- app/Livewire/Profile/ManageOAuthConnections.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Profile/ManageOAuthConnections.php b/app/Livewire/Profile/ManageOAuthConnections.php index 15ece9f..83517c0 100644 --- a/app/Livewire/Profile/ManageOAuthConnections.php +++ b/app/Livewire/Profile/ManageOAuthConnections.php @@ -29,12 +29,12 @@ class ManageOAuthConnections extends Component public $selectedConnectionId; /** - * The component’s listeners. + * The component's listeners. */ protected $listeners = ['saved' => 'refreshUser']; /** - * Initializes the component by loading the user’s OAuth connections. + * Initializes the component by loading the user's OAuth connections. */ public function mount(): void { From 0d043ff880cd3fb00e1d187ad4b8a45ea0c1a2b9 Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 16:43:31 -0400 Subject: [PATCH 08/15] Username Handling - When a new user is created using Discord OAuth information, if the username returned from Discord is already taken, append randomness to the end of the username. - Validates that a new account name is unique. - Validates that an updated account name is unique. --- app/Actions/Fortify/CreateNewUser.php | 2 +- app/Actions/Fortify/UpdateUserProfileInformation.php | 2 +- app/Http/Controllers/SocialiteController.php | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 566e51d..057f25c 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -20,7 +20,7 @@ class CreateNewUser implements CreatesNewUsers public function create(array $input): User { Validator::make($input, [ - 'name' => ['required', 'string', 'max:255'], + 'name' => ['required', 'string', 'max:36', 'unique:users'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => $this->passwordRules(), 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php index 170c9ce..845647b 100644 --- a/app/Actions/Fortify/UpdateUserProfileInformation.php +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -16,7 +16,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation public function update(User $user, array $input): void { Validator::make($input, [ - 'name' => ['required', 'string', 'max:255'], + 'name' => ['required', 'string', 'max:255', Rule::unique('users')->ignore($user->id)], 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], 'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'], diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php index 29488fd..474929a 100644 --- a/app/Http/Controllers/SocialiteController.php +++ b/app/Http/Controllers/SocialiteController.php @@ -78,14 +78,22 @@ class SocialiteController extends Controller return $oauthConnection->user; } + // If the username already exists in the database, append a random string to it to ensure uniqueness. + $username = $providerUser->getName() ?? $providerUser->getNickname(); + $random = ''; + while (User::whereName($username.$random)->exists()) { + $random = '-'.Str::random(5); + } + $username .= $random; + // The user has not connected their account with this OAuth provider before, so a new connection needs to be // established. Check if the user has an account with the same email address that's passed in from the provider. // If one exists, connect that account. Otherwise, create a new one. - return DB::transaction(function () use ($providerUser, $provider) { + return DB::transaction(function () use ($providerUser, $provider, $username) { $user = User::firstOrCreate(['email' => $providerUser->getEmail()], [ - 'name' => $providerUser->getName() ?? $providerUser->getNickname(), + 'name' => $username, 'password' => null, ]); From 319e5e65e901d9fc7b51c185596ca508f78039cb Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 22:52:44 -0400 Subject: [PATCH 09/15] Disable Lazy Loading in Development --- app/Providers/AppServiceProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d1ee748..5a64f5d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -40,6 +40,9 @@ class AppServiceProvider extends ServiceProvider // Allow mass assignment for all models. Be careful! Model::unguard(); + // Disable lazy loading in non-production environments. + Model::preventLazyLoading(! app()->isProduction()); + // Register model observers. $this->registerObservers(); From 1127f2b9dfa62abd5b7742917411d1b72c7c29c9 Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 22:53:32 -0400 Subject: [PATCH 10/15] Eager Load User Mods --- app/Http/Controllers/UserController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 6433105..065aee4 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -17,6 +17,11 @@ class UserController extends Controller ->firstOrFail(); $mods = $user->mods() + ->with([ + 'users', + 'latestVersion', + 'latestVersion.latestSptVersion', + ]) ->orderByDesc('created_at') ->paginate(10) ->fragment('mods'); From 30985541e7f834267134ac385e61264ecaada900 Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 22:54:15 -0400 Subject: [PATCH 11/15] Fixes PHPStan Issues --- app/Http/Controllers/SocialiteController.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php index 474929a..4fa021c 100644 --- a/app/Http/Controllers/SocialiteController.php +++ b/app/Http/Controllers/SocialiteController.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Laravel\Socialite\Contracts\User as ProviderUser; use Laravel\Socialite\Facades\Socialite; +use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse; class SocialiteController extends Controller { @@ -23,18 +24,19 @@ class SocialiteController extends Controller /** * Redirect the user to the provider's authentication page. */ - public function redirect(string $provider): RedirectResponse + public function redirect(string $provider): SymfonyRedirectResponse { if (! in_array($provider, $this->providers)) { return redirect()->route('login')->withErrors(__('Unsupported OAuth provider.')); } - return Socialite::driver('discord') - ->scopes([ - 'identify', - 'email', - ]) - ->redirect(); + $socialiteProvider = Socialite::driver($provider); + + if (method_exists($socialiteProvider, 'scopes')) { + return $socialiteProvider->scopes(['identify', 'email'])->redirect(); + } + + return $socialiteProvider->redirect(); } /** @@ -67,7 +69,7 @@ class SocialiteController extends Controller if ($oauthConnection) { $oauthConnection->update([ - 'token' => $providerUser->token, + 'token' => $providerUser->token ?? '', 'refresh_token' => $providerUser->refreshToken ?? '', 'nickname' => $providerUser->getNickname() ?? '', 'name' => $providerUser->getName() ?? '', @@ -100,7 +102,7 @@ class SocialiteController extends Controller $connection = $user->oAuthConnections()->create([ 'provider' => $provider, 'provider_id' => $providerUser->getId(), - 'token' => $providerUser->token, + 'token' => $providerUser->token ?? '', 'refresh_token' => $providerUser->refreshToken ?? '', 'nickname' => $providerUser->getNickname() ?? '', 'name' => $providerUser->getName() ?? '', From df8e7f958d669d2f33d0a7d2513c8c19b34dbea8 Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 22:54:45 -0400 Subject: [PATCH 12/15] Uses On Attribute for Livewire Listener --- app/Livewire/Profile/ManageOAuthConnections.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/Livewire/Profile/ManageOAuthConnections.php b/app/Livewire/Profile/ManageOAuthConnections.php index 83517c0..cb297fd 100644 --- a/app/Livewire/Profile/ManageOAuthConnections.php +++ b/app/Livewire/Profile/ManageOAuthConnections.php @@ -5,6 +5,7 @@ namespace App\Livewire\Profile; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\View\View; use Livewire\Attributes\Locked; +use Livewire\Attributes\On; use Livewire\Component; class ManageOAuthConnections extends Component @@ -28,11 +29,6 @@ class ManageOAuthConnections extends Component #[Locked] public $selectedConnectionId; - /** - * The component's listeners. - */ - protected $listeners = ['saved' => 'refreshUser']; - /** * Initializes the component by loading the user's OAuth connections. */ @@ -86,6 +82,7 @@ class ManageOAuthConnections extends Component /** * Refreshes the user instance. */ + #[On('saved')] public function refreshUser(): void { $this->user->refresh(); From 39a7640e92f94f3f9580debc43e91e9fe3fcddfd Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 22:56:06 -0400 Subject: [PATCH 13/15] Reworked Follow Livewire Components Fixes PHPStan errors and makes it a little more performant. Still not good enough. Making way to many queries for what it's doing. --- app/Livewire/User/FollowCard.php | 50 +++++++++++++------ .../views/livewire/user/follow-card.blade.php | 33 ++++++------ 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/app/Livewire/User/FollowCard.php b/app/Livewire/User/FollowCard.php index 325b585..8aa9679 100644 --- a/app/Livewire/User/FollowCard.php +++ b/app/Livewire/User/FollowCard.php @@ -3,7 +3,6 @@ namespace App\Livewire\User; use App\Models\User; -use Illuminate\Support\Collection; use Illuminate\View\View; use Livewire\Attributes\Locked; use Livewire\Attributes\On; @@ -40,12 +39,16 @@ class FollowCard extends Component public string $dialogTitle; /** - * The users to display in the card. - * - * @var Collection + * The user data to display in the card. */ #[Locked] - public Collection $followUsers; + public array $display = []; + + /** + * The limited user data to display in the card. + */ + #[Locked] + public array $displayLimit = []; /** * The maximum number of users to display on the card. @@ -64,6 +67,12 @@ class FollowCard extends Component #[Locked] public User $profileUser; + /** + * The number of users being displayed. + */ + #[Locked] + public int $followUsersCount; + /** * Called when the component is initialized. */ @@ -130,18 +139,31 @@ class FollowCard extends Component public function populateFollowUsers(): void { // Fetch IDs of all users the authenticated user is following. - $followingIds = auth()->user()->following()->pluck('following_id'); + $followingIds = collect(); + $authUser = auth()->user(); + if ($authUser) { + $followingIds = $authUser->following()->pluck('following_id'); + } - // Load the profile user's followers (or following) and map the follow status. - $this->followUsers = $this->profileUser->{$this->relationship} + // Load the profile user's followers (or following). + $users = $this->profileUser->{$this->relationship}()->with([])->get(); + + // Count the number of users. + $this->followUsersCount = $users->count(); + + // Load the users to display and whether the authenticated user is following each user. + $this->display = $users ->map(function (User $user) use ($followingIds) { - // Add the follow status based on the preloaded IDs. - $user->follows = $followingIds->contains($user->id); + return [ + 'user' => $user, + 'isFollowing' => $followingIds->contains($user->id), + ]; + })->toArray(); - // TODO: The above follows property doesn't exist on the User model. What was I smoking? - - return $user; - }); + // Store limited users for the main view. + $this->displayLimit = collect($this->display) + ->take($this->limit) + ->toArray(); } /** diff --git a/resources/views/livewire/user/follow-card.blade.php b/resources/views/livewire/user/follow-card.blade.php index 2601792..4006f13 100644 --- a/resources/views/livewire/user/follow-card.blade.php +++ b/resources/views/livewire/user/follow-card.blade.php @@ -4,37 +4,37 @@

{{ $title }}

- @if (! $followUsers->count()) + @if ($followUsersCount === 0)
{{ $emptyMessage }}
@else
- @foreach ($followUsers->slice(0, $limit) as $user) + @foreach ($displayLimit as $data) {{-- User Badge --}}
- - {{ $user->name }} + + {{ $data['user']->name }}
- {{ $user->name }} + {{ $data['user']->name }}
@endforeach - @if ($followUsers->count() > $limit) + @if ($followUsersCount > $limit) {{-- Count Badge --}}
- +
- {{ $followUsers->count() }} total + {{ $followUsersCount }} total
@endif
@endif - @if ($followUsers->count() > $limit) + @if ($followUsersCount > $limit) {{-- View All Button --}}
@@ -49,28 +49,27 @@
- @foreach ($followUsers as $user) + @foreach ($display as $data)
- - {{ $user->name }} + + {{ $data['user']->name }}
- {{ $user->name }} + {{ $data['user']->name }} {{ __("Member Since") }} - +
- @if (auth()->check() && auth()->user()->id !== $user->id) - + @if (auth()->check() && auth()->user()->id !== $data['user']->id) + @endif
@endforeach
- {{ __('Close') }} From 41555ca6748ec54fcb1c3283fc45cfd4b6a8d9bc Mon Sep 17 00:00:00 2001 From: Refringe Date: Mon, 30 Sep 2024 23:47:04 -0400 Subject: [PATCH 14/15] Eager Loading SPT Versions --- app/Observers/ModObserver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Observers/ModObserver.php b/app/Observers/ModObserver.php index e3b3814..fd2c926 100644 --- a/app/Observers/ModObserver.php +++ b/app/Observers/ModObserver.php @@ -20,6 +20,8 @@ class ModObserver */ public function saved(Mod $mod): void { + $mod->load('versions.sptVersions'); + foreach ($mod->versions as $modVersion) { $this->dependencyVersionService->resolve($modVersion); } @@ -44,6 +46,8 @@ class ModObserver */ public function deleted(Mod $mod): void { + $mod->load('versions.sptVersions'); + $this->updateRelatedSptVersions($mod); } } From 8b71dc2c02d4a4309e2d1f70b0bd09cc5d9c98ef Mon Sep 17 00:00:00 2001 From: Refringe Date: Tue, 1 Oct 2024 00:50:38 -0400 Subject: [PATCH 15/15] Fixes Tests --- database/factories/ModVersionFactory.php | 14 +------------- database/factories/SptVersionFactory.php | 14 +------------- tests/Feature/Mod/ModTest.php | 22 ---------------------- tests/Feature/User/AuthenticationTest.php | 4 ++-- 4 files changed, 4 insertions(+), 50 deletions(-) diff --git a/database/factories/ModVersionFactory.php b/database/factories/ModVersionFactory.php index 100093c..e3684a6 100644 --- a/database/factories/ModVersionFactory.php +++ b/database/factories/ModVersionFactory.php @@ -5,7 +5,6 @@ namespace Database\Factories; use App\Models\Mod; use App\Models\ModVersion; use App\Models\SptVersion; -use App\Support\Version; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; @@ -18,20 +17,9 @@ class ModVersionFactory extends Factory public function definition(): array { - $versionString = $this->faker->numerify('#.#.#'); - try { - $version = new Version($versionString); - } catch (\Exception $e) { - $version = new Version('0.0.0'); - } - return [ 'mod_id' => Mod::factory(), - 'version' => $versionString, - 'version_major' => $version->getMajor(), - 'version_minor' => $version->getMinor(), - 'version_patch' => $version->getPatch(), - 'version_pre_release' => $version->getPreRelease(), + 'version' => $this->faker->numerify('#.#.#'), 'description' => fake()->text(), 'link' => fake()->url(), diff --git a/database/factories/SptVersionFactory.php b/database/factories/SptVersionFactory.php index 96acc21..707ed46 100644 --- a/database/factories/SptVersionFactory.php +++ b/database/factories/SptVersionFactory.php @@ -3,7 +3,6 @@ namespace Database\Factories; use App\Models\SptVersion; -use App\Support\Version; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Carbon; @@ -16,19 +15,8 @@ class SptVersionFactory extends Factory public function definition(): array { - $versionString = $this->faker->numerify('#.#.#'); - try { - $version = new Version($versionString); - } catch (\Exception $e) { - $version = new Version('0.0.0'); - } - return [ - 'version' => $versionString, - 'version_major' => $version->getMajor(), - 'version_minor' => $version->getMinor(), - 'version_patch' => $version->getPatch(), - 'version_pre_release' => $version->getPreRelease(), + 'version' => $this->faker->numerify('#.#.#'), 'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']), 'link' => $this->faker->url, 'created_at' => Carbon::now(), diff --git a/tests/Feature/Mod/ModTest.php b/tests/Feature/Mod/ModTest.php index 1514cf8..363c26c 100644 --- a/tests/Feature/Mod/ModTest.php +++ b/tests/Feature/Mod/ModTest.php @@ -2,28 +2,6 @@ use App\Models\Mod; use App\Models\ModVersion; -use App\Models\SptVersion; -use Illuminate\Foundation\Testing\RefreshDatabase; - -uses(RefreshDatabase::class); - -it('displays homepage mod cards with the latest supported spt version number', function () { - $sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']); - $sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']); - $sptVersion3 = SptVersion::factory()->create(['version' => '3.0.0']); - - $mod1 = Mod::factory()->create(); - ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]); - ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]); - ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]); - ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]); - ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]); - ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]); - - $response = $this->get(route('home')); - - $response->assertSeeInOrder(explode(' ', "$mod1->name $sptVersion3->version_formatted")); -}); it('displays the latest version on the mod detail page', function () { $versions = [ diff --git a/tests/Feature/User/AuthenticationTest.php b/tests/Feature/User/AuthenticationTest.php index 9f705eb..e392fa6 100755 --- a/tests/Feature/User/AuthenticationTest.php +++ b/tests/Feature/User/AuthenticationTest.php @@ -32,10 +32,10 @@ test('users cannot authenticate with invalid password', function () { }); test('users can authenticate using Discord', function () { - $response = $this->get('/auth/discord/redirect'); + $response = $this->get(route('login.socialite', ['provider' => 'discord'])); $response->assertStatus(302); - $response->assertSessionHas('url.intended', route('dashboard', absolute: false)); + $response->assertRedirect(); }); test('user can not authenticate using a null password', function () {