diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index 94b6e65507..4501d81c57 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -264,6 +264,26 @@ class ApiTokens extends Component return $scopes; } + public function roll(int $id) + { + try { + $token = auth()->user()->tokens() + ->where('team_id', currentTeam()->id) + ->where('id', $id) + ->firstOrFail(); + $this->authorize('update', $token); + $plainTextToken = $token->roll(); + auditLog('ui.api_token.rolled', [ + 'team_id' => $token->team_id, + 'api_token_name' => $token->name, + ]); + $this->getTokens(); + session()->flash('token', $plainTextToken); + } catch (\Exception $e) { + return handleError($e, $this); + } + } + public function revoke(int $id) { try { diff --git a/app/Models/PersonalAccessToken.php b/app/Models/PersonalAccessToken.php index b1b60414dc..375187a46a 100644 --- a/app/Models/PersonalAccessToken.php +++ b/app/Models/PersonalAccessToken.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Support\Str; use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken; class PersonalAccessToken extends SanctumPersonalAccessToken @@ -23,4 +24,27 @@ class PersonalAccessToken extends SanctumPersonalAccessToken 'scopes' => 'array', ]; } + + public static function generateSecret(): string + { + return sprintf( + '%s%s%s', + config('sanctum.token_prefix', ''), + $tokenEntropy = Str::random(40), + hash('crc32b', $tokenEntropy) + ); + } + + /** + * Replace the secret and keep the abilities, scopes, and expiry. The old secret stops working at once. + * + * @return string the new plain-text token + */ + public function roll(): string + { + $secret = self::generateSecret(); + $this->forceFill(['token' => hash('sha256', $secret), 'last_used_at' => null])->save(); + + return $this->getKey().'|'.$secret; + } } diff --git a/app/Models/User.php b/app/Models/User.php index b771662f5d..b8c82530b4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -23,7 +23,6 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; -use Illuminate\Support\Str; use Laravel\Fortify\Contracts\PasskeyUser; use Laravel\Fortify\PasskeyAuthenticatable; use Laravel\Fortify\TwoFactorAuthenticatable; @@ -269,12 +268,7 @@ class User extends Authenticatable implements PasskeyUser, SendsEmail public function createToken(string $name, array $abilities = ['read'], ?DateTimeInterface $expiresAt = null, ?int $teamId = null) { - $plainTextToken = sprintf( - '%s%s%s', - config('sanctum.token_prefix', ''), - $tokenEntropy = Str::random(40), - hash('crc32b', $tokenEntropy) - ); + $plainTextToken = PersonalAccessToken::generateSecret(); $token = $this->tokens()->create([ 'name' => $name, diff --git a/resources/views/components/table/dropdown.blade.php b/resources/views/components/table/dropdown.blade.php index d3c9f3a453..f4de24a992 100644 --- a/resources/views/components/table/dropdown.blade.php +++ b/resources/views/components/table/dropdown.blade.php @@ -15,7 +15,7 @@ this.panelStyle = 'position: fixed; min-width: 0; visibility: hidden;'; this.open = true; - this.$nextTick(() => this.updatePosition()); + this.$nextTick(() => requestAnimationFrame(() => this.updatePosition())); }, close() { this.open = false; diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index db953bdc3b..988d83de32 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -247,21 +247,45 @@