mailwolt/app/Livewire/Ui/Security/Modal/RecoveryCodesModal.php

161 lines
5.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Livewire\Ui\Security\Modal;
use App\Models\TwoFactorRecoveryCode;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use LivewireUI\Modal\ModalComponent;
class RecoveryCodesModal extends ModalComponent
{
/** Plain Codes werden nur direkt nach Generierung gezeigt */
public array $plainCodes = [];
/** Zeigt an, dass in der DB bereits Codes existieren */
public bool $hasExisting = false;
/** Ob dieser View-Zyklus gerade neue Codes erzeugt hat (Download erlaubt) */
public bool $justGenerated = false;
/** Anzahl Codes & Länge */
public int $count = 10; // wie viele Codes
public int $length = 10; // wie lang (ohne Leerzeichen/Trenner)
public static function modalMaxWidth(): string
{
// Stelle sicher, dass Tailwind die Klasse kennt (safelisten, falls nötig):
// 'sm:max-w-md' / 'md:max-w-xl' etc. wir bleiben hier beim „md“ Preset
return 'md';
}
public function mount(): void
{
$user = Auth::user();
$this->hasExisting = TwoFactorRecoveryCode::where('user_id', $user->id)->exists();
// Wichtig: $plainCodes bleiben leer, außer nach generate().
// So werden bereits existierende Codes nie erneut gezeigt.
$this->plainCodes = [];
$this->justGenerated = false;
}
/** Erzeugt/rotiert Codes und zeigt sie EINMALIG an */
public function generate(): void
{
$user = Auth::user();
// Alte Codes verwerfen/überschreiben
TwoFactorRecoveryCode::where('user_id', $user->id)->delete();
// Neue Codes erzeugen (nur hier im Speicher im Klartext zeigen)
$new = [];
for ($i = 0; $i < $this->count; $i++) {
// 10 zufällige Großbuchstaben/Ziffern
$raw = strtoupper(Str::random($this->length));
$new[] = $raw;
TwoFactorRecoveryCode::create([
'user_id' => $user->id,
'code_hash' => hash('sha256', $raw),
'used_at' => null,
]);
}
// Einmalige Anzeige
$this->plainCodes = $new;
$this->hasExisting = true;
$this->justGenerated = true;
}
/** Optional: E-Mail leicht maskieren für die Dateiüberschrift */
protected function maskEmail(?string $email): string
{
if (!$email || !str_contains($email, '@')) return '—';
[$name, $domain] = explode('@', $email, 2);
$nameMasked = Str::substr($name, 0, 1) . str_repeat('•', max(1, strlen($name) - 2)) . Str::substr($name, -1);
$domainMasked = preg_replace('/(^.).*(?=\.)/u', '$1•••', $domain); // z.B. g•••gle.com
return "{$nameMasked}@{$domainMasked}";
}
/** Formatiert die TXT-Datei hübsch */
protected function buildTxtContent(array $codes): string
{
$app = config('app.name', 'App');
$user = Auth::user();
$who = $this->maskEmail($user->email ?? '');
$now = Carbon::now()->toDateTimeString();
// Codes als "ABCDE-FGHIJ" + Nummerierung 01), 02), …
$lines = [];
foreach (array_values($codes) as $i => $raw) {
// in 5er Blöcke mit Bindestrich
$pretty = trim(chunk_split(strtoupper($raw), 5, '-'), '-');
$nr = str_pad((string)($i + 1), 2, '0', STR_PAD_LEFT);
$lines[] = "{$nr}) {$pretty}";
}
$body = implode(PHP_EOL, $lines);
$header = <<<TXT
{$app} Recovery Codes
Benutzer: {$who}
Erstellt: {$now}
Hinweise:
• Jeder Code kann genau einmal verwendet werden.
• Bewahre diese Datei offline & sicher auf (z. B. ausdrucken).
• Teile diese Codes niemals mit Dritten.
Codes:
{$body}
TXT;
return $header;
}
/** Nur direkt nach Generierung möglich schöner Download-Content + Dateiname */
public function downloadTxt()
{
if (!$this->justGenerated || empty($this->plainCodes)) {
$this->dispatch('toast', body: 'Download nur direkt nach der Erstellung möglich.');
return null;
}
$app = Str::slug(config('app.name', 'app'));
$date = now()->format('Y-m-d_His');
$filename = "{$app}-recovery-codes_{$date}.txt";
$content = $this->buildTxtContent($this->plainCodes);
return response()->streamDownload(function () use ($content) {
echo $content . PHP_EOL; // final newline
}, $filename, [
'Content-Type' => 'text/plain; charset=UTF-8',
]);
}
/** Nur direkt nach Generierung möglich */
// public function downloadTxt()
// {
// if (!$this->justGenerated || empty($this->plainCodes)) {
// // nichts zurückgeben => Livewire bleibt im Modal
// $this->dispatch('toast', body: 'Download nur direkt nach der Erstellung möglich.');
// return null;
// }
//
// $filename = 'recovery-codes.txt';
// $content = implode(PHP_EOL, $this->plainCodes);
//
// return response()->streamDownload(function () use ($content) {
// echo $content;
// }, $filename);
// }
public function render()
{
return view('livewire.ui.security.modal.recovery-codes-modal');
}
}