Redirect legacy /q/* URLs to canonical ladl.link addresses.
Deploy Ladill Transfer / deploy (push) Successful in 33s

External /q requests 301/307 to ladl.link; internal X-Ladill-Internal requests still serve content for platform proxying.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-27 17:19:04 +00:00
co-authored by Cursor
parent a171276bc2
commit 1564eee380
4 changed files with 61 additions and 0 deletions
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use App\Support\LadillLink;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RedirectLegacyQrToLadillLink
{
public function handle(Request $request, Closure $next): Response
{
if (! preg_match('#^q/[a-z0-9]#i', ltrim($request->path(), '/'))) {
return $next($request);
}
if (LadillLink::isInternalRequest($request)) {
return $next($request);
}
return LadillLink::legacyRedirect($request);
}
}
+34
View File
@@ -2,11 +2,16 @@
namespace App\Support; namespace App\Support;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
/** /**
* Canonical short-link URLs for the Ladill platform (ladl.link). * Canonical short-link URLs for the Ladill platform (ladl.link).
*/ */
final class LadillLink final class LadillLink
{ {
public const INTERNAL_HEADER = 'X-Ladill-Internal';
public static function baseUrl(): string public static function baseUrl(): string
{ {
$domain = (string) config('link.public_domain', 'ladl.link'); $domain = (string) config('link.public_domain', 'ladl.link');
@@ -23,4 +28,33 @@ final class LadillLink
{ {
return self::url($slug).'/'.ltrim($suffix, '/'); return self::url($slug).'/'.ltrim($suffix, '/');
} }
public static function isInternalRequest(Request $request): bool
{
return $request->header(self::INTERNAL_HEADER) === '1';
}
public static function legacyRedirect(Request $request): RedirectResponse
{
$shortCode = $request->route('shortCode');
if (! is_string($shortCode) || $shortCode === '') {
abort(404);
}
$path = ltrim($request->path(), '/');
$prefix = 'q/'.$shortCode;
$suffix = '';
if ($path !== $prefix && str_starts_with($path, $prefix.'/')) {
$suffix = substr($path, strlen($prefix) + 1);
}
$target = $suffix === '' ? self::url($shortCode) : self::path($shortCode, $suffix);
if ($qs = $request->getQueryString()) {
$target .= '?'.$qs;
}
$status = in_array($request->method(), ['GET', 'HEAD'], true) ? 301 : 307;
return redirect()->away($target, $status);
}
} }
+1
View File
@@ -22,6 +22,7 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->alias([ $middleware->alias([
'auth.service' => \App\Http\Middleware\AuthenticateService::class, 'auth.service' => \App\Http\Middleware\AuthenticateService::class,
'platform.session' => \App\Http\Middleware\EnsurePlatformSession::class, 'platform.session' => \App\Http\Middleware\EnsurePlatformSession::class,
'redirect.legacy.qr' => \App\Http\Middleware\RedirectLegacyQrToLadillLink::class,
]); ]);
$middleware->web(append: [ $middleware->web(append: [
\App\Http\Middleware\InjectBootSplash::class, \App\Http\Middleware\InjectBootSplash::class,
+2
View File
@@ -30,12 +30,14 @@ Route::get('/sso/logout-frontchannel', [SsoLoginController::class, 'frontchannel
Route::get('/sso/platform-signed-out', [SsoLoginController::class, 'platformSignedOut'])->name('sso.platform-signed-out'); Route::get('/sso/platform-signed-out', [SsoLoginController::class, 'platformSignedOut'])->name('sso.platform-signed-out');
Route::get('/signed-out', fn () => auth()->check() ? redirect()->route('transfer.dashboard') : view('transfer.signed-out'))->name('transfer.signed-out'); Route::get('/signed-out', fn () => auth()->check() ? redirect()->route('transfer.dashboard') : view('transfer.signed-out'))->name('transfer.signed-out');
Route::middleware('redirect.legacy.qr')->group(function () {
Route::get('/q/{shortCode}', [QrScanController::class, 'resolve'])->name('qr.public.resolve'); Route::get('/q/{shortCode}', [QrScanController::class, 'resolve'])->name('qr.public.resolve');
Route::get('/q/{shortCode}/view', [QrScanController::class, 'view'])->name('qr.public.view'); Route::get('/q/{shortCode}/view', [QrScanController::class, 'view'])->name('qr.public.view');
Route::get('/q/{shortCode}/file', [QrScanController::class, 'file'])->name('qr.public.file'); Route::get('/q/{shortCode}/file', [QrScanController::class, 'file'])->name('qr.public.file');
Route::get('/q/{shortCode}/transfer', [TransferPublicController::class, 'landing'])->name('qr.public.transfer'); Route::get('/q/{shortCode}/transfer', [TransferPublicController::class, 'landing'])->name('qr.public.transfer');
Route::post('/q/{shortCode}/transfer/unlock', [TransferPublicController::class, 'unlock'])->name('qr.public.transfer.unlock'); Route::post('/q/{shortCode}/transfer/unlock', [TransferPublicController::class, 'unlock'])->name('qr.public.transfer.unlock');
Route::get('/q/{shortCode}/transfer/files/{file}', [TransferPublicController::class, 'download'])->name('qr.public.transfer.download'); Route::get('/q/{shortCode}/transfer/files/{file}', [TransferPublicController::class, 'download'])->name('qr.public.transfer.download');
});
Route::middleware(['auth', 'platform.session'])->group(function () { Route::middleware(['auth', 'platform.session'])->group(function () {
Route::get('/wallet/balance', [WalletBalanceController::class, 'balance'])->name('wallet.balance'); Route::get('/wallet/balance', [WalletBalanceController::class, 'balance'])->name('wallet.balance');