mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-13 04:30:41 -05:00
Merge branch 'user-profiles' into develop
This commit is contained in:
commit
65e416e4d9
@ -21,12 +21,17 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'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'],
|
||||||
])->validateWithBag('updateProfileInformation');
|
])->validateWithBag('updateProfileInformation');
|
||||||
|
|
||||||
if (isset($input['photo'])) {
|
if (isset($input['photo'])) {
|
||||||
$user->updateProfilePhoto($input['photo']);
|
$user->updateProfilePhoto($input['photo']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($input['cover'])) {
|
||||||
|
$user->updateCoverPhoto($input['cover']);
|
||||||
|
}
|
||||||
|
|
||||||
if ($input['email'] !== $user->email &&
|
if ($input['email'] !== $user->email &&
|
||||||
$user instanceof MustVerifyEmail) {
|
$user instanceof MustVerifyEmail) {
|
||||||
$this->updateVerifiedUser($user, $input);
|
$this->updateVerifiedUser($user, $input);
|
||||||
|
22
app/Http/Controllers/Api/V0/ApiController.php
Normal file
22
app/Http/Controllers/Api/V0/ApiController.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V0;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ApiController extends Controller
|
||||||
|
{
|
||||||
|
public static function shouldInclude(string $relationship): bool
|
||||||
|
{
|
||||||
|
$param = request()->get('include');
|
||||||
|
|
||||||
|
if (! $param) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$includeValues = explode(',', Str::lower($param));
|
||||||
|
|
||||||
|
return in_array(Str::lower($relationship), $includeValues);
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\V0;
|
namespace App\Http\Controllers\Api\V0;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\Api\V0\StoreModRequest;
|
use App\Http\Requests\Api\V0\StoreModRequest;
|
||||||
use App\Http\Requests\Api\V0\UpdateModRequest;
|
use App\Http\Requests\Api\V0\UpdateModRequest;
|
||||||
use App\Http\Resources\Api\V0\ModResource;
|
use App\Http\Resources\Api\V0\ModResource;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
|
|
||||||
class ModController extends Controller
|
class ModController extends ApiController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
|
@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\V0;
|
namespace App\Http\Controllers\Api\V0;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\Api\V0\StoreUserRequest;
|
use App\Http\Requests\Api\V0\StoreUserRequest;
|
||||||
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
||||||
use App\Http\Resources\Api\V0\UserResource;
|
use App\Http\Resources\Api\V0\UserResource;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
class UsersController extends Controller
|
class UsersController extends ApiController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
|
25
app/Http/Controllers/UserController.php
Normal file
25
app/Http/Controllers/UserController.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function show(Request $request, User $user, string $username)
|
||||||
|
{
|
||||||
|
if ($user->slug() !== $username) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()?->cannot('view', $user)) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('user.show', compact('user'));
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources\Api\V0;
|
namespace App\Http\Resources\Api\V0;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V0\ApiController;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
@ -35,23 +36,29 @@ class ModResource extends JsonResource
|
|||||||
'published_at' => $this->published_at,
|
'published_at' => $this->published_at,
|
||||||
],
|
],
|
||||||
'relationships' => [
|
'relationships' => [
|
||||||
'users' => [
|
'users' => $this->users->map(fn ($user) => [
|
||||||
'data' => $this->users->map(fn ($user) => [
|
'data' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
])->toArray(),
|
|
||||||
|
|
||||||
// TODO: Provide 'links.self' to user profile
|
|
||||||
//'links' => ['self' => '#'],
|
|
||||||
],
|
],
|
||||||
|
'links' => [
|
||||||
|
'self' => $user->profileUrl(),
|
||||||
|
],
|
||||||
|
])->toArray(),
|
||||||
'license' => [
|
'license' => [
|
||||||
|
[
|
||||||
'data' => [
|
'data' => [
|
||||||
'type' => 'license',
|
'type' => 'license',
|
||||||
'id' => $this->license_id,
|
'id' => $this->license_id,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'included' => $this->users->map(fn ($user) => new UserResource($user)),
|
],
|
||||||
|
|
||||||
|
'includes' => $this->when(
|
||||||
|
ApiController::shouldInclude('users'),
|
||||||
|
fn () => $this->users->map(fn ($user) => new UserResource($user))
|
||||||
|
),
|
||||||
|
|
||||||
// TODO: Provide 'included' data for attached 'license':
|
// TODO: Provide 'included' data for attached 'license':
|
||||||
//new LicenseResource($this->license)
|
//new LicenseResource($this->license)
|
||||||
|
@ -30,11 +30,13 @@ class UserResource extends JsonResource
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
// TODO: Provide 'included' data for attached 'user_role'
|
// TODO: Provide 'included' data for attached 'user_role'
|
||||||
//'included' => [new UserRoleResource($this->role)],
|
//'included' => [new UserRoleResource($this->role)],
|
||||||
|
|
||||||
// TODO: Provide 'links.self' to user profile:
|
'links' => [
|
||||||
//'links' => ['self' => '#'],
|
'self' => $this->profileUrl(),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
{
|
{
|
||||||
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
|
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
|
||||||
// tables to store the data to save on memory; we don't want this to be a hog.
|
// tables to store the data to save on memory; we don't want this to be a hog.
|
||||||
|
$this->bringUserAvatarLocal();
|
||||||
$this->bringFileAuthorsLocal();
|
$this->bringFileAuthorsLocal();
|
||||||
$this->bringFileOptionsLocal();
|
$this->bringFileOptionsLocal();
|
||||||
$this->bringFileContentLocal();
|
$this->bringFileContentLocal();
|
||||||
@ -58,6 +59,34 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
Artisan::call('cache:clear');
|
Artisan::call('cache:clear');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the user avatar table from the Hub database to the local database temporary table.
|
||||||
|
*/
|
||||||
|
protected function bringUserAvatarLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_user_avatar (
|
||||||
|
avatarID INT,
|
||||||
|
avatarExtension VARCHAR(255),
|
||||||
|
userID INT,
|
||||||
|
fileHash VARCHAR(255)
|
||||||
|
)');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('wcf1_user_avatar')
|
||||||
|
->orderBy('avatarID')
|
||||||
|
->chunk(200, function ($avatars) {
|
||||||
|
foreach ($avatars as $avatar) {
|
||||||
|
DB::table('temp_user_avatar')->insert([
|
||||||
|
'avatarID' => (int) $avatar->avatarID,
|
||||||
|
'avatarExtension' => $avatar->avatarExtension,
|
||||||
|
'userID' => (int) $avatar->userID,
|
||||||
|
'fileHash' => $avatar->fileHash,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bring the file authors from the Hub database to the local database temporary table.
|
* Bring the file authors from the Hub database to the local database temporary table.
|
||||||
*/
|
*/
|
||||||
@ -172,15 +201,33 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
*/
|
*/
|
||||||
protected function importUsers(): void
|
protected function importUsers(): void
|
||||||
{
|
{
|
||||||
|
// Initialize a cURL handler for downloading mod thumbnails.
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('wcf1_user as u')
|
->table('wcf1_user as u')
|
||||||
->select('u.userID', 'u.username', 'u.email', 'u.password', 'u.registrationDate', 'u.banned', 'u.banReason', 'u.banExpires', 'u.rankID', 'r.rankTitle')
|
->select(
|
||||||
|
'u.userID',
|
||||||
|
'u.username',
|
||||||
|
'u.email',
|
||||||
|
'u.password',
|
||||||
|
'u.registrationDate',
|
||||||
|
'u.banned',
|
||||||
|
'u.banReason',
|
||||||
|
'u.banExpires',
|
||||||
|
'u.coverPhotoHash',
|
||||||
|
'u.coverPhotoExtension',
|
||||||
|
'u.rankID',
|
||||||
|
'r.rankTitle',
|
||||||
|
)
|
||||||
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
|
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
|
||||||
->chunkById(250, function (Collection $users) {
|
->chunkById(250, function (Collection $users) use ($curl) {
|
||||||
$userData = $bannedUsers = $userRanks = [];
|
$userData = $bannedUsers = $userRanks = [];
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$userData[] = $this->collectUserData($user);
|
$userData[] = $this->collectUserData($curl, $user);
|
||||||
|
|
||||||
$bannedUserData = $this->collectBannedUserData($user);
|
$bannedUserData = $this->collectBannedUserData($user);
|
||||||
if ($bannedUserData) {
|
if ($bannedUserData) {
|
||||||
@ -197,15 +244,20 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
$this->handleBannedUsers($bannedUsers);
|
$this->handleBannedUsers($bannedUsers);
|
||||||
$this->handleUserRoles($userRanks);
|
$this->handleUserRoles($userRanks);
|
||||||
}, 'userID');
|
}, 'userID');
|
||||||
|
|
||||||
|
// Close the cURL handler.
|
||||||
|
curl_close($curl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function collectUserData($user): array
|
protected function collectUserData(CurlHandle $curl, object $user): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'hub_id' => (int) $user->userID,
|
'hub_id' => (int) $user->userID,
|
||||||
'name' => $user->username,
|
'name' => $user->username,
|
||||||
'email' => mb_convert_case($user->email, MB_CASE_LOWER, 'UTF-8'),
|
'email' => Str::lower($user->email),
|
||||||
'password' => $this->cleanPasswordHash($user->password),
|
'password' => $this->cleanPasswordHash($user->password),
|
||||||
|
'profile_photo_path' => $this->fetchUserAvatar($curl, $user),
|
||||||
|
'cover_photo_path' => $this->fetchUserCoverPhoto($curl, $user),
|
||||||
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
||||||
'updated_at' => now('UTC')->toDateTimeString(),
|
'updated_at' => now('UTC')->toDateTimeString(),
|
||||||
];
|
];
|
||||||
@ -224,6 +276,75 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
return str_starts_with($clean, '$2') ? $clean : '';
|
return str_starts_with($clean, '$2') ? $clean : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the user avatar from the Hub and store it anew.
|
||||||
|
*/
|
||||||
|
protected function fetchUserAvatar(CurlHandle $curl, object $user): string
|
||||||
|
{
|
||||||
|
// Fetch the user's avatar data from the temporary table.
|
||||||
|
$avatar = DB::table('temp_user_avatar')->where('userID', $user->userID)->first();
|
||||||
|
|
||||||
|
if (! $avatar) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
|
||||||
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and store an image from the Hub.
|
||||||
|
*/
|
||||||
|
protected function fetchAndStoreImage(CurlHandle $curl, string $hubUrl, string $relativePath): string
|
||||||
|
{
|
||||||
|
// Determine the disk to use based on the environment.
|
||||||
|
$disk = match (config('app.env')) {
|
||||||
|
'production' => 'r2', // Cloudflare R2 Storage
|
||||||
|
default => 'public', // Local
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check to make sure the image doesn't already exist.
|
||||||
|
if (Storage::disk($disk)->exists($relativePath)) {
|
||||||
|
return $relativePath; // Already exists, return the path.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the image using the cURL handler.
|
||||||
|
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||||
|
$image = curl_exec($curl);
|
||||||
|
|
||||||
|
if ($image === false) {
|
||||||
|
Log::error('There was an error attempting to download the image. cURL error: '.curl_error($curl));
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the image on the disk.
|
||||||
|
Storage::disk($disk)->put($relativePath, $image);
|
||||||
|
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the user avatar from the Hub and store it anew.
|
||||||
|
*/
|
||||||
|
protected function fetchUserCoverPhoto(CurlHandle $curl, object $user): string
|
||||||
|
{
|
||||||
|
if (empty($user->coverPhotoHash) || empty($user->coverPhotoExtension)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashShort = substr($user->coverPhotoHash, 0, 2);
|
||||||
|
$fileName = $user->coverPhotoHash.'.'.$user->coverPhotoExtension;
|
||||||
|
$hubUrl = 'https://hub.sp-tarkov.com/images/coverPhotos/'.$hashShort.'/'.$user->userID.'-'.$fileName;
|
||||||
|
$relativePath = 'user-covers/'.$fileName;
|
||||||
|
|
||||||
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean the registration date from the Hub database.
|
* Clean the registration date from the Hub database.
|
||||||
*/
|
*/
|
||||||
@ -295,9 +416,6 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Build an array of user rank data ready to be inserted into the local database.
|
|
||||||
*/
|
|
||||||
protected function collectUserRankData($user): ?array
|
protected function collectUserRankData($user): ?array
|
||||||
{
|
{
|
||||||
if ($user->rankID && $user->rankTitle) {
|
if ($user->rankID && $user->rankTitle) {
|
||||||
@ -590,31 +708,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
$hubUrl = 'https://hub.sp-tarkov.com/files/images/file/'.$hashShort.'/'.$fileName;
|
$hubUrl = 'https://hub.sp-tarkov.com/files/images/file/'.$hashShort.'/'.$fileName;
|
||||||
$relativePath = 'mods/'.$fileName;
|
$relativePath = 'mods/'.$fileName;
|
||||||
|
|
||||||
// Determine the disk to use based on the environment.
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
$disk = match (config('app.env')) {
|
|
||||||
'production' => 'r2', // Cloudflare R2 Storage
|
|
||||||
default => 'public', // Local
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check to make sure the image doesn't already exist.
|
|
||||||
if (Storage::disk($disk)->exists($relativePath)) {
|
|
||||||
return $relativePath; // Already exists, return the path.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download the image using the cURL handler.
|
|
||||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
|
||||||
$image = curl_exec($curl);
|
|
||||||
|
|
||||||
if ($image === false) {
|
|
||||||
Log::error('There was an error attempting to download a mod thumbnail. cURL error: '.curl_error($curl));
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the image on the disk.
|
|
||||||
Storage::disk($disk)->put($relativePath, $image);
|
|
||||||
|
|
||||||
return $relativePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -693,6 +787,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
public function failed(Exception $exception): void
|
public function failed(Exception $exception): void
|
||||||
{
|
{
|
||||||
// Explicitly drop the temporary tables.
|
// Explicitly drop the temporary tables.
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
|
76
app/Livewire/Profile/UpdateProfileForm.php
Normal file
76
app/Livewire/Profile/UpdateProfileForm.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Profile;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||||
|
use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm;
|
||||||
|
use Livewire\Features\SupportRedirects\Redirector;
|
||||||
|
|
||||||
|
class UpdateProfileForm extends UpdateProfileInformationForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The new cover photo for the user.
|
||||||
|
*
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
public $cover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the photo is temporarily uploaded.
|
||||||
|
*/
|
||||||
|
public function updatedPhoto(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'photo' => 'image|mimes:jpg,jpeg,png|max:1024', // 1MB Max
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the cover is temporarily uploaded.
|
||||||
|
*/
|
||||||
|
public function updatedCover(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'cover' => 'image|mimes:jpg,jpeg,png|max:2048', // 2MB Max
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function updateProfileInformation(UpdatesUserProfileInformation $updater): RedirectResponse|Redirector|null
|
||||||
|
{
|
||||||
|
$this->resetErrorBag();
|
||||||
|
|
||||||
|
$updater->update(
|
||||||
|
Auth::user(),
|
||||||
|
$this->photo || $this->cover
|
||||||
|
? array_merge($this->state, array_filter([
|
||||||
|
'photo' => $this->photo,
|
||||||
|
'cover' => $this->cover,
|
||||||
|
])) : $this->state
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($this->photo) || isset($this->cover)) {
|
||||||
|
return redirect()->route('profile.show');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('saved');
|
||||||
|
|
||||||
|
$this->dispatch('refresh-navigation-menu');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user's profile photo.
|
||||||
|
*/
|
||||||
|
public function deleteCoverPhoto(): void
|
||||||
|
{
|
||||||
|
Auth::user()->deleteCoverPhoto();
|
||||||
|
|
||||||
|
$this->dispatch('refresh-navigation-menu');
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,9 @@ class License extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a license and mod.
|
||||||
|
*/
|
||||||
public function mods(): HasMany
|
public function mods(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Mod::class);
|
return $this->hasMany(Mod::class);
|
||||||
|
@ -25,6 +25,9 @@ class Mod extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, Searchable, SoftDeletes;
|
use HasFactory, Searchable, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post boot method to configure the model.
|
||||||
|
*/
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
// Apply the global scope to exclude disabled mods.
|
// Apply the global scope to exclude disabled mods.
|
||||||
@ -34,18 +37,24 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The users that belong to the mod.
|
* The relationship between a mod and its users.
|
||||||
*/
|
*/
|
||||||
public function users(): BelongsToMany
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class);
|
return $this->belongsToMany(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod and its license.
|
||||||
|
*/
|
||||||
public function license(): BelongsTo
|
public function license(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(License::class);
|
return $this->belongsTo(License::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod and its versions.
|
||||||
|
*/
|
||||||
public function versions(): HasMany
|
public function versions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ModVersion::class)->orderByDesc('version');
|
return $this->hasMany(ModVersion::class)->orderByDesc('version');
|
||||||
@ -62,6 +71,9 @@ class Mod extends Model
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod and its last updated version.
|
||||||
|
*/
|
||||||
public function lastUpdatedVersion(): HasOne
|
public function lastUpdatedVersion(): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(ModVersion::class)
|
return $this->hasOne(ModVersion::class)
|
||||||
@ -69,7 +81,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the indexable data array for the model.
|
* The data that is searchable by Scout.
|
||||||
*/
|
*/
|
||||||
public function toSearchableArray(): array
|
public function toSearchableArray(): array
|
||||||
{
|
{
|
||||||
@ -97,11 +109,12 @@ class Mod extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasOne(ModVersion::class)
|
return $this->hasOne(ModVersion::class)
|
||||||
->orderByDesc('version')
|
->orderByDesc('version')
|
||||||
|
->orderByDesc('updated_at')
|
||||||
->take(1);
|
->take(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the model should be searchable.
|
* Determine if the model instance should be searchable.
|
||||||
*/
|
*/
|
||||||
public function shouldBeSearchable(): bool
|
public function shouldBeSearchable(): bool
|
||||||
{
|
{
|
||||||
@ -109,7 +122,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URL to the thumbnail.
|
* Build the URL to the mod's thumbnail.
|
||||||
*/
|
*/
|
||||||
public function thumbnailUrl(): Attribute
|
public function thumbnailUrl(): Attribute
|
||||||
{
|
{
|
||||||
@ -121,7 +134,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the disk where the thumbnail is stored.
|
* Get the disk where the thumbnail is stored based on the current environment.
|
||||||
*/
|
*/
|
||||||
protected function thumbnailDisk(): string
|
protected function thumbnailDisk(): string
|
||||||
{
|
{
|
||||||
@ -131,6 +144,9 @@ class Mod extends Model
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast to native types.
|
||||||
|
*/
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -142,7 +158,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the slug is always lower case when retrieved and slugified when saved.
|
* Mutate the slug attribute to always be lower case on get and slugified on set.
|
||||||
*/
|
*/
|
||||||
protected function slug(): Attribute
|
protected function slug(): Attribute
|
||||||
{
|
{
|
||||||
|
@ -19,12 +19,18 @@ class ModVersion extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post boot method to configure the model.
|
||||||
|
*/
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
static::addGlobalScope(new DisabledScope);
|
static::addGlobalScope(new DisabledScope);
|
||||||
static::addGlobalScope(new PublishedScope);
|
static::addGlobalScope(new PublishedScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod version and mod.
|
||||||
|
*/
|
||||||
public function mod(): BelongsTo
|
public function mod(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Mod::class);
|
return $this->belongsTo(Mod::class);
|
||||||
@ -38,6 +44,9 @@ class ModVersion extends Model
|
|||||||
return $this->hasMany(ModDependency::class);
|
return $this->hasMany(ModDependency::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod version and SPT version.
|
||||||
|
*/
|
||||||
public function sptVersion(): BelongsTo
|
public function sptVersion(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(SptVersion::class);
|
return $this->belongsTo(SptVersion::class);
|
||||||
|
@ -11,6 +11,9 @@ class SptVersion extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between an SPT version and mod version.
|
||||||
|
*/
|
||||||
public function modVersions(): HasMany
|
public function modVersions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ModVersion::class);
|
return $this->hasMany(ModVersion::class);
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Notifications\ResetPassword;
|
use App\Notifications\ResetPassword;
|
||||||
use App\Notifications\VerifyEmail;
|
use App\Notifications\VerifyEmail;
|
||||||
|
use App\Traits\HasCoverPhoto;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
@ -21,6 +22,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
{
|
{
|
||||||
use Bannable;
|
use Bannable;
|
||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
|
use HasCoverPhoto;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HasProfilePhoto;
|
use HasProfilePhoto;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
@ -38,11 +40,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
'profile_photo_url',
|
'profile_photo_url',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user and their mods.
|
||||||
|
*/
|
||||||
public function mods(): BelongsToMany
|
public function mods(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Mod::class);
|
return $this->belongsToMany(Mod::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data that is searchable by Scout.
|
||||||
|
*/
|
||||||
public function toSearchableArray(): array
|
public function toSearchableArray(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -51,28 +59,25 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the model instance should be searchable.
|
||||||
|
*/
|
||||||
public function shouldBeSearchable(): bool
|
public function shouldBeSearchable(): bool
|
||||||
{
|
{
|
||||||
return ! is_null($this->email_verified_at);
|
return ! is_null($this->email_verified_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function assignRole(UserRole $role): bool
|
/**
|
||||||
{
|
* Check if the user has the role of a moderator.
|
||||||
$this->role()->associate($role);
|
*/
|
||||||
|
|
||||||
return $this->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function role(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(UserRole::class, 'user_role_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isMod(): bool
|
public function isMod(): bool
|
||||||
{
|
{
|
||||||
return Str::lower($this->role?->name) === 'moderator';
|
return Str::lower($this->role?->name) === 'moderator';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has the role of an administrator.
|
||||||
|
*/
|
||||||
public function isAdmin(): bool
|
public function isAdmin(): bool
|
||||||
{
|
{
|
||||||
return Str::lower($this->role?->name) === 'administrator';
|
return Str::lower($this->role?->name) === 'administrator';
|
||||||
@ -94,12 +99,41 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
$this->notify(new ResetPassword($token));
|
$this->notify(new ResetPassword($token));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function casts(): array
|
/**
|
||||||
|
* Get the relative URL to the user's profile page.
|
||||||
|
*/
|
||||||
|
public function profileUrl(): string
|
||||||
{
|
{
|
||||||
return [
|
return route('user.show', [
|
||||||
'email_verified_at' => 'datetime',
|
'user' => $this->id,
|
||||||
'password' => 'hashed',
|
'username' => $this->slug(),
|
||||||
];
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the slug of the user's name.
|
||||||
|
*/
|
||||||
|
public function slug(): string
|
||||||
|
{
|
||||||
|
return Str::lower(Str::slug($this->name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a role to the user.
|
||||||
|
*/
|
||||||
|
public function assignRole(UserRole $role): bool
|
||||||
|
{
|
||||||
|
$this->role()->associate($role);
|
||||||
|
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user and their role.
|
||||||
|
*/
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserRole::class, 'user_role_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,9 +141,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
*/
|
*/
|
||||||
protected function profilePhotoDisk(): string
|
protected function profilePhotoDisk(): string
|
||||||
{
|
{
|
||||||
return match (config('app.env')) {
|
return config('filesystems.asset_upload', 'public');
|
||||||
'production' => 'r2', // Cloudflare R2 Storage
|
}
|
||||||
default => 'public', // Local
|
|
||||||
};
|
/**
|
||||||
|
* The attributes that should be cast to native types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,9 @@ class UserRole extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user role and users.
|
||||||
|
*/
|
||||||
public function users(): HasMany
|
public function users(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(User::class);
|
return $this->hasMany(User::class);
|
||||||
|
@ -8,7 +8,7 @@ use App\Models\User;
|
|||||||
class ModPolicy
|
class ModPolicy
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can view any models.
|
* Determine whether the user can view multiple models.
|
||||||
*/
|
*/
|
||||||
public function viewAny(User $user): bool
|
public function viewAny(User $user): bool
|
||||||
{
|
{
|
||||||
@ -16,7 +16,7 @@ class ModPolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can view the model.
|
* Determine whether the user can view a specific model.
|
||||||
*/
|
*/
|
||||||
public function view(?User $user, Mod $mod): bool
|
public function view(?User $user, Mod $mod): bool
|
||||||
{
|
{
|
||||||
|
47
app/Policies/UserPolicy.php
Normal file
47
app/Policies/UserPolicy.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class UserPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $userCurrent, User $userResource): bool
|
||||||
|
{
|
||||||
|
// TODO: check to see if the userResource has blocked the userCurrent.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restore(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forceDelete(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
72
app/Traits/HasCoverPhoto.php
Normal file
72
app/Traits/HasCoverPhoto.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
trait HasCoverPhoto
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's cover photo.
|
||||||
|
*/
|
||||||
|
public function updateCoverPhoto(UploadedFile $cover, $storagePath = 'cover-photos'): void
|
||||||
|
{
|
||||||
|
tap($this->cover_photo_path, function ($previous) use ($cover, $storagePath) {
|
||||||
|
$this->forceFill([
|
||||||
|
'cover_photo_path' => $cover->storePublicly(
|
||||||
|
$storagePath, ['disk' => $this->coverPhotoDisk()]
|
||||||
|
),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
if ($previous) {
|
||||||
|
Storage::disk($this->coverPhotoDisk())->delete($previous);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the disk that cover photos should be stored on.
|
||||||
|
*/
|
||||||
|
protected function coverPhotoDisk(): string
|
||||||
|
{
|
||||||
|
return config('filesystems.asset_upload', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's cover photo.
|
||||||
|
*/
|
||||||
|
public function deleteCoverPhoto(): void
|
||||||
|
{
|
||||||
|
if (is_null($this->cover_photo_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::disk($this->coverPhotoDisk())->delete($this->cover_photo_path);
|
||||||
|
|
||||||
|
$this->forceFill([
|
||||||
|
'cover_photo_path' => null,
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL to the user's cover photo.
|
||||||
|
*/
|
||||||
|
public function coverPhotoUrl(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::get(function (): string {
|
||||||
|
return $this->cover_photo_path
|
||||||
|
? Storage::disk($this->coverPhotoDisk())->url($this->cover_photo_path)
|
||||||
|
: $this->defaultCoverPhotoUrl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default profile photo URL if no profile photo has been uploaded.
|
||||||
|
*/
|
||||||
|
protected function defaultCoverPhotoUrl(): string
|
||||||
|
{
|
||||||
|
return 'https://picsum.photos/seed/'.urlencode($this->name).'/720/100?blur=2';
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,18 @@ return [
|
|||||||
|
|
||||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Asset Upload Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that assets should be
|
||||||
|
| uploaded to. Typically, this will be either the "public" or "r2" disk.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'asset_upload' => env('ASSET_UPLOAD_DISK', 'public'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Filesystem Disks
|
| Filesystem Disks
|
||||||
|
@ -207,7 +207,7 @@ return [
|
|||||||
'maxJobs' => 0,
|
'maxJobs' => 0,
|
||||||
'memory' => 256,
|
'memory' => 256,
|
||||||
'tries' => 1,
|
'tries' => 1,
|
||||||
'timeout' => 900, // 15 Minutes
|
'timeout' => 1500, // 25 Minutes
|
||||||
'nice' => 0,
|
'nice' => 0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -64,17 +64,17 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'temporary_file_upload' => [
|
'temporary_file_upload' => [
|
||||||
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
'disk' => null,
|
||||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
'rules' => ['file', 'max:12288'],
|
||||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
'directory' => null,
|
||||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
'middleware' => 'throttle:5,1',
|
||||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
'preview_mimes' => [
|
||||||
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||||
],
|
],
|
||||||
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
'max_upload_time' => 5,
|
||||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
'cleanup' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -29,7 +29,8 @@ return new class extends Migration
|
|||||||
->nullOnDelete()
|
->nullOnDelete()
|
||||||
->cascadeOnUpdate();
|
->cascadeOnUpdate();
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->string('profile_photo_path', 2048)->nullable();
|
$table->string('profile_photo_path', 2048)->nullable()->default(null);
|
||||||
|
$table->string('cover_photo_path', 2048)->nullable()->default(null);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,7 +21,9 @@
|
|||||||
{{ $mod->{$versionScope}->sptVersion->version }}
|
{{ $mod->{$versionScope}->sptVersion->version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">By {{ $mod->users->pluck('name')->implode(', ') }}</p>
|
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||||
|
By {{ $mod->users->pluck('name')->implode(', ') }}
|
||||||
|
</p>
|
||||||
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ $mod->teaser }}</p>
|
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ $mod->teaser }}</p>
|
||||||
</div>
|
</div>
|
||||||
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
||||||
|
@ -32,7 +32,12 @@
|
|||||||
{{ $latestVersion->sptVersion->version }}
|
{{ $latestVersion->sptVersion->version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ __('Created by') }} {{ $mod->users->pluck('name')->implode(', ') }}</p>
|
<p>
|
||||||
|
{{ __('Created by') }}
|
||||||
|
@foreach ($mod->users as $user)
|
||||||
|
<a href="{{ $user->profileUrl() }}" class="text-slate-600 dark:text-gray-200 hover:underline">{{ $user->name }}</a>{{ $loop->last ? '' : ',' }}
|
||||||
|
@endforeach
|
||||||
|
</p>
|
||||||
<p>{{ $latestVersion->sptVersion->version }} {{ __('Compatible') }}</p>
|
<p>{{ $latestVersion->sptVersion->version }} {{ __('Compatible') }}</p>
|
||||||
<p>{{ Number::format($mod->total_downloads) }} {{ __('Downloads') }}</p>
|
<p>{{ Number::format($mod->total_downloads) }} {{ __('Downloads') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,19 +63,25 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<a href="{{ auth()->user()->profileUrl() }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||||
<div class="flex flex-col py-1.5">
|
|
||||||
<a href="{{ route('profile.show') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||||
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
{{ __('Profile') }}
|
{{ __('Profile') }}
|
||||||
</a>
|
</a>
|
||||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
</div>
|
||||||
<a href="{{ route('api-tokens.index') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
<div class="flex flex-col py-1.5">
|
||||||
|
<a href="{{ route('profile.show') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||||
<path fill-rule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 00-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 00-2.282.819l-.922 1.597a1.875 1.875 0 00.432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 000 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 00-.432 2.385l.922 1.597a1.875 1.875 0 002.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 002.28-.819l.923-1.597a1.875 1.875 0 00-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 000-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 00-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 00-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 00-1.85-1.567h-1.843zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 00-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 00-2.282.819l-.922 1.597a1.875 1.875 0 00.432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 000 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 00-.432 2.385l.922 1.597a1.875 1.875 0 002.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 002.28-.819l.923-1.597a1.875 1.875 0 00-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 000-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 00-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 00-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 00-1.85-1.567h-1.843zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" clip-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
{{ __('Edit Profile') }}
|
||||||
|
</a>
|
||||||
|
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||||
|
<a href="{{ route('api-tokens.index') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23-.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
|
||||||
|
</svg>
|
||||||
{{ __('API Tokens') }}
|
{{ __('API Tokens') }}
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||||
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
||||||
@livewire('profile.update-profile-information-form')
|
@livewire('profile.update-profile-form')
|
||||||
|
|
||||||
<x-section-border />
|
<x-section-border />
|
||||||
@endif
|
@endif
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<x-slot name="form">
|
<x-slot name="form">
|
||||||
<!-- Profile Photo -->
|
<!-- Profile Picture -->
|
||||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||||
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
||||||
<!-- Profile Photo File Input -->
|
<!-- Profile Photo File Input -->
|
||||||
@ -24,7 +24,7 @@
|
|||||||
reader.readAsDataURL($refs.photo.files[0]);
|
reader.readAsDataURL($refs.photo.files[0]);
|
||||||
" />
|
" />
|
||||||
|
|
||||||
<x-label for="photo" value="{{ __('Photo') }}" />
|
<x-label for="photo" value="{{ __('Profile Picture') }}" />
|
||||||
|
|
||||||
<!-- Current Profile Photo -->
|
<!-- Current Profile Photo -->
|
||||||
<div class="mt-2" x-show="! photoPreview">
|
<div class="mt-2" x-show="! photoPreview">
|
||||||
@ -52,6 +52,48 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<!-- Cover Picture -->
|
||||||
|
<div x-data="{coverName: null, coverPreview: null}" class="col-span-6 sm:col-span-4">
|
||||||
|
<!-- Cover Picture File Input -->
|
||||||
|
<input type="file" id="cover" class="hidden"
|
||||||
|
wire:model.live="cover"
|
||||||
|
x-ref="cover"
|
||||||
|
x-on:change="
|
||||||
|
coverName = $refs.cover.files[0].name;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
coverPreview = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL($refs.cover.files[0]);
|
||||||
|
" />
|
||||||
|
|
||||||
|
<x-label for="cover" value="{{ __('Cover Picture') }}" />
|
||||||
|
|
||||||
|
<!-- Current Cover Photo -->
|
||||||
|
<div class="mt-2" x-show="! coverPreview">
|
||||||
|
<img src="{{ $this->user->cover_photo_url }}" alt="{{ $this->user->name }}" class="rounded-sm h-20 w-60 object-cover">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Cover Photo Preview -->
|
||||||
|
<div class="mt-2" x-show="coverPreview" style="display: none;">
|
||||||
|
<span class="block h-20 w-60 bg-cover bg-no-repeat bg-center"
|
||||||
|
x-bind:style="'background-image: url(\'' + coverPreview + '\');'">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-secondary-button class="mt-2 me-2" type="button" x-on:click.prevent="$refs.cover.click()">
|
||||||
|
{{ __('Select A New Cover Photo') }}
|
||||||
|
</x-secondary-button>
|
||||||
|
|
||||||
|
@if ($this->user->cover_photo_path)
|
||||||
|
<x-secondary-button type="button" class="mt-2" wire:click="deleteCoverPhoto">
|
||||||
|
{{ __('Remove Cover Photo') }}
|
||||||
|
</x-secondary-button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<x-input-error for="cover" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Name -->
|
<!-- Name -->
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<x-label for="name" value="{{ __('Name') }}" />
|
<x-label for="name" value="{{ __('Name') }}" />
|
||||||
@ -88,7 +130,7 @@
|
|||||||
{{ __('Saved.') }}
|
{{ __('Saved.') }}
|
||||||
</x-action-message>
|
</x-action-message>
|
||||||
|
|
||||||
<x-button wire:loading.attr="disabled" wire:target="photo">
|
<x-button wire:loading.attr="disabled" wire:target="photo,cover">
|
||||||
{{ __('Save') }}
|
{{ __('Save') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
35
resources/views/user/show.blade.php
Normal file
35
resources/views/user/show.blade.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
|
||||||
|
<div class="sm:-mt-12 dark:bg-gray-800 dark:text-gray-100">
|
||||||
|
<div>
|
||||||
|
<img class="h-32 w-full object-cover lg:h-48" src="{{ $user->cover_photo_url }}" alt="{{ $user->name }}">
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="-mt-12 sm:-mt-16 sm:flex sm:items-end sm:space-x-5">
|
||||||
|
<div class="flex">
|
||||||
|
<img class="h-24 w-24 rounded-full ring-4 ring-white dark:ring-gray-800 sm:h-32 sm:w-32" src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 sm:flex sm:min-w-0 sm:flex-1 sm:items-center sm:justify-end sm:space-x-6 sm:pb-1">
|
||||||
|
<div class="mt-6 min-w-0 flex-1 sm:hidden md:block">
|
||||||
|
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||||
|
</div>
|
||||||
|
{{--
|
||||||
|
<div class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||||
|
<button type="button" class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M3 4a2 2 0 00-2 2v1.161l8.441 4.221a1.25 1.25 0 001.118 0L19 7.162V6a2 2 0 00-2-2H3z" />
|
||||||
|
<path d="M19 8.839l-7.77 3.885a2.75 2.75 0 01-2.46 0L1 8.839V14a2 2 0 002 2h14a2 2 0 002-2V8.839z" />
|
||||||
|
</svg>
|
||||||
|
<span>Message</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
--}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 hidden min-w-0 flex-1 sm:block md:hidden">
|
||||||
|
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</x-app-layout>
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ModController;
|
use App\Http\Controllers\ModController;
|
||||||
|
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 () {
|
||||||
@ -11,7 +12,11 @@ Route::middleware(['auth.banned'])->group(function () {
|
|||||||
|
|
||||||
Route::controller(ModController::class)->group(function () {
|
Route::controller(ModController::class)->group(function () {
|
||||||
Route::get('/mods', 'index')->name('mods');
|
Route::get('/mods', 'index')->name('mods');
|
||||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
|
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::controller(UserController::class)->group(function () {
|
||||||
|
Route::get('/user/{user}/{username}', 'show')->where(['user' => '[0-9]+'])->name('user.show');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user