'array', 'expires_at' => 'datetime', 'last_used_at' => 'datetime', 'revoked_at' => 'datetime', ]; } /** * @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Make one, and return the plain token alongside: the only time it exists * anywhere but in the caller's hands. * * @param array $abilities * @return array{0: self, 1: string} */ public static function issue(User $owner, string $name, array $abilities, CarbonInterface $expiresAt): array { $plain = self::PREFIX.Str::random(40); $token = new self(['name' => $name, 'abilities' => array_values($abilities), 'expires_at' => $expiresAt]); $token->user()->associate($owner); $token->forceFill([ 'token_hash' => self::hash($plain), 'prefix' => substr($plain, 0, 12), ])->save(); return [$token, $plain]; } public static function hash(string $plain): string { return hash('sha256', $plain); } public static function findUsable(string $plain): ?self { $token = self::query()->with('user')->where('token_hash', self::hash($plain))->first(); return $token?->isUsable() ? $token : null; } public function isUsable(): bool { return $this->revoked_at === null && $this->expires_at->isFuture(); } /** * Whether the token was given this permission. `pages.*` covers every * permission on pages. */ public function allows(string $permission): bool { foreach ($this->abilities ?? [] as $ability) { if ($ability === $permission || Str::is($ability, $permission)) { return true; } } return false; } /** * Recorded at most once a minute: a busy script would otherwise turn * every request into a write. */ public function touchLastUsed(): void { if ($this->last_used_at === null || $this->last_used_at->lt(now()->subMinute())) { $this->forceFill(['last_used_at' => now()])->saveQuietly(); } } public function revoke(): void { $this->forceFill(['revoked_at' => now()])->save(); } /** * @param Builder<$this> $query * @return Builder<$this> */ public function scopeUsable(Builder $query): Builder { return $query->whereNull('revoked_at')->where('expires_at', '>', now()); } }