mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-13 04:30:41 -05:00
Merge remote-tracking branch 'upstream/develop' into impl/results-per-page
This commit is contained in:
commit
de63309649
@ -92,3 +92,8 @@ GITEA_TOKEN=
|
|||||||
|
|
||||||
# API key for Scribe documentation.
|
# API key for Scribe documentation.
|
||||||
SCRIBE_AUTH_KEY=
|
SCRIBE_AUTH_KEY=
|
||||||
|
|
||||||
|
# Discord OAuth Credentials
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_REDIRECT_URI=${APP_URL}/login/discord/callback
|
||||||
|
@ -45,3 +45,8 @@ MAIL_FROM_NAME="${APP_NAME}"
|
|||||||
|
|
||||||
# API key for Scribe documentation.
|
# API key for Scribe documentation.
|
||||||
SCRIBE_AUTH_KEY=
|
SCRIBE_AUTH_KEY=
|
||||||
|
|
||||||
|
# Discord OAuth Credentials
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_REDIRECT_URI=${APP_URL}/login/discord/callback
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -16,6 +16,10 @@ public/build
|
|||||||
public/hot
|
public/hot
|
||||||
public/storage
|
public/storage
|
||||||
storage/*.key
|
storage/*.key
|
||||||
|
storage/app/livewire-tmp
|
||||||
|
storage/app/public
|
||||||
|
!storage/app/public/cover-photos/.gitkeep
|
||||||
|
!storage/app/public/profile-photos/.gitkeep
|
||||||
vendor
|
vendor
|
||||||
auth.json
|
auth.json
|
||||||
frankenphp
|
frankenphp
|
||||||
|
@ -20,7 +20,7 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
public function create(array $input): User
|
public function create(array $input): User
|
||||||
{
|
{
|
||||||
Validator::make($input, [
|
Validator::make($input, [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:36', 'unique:users'],
|
||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||||
'password' => $this->passwordRules(),
|
'password' => $this->passwordRules(),
|
||||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
||||||
|
@ -16,7 +16,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
public function update(User $user, array $input): void
|
public function update(User $user, array $input): void
|
||||||
{
|
{
|
||||||
Validator::make($input, [
|
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)],
|
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||||
'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'],
|
'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'],
|
||||||
|
152
app/Http/Controllers/SocialiteController.php
Normal file
152
app/Http/Controllers/SocialiteController.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\OAuthConnection;
|
||||||
|
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;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
||||||
|
|
||||||
|
class SocialiteController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The providers that are supported.
|
||||||
|
*/
|
||||||
|
protected array $providers = ['discord'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect the user to the provider's authentication page.
|
||||||
|
*/
|
||||||
|
public function redirect(string $provider): SymfonyRedirectResponse
|
||||||
|
{
|
||||||
|
if (! in_array($provider, $this->providers)) {
|
||||||
|
return redirect()->route('login')->withErrors(__('Unsupported OAuth provider.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$socialiteProvider = Socialite::driver($provider);
|
||||||
|
|
||||||
|
if (method_exists($socialiteProvider, 'scopes')) {
|
||||||
|
return $socialiteProvider->scopes(['identify', 'email'])->redirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $socialiteProvider->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 ?? '',
|
||||||
|
'nickname' => $providerUser->getNickname() ?? '',
|
||||||
|
'name' => $providerUser->getName() ?? '',
|
||||||
|
'email' => $providerUser->getEmail() ?? '',
|
||||||
|
'avatar' => $providerUser->getAvatar() ?? '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
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, $username) {
|
||||||
|
|
||||||
|
$user = User::firstOrCreate(['email' => $providerUser->getEmail()], [
|
||||||
|
'name' => $username,
|
||||||
|
'password' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$connection = $user->oAuthConnections()->create([
|
||||||
|
'provider' => $provider,
|
||||||
|
'provider_id' => $providerUser->getId(),
|
||||||
|
'token' => $providerUser->token ?? '',
|
||||||
|
'refresh_token' => $providerUser->refreshToken ?? '',
|
||||||
|
'nickname' => $providerUser->getNickname() ?? '',
|
||||||
|
'name' => $providerUser->getName() ?? '',
|
||||||
|
'email' => $providerUser->getEmail() ?? '',
|
||||||
|
'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();
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,11 @@ class UserController extends Controller
|
|||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$mods = $user->mods()
|
$mods = $user->mods()
|
||||||
|
->with([
|
||||||
|
'users',
|
||||||
|
'latestVersion',
|
||||||
|
'latestVersion.latestSptVersion',
|
||||||
|
])
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->paginate(10)
|
->paginate(10)
|
||||||
->fragment('mods');
|
->fragment('mods');
|
||||||
|
@ -392,7 +392,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
|||||||
$hashShort = substr($avatar->fileHash, 0, 2);
|
$hashShort = substr($avatar->fileHash, 0, 2);
|
||||||
$fileName = $avatar->fileHash.'.'.$avatar->avatarExtension;
|
$fileName = $avatar->fileHash.'.'.$avatar->avatarExtension;
|
||||||
$hubUrl = 'https://hub.sp-tarkov.com/images/avatars/'.$hashShort.'/'.$avatar->avatarID.'-'.$fileName;
|
$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);
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
}
|
}
|
||||||
|
98
app/Livewire/Profile/ManageOAuthConnections.php
Normal file
98
app/Livewire/Profile/ManageOAuthConnections.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the current user.
|
||||||
|
*/
|
||||||
|
#[Locked]
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the confirmation modal visibility.
|
||||||
|
*/
|
||||||
|
public $confirmingConnectionDeletion = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the ID of the connection to be deleted.
|
||||||
|
*/
|
||||||
|
#[Locked]
|
||||||
|
public $selectedConnectionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
#[On('saved')]
|
||||||
|
public function refreshUser(): void
|
||||||
|
{
|
||||||
|
$this->user->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the component view.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('livewire.profile.manage-oauth-connections');
|
||||||
|
}
|
||||||
|
}
|
52
app/Livewire/Profile/UpdatePasswordForm.php
Normal file
52
app/Livewire/Profile/UpdatePasswordForm.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?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;
|
||||||
|
use Override;
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
#[Override]
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@
|
|||||||
namespace App\Livewire\User;
|
namespace App\Livewire\User;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
@ -40,12 +39,16 @@ class FollowCard extends Component
|
|||||||
public string $dialogTitle;
|
public string $dialogTitle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The users to display in the card.
|
* The user data to display in the card.
|
||||||
*
|
|
||||||
* @var Collection<User>
|
|
||||||
*/
|
*/
|
||||||
#[Locked]
|
#[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.
|
* The maximum number of users to display on the card.
|
||||||
@ -64,6 +67,12 @@ class FollowCard extends Component
|
|||||||
#[Locked]
|
#[Locked]
|
||||||
public User $profileUser;
|
public User $profileUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of users being displayed.
|
||||||
|
*/
|
||||||
|
#[Locked]
|
||||||
|
public int $followUsersCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the component is initialized.
|
* Called when the component is initialized.
|
||||||
*/
|
*/
|
||||||
@ -130,18 +139,31 @@ class FollowCard extends Component
|
|||||||
public function populateFollowUsers(): void
|
public function populateFollowUsers(): void
|
||||||
{
|
{
|
||||||
// Fetch IDs of all users the authenticated user is following.
|
// 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.
|
// Load the profile user's followers (or following).
|
||||||
$this->followUsers = $this->profileUser->{$this->relationship}
|
$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) {
|
->map(function (User $user) use ($followingIds) {
|
||||||
// Add the follow status based on the preloaded IDs.
|
return [
|
||||||
$user->follows = $followingIds->contains($user->id);
|
'user' => $user,
|
||||||
|
'isFollowing' => $followingIds->contains($user->id),
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
// TODO: The above follows property doesn't exist on the User model. What was I smoking?
|
// Store limited users for the main view.
|
||||||
|
$this->displayLimit = collect($this->display)
|
||||||
return $user;
|
->take($this->limit)
|
||||||
});
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
19
app/Models/OAuthConnection.php
Normal file
19
app/Models/OAuthConnection.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class OAuthConnection extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'oauth_connections';
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -43,6 +44,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
'profile_photo_url',
|
'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.
|
* The relationship between a user and their mods.
|
||||||
*
|
*
|
||||||
@ -211,7 +220,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
|
protected function about(): Attribute
|
||||||
{
|
{
|
||||||
|
@ -20,6 +20,8 @@ class ModObserver
|
|||||||
*/
|
*/
|
||||||
public function saved(Mod $mod): void
|
public function saved(Mod $mod): void
|
||||||
{
|
{
|
||||||
|
$mod->load('versions.sptVersions');
|
||||||
|
|
||||||
foreach ($mod->versions as $modVersion) {
|
foreach ($mod->versions as $modVersion) {
|
||||||
$this->dependencyVersionService->resolve($modVersion);
|
$this->dependencyVersionService->resolve($modVersion);
|
||||||
}
|
}
|
||||||
@ -44,6 +46,8 @@ class ModObserver
|
|||||||
*/
|
*/
|
||||||
public function deleted(Mod $mod): void
|
public function deleted(Mod $mod): void
|
||||||
{
|
{
|
||||||
|
$mod->load('versions.sptVersions');
|
||||||
|
|
||||||
$this->updateRelatedSptVersions($mod);
|
$this->updateRelatedSptVersions($mod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
app/Policies/OAuthConnectionPolicy.php
Normal file
25
app/Policies/OAuthConnectionPolicy.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\OAuthConnection;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class OAuthConnectionPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, OAuthConnection $oauthConnection): bool
|
||||||
|
{
|
||||||
|
return $user->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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Livewire\Profile\UpdatePasswordForm;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use App\Models\ModDependency;
|
use App\Models\ModDependency;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
@ -13,9 +14,13 @@ use App\Observers\ModVersionObserver;
|
|||||||
use App\Observers\SptVersionObserver;
|
use App\Observers\SptVersionObserver;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Number;
|
use Illuminate\Support\Number;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use SocialiteProviders\Discord\Provider;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@ -35,15 +40,28 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
// Allow mass assignment for all models. Be careful!
|
// Allow mass assignment for all models. Be careful!
|
||||||
Model::unguard();
|
Model::unguard();
|
||||||
|
|
||||||
|
// Disable lazy loading in non-production environments.
|
||||||
|
Model::preventLazyLoading(! app()->isProduction());
|
||||||
|
|
||||||
|
// Register model observers.
|
||||||
$this->registerObservers();
|
$this->registerObservers();
|
||||||
|
|
||||||
|
// Register custom macros.
|
||||||
$this->registerNumberMacros();
|
$this->registerNumberMacros();
|
||||||
$this->registerCarbonMacros();
|
$this->registerCarbonMacros();
|
||||||
|
|
||||||
|
// Register Livewire component overrides.
|
||||||
|
$this->registerLivewireOverrides();
|
||||||
|
|
||||||
// This gate determines who can access the Pulse dashboard.
|
// This gate determines who can access the Pulse dashboard.
|
||||||
Gate::define('viewPulse', function (User $user) {
|
Gate::define('viewPulse', function (User $user) {
|
||||||
return $user->isAdmin();
|
return $user->isAdmin();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register the Discord socialite provider.
|
||||||
|
Event::listen(function (SocialiteWasCalled $event) {
|
||||||
|
$event->extendSocialite('discord', Provider::class);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,4 +108,12 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
return $date->format('M jS, g:i A');
|
return $date->format('M jS, g:i A');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Livewire component overrides.
|
||||||
|
*/
|
||||||
|
private function registerLivewireOverrides(): void
|
||||||
|
{
|
||||||
|
Livewire::component('profile.update-password-form', UpdatePasswordForm::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,5 @@ return [
|
|||||||
App\Providers\HorizonServiceProvider::class,
|
App\Providers\HorizonServiceProvider::class,
|
||||||
App\Providers\JetstreamServiceProvider::class,
|
App\Providers\JetstreamServiceProvider::class,
|
||||||
App\Providers\Filament\AdminPanelProvider::class,
|
App\Providers\Filament\AdminPanelProvider::class,
|
||||||
|
\SocialiteProviders\Manager\ServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
@ -19,12 +19,14 @@
|
|||||||
"laravel/pulse": "^1.2",
|
"laravel/pulse": "^1.2",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/scout": "^10.10",
|
"laravel/scout": "^10.10",
|
||||||
|
"laravel/socialite": "^5.16",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
"league/flysystem-aws-s3-v3": "^3.28",
|
"league/flysystem-aws-s3-v3": "^3.28",
|
||||||
"league/html-to-markdown": "^5.1",
|
"league/html-to-markdown": "^5.1",
|
||||||
"livewire/livewire": "^3.5",
|
"livewire/livewire": "^3.5",
|
||||||
"mchev/banhammer": "^2.3",
|
"mchev/banhammer": "^2.3",
|
||||||
"meilisearch/meilisearch-php": "^1.8",
|
"meilisearch/meilisearch-php": "^1.8",
|
||||||
|
"socialiteproviders/discord": "^4.2",
|
||||||
"stevebauman/purify": "^6.2"
|
"stevebauman/purify": "^6.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
497
composer.lock
generated
497
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "645a73e7a66339c2396753a2f35c5eea",
|
"content-hash": "2e603ffae8f6f8a4c834c9bff55cc30f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@ -1867,6 +1867,69 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-09-23T14:10:16+00:00"
|
"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",
|
"name": "fruitcake/php-cors",
|
||||||
"version": "v1.3.0",
|
"version": "v1.3.0",
|
||||||
@ -3473,6 +3536,78 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-08-02T07:48:17+00:00"
|
"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",
|
"name": "laravel/tinker",
|
||||||
"version": "v2.9.0",
|
"version": "v2.9.0",
|
||||||
@ -4147,6 +4282,82 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-09-21T08:32:55+00:00"
|
"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",
|
"name": "league/uri",
|
||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
@ -5390,6 +5601,56 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-05-08T12:36:18+00:00"
|
"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",
|
"name": "php-http/discovery",
|
||||||
"version": "1.19.4",
|
"version": "1.19.4",
|
||||||
@ -5544,6 +5805,116 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-07-20T21:41:07+00:00"
|
"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",
|
"name": "pragmarx/google2fa",
|
||||||
"version": "v8.0.3",
|
"version": "v8.0.3",
|
||||||
@ -6439,6 +6810,130 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-02-26T18:08:49+00:00"
|
"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",
|
"name": "spatie/color",
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
|
@ -40,4 +40,12 @@ return [
|
|||||||
'token' => env('GITEA_TOKEN', ''),
|
'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', 'webp'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -5,7 +5,6 @@ namespace Database\Factories;
|
|||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
use App\Models\SptVersion;
|
use App\Models\SptVersion;
|
||||||
use App\Support\Version;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
@ -18,20 +17,9 @@ class ModVersionFactory extends Factory
|
|||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
$versionString = $this->faker->numerify('#.#.#');
|
|
||||||
try {
|
|
||||||
$version = new Version($versionString);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$version = new Version('0.0.0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'mod_id' => Mod::factory(),
|
'mod_id' => Mod::factory(),
|
||||||
'version' => $versionString,
|
'version' => $this->faker->numerify('#.#.#'),
|
||||||
'version_major' => $version->getMajor(),
|
|
||||||
'version_minor' => $version->getMinor(),
|
|
||||||
'version_patch' => $version->getPatch(),
|
|
||||||
'version_pre_release' => $version->getPreRelease(),
|
|
||||||
'description' => fake()->text(),
|
'description' => fake()->text(),
|
||||||
'link' => fake()->url(),
|
'link' => fake()->url(),
|
||||||
|
|
||||||
|
29
database/factories/OAuthConnectionFactory.php
Normal file
29
database/factories/OAuthConnectionFactory.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
protected $model = OAuthConnection::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*/
|
||||||
|
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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@
|
|||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\SptVersion;
|
use App\Models\SptVersion;
|
||||||
use App\Support\Version;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
@ -16,19 +15,8 @@ class SptVersionFactory extends Factory
|
|||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
$versionString = $this->faker->numerify('#.#.#');
|
|
||||||
try {
|
|
||||||
$version = new Version($versionString);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$version = new Version('0.0.0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'version' => $versionString,
|
'version' => $this->faker->numerify('#.#.#'),
|
||||||
'version_major' => $version->getMajor(),
|
|
||||||
'version_minor' => $version->getMinor(),
|
|
||||||
'version_patch' => $version->getPatch(),
|
|
||||||
'version_pre_release' => $version->getPreRelease(),
|
|
||||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||||
'link' => $this->faker->url,
|
'link' => $this->faker->url,
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
|
@ -14,14 +14,12 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->bigInteger('hub_id')
|
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
|
||||||
->nullable()
|
$table->unsignedBigInteger('discord_id')->nullable()->default(null)->unique();
|
||||||
->default(null)
|
|
||||||
->unique();
|
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password')->nullable();
|
||||||
$table->longText('about')->nullable()->default(null);
|
$table->longText('about')->nullable()->default(null);
|
||||||
$table->foreignIdFor(UserRole::class)
|
$table->foreignIdFor(UserRole::class)
|
||||||
->nullable()
|
->nullable()
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('oauth_connections', function (Blueprint $table) {
|
||||||
|
$table->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->string('nickname')->default('');
|
||||||
|
$table->string('name')->default('');
|
||||||
|
$table->string('email')->default('');
|
||||||
|
$table->string('avatar')->default('');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['provider', 'provider_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('oauth_providers');
|
||||||
|
}
|
||||||
|
};
|
@ -44,5 +44,23 @@
|
|||||||
</x-button>
|
</x-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@if (config('services.discord.client_id') && config('services.discord.client_secret'))
|
||||||
|
<div class="relative my-6">
|
||||||
|
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div class="w-full border-t border-gray-300 dark:border-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center">
|
||||||
|
<span class="bg-white dark:bg-black px-2 text-sm text-gray-500 dark:text-gray-500">OR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ route('login.socialite', ['provider' => 'discord']) }}" class="w-full text-white bg-[#5865F2] hover:bg-[#5865F2]/90 focus:ring-4 focus:outline-none focus:ring-[#5865F2]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center justify-center dark:focus:ring-[#5865F2]/55 me-2 mb-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-4 h-4 mr-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"/>
|
||||||
|
</svg>
|
||||||
|
{{ __('Login with Discord') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
</x-authentication-card>
|
</x-authentication-card>
|
||||||
</x-guest-layout>
|
</x-guest-layout>
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
<x-action-section>
|
||||||
|
<x-slot name="title">
|
||||||
|
{{ __('Connected Accounts') }}
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="description">
|
||||||
|
{{ __('Manage your connected OAuth accounts.') }}
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="content">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ __('You can manage your OAuth connections here') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
@if ($user->password === null)
|
||||||
|
<div class="mt-3 max-w-xl text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<p>{{ __('Before you can remove a connection you must have an account password set.') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session()->has('status'))
|
||||||
|
<div class="mt-3 font-medium text-sm text-green-600 dark:text-green-400">
|
||||||
|
{{ session('status') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session()->has('error'))
|
||||||
|
<div class="mt-3 font-medium text-sm text-red-600 dark:text-red-400">
|
||||||
|
{{ session('error') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-5 space-y-6">
|
||||||
|
@forelse ($user->oauthConnections as $connection)
|
||||||
|
<div class="flex items-center text-gray-600 dark:text-gray-400">
|
||||||
|
<div>
|
||||||
|
@switch ($connection->provider)
|
||||||
|
@case ('discord')
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-4 h-4" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@default
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
||||||
|
</svg>
|
||||||
|
@endswitch
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ms-3">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ ucfirst($connection->provider) }} - {{ $connection->name }} - {{ $connection->email }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{{ __('Connected') }} {{ $connection->created_at->format('M d, Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ms-auto">
|
||||||
|
@can('delete', $connection)
|
||||||
|
<x-danger-button wire:click="confirmConnectionDeletion({{ $connection->id }})" wire:loading.attr="disabled">
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</x-danger-button>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ __('You have no connected accounts.') }}
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<x-dialog-modal wire:model="confirmingConnectionDeletion">
|
||||||
|
<x-slot name="title">
|
||||||
|
{{ __('Remove Connected Account') }}
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="content">
|
||||||
|
{{ __('Are you sure you want to remove this connected account? This action cannot be undone.') }}
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="footer">
|
||||||
|
<x-secondary-button wire:click="$toggle('confirmingConnectionDeletion')" wire:loading.attr="disabled">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</x-secondary-button>
|
||||||
|
|
||||||
|
<x-danger-button class="ms-3" wire:click="deleteConnection" wire:loading.attr="disabled">
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</x-danger-button>
|
||||||
|
</x-slot>
|
||||||
|
</x-dialog-modal>
|
||||||
|
</x-slot>
|
||||||
|
</x-action-section>
|
@ -4,37 +4,37 @@
|
|||||||
<h2 class="text-2xl">{{ $title }}</h2>
|
<h2 class="text-2xl">{{ $title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (! $followUsers->count())
|
@if ($followUsersCount === 0)
|
||||||
<div class="flex justify-center text-sm pt-2">
|
<div class="flex justify-center text-sm pt-2">
|
||||||
{{ $emptyMessage }}
|
{{ $emptyMessage }}
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="flex ml-6 py-2 justify-center items-center">
|
<div class="flex ml-6 py-2 justify-center items-center">
|
||||||
@foreach ($followUsers->slice(0, $limit) as $user)
|
@foreach ($displayLimit as $data)
|
||||||
{{-- User Badge --}}
|
{{-- User Badge --}}
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<a href="{{ $user->profileUrl() }}" class="rounded-full -ml-6 z-20 bg-[#ebf4ff] h-16 w-16 flex justify-center items-center border">
|
<a href="{{ $data['user']->profileUrl() }}" class="rounded-full -ml-6 z-20 bg-[#ebf4ff] h-16 w-16 flex justify-center items-center border">
|
||||||
<img src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" class="h-full w-full rounded-full" />
|
<img src="{{ $data['user']->profile_photo_url }}" alt="{{ $data['user']->name }}" class="h-full w-full rounded-full" />
|
||||||
</a>
|
</a>
|
||||||
<div class="absolute bottom-full -ml-3 left-1/2 transform -translate-x-1/2 mb-2 w-max px-2 py-1 text-sm text-white bg-gray-700 rounded shadow-lg opacity-0 group-hover:opacity-100">
|
<div class="absolute bottom-full -ml-3 left-1/2 transform -translate-x-1/2 mb-2 w-max px-2 py-1 text-sm text-white bg-gray-700 rounded shadow-lg opacity-0 group-hover:opacity-100">
|
||||||
{{ $user->name }}
|
{{ $data['user']->name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
@if ($followUsers->count() > $limit)
|
@if ($followUsersCount > $limit)
|
||||||
{{-- Count Badge --}}
|
{{-- Count Badge --}}
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<button wire:click="toggleFollowDialog" class="rounded-full -ml-6 z-20 bg-cyan-500 dark:bg-cyan-700 h-16 w-16 flex justify-center items-center border text-white">+{{ $followUsers->count() - $limit }}</button>
|
<button wire:click="toggleFollowDialog" class="rounded-full -ml-6 z-20 bg-cyan-500 dark:bg-cyan-700 h-16 w-16 flex justify-center items-center border text-white">+{{ $followUsersCount - $limit }}</button>
|
||||||
<div class="absolute bottom-full -ml-3 left-1/2 transform -translate-x-1/2 mb-2 w-max px-2 py-1 text-sm text-white bg-gray-700 rounded shadow-lg opacity-0 group-hover:opacity-100">
|
<div class="absolute bottom-full -ml-3 left-1/2 transform -translate-x-1/2 mb-2 w-max px-2 py-1 text-sm text-white bg-gray-700 rounded shadow-lg opacity-0 group-hover:opacity-100">
|
||||||
{{ $followUsers->count() }} total
|
{{ $followUsersCount }} total
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($followUsers->count() > $limit)
|
@if ($followUsersCount > $limit)
|
||||||
{{-- View All Button --}}
|
{{-- View All Button --}}
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<button wire:click="toggleFollowDialog" class="hover:underline active:underline">View All</button>
|
<button wire:click="toggleFollowDialog" class="hover:underline active:underline">View All</button>
|
||||||
@ -49,28 +49,27 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
<x-slot name="content">
|
<x-slot name="content">
|
||||||
<div class="h-96 overflow-y-auto">
|
<div class="h-96 overflow-y-auto">
|
||||||
@foreach ($followUsers as $user)
|
@foreach ($display as $data)
|
||||||
<div class="flex group/item dark:hover:bg-gray-950 items-center p-2 pr-3 rounded-md">
|
<div class="flex group/item dark:hover:bg-gray-950 items-center p-2 pr-3 rounded-md">
|
||||||
<a href="{{ $user->profileUrl() }}" class="flex-shrink-0 w-16 h-16 items-center">
|
<a href="{{ $data['user']->profileUrl() }}" class="flex-shrink-0 w-16 h-16 items-center">
|
||||||
<img src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" class="block w-full h-full rounded-full" />
|
<img src="{{ $data['user']->profile_photo_url }}" alt="{{ $data['user']->name }}" class="block w-full h-full rounded-full" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="flex flex-col w-full pl-3">
|
<div class="flex flex-col w-full pl-3">
|
||||||
<a href="{{ $user->profileUrl() }}" class="text-2xl group-hover/item:underline group-hover/item:text-white">{{ $user->name }}</a>
|
<a href="{{ $data['user']->profileUrl() }}" class="text-2xl group-hover/item:underline group-hover/item:text-white">{{ $data['user']->name }}</a>
|
||||||
<span>
|
<span>
|
||||||
{{ __("Member Since") }}
|
{{ __("Member Since") }}
|
||||||
<x-time :datetime="$user->created_at" />
|
<x-time :datetime="$data['user']->created_at" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (auth()->check() && auth()->user()->id !== $user->id)
|
@if (auth()->check() && auth()->user()->id !== $data['user']->id)
|
||||||
<livewire:user.follow-buttons :profile-user-id="$user->id" :is-following="$user->follows" />
|
<livewire:user.follow-buttons :profile-user-id="$data['user']->id" :is-following="$data['isFollowing']" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<x-slot name="footer">
|
<x-slot name="footer">
|
||||||
<x-button x-on:click="show = false">
|
<x-button x-on:click="show = false">
|
||||||
{{ __('Close') }}
|
{{ __('Close') }}
|
||||||
|
@ -29,6 +29,12 @@
|
|||||||
<x-section-border />
|
<x-section-border />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{-- OAuth Management --}}
|
||||||
|
<div class="mt-10 sm:mt-0">
|
||||||
|
@livewire('profile.manage-oauth-connections')
|
||||||
|
</div>
|
||||||
|
<x-section-border />
|
||||||
|
|
||||||
@if (config('session.driver') === 'database')
|
@if (config('session.driver') === 'database')
|
||||||
<div class="mt-10 sm:mt-0">
|
<div class="mt-10 sm:mt-0">
|
||||||
@livewire('profile.logout-other-browser-sessions-form')
|
@livewire('profile.logout-other-browser-sessions-form')
|
||||||
|
@ -8,11 +8,18 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<x-slot name="form">
|
<x-slot name="form">
|
||||||
|
|
||||||
|
@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">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<x-label for="current_password" value="{{ __('Current Password') }}" />
|
<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 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" />
|
<x-input-error for="current_password" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<x-label for="password" value="{{ __('New Password') }}" />
|
<x-label for="password" value="{{ __('New Password') }}" />
|
||||||
|
@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\ModController;
|
use App\Http\Controllers\ModController;
|
||||||
use App\Http\Controllers\ModVersionController;
|
use App\Http\Controllers\ModVersionController;
|
||||||
|
use App\Http\Controllers\SocialiteController;
|
||||||
use App\Http\Controllers\UserController;
|
use App\Http\Controllers\UserController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware(['auth.banned'])->group(function () {
|
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 () {
|
Route::get('/', function () {
|
||||||
return view('home');
|
return view('home');
|
||||||
})->name('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');
|
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Download Link
|
||||||
Route::controller(ModVersionController::class)->group(function () {
|
Route::controller(ModVersionController::class)->group(function () {
|
||||||
Route::get('/mod/download/{mod}/{slug}/{version}', 'show')
|
Route::get('/mod/download/{mod}/{slug}/{version}', 'show')
|
||||||
->where([
|
->where(['mod' => '[0-9]+', 'slug' => '[a-z0-9-]+'])
|
||||||
'mod' => '[0-9]+',
|
|
||||||
'slug' => '[a-z0-9-]+',
|
|
||||||
])
|
|
||||||
->name('mod.version.download');
|
->name('mod.version.download');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
3
storage/app/.gitignore
vendored
3
storage/app/.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
*
|
|
||||||
!public/
|
|
||||||
!.gitignore
|
|
2
storage/app/public/.gitignore
vendored
2
storage/app/public/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
0
storage/app/public/cover-photos/.gitkeep
Normal file
0
storage/app/public/cover-photos/.gitkeep
Normal file
0
storage/app/public/profile-photos/.gitkeep
Normal file
0
storage/app/public/profile-photos/.gitkeep
Normal file
@ -2,28 +2,6 @@
|
|||||||
|
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use App\Models\ModVersion;
|
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 () {
|
it('displays the latest version on the mod detail page', function () {
|
||||||
$versions = [
|
$versions = [
|
||||||
|
@ -30,3 +30,22 @@ test('users cannot authenticate with invalid password', function () {
|
|||||||
|
|
||||||
$this->assertGuest();
|
$this->assertGuest();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('users can authenticate using Discord', function () {
|
||||||
|
$response = $this->get(route('login.socialite', ['provider' => 'discord']));
|
||||||
|
|
||||||
|
$response->assertStatus(302);
|
||||||
|
$response->assertRedirect();
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
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