diff --git a/app/Livewire/Ui/Security/Modal/TotpSetupModal.php b/app/Livewire/Ui/Security/Modal/TotpSetupModal.php index 8b666d2..e931055 100644 --- a/app/Livewire/Ui/Security/Modal/TotpSetupModal.php +++ b/app/Livewire/Ui/Security/Modal/TotpSetupModal.php @@ -2,90 +2,54 @@ namespace App\Livewire\Ui\Security\Modal; +use App\Services\TotpService; use Illuminate\Support\Facades\Auth; use Livewire\Attributes\On; use LivewireUI\Modal\ModalComponent; -use Vectorface\GoogleAuthenticator; class TotpSetupModal extends ModalComponent { - public string $secret; - public string $otp = ''; - public string $qrPng; // PNG Data-URI - public bool $alreadyActive = false; + public string $step = 'scan'; + public string $code = ''; + public string $secret = ''; + public array $recoveryCodes = []; + public string $qrSvg = ''; - // << Wichtig: je Modal eigene Breite >> - public static function modalMaxWidth(): string - { - // mögliche Werte: 'sm','md','lg','xl','2xl','3xl','4xl','5xl','6xl','7xl' - return 'xl'; // kompakt für TOTP - } + public static function modalMaxWidth(): string { return 'md'; } public function mount(): void { - $user = Auth::user(); - - $ga = new GoogleAuthenticator(); - - // Falls User schon Secret hat: wiederverwenden, sonst neues anlegen - $this->secret = $user->totp_secret ?: $ga->createSecret(); - - $issuer = config('app.name', 'MailWolt'); - // getQRCodeUrl(accountName, secret, issuer) => PNG Data-URI - $this->qrPng = $ga->getQRCodeUrl($user->email, $this->secret, $issuer); - - $this->alreadyActive = (bool) ($user->two_factor_enabled ?? false); + $totp = app(TotpService::class); + $this->secret = $totp->generateSecret(); + $this->qrSvg = $totp->qrCodeSvg(Auth::user(), $this->secret); } - #[On('security:totp:enable')] - public function verifyAndEnable(string $code): void + public function verify(): void { - $code = preg_replace('/\D/', '', $code ?? ''); - if (strlen($code) !== 6) { - $this->dispatch('toast', body: 'Bitte 6-stelligen Code eingeben.'); + $this->validate(['code' => 'required|digits:6']); + + $totp = app(TotpService::class); + + if (!$totp->verify($this->secret, $this->code)) { + $this->addError('code', 'Ungültiger Code. Bitte erneut versuchen.'); return; } - $ga = new GoogleAuthenticator(); - $ok = $ga->verifyCode($this->secret, $code, 2); // 2 × 30 s Toleranz - - if (!$ok) { - $this->dispatch('toast', body: 'Code ungültig. Versuche es erneut.'); - return; - } - - $user = Auth::user(); - $user->totp_secret = $this->secret; - $user->two_factor_enabled = true; - $user->save(); - - $this->dispatch('totp-enabled'); - $this->dispatch('toast', body: 'TOTP aktiviert.'); - $this->dispatch('closeModal'); + $this->recoveryCodes = $totp->enable(Auth::user(), $this->secret); + $this->step = 'codes'; } - public function disable(): void + public function done(): void { - $user = Auth::user(); - $user->totp_secret = null; - $user->two_factor_enabled = false; - $user->save(); - - $this->dispatch('totp-disabled'); - $this->dispatch('toast', body: 'TOTP deaktiviert.'); - $this->dispatch('closeModal'); + $this->dispatch('toast', type: 'done', badge: '2FA', + title: 'TOTP aktiviert', + text: 'Zwei-Faktor-Authentifizierung ist jetzt aktiv.', duration: 5000); + $this->dispatch('2fa-status-changed'); + $this->closeModal(); } - public function saveAccount() { /* $this->validate(..); user->update([...]) */ } - public function changePassword() { /* validate & set */ } - public function changeEmail() { /* validate, send verify link, etc. */ } - - public function openRecovery() { /* optional modal or page */ } - public function logoutOthers() { /* … */ } - public function logoutSession(string $id) { /* … */ } - public function render() { - return view('livewire.ui.security.modal.totp-setup-modal'); + return view('livewire.ui.system.modal.totp-setup-modal'); } } diff --git a/app/Livewire/Ui/System/ProfilePage.php b/app/Livewire/Ui/System/ProfilePage.php new file mode 100644 index 0000000..3fbeeee --- /dev/null +++ b/app/Livewire/Ui/System/ProfilePage.php @@ -0,0 +1,99 @@ +name = $u->name ?? ''; + $this->email = $u->email ?? ''; + $this->totpEnabled = app(TotpService::class)->isEnabled($u); + } + + #[On('2fa-status-changed')] + public function refreshTotpStatus(): void + { + $this->totpEnabled = app(TotpService::class)->isEnabled(Auth::user()); + } + + public function saveProfile(): void + { + $this->validate([ + 'name' => 'required|string|max:100', + ]); + + $u = Auth::user(); + $u->name = $this->name; + $u->save(); + + $this->dispatch('toast', type: 'done', badge: 'Profil', + title: 'Gespeichert', + text: 'Profilname wurde aktualisiert.', duration: 4000); + } + + public function changePassword(): void + { + if ($this->new_password === '') { + $this->addError('new_password', 'Kein neues Passwort eingegeben.'); + return; + } + + $this->validate([ + 'current_password' => 'required|string', + 'new_password' => 'required|string|min:8', + 'new_password_confirmation' => 'required|same:new_password', + ]); + + $u = Auth::user(); + if (!Hash::check($this->current_password, $u->password)) { + $this->addError('current_password', 'Aktuelles Passwort ist falsch.'); + return; + } + + $u->password = Hash::make($this->new_password); + $u->save(); + + $this->reset(['current_password', 'new_password', 'new_password_confirmation']); + + $this->dispatch('toast', type: 'done', badge: 'Profil', + title: 'Passwort geändert', + text: 'Dein Passwort wurde aktualisiert.', duration: 4000); + } + + public function disableTotp(): void + { + app(TotpService::class)->disable(Auth::user()); + session()->forget('2fa_verified'); + $this->totpEnabled = false; + + $this->dispatch('toast', type: 'done', badge: '2FA', + title: 'TOTP deaktiviert', + text: 'Zwei-Faktor-Authentifizierung wurde deaktiviert.', duration: 5000); + } + + public function render() + { + return view('livewire.ui.system.profile-page'); + } +} diff --git a/database/migrations/2026_04_27_191654_alter_two_factor_methods_secret_column.php b/database/migrations/2026_04_27_191654_alter_two_factor_methods_secret_column.php new file mode 100644 index 0000000..ab3742a --- /dev/null +++ b/database/migrations/2026_04_27_191654_alter_two_factor_methods_secret_column.php @@ -0,0 +1,25 @@ +text('secret')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('two_factor_methods', function (Blueprint $table) { + $table->string('secret', 255)->nullable()->change(); + }); + } +}; diff --git a/resources/views/layouts/dvx.blade.php b/resources/views/layouts/dvx.blade.php index 129c070..b841dfc 100644 --- a/resources/views/layouts/dvx.blade.php +++ b/resources/views/layouts/dvx.blade.php @@ -148,11 +148,13 @@
-
{{ strtoupper(substr(auth()->user()->name ?? 'MW', 0, 2)) }}
+ +
{{ strtoupper(substr(auth()->user()->name ?? 'MW', 0, 2)) }}
{{ auth()->user()->name ?? 'Admin' }}
{{ auth()->user()->email ?? '' }}
+
@csrf +
+ + + + {{-- Passwort --}} +
+
+
+ Passwort ändern +
+
+
+
+ + + @error('current_password')
{{ $message }}
@enderror +
+
+ + + @error('new_password')
{{ $message }}
@enderror +
+
+ + + @error('new_password_confirmation')
{{ $message }}
@enderror +
+
+ +
+
+
+ + {{-- 2FA --}} +
+
+
+ Zwei-Faktor-Authentifizierung +
+ @if($totpEnabled) + Aktiv + @else + Nicht eingerichtet + @endif +
+
+ +
+ + + + + +
+ Bei aktiviertem 2FA wird beim Login nach dem Passwort ein + 6-stelliger TOTP-Code + aus deiner Authenticator-App verlangt. +
+
+ + @if($totpEnabled) +
+ Dein Konto ist mit TOTP geschützt (Google Authenticator, Authy oder kompatible App). +
+
+ +
+ @else +
+ Schütze dein Konto mit einem Einmalcode-Generator (TOTP). Einzurichten in unter einer Minute. +
+
+ +
+ @endif + +
+
+ + diff --git a/routes/web.php b/routes/web.php index bb96fed..f448991 100644 --- a/routes/web.php +++ b/routes/web.php @@ -97,6 +97,9 @@ Route::middleware(['auth.user', 'require2fa'])->name('ui.')->group(function () { // }); // }); + #PROFILE ROUTE + Route::get('/profile', \App\Livewire\Ui\System\ProfilePage::class)->name('profile'); + #LOGOUT ROUTE Route::post('/logout', [LoginController::class, 'logout'])->name('logout'); });