mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 20:20:41 -05:00
Merge branch 'download-tracking' into develop
This commit is contained in:
commit
a94a1418ef
42
app/Http/Controllers/ModVersionController.php
Normal file
42
app/Http/Controllers/ModVersionController.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
|
||||||
|
class ModVersionController extends Controller
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function show(Request $request, int $modId, string $slug, string $version): RedirectResponse
|
||||||
|
{
|
||||||
|
$modVersion = ModVersion::whereModId($modId)
|
||||||
|
->whereVersion($version)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
if ($modVersion->mod->slug !== $slug) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('view', $modVersion);
|
||||||
|
|
||||||
|
// Rate limit the downloads.
|
||||||
|
$rateKey = 'mod-download:'.($request->user()?->id ?: $request->ip());
|
||||||
|
if (RateLimiter::tooManyAttempts($rateKey, maxAttempts: 5)) { // Max attempts is per minute.
|
||||||
|
abort(429);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment downloads counts in the background.
|
||||||
|
defer(fn () => $modVersion->incrementDownloads());
|
||||||
|
|
||||||
|
// Increment the rate limiter.
|
||||||
|
RateLimiter::increment($rateKey);
|
||||||
|
|
||||||
|
// Redirect to the download link, using a 307 status code to prevent browsers from caching.
|
||||||
|
return redirect($modVersion->link, 307);
|
||||||
|
}
|
||||||
|
}
|
@ -138,6 +138,8 @@ class FollowCard extends Component
|
|||||||
// Add the follow status based on the preloaded IDs.
|
// Add the follow status based on the preloaded IDs.
|
||||||
$user->follows = $followingIds->contains($user->id);
|
$user->follows = $followingIds->contains($user->id);
|
||||||
|
|
||||||
|
// TODO: The above follows property doesn't exist on the User model. What was I smoking?
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ namespace App\Models;
|
|||||||
use App\Http\Filters\V1\QueryFilter;
|
use App\Http\Filters\V1\QueryFilter;
|
||||||
use App\Models\Scopes\DisabledScope;
|
use App\Models\Scopes\DisabledScope;
|
||||||
use App\Models\Scopes\PublishedScope;
|
use App\Models\Scopes\PublishedScope;
|
||||||
use Database\Factories\ModFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@ -22,9 +21,7 @@ use Laravel\Scout\Searchable;
|
|||||||
|
|
||||||
class Mod extends Model
|
class Mod extends Model
|
||||||
{
|
{
|
||||||
/** @use HasFactory<ModFactory> */
|
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use Searchable;
|
use Searchable;
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
|
|
||||||
@ -46,6 +43,18 @@ class Mod extends Model
|
|||||||
$this->saveQuietly();
|
$this->saveQuietly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the URL to download the latest version of this mod.
|
||||||
|
*/
|
||||||
|
public function downloadUrl(bool $absolute = false): string
|
||||||
|
{
|
||||||
|
return route('mod.version.download', [
|
||||||
|
$this->id,
|
||||||
|
$this->slug,
|
||||||
|
$this->latestVersion->version,
|
||||||
|
], absolute: $absolute);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relationship between a mod and its users.
|
* The relationship between a mod and its users.
|
||||||
*
|
*
|
||||||
|
@ -130,4 +130,26 @@ class ModVersion extends Model
|
|||||||
->orderByDesc('version_patch')
|
->orderByDesc('version_patch')
|
||||||
->orderByDesc('version_pre_release');
|
->orderByDesc('version_pre_release');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the download URL for this mod version.
|
||||||
|
*/
|
||||||
|
public function downloadUrl(bool $absolute = false): string
|
||||||
|
{
|
||||||
|
return route('mod.version.download', [$this->mod->id, $this->mod->slug, $this->version], absolute: $absolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the download count for this mod version.
|
||||||
|
*/
|
||||||
|
public function incrementDownloads(): int
|
||||||
|
{
|
||||||
|
$this->downloads++;
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
// Recalculate the total download count for this mod.
|
||||||
|
$this->mod->calculateDownloads();
|
||||||
|
|
||||||
|
return $this->downloads;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
68
app/Policies/ModVersionPolicy.php
Normal file
68
app/Policies/ModVersionPolicy.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class ModVersionPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any mod versions.
|
||||||
|
*/
|
||||||
|
public function viewAny(?User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the mod version.
|
||||||
|
*/
|
||||||
|
public function view(?User $user, ModVersion $modVersion): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create mod versions.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the mod version.
|
||||||
|
*/
|
||||||
|
public function update(User $user, ModVersion $modVersion): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the mod version.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, ModVersion $modVersion): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the mod version.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, ModVersion $modVersion): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the mod version.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, ModVersion $modVersion): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Mobile Download Button --}}
|
{{-- Mobile Download Button --}}
|
||||||
<a href="{{ $mod->latestVersion->link }}" class="block lg:hidden">
|
<a href="{{ $mod->downloadUrl() }}" class="block lg:hidden">
|
||||||
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $mod->latestVersion->version }})</button>
|
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $mod->latestVersion->version }})</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -100,7 +100,7 @@
|
|||||||
<div class="p-4 mb-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
<div class="p-4 mb-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||||
<div class="pb-6 border-b-2 border-gray-200 dark:border-gray-800">
|
<div class="pb-6 border-b-2 border-gray-200 dark:border-gray-800">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<a class="text-2xl font-extrabold" href="{{ $version->link }}">
|
<a class="text-2xl font-extrabold" href="{{ $version->downloadUrl() }}">
|
||||||
{{ __('Version') }} {{ $version->version }}
|
{{ __('Version') }} {{ $version->version }}
|
||||||
</a>
|
</a>
|
||||||
<p class="text-gray-700 dark:text-gray-300" title="{{ __('Exactly') }} {{ $version->downloads }}">{{ Number::downloads($version->downloads) }} {{ __('Downloads') }}</p>
|
<p class="text-gray-700 dark:text-gray-300" title="{{ __('Exactly') }} {{ $version->downloads }}">{{ Number::downloads($version->downloads) }} {{ __('Downloads') }}</p>
|
||||||
@ -151,7 +151,7 @@
|
|||||||
<div class="col-span-1 flex flex-col gap-6">
|
<div class="col-span-1 flex flex-col gap-6">
|
||||||
|
|
||||||
{{-- Desktop Download Button --}}
|
{{-- Desktop Download Button --}}
|
||||||
<a href="{{ $mod->latestVersion->link }}" class="hidden lg:block">
|
<a href="{{ $mod->downloadUrl() }}" class="hidden lg:block">
|
||||||
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $mod->latestVersion->version }})</button>
|
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $mod->latestVersion->version }})</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ModController;
|
use App\Http\Controllers\ModController;
|
||||||
|
use App\Http\Controllers\ModVersionController;
|
||||||
use App\Http\Controllers\UserController;
|
use App\Http\Controllers\UserController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -15,6 +16,15 @@ Route::middleware(['auth.banned'])->group(function () {
|
|||||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::controller(ModVersionController::class)->group(function () {
|
||||||
|
Route::get('/mod/download/{mod}/{slug}/{version}', 'show')
|
||||||
|
->where([
|
||||||
|
'mod' => '[0-9]+',
|
||||||
|
'slug' => '[a-z0-9-]+',
|
||||||
|
])
|
||||||
|
->name('mod.version.download');
|
||||||
|
});
|
||||||
|
|
||||||
Route::controller(UserController::class)->group(function () {
|
Route::controller(UserController::class)->group(function () {
|
||||||
Route::get('/user/{user}/{username}', 'show')->where(['user' => '[0-9]+'])->name('user.show');
|
Route::get('/user/{user}/{username}', 'show')->where(['user' => '[0-9]+'])->name('user.show');
|
||||||
});
|
});
|
||||||
|
@ -50,3 +50,12 @@ it('displays the latest version on the mod detail page', function () {
|
|||||||
// Assert the latest version is in the latest download button
|
// Assert the latest version is in the latest download button
|
||||||
$response->assertSeeText(__('Download Latest Version')." ($latestVersion)");
|
$response->assertSeeText(__('Download Latest Version')." ($latestVersion)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('builds download links using the latest mod version', function () {
|
||||||
|
$mod = Mod::factory()->create(['id' => 1, 'slug' => 'test-mod']);
|
||||||
|
ModVersion::factory()->recycle($mod)->create(['version' => '1.2.3']);
|
||||||
|
ModVersion::factory()->recycle($mod)->create(['version' => '1.3.0']);
|
||||||
|
$modVersion = ModVersion::factory()->recycle($mod)->create(['version' => '1.3.4']);
|
||||||
|
|
||||||
|
expect($mod->downloadUrl())->toEqual("/mod/download/$mod->id/$mod->slug/$modVersion->version");
|
||||||
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Mod;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
use App\Models\SptVersion;
|
use App\Models\SptVersion;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@ -141,3 +142,48 @@ it('updates the parent mods updated_at column when updated', function () {
|
|||||||
expect($version->mod->updated_at)->not->toEqual($originalDate)
|
expect($version->mod->updated_at)->not->toEqual($originalDate)
|
||||||
->and($version->mod->updated_at->format('Y-m-d'))->toEqual(now()->format('Y-m-d'));
|
->and($version->mod->updated_at->format('Y-m-d'))->toEqual(now()->format('Y-m-d'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('builds download links using the specified version', function () {
|
||||||
|
$mod = Mod::factory()->create(['id' => 1, 'slug' => 'test-mod']);
|
||||||
|
$modVersion1 = ModVersion::factory()->recycle($mod)->create(['version' => '1.2.3']);
|
||||||
|
$modVersion2 = ModVersion::factory()->recycle($mod)->create(['version' => '1.3.0']);
|
||||||
|
$modVersion3 = ModVersion::factory()->recycle($mod)->create(['version' => '1.3.4']);
|
||||||
|
|
||||||
|
expect($modVersion1->downloadUrl())->toEqual("/mod/download/$mod->id/$mod->slug/$modVersion1->version")
|
||||||
|
->and($modVersion2->downloadUrl())->toEqual("/mod/download/$mod->id/$mod->slug/$modVersion2->version")
|
||||||
|
->and($modVersion3->downloadUrl())->toEqual("/mod/download/$mod->id/$mod->slug/$modVersion3->version");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments download counts when downloaded', function () {
|
||||||
|
$mod = Mod::factory()->create(['downloads' => 0]);
|
||||||
|
$modVersion = ModVersion::factory()->recycle($mod)->create(['downloads' => 0]);
|
||||||
|
|
||||||
|
$request = $this->get($modVersion->downloadUrl());
|
||||||
|
$request->assertStatus(307);
|
||||||
|
|
||||||
|
$modVersion->refresh();
|
||||||
|
|
||||||
|
expect($modVersion->downloads)->toEqual(1)
|
||||||
|
->and($modVersion->mod->downloads)->toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rate limits download links from being hit', function () {
|
||||||
|
$mod = Mod::factory()->create(['downloads' => 0]);
|
||||||
|
$modVersion = ModVersion::factory()->recycle($mod)->create(['downloads' => 0]);
|
||||||
|
|
||||||
|
// The first 5 requests should be fine.
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$request = $this->get($modVersion->downloadUrl());
|
||||||
|
$request->assertStatus(307);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 6th request should be rate limited.
|
||||||
|
$request = $this->get($modVersion->downloadUrl());
|
||||||
|
$request->assertStatus(429);
|
||||||
|
|
||||||
|
$modVersion->refresh();
|
||||||
|
|
||||||
|
// The download count should still be 5.
|
||||||
|
expect($modVersion->downloads)->toEqual(5)
|
||||||
|
->and($modVersion->mod->downloads)->toEqual(5);
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user