mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10: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.
|
||||
$user->follows = $followingIds->contains($user->id);
|
||||
|
||||
// TODO: The above follows property doesn't exist on the User model. What was I smoking?
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ namespace App\Models;
|
||||
use App\Http\Filters\V1\QueryFilter;
|
||||
use App\Models\Scopes\DisabledScope;
|
||||
use App\Models\Scopes\PublishedScope;
|
||||
use Database\Factories\ModFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@ -22,9 +21,7 @@ use Laravel\Scout\Searchable;
|
||||
|
||||
class Mod extends Model
|
||||
{
|
||||
/** @use HasFactory<ModFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use Searchable;
|
||||
use SoftDeletes;
|
||||
|
||||
@ -46,6 +43,18 @@ class Mod extends Model
|
||||
$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.
|
||||
*
|
||||
|
@ -130,4 +130,26 @@ class ModVersion extends Model
|
||||
->orderByDesc('version_patch')
|
||||
->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>
|
||||
|
||||
{{-- 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>
|
||||
</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="pb-6 border-b-2 border-gray-200 dark:border-gray-800">
|
||||
<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 }}
|
||||
</a>
|
||||
<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">
|
||||
|
||||
{{-- 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>
|
||||
</a>
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ModController;
|
||||
use App\Http\Controllers\ModVersionController;
|
||||
use App\Http\Controllers\UserController;
|
||||
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::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::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
|
||||
$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
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
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)
|
||||
->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