refactor: move /v1/* API routes exclusively to Website/Api module

Production stack has honeypot that null-routes API payloads sent to
the web domain. API routes now only register via Website/Api module
(api.lthn.io). Mod modules stripped to web-only routes. Frontend JS
fetch calls use configurable API_URL for cross-origin API access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-04 09:36:59 +01:00
parent 6b2032c687
commit d83c9094cd
No known key found for this signature in database
GPG key ID: AF404715446AEB41
13 changed files with 45 additions and 63 deletions

View file

@ -9,4 +9,5 @@ return [
'network' => env('CHAIN_NETWORK', 'testnet'), // testnet or mainnet
'lns_url' => env('LNS_URL', 'http://127.0.0.1:5553'),
'api_token' => env('API_TOKEN', ''),
'api_url' => env('API_URL', ''), // empty = same origin, set to https://api.lthn.io for production
];

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Mod\Explorer;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Route;
class Boot
@ -17,11 +16,6 @@ class Boot
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('explorer', __DIR__ . '/Views');
$event->routes(function () {
Route::prefix('explorer')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/explorer')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php');
});
$event->routes(fn () => Route::prefix('explorer')->group(__DIR__ . '/Routes/web.php'));
}
}

View file

@ -94,7 +94,7 @@
}
function poll() {
fetch('/v1/explorer/info', {headers: {'Accept': 'application/json'}})
fetch((window.LTHN_API || '') + '/v1/explorer/info', {headers: {'Accept': 'application/json'}})
.then(function(r) { return r.json(); })
.then(function(data) {
var h = data.height || 0;

View file

@ -5,25 +5,15 @@ declare(strict_types=1);
namespace Mod\Gateway;
use Core\Events\FrameworkBooted;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Route;
use Mod\Gateway\Services\GatewayRegistry;
class Boot
{
// API routes registered by Website/Api — this module only provides services
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
FrameworkBooted::class => 'onFrameworkBooted',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(fn () => Route::prefix('v1/gateway')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php'));
}
public function onFrameworkBooted(FrameworkBooted $event): void
{
app()->singleton(GatewayRegistry::class);

View file

@ -6,7 +6,6 @@ namespace Mod\Names;
use Core\Events\ConsoleBooting;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Route;
use Mod\Names\Commands\RetryDnsTickets;
@ -20,12 +19,7 @@ class Boot
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('names', __DIR__ . '/Views');
$event->routes(function () {
Route::prefix('names')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/names')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php');
});
$event->routes(fn () => Route::prefix('names')->group(__DIR__ . '/Routes/web.php'));
}
public function onConsole(ConsoleBooting $event): void

View file

@ -67,7 +67,7 @@
result.appendChild(el('p', {color: '#9ca3af', fontSize: '0.9rem'}, 'Checking...'));
btn.disabled = true;
fetch('/v1/names/available/' + encodeURIComponent(name), {headers: {'Accept': 'application/json'}})
fetch((window.LTHN_API || '') + '/v1/names/available/' + encodeURIComponent(name), {headers: {'Accept': 'application/json'}})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
@ -95,7 +95,7 @@
if (!email) { return; }
claimBtn.disabled = true;
claimBtn.textContent = 'Submitting...';
fetch('/v1/names/claim', {
fetch((window.LTHN_API || '') + '/v1/names/claim', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
body: JSON.stringify({name: name, email: email})

View file

@ -6,7 +6,6 @@ namespace Mod\Pool;
use Core\Events\FrameworkBooted;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Route;
use Mod\Pool\Services\PoolClient;
@ -19,12 +18,7 @@ class Boot
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(function () {
Route::prefix('pool')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/pool')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php');
});
$event->routes(fn () => Route::prefix('pool')->group(__DIR__ . '/Routes/web.php'));
}
public function onFrameworkBooted(FrameworkBooted $event): void

View file

@ -4,20 +4,8 @@ declare(strict_types=1);
namespace Mod\Proxy;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Route;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(fn () => Route::prefix('v1/proxy')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php'));
}
// API-only module — routes registered by Website/Api
public static array $listens = [];
}

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Mod\Trade;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Route;
class Boot
@ -16,11 +15,6 @@ class Boot
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(function () {
Route::prefix('trade')->group(__DIR__ . '/Routes/web.php');
Route::prefix('v1/trade')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php');
});
$event->routes(fn () => Route::prefix('trade')->group(__DIR__ . '/Routes/web.php'));
}
}

View file

@ -6,16 +6,21 @@ namespace Website\Api;
use Core\Events\DomainResolving;
use Core\Events\WebRoutesRegistering;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
/**
* api.lthn.io REST API homepage.
* api.lthn.io REST API.
*
* When api.lthn.io is the resolved domain, serves an API reference
* landing page at /. API endpoints themselves are registered by Mod
* modules via ApiRoutesRegistering.
* Registers all /v1/* API routes. In production, Traefik routes
* api.lthn.io traffic here. The honeypot null-routes API payloads
* sent to the web domain, so these routes MUST NOT be on lthn.io.
*
* DomainResolving handles the homepage (/) only on api.lthn.io.
* API routes register unconditionally since Blesta connects via
* host.docker.internal in homelab.
*/
class Boot extends ServiceProvider
{
@ -53,11 +58,16 @@ class Boot extends ServiceProvider
public function onWebRoutes(WebRoutesRegistering $event): void
{
$host = $_SERVER['HTTP_HOST'] ?? '';
// API routes — no CSRF, no session needed
$event->routes(fn () => Route::prefix('v1')
->withoutMiddleware(ValidateCsrfToken::class)
->group(__DIR__ . '/Routes/api.php'));
// Homepage only on API domain
$host = $_SERVER['HTTP_HOST'] ?? '';
foreach (static::$domains as $pattern) {
if (preg_match($pattern, $host)) {
$event->routes(fn () => Route::group([], __DIR__ . '/Routes/web.php'));
$event->routes(fn () => Route::group([], __DIR__ . '/Routes/home.php'));
return;
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// All v1 API routes — served exclusively on api.lthn.io
// Production: Traefik routes api.lthn.io here, honeypot blocks /v1 on lthn.io
// Homelab: both domains hit same app, Blesta uses host.docker.internal
Route::prefix('names')->group(base_path('app/Mod/Names/Routes/api.php'));
Route::prefix('explorer')->group(base_path('app/Mod/Explorer/Routes/api.php'));
Route::prefix('proxy')->group(base_path('app/Mod/Proxy/Routes/api.php'));
Route::prefix('gateway')->group(base_path('app/Mod/Gateway/Routes/api.php'));
Route::prefix('pool')->group(base_path('app/Mod/Pool/Routes/api.php'));
Route::prefix('trade')->group(base_path('app/Mod/Trade/Routes/api.php'));

View file

@ -5,5 +5,5 @@ declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Website\Api\Controllers\ApiDocsController;
// Homepage — API reference (only serves on api.lthn.io domain)
// API homepage — only registered on api.lthn.io domain
Route::get('/', [ApiDocsController::class, 'index'])->name('api.index');

View file

@ -96,6 +96,7 @@
code { background: var(--surface); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.85em; }
</style>
<script>window.LTHN_API = '{{ config('chain.api_url', '') }}';</script>
</head>
<body>
<nav>