fix(traffic): release the throttle at month end; close the Livewire bypass
Two from Codex: - A throttled instance kept its NIC limit into the new month: the new period row starts unthrottled, so neither branch of enforce() fired and nothing ever took the limit off. A customer would have stayed slow through a month they had already paid for. - PublicSiteGate exempted livewire/* wholesale. That endpoint is shared by the console and the portal, so a signed-in customer could keep driving portal components — billing included — while the portal was supposed to be offline. Operators pass the normal check anyway, which is all the console needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
c1e81808a7
commit
2b70c9226a
|
|
@ -23,11 +23,16 @@ use Symfony\Component\HttpFoundation\Response;
|
|||
class PublicSiteGate
|
||||
{
|
||||
/**
|
||||
* Paths that must keep working regardless: the console itself (otherwise
|
||||
* the switch could not be flipped back), Livewire's endpoint that drives it,
|
||||
* Stripe's webhook, and the health check.
|
||||
* Paths that must keep working regardless: the console itself (otherwise the
|
||||
* switch could not be flipped back), Stripe's webhook, and the health check.
|
||||
*
|
||||
* Livewire is deliberately NOT in this list. Its endpoint is shared by the
|
||||
* console and the portal, so exempting it would leave a signed-in customer
|
||||
* able to drive portal components — including billing — while the portal is
|
||||
* supposed to be offline. Operators pass the check below anyway, which is
|
||||
* what the console actually needs.
|
||||
*/
|
||||
private const ALWAYS_ALLOWED = ['admin', 'admin/*', 'livewire/*', 'webhooks/*', 'up', 'robots.txt'];
|
||||
private const ALWAYS_ALLOWED = ['admin', 'admin/*', 'webhooks/*', 'up', 'robots.txt'];
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,6 +74,13 @@ class CollectInstanceTraffic implements ShouldBeUnique, ShouldQueue
|
|||
['last_netin' => $netin, 'last_netout' => $netout, 'sampled_at' => now()],
|
||||
);
|
||||
|
||||
// The month rolled over: the new row starts unthrottled, but the NIC
|
||||
// still carries last month's limit and nothing below would ever take it
|
||||
// off — a customer would stay slow into a month they have paid for.
|
||||
if ($row->wasRecentlyCreated) {
|
||||
$this->releasePreviousPeriodThrottle($client, $instance);
|
||||
}
|
||||
|
||||
// A fresh row starts from this sample: whatever the VM transferred
|
||||
// before we started counting this month is not ours to bill.
|
||||
if (! $row->wasRecentlyCreated) {
|
||||
|
|
@ -88,6 +95,24 @@ class CollectInstanceTraffic implements ShouldBeUnique, ShouldQueue
|
|||
$this->enforce($client, $instance, $row);
|
||||
}
|
||||
|
||||
private function releasePreviousPeriodThrottle($client, Instance $instance): void
|
||||
{
|
||||
$previous = InstanceTraffic::query()
|
||||
->where('instance_id', $instance->id)
|
||||
->where('period', '!=', InstanceTraffic::currentPeriod())
|
||||
->where('throttled', true)
|
||||
->latest('period')
|
||||
->first();
|
||||
|
||||
if ($previous === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$client->setNetworkRate($instance->host->node ?? 'pve', $instance->vmid, null);
|
||||
$previous->update(['throttled' => false, 'throttled_at' => null]);
|
||||
Log::info('throttle released for the new period', ['instance' => $instance->uuid]);
|
||||
}
|
||||
|
||||
/** Counter went backwards → the VM restarted, so the current value is the delta. */
|
||||
private function delta(int $current, int $previous): int
|
||||
{
|
||||
|
|
|
|||
|
|
@ -97,3 +97,23 @@ it('keeps serving when the settings table is unavailable', function () {
|
|||
|
||||
$this->get('/')->assertOk();
|
||||
});
|
||||
|
||||
it('does not leave the portal drivable through Livewire while hidden', function () {
|
||||
Settings::set('site.public', false);
|
||||
|
||||
// A customer with a live session must not be able to keep using the portal
|
||||
// through the shared Livewire endpoint while it is supposed to be offline.
|
||||
$this->actingAs(User::factory()->create())
|
||||
->withServerVariables(['REMOTE_ADDR' => '203.0.113.50'])
|
||||
->post('/livewire/update', [])
|
||||
->assertStatus(503);
|
||||
|
||||
// Operators keep working — that is what the console needs. Whatever
|
||||
// Livewire makes of an empty payload, the gate must not be what answers.
|
||||
$status = $this->actingAs(operator('Owner'))
|
||||
->withServerVariables(['REMOTE_ADDR' => '203.0.113.50'])
|
||||
->post('/livewire/update', [])
|
||||
->getStatusCode();
|
||||
|
||||
expect($status)->not->toBe(503);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -157,3 +157,25 @@ it('shows allowances in the units they are sold in', function () {
|
|||
->and(App\Support\Bytes::human(1_500_000))->toBe('1,5 MB')
|
||||
->and(App\Support\Bytes::human(512))->toBe('512 B');
|
||||
});
|
||||
|
||||
it('lets a throttled instance run again when the new month starts', function () {
|
||||
[$pve, , $instance] = trafficSetup();
|
||||
|
||||
// Last month ended with the allowance exhausted and the NIC limited.
|
||||
InstanceTraffic::create([
|
||||
'instance_id' => $instance->id,
|
||||
'period' => now()->subMonth()->format('Y-m'),
|
||||
'tx_bytes' => 9_999_999_999_999,
|
||||
'throttled' => true,
|
||||
'throttled_at' => now()->subDays(3),
|
||||
]);
|
||||
$pve->networkRates[1042] = 0.25;
|
||||
|
||||
$pve->counters[1042] = ['netin' => 0, 'netout' => 0];
|
||||
(new CollectInstanceTraffic)->handle($pve);
|
||||
|
||||
// The new period starts clean — and the limit must actually come off the
|
||||
// NIC, not just out of the database.
|
||||
expect($pve->networkRates[1042])->toBeNull()
|
||||
->and(InstanceTraffic::query()->where('throttled', true)->count())->toBe(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue