Expand Link app with analytics, settings, and custom domains.
Deploy Ladill Link / deploy (push) Successful in 37s
Deploy Ladill Link / deploy (push) Successful in 37s
Add Bitly-style branded domain support via Ladill Domains SSL API, account analytics dashboard, settings page with default domain picker, and fix SSO/dashboard issues from QR Plus template leftovers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CustomDomain\LinkCustomDomainService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SslCallbackController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, LinkCustomDomainService $service): JsonResponse
|
||||
{
|
||||
$secret = (string) config('customdomain.callback_secret');
|
||||
$body = $request->getContent();
|
||||
$signature = (string) $request->header('X-Ladill-Signature', '');
|
||||
|
||||
if ($secret === '' || ! hash_equals(hash_hmac('sha256', $body, $secret), $signature)) {
|
||||
return response()->json(['error' => 'Invalid signature.'], 401);
|
||||
}
|
||||
|
||||
$payload = json_decode($body, true);
|
||||
$data = (array) ($payload['data'] ?? []);
|
||||
$host = (string) ($data['host'] ?? '');
|
||||
if ($host === '') {
|
||||
return response()->json(['error' => 'Missing host.'], 422);
|
||||
}
|
||||
|
||||
$service->applyCallback(
|
||||
$host,
|
||||
(string) ($data['status'] ?? 'failed'),
|
||||
$data['expires_at'] ?? null,
|
||||
$data['last_error'] ?? null,
|
||||
);
|
||||
|
||||
return response()->json(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QrTeamMember;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Client\Response as HttpResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -81,7 +80,7 @@ class SsoLoginController extends Controller
|
||||
|
||||
public function callback(Request $request): RedirectResponse|View
|
||||
{
|
||||
$intended = (string) $request->session()->get('sso.intended', route('qr.dashboard'));
|
||||
$intended = (string) $request->session()->get('sso.intended', route('link.dashboard'));
|
||||
|
||||
$popup = (bool) $request->session()->get('sso.popup');
|
||||
|
||||
@@ -118,8 +117,6 @@ class SsoLoginController extends Controller
|
||||
return $this->finishCallback($request, $intended, 'userinfo_failed', $popup);
|
||||
}
|
||||
|
||||
QrTeamMember::linkPendingInvitesFor($user);
|
||||
|
||||
Auth::login($user, remember: true);
|
||||
$request->session()->regenerate();
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Link;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Link\LinkAnalyticsService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AnalyticsController extends Controller
|
||||
{
|
||||
public function __construct(private LinkAnalyticsService $analytics) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
return view('links.analytics.index', [
|
||||
'summary' => $this->analytics->summaryForAccount($account),
|
||||
'dailyClicks' => $this->analytics->dailyClicksForAccount($account, 30),
|
||||
'topLinks' => $this->analytics->topLinksForAccount($account),
|
||||
'recentClicks' => $this->analytics->recentClicksForAccount($account),
|
||||
'referrers' => $this->analytics->referrersForAccount($account),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Link;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LinkCustomDomain;
|
||||
use App\Services\CustomDomain\DomainsSslClient;
|
||||
use App\Services\CustomDomain\LinkCustomDomainService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CustomDomainController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LinkCustomDomainService $service,
|
||||
private readonly DomainsSslClient $domains,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
return view('links.domains.index', [
|
||||
'domains' => $account->linkCustomDomains()->latest()->get(),
|
||||
'enabled' => $this->service->enabled(),
|
||||
'serverIp' => (string) config('customdomain.server_ip'),
|
||||
'publicDomain' => (string) config('link.public_domain', 'ladl.link'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($this->service->enabled(), 404);
|
||||
|
||||
$data = $request->validate([
|
||||
'host' => ['required', 'string', 'max:255', 'regex:/^(?!-)([a-z0-9-]+\.)+[a-z]{2,}$/i'],
|
||||
'include_www' => ['nullable', 'boolean'],
|
||||
'make_default' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$host = strtolower(preg_replace('/^www\./', '', trim($data['host'])));
|
||||
|
||||
if (LinkCustomDomain::where('host', $host)->exists()) {
|
||||
return back()->withErrors(['host' => 'That domain is already connected.']);
|
||||
}
|
||||
|
||||
$domain = $this->service->attach(
|
||||
$request->user(),
|
||||
$host,
|
||||
(bool) ($data['include_www'] ?? true),
|
||||
(bool) ($data['make_default'] ?? false),
|
||||
);
|
||||
|
||||
$this->domains->registerConnected(
|
||||
$host,
|
||||
(string) $request->user()->public_id,
|
||||
'Ladill Link',
|
||||
route('link.domains.index'),
|
||||
);
|
||||
|
||||
return back()->with('success', "Domain added. Point an A record for {$host} to ".config('customdomain.server_ip').', then click Verify.');
|
||||
}
|
||||
|
||||
public function verify(Request $request, LinkCustomDomain $customDomain): RedirectResponse
|
||||
{
|
||||
abort_unless($customDomain->user_id === $request->user()->id, 403);
|
||||
|
||||
[$ok, $message] = $this->service->verifyAndProvision($customDomain);
|
||||
|
||||
return back()->with($ok ? 'success' : 'error', $message);
|
||||
}
|
||||
|
||||
public function makeDefault(Request $request, LinkCustomDomain $customDomain): RedirectResponse
|
||||
{
|
||||
abort_unless($customDomain->user_id === $request->user()->id, 403);
|
||||
abort_unless($customDomain->isLive(), 422);
|
||||
|
||||
$this->service->setDefault($customDomain);
|
||||
|
||||
return back()->with('success', $customDomain->host.' is now your default short-link domain.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, LinkCustomDomain $customDomain): RedirectResponse
|
||||
{
|
||||
abort_unless($customDomain->user_id === $request->user()->id, 403);
|
||||
|
||||
$host = $customDomain->host;
|
||||
$customDomain->delete();
|
||||
$this->domains->removeConnected($host);
|
||||
|
||||
return back()->with('success', 'Custom domain removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Link;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LinkCustomDomain;
|
||||
use App\Models\LinkWallet;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
return view('links.settings.index', [
|
||||
'pricePerLink' => LinkWallet::pricePerLink(),
|
||||
'defaultDomain' => $account->defaultLinkDomain(),
|
||||
'publicDomain' => (string) config('link.public_domain', 'ladl.link'),
|
||||
'customDomains' => $account->linkCustomDomains()->latest()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateDefaultDomain(Request $request): RedirectResponse
|
||||
{
|
||||
$account = ladill_account();
|
||||
$validated = $request->validate([
|
||||
'domain' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
if ($validated['domain'] === 'platform') {
|
||||
LinkCustomDomain::where('user_id', $account->id)->update(['is_default' => false]);
|
||||
|
||||
return back()->with('success', 'Default short-link domain set to ladl.link.');
|
||||
}
|
||||
|
||||
$domain = LinkCustomDomain::query()
|
||||
->where('user_id', $account->id)
|
||||
->where('id', $validated['domain'])
|
||||
->firstOrFail();
|
||||
|
||||
abort_unless($domain->isLive(), 422);
|
||||
|
||||
LinkCustomDomain::where('user_id', $account->id)->update(['is_default' => false]);
|
||||
$domain->forceFill(['is_default' => true])->save();
|
||||
|
||||
return back()->with('success', 'Default short-link domain updated.');
|
||||
}
|
||||
}
|
||||
@@ -4,28 +4,28 @@ namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Link\LinkManagerService;
|
||||
use App\Services\Link\LinkPlatformProxy;
|
||||
use App\Support\LadillLink;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LinkRedirectController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private LinkManagerService $links,
|
||||
private LinkPlatformProxy $platform,
|
||||
) {}
|
||||
|
||||
public function resolve(Request $request, string $slug): RedirectResponse|View
|
||||
public function resolve(Request $request, string $slug, ?string $path = null): RedirectResponse|Response
|
||||
{
|
||||
$link = $this->links->resolve($request, $slug);
|
||||
|
||||
if ($link !== null) {
|
||||
return redirect()->away($link->destination_url, 302);
|
||||
if ($path === null || $path === '') {
|
||||
$link = $this->links->resolve($request, $slug);
|
||||
if ($link !== null) {
|
||||
return redirect()->away($link->destination_url, 302);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy QR / storefront codes still resolve on ladill.com/q/*
|
||||
$legacy = rtrim((string) config('app.platform_url', 'https://ladill.com'), '/').'/q/'.$slug;
|
||||
$qs = $request->getQueryString();
|
||||
|
||||
return redirect()->away($legacy.($qs ? '?'.$qs : ''), 302);
|
||||
return $this->platform->forward($request, $slug, $path);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user