diff --git a/app/Models/User.php b/app/Models/User.php index 37463ec..f959f09 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; +use PragmaRX\Google2FA\Google2FA; class User extends Authenticatable { @@ -66,4 +67,44 @@ class User extends Authenticatable { return $this->hasMany(\App\Domains\Workspace\Models\WorkspaceMember::class); } + + public function enableTwoFactor(string $secret): array + { + $recoveryCodes = collect(range(1, 8))->map(fn() => implode('-', [ + strtoupper(substr(bin2hex(random_bytes(5)), 0, 5)), + strtoupper(substr(bin2hex(random_bytes(5)), 0, 5)), + ]))->all(); + + $this->update([ + 'two_factor_secret' => encrypt($secret), + 'two_factor_recovery' => encrypt(json_encode($recoveryCodes)), + ]); + + return $recoveryCodes; + } + + public function disableTwoFactor(): void + { + $this->update([ + 'two_factor_secret' => null, + 'two_factor_recovery' => null, + 'two_factor_confirmed_at' => null, + ]); + } + + public function hasTwoFactorEnabled(): bool + { + return $this->two_factor_confirmed_at !== null; + } + + public function verifyTwoFactor(string $code): bool + { + if (! $this->two_factor_secret) { + return false; + } + + $secret = decrypt($this->two_factor_secret); + $google2fa = new Google2FA; + return $google2fa->verifyKey($secret, $code) !== false; + } } diff --git a/tests/Feature/Settings/TwoFactorTest.php b/tests/Feature/Settings/TwoFactorTest.php new file mode 100644 index 0000000..c70507b --- /dev/null +++ b/tests/Feature/Settings/TwoFactorTest.php @@ -0,0 +1,31 @@ +create(); + + $codes = $user->enableTwoFactor('JBSWY3DPEHPK3PXP'); + + expect($codes)->toHaveCount(8) + ->and($user->fresh()->two_factor_secret)->not->toBeNull(); +}); + +it('disables 2FA and clears secrets', function () { + $user = User::factory()->create(); + $user->enableTwoFactor('JBSWY3DPEHPK3PXP'); + + $user->disableTwoFactor(); + + $fresh = $user->fresh(); + expect($fresh->two_factor_secret)->toBeNull() + ->and($fresh->two_factor_confirmed_at)->toBeNull(); +}); + +it('reports 2FA as disabled before confirmation', function () { + $user = User::factory()->create(); + $user->enableTwoFactor('JBSWY3DPEHPK3PXP'); + + // two_factor_confirmed_at is null → hasTwoFactorEnabled() = false + expect($user->fresh()->hasTwoFactorEnabled())->toBeFalse(); +});