config('pin_length') ?? 6); $expiryMinutes = (int) ($extension?->config('pin_expiry_minutes') ?? 15); $plainPin = $this->randomPin($length); $lookup = hash('sha256', $plainPin); SupportPin::query() ->where('user_id', $user->getKey()) ->whereNull('revoked_at') ->update(['revoked_at' => now()]); $pin = SupportPin::create([ 'user_id' => $user->getKey(), 'pin_hash' => Hash::make($plainPin), 'pin_lookup' => $lookup, 'expires_at' => now()->addMinutes($expiryMinutes), 'created_ip' => request()->ip(), 'created_user_agent' => request()->header('User-Agent'), ]); $this->auditLog->record( category: 'support_pin', event: 'generated', context: [ 'support_pin_id' => $pin->getKey(), 'expires_at' => $pin->expires_at?->toAtomString(), ], subject: $user ); return [$pin, $plainPin]; } public function revoke(SupportPin $pin, ?User $actor = null): SupportPin { if ($pin->revoked_at) { return $pin; } $pin->forceFill(['revoked_at' => now()])->save(); $this->auditLog->record( category: 'support_pin', event: 'revoked', context: ['support_pin_id' => $pin->getKey()], subject: $pin->user, actor: $actor ); return $pin; } public function validate(string $plainPin): ?SupportPin { $plainPin = strtoupper(trim($plainPin)); if ($plainPin === '') { return null; } $lookup = hash('sha256', $plainPin); $pin = SupportPin::query() ->where('pin_lookup', $lookup) ->first(); if (!$pin instanceof SupportPin) { return null; } if (!Hash::check($plainPin, $pin->pin_hash)) { return null; } if ($pin->revoked_at || $pin->expires_at->isPast()) { return null; } $pin->forceFill(['last_validated_at' => now()])->save(); $this->auditLog->record( category: 'support_pin', event: 'validated', context: [ 'support_pin_id' => $pin->getKey(), ], subject: $pin->user ); return $pin; } protected function randomPin(int $length): string { $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $poolLength = strlen($alphabet) - 1; $pin = ''; for ($i = 0; $i < $length; $i++) { $pin .= $alphabet[random_int(0, $poolLength)]; } return $pin; } }