Let the stored key say which mode it belongs to
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/betriebsmodus
parent
59e8689630
commit
ecf948f30f
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
use App\Services\Secrets\SecretCipher;
|
||||
use App\Support\OperatingMode;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Jedes Zugangsdatum bekommt zwei Plätze — und die vorhandene Zeile wird
|
||||
* einsortiert statt umbenannt.
|
||||
*
|
||||
* Für vier der fünf Einträge ist die Antwort dieselbe: was da liegt, wurde für
|
||||
* den echten Betrieb hinterlegt, also `:live`. Nur Stripe kann es selbst sagen,
|
||||
* weil sein Schlüssel sein Präfix trägt — und daran hängt mehr als eine Zeile:
|
||||
* liegt dort ein Testschlüssel, war diese Installation erkennbar im
|
||||
* Testbetrieb, und der Modus folgt daraus statt aus einer Vorgabe.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$stripeMode = null;
|
||||
|
||||
foreach (DB::table('app_secrets')->get() as $row) {
|
||||
// Schon einsortiert (etwa bei einem zweiten Lauf): unangetastet.
|
||||
if (Str::contains($row->key, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mode = OperatingMode::Live;
|
||||
|
||||
if ($row->key === 'stripe.secret') {
|
||||
$mode = $this->stripeModeOf($row->value);
|
||||
$stripeMode = $mode;
|
||||
}
|
||||
|
||||
DB::table('app_secrets')
|
||||
->where('id', $row->id)
|
||||
->update(['key' => $row->key.':'.$mode->value]);
|
||||
}
|
||||
|
||||
// Nur wenn ein Stripe-Schlüssel da war. Ohne ihn bleibt die Vorgabe
|
||||
// stehen, und die ist live.
|
||||
if ($stripeMode !== null) {
|
||||
Settings::set(OperatingMode::SETTING, $stripeMode->value);
|
||||
}
|
||||
}
|
||||
|
||||
private function stripeModeOf(string $stored): OperatingMode
|
||||
{
|
||||
try {
|
||||
$secret = app(SecretCipher::class)->decrypt($stored);
|
||||
} catch (Throwable) {
|
||||
// Unlesbar heißt: wir wissen es nicht. `:live` ist der Zustand vor
|
||||
// dieser Migration, also die Fortsetzung des Status quo statt einer
|
||||
// neuen Behauptung. Die Bereitschaftsseite meldet ihn ohnehin.
|
||||
return OperatingMode::Live;
|
||||
}
|
||||
|
||||
return Str::contains($secret, '_test_') ? OperatingMode::Test : OperatingMode::Live;
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (DB::table('app_secrets')->get() as $row) {
|
||||
if (! Str::endsWith($row->key, [':live', ':test'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bare = Str::beforeLast($row->key, ':');
|
||||
|
||||
// Eine Zeile je Eintrag kann zurück; die zweite hat im alten Schema
|
||||
// keinen Platz und würde am Eindeutigkeitsindex scheitern.
|
||||
if (DB::table('app_secrets')->where('key', $bare)->exists()) {
|
||||
DB::table('app_secrets')->where('id', $row->id)->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('app_secrets')->where('id', $row->id)->update(['key' => $bare]);
|
||||
}
|
||||
|
||||
Settings::forget(OperatingMode::SETTING);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
use App\Services\Secrets\SecretCipher;
|
||||
use App\Support\OperatingMode;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Die Migration rät nicht.
|
||||
*
|
||||
* Der Stripe-Schlüssel sagt selbst, wohin er gehört: `sk_test_` ist ein
|
||||
* Testschlüssel, unabhängig davon, in welchem Feld er gerade liegt. Ihn stumpf
|
||||
* als „Live" einzusortieren, würde eine Installation, die erkennbar im
|
||||
* Testbetrieb läuft, beim ersten Kauf echtes Geld abbuchen lassen.
|
||||
*/
|
||||
function runSlotMigration(): void
|
||||
{
|
||||
(require base_path('database/migrations/2026_08_01_090000_give_every_secret_a_test_slot.php'))->up();
|
||||
}
|
||||
|
||||
it('files a stripe test key in the test slot and switches the mode', function () {
|
||||
DB::table('app_secrets')->insert([
|
||||
'key' => 'stripe.secret',
|
||||
// SecretVault::put() always encrypts with SecretCipher (its own
|
||||
// SECRETS_KEY), never with the app's default APP_KEY-based encrypt()
|
||||
// helper — so a row built with the latter is not a value this
|
||||
// migration would ever actually meet, only one that looks unreadable.
|
||||
'value' => app(SecretCipher::class)->encrypt('sk_test_abc'),
|
||||
'created_at' => now(), 'updated_at' => now(),
|
||||
]);
|
||||
|
||||
runSlotMigration();
|
||||
|
||||
expect(DB::table('app_secrets')->where('key', 'stripe.secret:test')->exists())->toBeTrue();
|
||||
expect(DB::table('app_secrets')->where('key', 'stripe.secret:live')->exists())->toBeFalse();
|
||||
expect(Settings::get(OperatingMode::SETTING))->toBe('test');
|
||||
});
|
||||
|
||||
it('files a stripe live key in the live slot and leaves the mode alone', function () {
|
||||
DB::table('app_secrets')->insert([
|
||||
'key' => 'stripe.secret',
|
||||
'value' => app(SecretCipher::class)->encrypt('sk_live_abc'),
|
||||
'created_at' => now(), 'updated_at' => now(),
|
||||
]);
|
||||
|
||||
runSlotMigration();
|
||||
|
||||
expect(DB::table('app_secrets')->where('key', 'stripe.secret:live')->exists())->toBeTrue();
|
||||
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
||||
});
|
||||
|
||||
it('files every other credential in the live slot', function () {
|
||||
DB::table('app_secrets')->insert([
|
||||
'key' => 'dns.token',
|
||||
'value' => app(SecretCipher::class)->encrypt('whatever'),
|
||||
'created_at' => now(), 'updated_at' => now(),
|
||||
]);
|
||||
|
||||
runSlotMigration();
|
||||
|
||||
expect(DB::table('app_secrets')->where('key', 'dns.token:live')->exists())->toBeTrue();
|
||||
expect(DB::table('app_secrets')->where('key', 'dns.token')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('leaves an installation with no stripe key on live', function () {
|
||||
runSlotMigration();
|
||||
|
||||
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
||||
});
|
||||
Loading…
Reference in New Issue