mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 20:20: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.
|
||||
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.
|
||||
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/storage
|
||||
storage/*.key
|
||||
storage/app/livewire-tmp
|
||||
storage/app/public
|
||||
!storage/app/public/cover-photos/.gitkeep
|
||||
!storage/app/public/profile-photos/.gitkeep
|
||||
vendor
|
||||
auth.json
|
||||
frankenphp
|
||||
|
@ -20,7 +20,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name' => ['required', 'string', 'max:36', 'unique:users'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => $this->passwordRules(),
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
||||
|
@ -16,7 +16,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'name' => ['required', 'string', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||
'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'],
|
||||
|
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();
|
||||
|
||||
$mods = $user->mods()
|
||||
->with([
|
||||
'users',
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(10)
|
||||
->fragment('mods');
|
||||
|
@ -392,7 +392,7 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
$hashShort = substr($avatar->fileHash, 0, 2);
|
||||
$fileName = $avatar->fileHash.'.'.$avatar->avatarExtension;
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/images/avatars/'.$hashShort.'/'.$avatar->avatarID.'-'.$fileName;
|
||||
$relativePath = 'user-avatars/'.$fileName;
|
||||
$relativePath = User::profilePhotoStoragePath().'/'.$fileName;
|
||||
|
||||
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||
}
|
||||
|
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;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\On;
|
||||
@ -40,12 +39,16 @@ class FollowCard extends Component
|
||||
public string $dialogTitle;
|
||||
|
||||
/**
|
||||
* The users to display in the card.
|
||||
*
|
||||
* @var Collection<User>
|
||||
* The user data to display in the card.
|
||||
*/
|
||||
#[Locked]
|
||||
public Collection $followUsers;
|
||||
public array $display = [];
|
||||
|
||||
/**
|
||||
* The limited user data to display in the card.
|
||||
*/
|
||||
#[Locked]
|
||||
public array $displayLimit = [];
|
||||
|
||||
/**
|
||||
* The maximum number of users to display on the card.
|
||||
@ -64,6 +67,12 @@ class FollowCard extends Component
|
||||
#[Locked]
|
||||
public User $profileUser;
|
||||
|
||||
/**
|
||||
* The number of users being displayed.
|
||||
*/
|
||||
#[Locked]
|
||||
public int $followUsersCount;
|
||||
|
||||
/**
|
||||
* Called when the component is initialized.
|
||||
*/
|
||||
@ -130,18 +139,31 @@ class FollowCard extends Component
|
||||
public function populateFollowUsers(): void
|
||||
{
|
||||
// Fetch IDs of all users the authenticated user is following.
|
||||
$followingIds = auth()->user()->following()->pluck('following_id');
|
||||
$followingIds = collect();
|
||||
$authUser = auth()->user();
|
||||
if ($authUser) {
|
||||
$followingIds = $authUser->following()->pluck('following_id');
|
||||
}
|
||||
|
||||
// Load the profile user's followers (or following) and map the follow status.
|
||||
$this->followUsers = $this->profileUser->{$this->relationship}
|
||||
// Load the profile user's followers (or following).
|
||||
$users = $this->profileUser->{$this->relationship}()->with([])->get();
|
||||
|
||||
// Count the number of users.
|
||||
$this->followUsersCount = $users->count();
|
||||
|
||||
// Load the users to display and whether the authenticated user is following each user.
|
||||
$this->display = $users
|
||||
->map(function (User $user) use ($followingIds) {
|
||||
// Add the follow status based on the preloaded IDs.
|
||||
$user->follows = $followingIds->contains($user->id);
|
||||
return [
|
||||
'user' => $user,
|
||||
'isFollowing' => $followingIds->contains($user->id),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
// TODO: The above follows property doesn't exist on the User model. What was I smoking?
|
||||
|
||||
return $user;
|
||||
});
|
||||
// Store limited users for the main view.
|
||||
$this->displayLimit = collect($this->display)
|
||||
->take($this->limit)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
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\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Str;
|
||||
@ -43,6 +44,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the storage path for profile photos.
|
||||
*/
|
||||
public static function profilePhotoStoragePath(): string
|
||||
{
|
||||
return 'profile-photos';
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a user and their mods.
|
||||
*
|
||||
@ -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
|
||||
{
|
||||
|
@ -20,6 +20,8 @@ class ModObserver
|
||||
*/
|
||||
public function saved(Mod $mod): void
|
||||
{
|
||||
$mod->load('versions.sptVersions');
|
||||
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
@ -44,6 +46,8 @@ class ModObserver
|
||||
*/
|
||||
public function deleted(Mod $mod): void
|
||||
{
|
||||
$mod->load('versions.sptVersions');
|
||||
|
||||
$this->updateRelatedSptVersions($mod);
|
||||
}
|
||||
}
|
||||
|
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;
|
||||
|
||||
use App\Livewire\Profile\UpdatePasswordForm;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
@ -13,9 +14,13 @@ use App\Observers\ModVersionObserver;
|
||||
use App\Observers\SptVersionObserver;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Number;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Livewire;
|
||||
use SocialiteProviders\Discord\Provider;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -35,15 +40,28 @@ class AppServiceProvider extends ServiceProvider
|
||||
// Allow mass assignment for all models. Be careful!
|
||||
Model::unguard();
|
||||
|
||||
// Disable lazy loading in non-production environments.
|
||||
Model::preventLazyLoading(! app()->isProduction());
|
||||
|
||||
// Register model observers.
|
||||
$this->registerObservers();
|
||||
|
||||
// Register custom macros.
|
||||
$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();
|
||||
});
|
||||
|
||||
// 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');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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\JetstreamServiceProvider::class,
|
||||
App\Providers\Filament\AdminPanelProvider::class,
|
||||
\SocialiteProviders\Manager\ServiceProvider::class,
|
||||
];
|
||||
|
@ -19,12 +19,14 @@
|
||||
"laravel/pulse": "^1.2",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/scout": "^10.10",
|
||||
"laravel/socialite": "^5.16",
|
||||
"laravel/tinker": "^2.9",
|
||||
"league/flysystem-aws-s3-v3": "^3.28",
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"livewire/livewire": "^3.5",
|
||||
"mchev/banhammer": "^2.3",
|
||||
"meilisearch/meilisearch-php": "^1.8",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"stevebauman/purify": "^6.2"
|
||||
},
|
||||
"require-dev": {
|
||||
|
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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "645a73e7a66339c2396753a2f35c5eea",
|
||||
"content-hash": "2e603ffae8f6f8a4c834c9bff55cc30f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "anourvalar/eloquent-serialize",
|
||||
@ -1867,6 +1867,69 @@
|
||||
},
|
||||
"time": "2024-09-23T14:10:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v6.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/php-jwt.git",
|
||||
"reference": "500501c2ce893c824c801da135d02661199f60c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5",
|
||||
"reference": "500501c2ce893c824c801da135d02661199f60c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Firebase\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Neuman Vong",
|
||||
"email": "neuman+pear@twilio.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Anant Narayanan",
|
||||
"email": "anant@php.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
|
||||
"homepage": "https://github.com/firebase/php-jwt",
|
||||
"keywords": [
|
||||
"jwt",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v6.10.1"
|
||||
},
|
||||
"time": "2024-05-18T18:05:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
"version": "v1.3.0",
|
||||
@ -3473,6 +3536,78 @@
|
||||
},
|
||||
"time": "2024-08-02T07:48:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/socialite",
|
||||
"version": "v5.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/socialite.git",
|
||||
"reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
|
||||
"reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"firebase/php-jwt": "^6.4",
|
||||
"guzzlehttp/guzzle": "^6.0|^7.0",
|
||||
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
|
||||
"league/oauth1-client": "^1.10.1",
|
||||
"php": "^7.2|^8.0",
|
||||
"phpseclib/phpseclib": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^8.0|^9.3|^10.4"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Socialite\\SocialiteServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Socialite": "Laravel\\Socialite\\Facades\\Socialite"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Socialite\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.",
|
||||
"homepage": "https://laravel.com",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"oauth"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/socialite/issues",
|
||||
"source": "https://github.com/laravel/socialite"
|
||||
},
|
||||
"time": "2024-09-03T09:46:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/tinker",
|
||||
"version": "v2.9.0",
|
||||
@ -4147,6 +4282,82 @@
|
||||
],
|
||||
"time": "2024-09-21T08:32:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/oauth1-client",
|
||||
"version": "v1.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/oauth1-client.git",
|
||||
"reference": "d6365b901b5c287dd41f143033315e2f777e1167"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167",
|
||||
"reference": "d6365b901b5c287dd41f143033315e2f777e1167",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"guzzlehttp/guzzle": "^6.0|^7.0",
|
||||
"guzzlehttp/psr7": "^1.7|^2.0",
|
||||
"php": ">=7.1||>=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-simplexml": "*",
|
||||
"friendsofphp/php-cs-fixer": "^2.17",
|
||||
"mockery/mockery": "^1.3.3",
|
||||
"phpstan/phpstan": "^0.12.42",
|
||||
"phpunit/phpunit": "^7.5||9.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-simplexml": "For decoding XML-based responses."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev",
|
||||
"dev-develop": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\OAuth1\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Corlett",
|
||||
"email": "bencorlett@me.com",
|
||||
"homepage": "http://www.webcomm.com.au",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "OAuth 1.0 Client Library",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"SSO",
|
||||
"authorization",
|
||||
"bitbucket",
|
||||
"identity",
|
||||
"idp",
|
||||
"oauth",
|
||||
"oauth1",
|
||||
"single sign on",
|
||||
"trello",
|
||||
"tumblr",
|
||||
"twitter"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/oauth1-client/issues",
|
||||
"source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1"
|
||||
},
|
||||
"time": "2022-04-15T14:02:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/uri",
|
||||
"version": "7.4.1",
|
||||
@ -5390,6 +5601,56 @@
|
||||
},
|
||||
"time": "2024-05-08T12:36:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
"version": "v9.99.100",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/random_compat.git",
|
||||
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">= 7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*|5.*",
|
||||
"vimeo/psalm": "^1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||
},
|
||||
"type": "library",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com",
|
||||
"homepage": "https://paragonie.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||
"keywords": [
|
||||
"csprng",
|
||||
"polyfill",
|
||||
"pseudorandom",
|
||||
"random"
|
||||
],
|
||||
"support": {
|
||||
"email": "info@paragonie.com",
|
||||
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||
"source": "https://github.com/paragonie/random_compat"
|
||||
},
|
||||
"time": "2020-10-15T08:29:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/discovery",
|
||||
"version": "1.19.4",
|
||||
@ -5544,6 +5805,116 @@
|
||||
],
|
||||
"time": "2024-07-20T21:41:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.42",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98",
|
||||
"reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"paragonie/constant_time_encoding": "^1|^2|^3",
|
||||
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||
"php": ">=5.6.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
|
||||
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
|
||||
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
|
||||
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
|
||||
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"phpseclib/bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"phpseclib3\\": "phpseclib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jim Wigginton",
|
||||
"email": "terrafrost@php.net",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Patrick Monnerat",
|
||||
"email": "pm@datasphere.ch",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Andreas Fischer",
|
||||
"email": "bantu@phpbb.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Hans-Jürgen Petrich",
|
||||
"email": "petrich@tronic-media.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "graham@alt-three.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
|
||||
"homepage": "http://phpseclib.sourceforge.net",
|
||||
"keywords": [
|
||||
"BigInteger",
|
||||
"aes",
|
||||
"asn.1",
|
||||
"asn1",
|
||||
"blowfish",
|
||||
"crypto",
|
||||
"cryptography",
|
||||
"encryption",
|
||||
"rsa",
|
||||
"security",
|
||||
"sftp",
|
||||
"signature",
|
||||
"signing",
|
||||
"ssh",
|
||||
"twofish",
|
||||
"x.509",
|
||||
"x509"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.42"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/terrafrost",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpseclib",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-16T03:06:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pragmarx/google2fa",
|
||||
"version": "v8.0.3",
|
||||
@ -6439,6 +6810,130 @@
|
||||
],
|
||||
"time": "2024-02-26T18:08:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/discord",
|
||||
"version": "4.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SocialiteProviders/Discord.git",
|
||||
"reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c71c379acfdca5ba4aa65a3db5ae5222852a919c",
|
||||
"reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"socialiteproviders/manager": "~4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SocialiteProviders\\Discord\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christopher Eklund",
|
||||
"email": "eklundchristopher@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Discord OAuth2 Provider for Laravel Socialite",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"laravel",
|
||||
"oauth",
|
||||
"provider",
|
||||
"socialite"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://socialiteproviders.com/discord",
|
||||
"issues": "https://github.com/socialiteproviders/providers/issues",
|
||||
"source": "https://github.com/socialiteproviders/providers"
|
||||
},
|
||||
"time": "2023-07-24T23:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/manager",
|
||||
"version": "v4.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SocialiteProviders/Manager.git",
|
||||
"reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/dea5190981c31b89e52259da9ab1ca4e2b258b21",
|
||||
"reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0",
|
||||
"laravel/socialite": "^5.5",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.2",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"SocialiteProviders\\Manager\\ServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SocialiteProviders\\Manager\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andy Wendt",
|
||||
"email": "andy@awendt.com"
|
||||
},
|
||||
{
|
||||
"name": "Anton Komarev",
|
||||
"email": "a.komarev@cybercog.su"
|
||||
},
|
||||
{
|
||||
"name": "Miguel Piedrafita",
|
||||
"email": "soy@miguelpiedrafita.com"
|
||||
},
|
||||
{
|
||||
"name": "atymic",
|
||||
"email": "atymicq@gmail.com",
|
||||
"homepage": "https://atymic.dev"
|
||||
}
|
||||
],
|
||||
"description": "Easily add new or override built-in providers in Laravel Socialite.",
|
||||
"homepage": "https://socialiteproviders.com",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"manager",
|
||||
"oauth",
|
||||
"providers",
|
||||
"socialite"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/socialiteproviders/manager/issues",
|
||||
"source": "https://github.com/socialiteproviders/manager"
|
||||
},
|
||||
"time": "2024-05-04T07:57:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/color",
|
||||
"version": "1.6.0",
|
||||
|
@ -40,4 +40,12 @@ return [
|
||||
'token' => env('GITEA_TOKEN', ''),
|
||||
],
|
||||
|
||||
'discord' => [
|
||||
'client_id' => env('DISCORD_CLIENT_ID'),
|
||||
'client_secret' => env('DISCORD_CLIENT_SECRET'),
|
||||
'redirect' => env('DISCORD_REDIRECT_URI'),
|
||||
'allow_gif_avatars' => (bool) env('DISCORD_AVATAR_GIF', true),
|
||||
'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'webp'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -5,7 +5,6 @@ namespace Database\Factories;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use App\Support\Version;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@ -18,20 +17,9 @@ class ModVersionFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$versionString = $this->faker->numerify('#.#.#');
|
||||
try {
|
||||
$version = new Version($versionString);
|
||||
} catch (\Exception $e) {
|
||||
$version = new Version('0.0.0');
|
||||
}
|
||||
|
||||
return [
|
||||
'mod_id' => Mod::factory(),
|
||||
'version' => $versionString,
|
||||
'version_major' => $version->getMajor(),
|
||||
'version_minor' => $version->getMinor(),
|
||||
'version_patch' => $version->getPatch(),
|
||||
'version_pre_release' => $version->getPreRelease(),
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'description' => fake()->text(),
|
||||
'link' => fake()->url(),
|
||||
|
||||
|
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;
|
||||
|
||||
use App\Models\SptVersion;
|
||||
use App\Support\Version;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@ -16,19 +15,8 @@ class SptVersionFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$versionString = $this->faker->numerify('#.#.#');
|
||||
try {
|
||||
$version = new Version($versionString);
|
||||
} catch (\Exception $e) {
|
||||
$version = new Version('0.0.0');
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => $versionString,
|
||||
'version_major' => $version->getMajor(),
|
||||
'version_minor' => $version->getMinor(),
|
||||
'version_patch' => $version->getPatch(),
|
||||
'version_pre_release' => $version->getPreRelease(),
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||
'link' => $this->faker->url,
|
||||
'created_at' => Carbon::now(),
|
||||
|
@ -14,14 +14,12 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('hub_id')
|
||||
->nullable()
|
||||
->default(null)
|
||||
->unique();
|
||||
$table->bigInteger('hub_id')->nullable()->default(null)->unique();
|
||||
$table->unsignedBigInteger('discord_id')->nullable()->default(null)->unique();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('password')->nullable();
|
||||
$table->longText('about')->nullable()->default(null);
|
||||
$table->foreignIdFor(UserRole::class)
|
||||
->nullable()
|
||||
|
@ -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>
|
||||
</div>
|
||||
</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-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>
|
||||
</div>
|
||||
|
||||
@if (! $followUsers->count())
|
||||
@if ($followUsersCount === 0)
|
||||
<div class="flex justify-center text-sm pt-2">
|
||||
{{ $emptyMessage }}
|
||||
</div>
|
||||
@else
|
||||
<div class="flex ml-6 py-2 justify-center items-center">
|
||||
@foreach ($followUsers->slice(0, $limit) as $user)
|
||||
@foreach ($displayLimit as $data)
|
||||
{{-- User Badge --}}
|
||||
<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">
|
||||
<img src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" class="h-full w-full rounded-full" />
|
||||
<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="{{ $data['user']->profile_photo_url }}" alt="{{ $data['user']->name }}" class="h-full w-full rounded-full" />
|
||||
</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">
|
||||
{{ $user->name }}
|
||||
{{ $data['user']->name }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if ($followUsers->count() > $limit)
|
||||
@if ($followUsersCount > $limit)
|
||||
{{-- Count Badge --}}
|
||||
<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">
|
||||
{{ $followUsers->count() }} total
|
||||
{{ $followUsersCount }} total
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($followUsers->count() > $limit)
|
||||
@if ($followUsersCount > $limit)
|
||||
{{-- View All Button --}}
|
||||
<div class="flex justify-center items-center">
|
||||
<button wire:click="toggleFollowDialog" class="hover:underline active:underline">View All</button>
|
||||
@ -49,28 +49,27 @@
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<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">
|
||||
<a href="{{ $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" />
|
||||
<a href="{{ $data['user']->profileUrl() }}" class="flex-shrink-0 w-16 h-16 items-center">
|
||||
<img src="{{ $data['user']->profile_photo_url }}" alt="{{ $data['user']->name }}" class="block w-full h-full rounded-full" />
|
||||
</a>
|
||||
|
||||
<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>
|
||||
{{ __("Member Since") }}
|
||||
<x-time :datetime="$user->created_at" />
|
||||
<x-time :datetime="$data['user']->created_at" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (auth()->check() && auth()->user()->id !== $user->id)
|
||||
<livewire:user.follow-buttons :profile-user-id="$user->id" :is-following="$user->follows" />
|
||||
@if (auth()->check() && auth()->user()->id !== $data['user']->id)
|
||||
<livewire:user.follow-buttons :profile-user-id="$data['user']->id" :is-following="$data['isFollowing']" />
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-button x-on:click="show = false">
|
||||
{{ __('Close') }}
|
||||
|
@ -29,6 +29,12 @@
|
||||
<x-section-border />
|
||||
@endif
|
||||
|
||||
{{-- OAuth Management --}}
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.manage-oauth-connections')
|
||||
</div>
|
||||
<x-section-border />
|
||||
|
||||
@if (config('session.driver') === 'database')
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.logout-other-browser-sessions-form')
|
||||
|
@ -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') }}" />
|
||||
|
@ -2,11 +2,17 @@
|
||||
|
||||
use App\Http\Controllers\ModController;
|
||||
use App\Http\Controllers\ModVersionController;
|
||||
use App\Http\Controllers\SocialiteController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth.banned'])->group(function () {
|
||||
|
||||
Route::controller(SocialiteController::class)->group(function () {
|
||||
Route::get('/login/{provider}/redirect', 'redirect')->name('login.socialite');
|
||||
Route::get('/login/{provider}/callback', 'callback');
|
||||
});
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('home');
|
||||
})->name('home');
|
||||
@ -16,12 +22,10 @@ Route::middleware(['auth.banned'])->group(function () {
|
||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
||||
});
|
||||
|
||||
// Download Link
|
||||
Route::controller(ModVersionController::class)->group(function () {
|
||||
Route::get('/mod/download/{mod}/{slug}/{version}', 'show')
|
||||
->where([
|
||||
'mod' => '[0-9]+',
|
||||
'slug' => '[a-z0-9-]+',
|
||||
])
|
||||
->where(['mod' => '[0-9]+', 'slug' => '[a-z0-9-]+'])
|
||||
->name('mod.version.download');
|
||||
});
|
||||
|
||||
|
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\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('displays homepage mod cards with the latest supported spt version number', function () {
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
$sptVersion3 = SptVersion::factory()->create(['version' => '3.0.0']);
|
||||
|
||||
$mod1 = Mod::factory()->create();
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]);
|
||||
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertSeeInOrder(explode(' ', "$mod1->name $sptVersion3->version_formatted"));
|
||||
});
|
||||
|
||||
it('displays the latest version on the mod detail page', function () {
|
||||
$versions = [
|
||||
|
@ -30,3 +30,22 @@ test('users cannot authenticate with invalid password', function () {
|
||||
|
||||
$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