1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace App\Models;
9
10use App\Traits\Validatable;
11use Cache;
12use Illuminate\Database\Eloquent\Builder;
13use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
15/**
16 * @property string $api_key
17 * @property string $app_name
18 * @property string $app_url
19 * @property int $enabled
20 * @property int $hit_count
21 * @property int $key
22 * @property int $miss_count
23 * @property int $revoked
24 * @property-read User|null $user
25 * @property int $user_id
26 */
27class ApiKey extends Model
28{
29 use Validatable;
30
31 const MAX_FIELD_LENGTHS = [
32 'app_name' => 100,
33 'app_url' => 512,
34 ];
35
36 public $casts = [
37 'revoked' => 'boolean',
38 ];
39 public $timestamps = false;
40
41 protected $table = 'osu_apikeys';
42 protected $primaryKey = 'key';
43
44 public function user(): BelongsTo
45 {
46 return $this->belongsTo(User::class, 'user_id');
47 }
48
49 public function scopeAvailable(Builder $query): Builder
50 {
51 return $query->where('revoked', false);
52 }
53
54 public function isValid(): bool
55 {
56 $this->validationErrors()->reset();
57
58 $this->validateDbFieldLengths();
59
60 foreach (['app_name', 'api_key'] as $field) {
61 if (!present($this->$field)) {
62 $this->validationErrors()->add($field, 'required');
63 }
64 }
65
66 if (!filter_var($this->app_url ?? '', FILTER_VALIDATE_URL)) {
67 $this->validationErrors()->add('app_url', 'url');
68 }
69
70 if (!$this->exists && static::where(['user_id' => $this->user_id])->available()->exists()) {
71 $this->validationErrors()->add('base', '.exists');
72 }
73
74 return $this->validationErrors()->isEmpty();
75 }
76
77 public function save(array $options = [])
78 {
79 // Prevent multiple isValid check from running simultaneously
80 // as it checks for some sort of uniqueness without database
81 // constraint.
82 $lock = Cache::lock("legacy_api_key_store:{$this->user_id}", 600);
83
84 try {
85 $lock->block(5);
86
87 return $this->isValid() && parent::save($options);
88 } finally {
89 $lock->release();
90 }
91 }
92
93 public function validationErrorsTranslationPrefix(): string
94 {
95 return 'legacy_api_key';
96 }
97}