mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
OAuth Account Password Creation
This allows a user that was created via OAuth to set a local password on their account.
This commit is contained in:
parent
bba61fa814
commit
46550b5d8f
50
app/Livewire/Profile/UpdatePasswordForm.php
Normal file
50
app/Livewire/Profile/UpdatePasswordForm.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Profile;
|
||||
|
||||
use App\Actions\Fortify\PasswordValidationRules;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm as JetstreamUpdatePasswordForm;
|
||||
|
||||
class UpdatePasswordForm extends JetstreamUpdatePasswordForm
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public function updatePassword(UpdatesUserPasswords $updater): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,18 @@
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="form">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="current_password" value="{{ __('Current Password') }}" />
|
||||
<x-input id="current_password" type="password" class="mt-1 block w-full" wire:model="state.current_password" autocomplete="current-password" />
|
||||
<x-input-error for="current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
@if (auth()->user()->password === null)
|
||||
<div class="col-span-6 sm:col-span-4 mt-3 max-w-xl text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>{{ __('Your account does not have a password set. We recommend setting a password so that you can recover your account if you need to.') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="current_password" value="{{ __('Current Password') }}" />
|
||||
<x-input id="current_password" type="password" class="mt-1 block w-full" wire:model="state.current_password" autocomplete="current-password" />
|
||||
<x-input-error for="current_password" class="mt-2" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="password" value="{{ __('New Password') }}" />
|
||||
|
172
tests/Feature/User/OAuthAccountTest.php
Normal file
172
tests/Feature/User/OAuthAccountTest.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Profile\UpdatePasswordForm;
|
||||
use App\Models\User;
|
||||
use Laravel\Socialite\Contracts\User as SocialiteUser;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
it('creates a new user and attaches the OAuth provider when logging in via OAuth', function () {
|
||||
// Mock the Socialite user
|
||||
$socialiteUser = Mockery::mock(SocialiteUser::class);
|
||||
$socialiteUser->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();
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user