|
|
@@ -4,6 +4,7 @@
|
|
|
|
|
|
use App\Models\User;
|
|
|
use Closure;
|
|
|
+use Illuminate\Cache\DatabaseStore;
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
use Illuminate\Support\Str;
|
|
|
@@ -17,6 +18,8 @@ class StudentRegistrationDraftService
|
|
|
|
|
|
private const LOCK_SECONDS = 60;
|
|
|
|
|
|
+ private const TOKENS_KEY = self::CACHE_PREFIX.'tokens';
|
|
|
+
|
|
|
private const UNIQUE_FIELDS = [
|
|
|
'document_number',
|
|
|
'email',
|
|
|
@@ -98,6 +101,8 @@ public function store(User $user, array $data): array
|
|
|
|
|
|
Cache::put($this->draftKey($token), $draft, $expiresAt);
|
|
|
|
|
|
+ $this->registerToken($token);
|
|
|
+
|
|
|
return [
|
|
|
'registration_draft_token' => $token,
|
|
|
'expires_at' => $draft['expires_at'],
|
|
|
@@ -105,22 +110,7 @@ public function store(User $user, array $data): array
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- public function findOwnedDraft(User $user, string $token, ?int $unitId = null): ?array
|
|
|
- {
|
|
|
- $draft = Cache::get($this->draftKey($token));
|
|
|
-
|
|
|
- if (! is_array($draft)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $unitId ??= $this->resolveUnitId($user);
|
|
|
-
|
|
|
- if ($draft['user_id'] !== $user->id || $draft['unit_id'] !== $unitId) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return $draft;
|
|
|
- }
|
|
|
+ //
|
|
|
|
|
|
public function consume(User $user, string $token, Closure $callback): mixed
|
|
|
{
|
|
|
@@ -146,6 +136,8 @@ public function consume(User $user, string $token, Closure $callback): mixed
|
|
|
|
|
|
Cache::forget($this->draftKey($token));
|
|
|
|
|
|
+ $this->unregisterToken($token);
|
|
|
+
|
|
|
return $result;
|
|
|
});
|
|
|
}
|
|
|
@@ -171,46 +163,85 @@ public function executeWithoutDraft(User $user, array $data, Closure $callback):
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ public function findOwnedDraft(User $user, string $token, ?int $unitId = null): ?array
|
|
|
+ {
|
|
|
+ $draft = Cache::get($this->draftKey($token));
|
|
|
+
|
|
|
+ if (! is_array($draft)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $unitId ??= $this->resolveUnitId($user);
|
|
|
+
|
|
|
+ if ($draft['user_id'] !== $user->id || $draft['unit_id'] !== $unitId) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $draft;
|
|
|
+ }
|
|
|
+
|
|
|
//
|
|
|
|
|
|
- private function resolveUnitId(User $user): int
|
|
|
+ public function clearAll(): int
|
|
|
{
|
|
|
- $activeUnitId = request()->input('active_unit_id');
|
|
|
+ return Cache::lock(self::LOCK_KEY, self::LOCK_SECONDS)->block(5, function (): int {
|
|
|
+ $databaseStoreResult = $this->clearDatabaseStore();
|
|
|
|
|
|
- if ($activeUnitId) {
|
|
|
- $unit = $user->units()->where('units.id', $activeUnitId)->first();
|
|
|
+ if ($databaseStoreResult !== null) {
|
|
|
+ return $databaseStoreResult;
|
|
|
+ }
|
|
|
|
|
|
- abort_if(! $unit, 403, 'Unidade não autorizada para este usuário.');
|
|
|
+ $clearedDrafts = 0;
|
|
|
|
|
|
- return $unit->id;
|
|
|
- }
|
|
|
+ foreach ($this->registeredTokens() as $token) {
|
|
|
+ $draft = Cache::get($this->draftKey($token));
|
|
|
|
|
|
- $unit = $user->units()->first();
|
|
|
+ if (is_array($draft)) {
|
|
|
+ $this->releaseReservations($draft['reservations'] ?? [], $token);
|
|
|
+ $clearedDrafts++;
|
|
|
+ }
|
|
|
|
|
|
- abort_if(! $unit, 403, 'Usuário sem unidade associada.');
|
|
|
+ Cache::forget($this->draftKey($token));
|
|
|
+ }
|
|
|
|
|
|
- return $unit->id;
|
|
|
+ Cache::forget(self::TOKENS_KEY);
|
|
|
+
|
|
|
+ return $clearedDrafts;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- private function reservationKeys(array $data): array
|
|
|
+ private function clearDatabaseStore(): ?int
|
|
|
{
|
|
|
- $keys = [];
|
|
|
+ $store = Cache::getStore();
|
|
|
|
|
|
- foreach (self::UNIQUE_FIELDS as $field) {
|
|
|
- $value = $data[$field] ?? null;
|
|
|
+ if (!$store instanceof DatabaseStore) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- if (! is_string($value) || $value === '') {
|
|
|
- continue;
|
|
|
- }
|
|
|
+ $table = config('cache.stores.'.config('cache.default').'.table', 'cache');
|
|
|
|
|
|
- $normalizedValue = Str::lower(trim($value));
|
|
|
+ $keyPrefix = $store->getPrefix().self::CACHE_PREFIX;
|
|
|
|
|
|
- $keys[$field] = self::CACHE_PREFIX."reservation:{$field}:".hash('sha256', $normalizedValue);
|
|
|
+ $keys = $store->getConnection()
|
|
|
+ ->table($table)
|
|
|
+ ->where('key', 'like', $keyPrefix.'%')
|
|
|
+ ->pluck('key')
|
|
|
+ ->filter(fn (string $key): bool => str_starts_with($key, $keyPrefix));
|
|
|
+
|
|
|
+ $clearedDrafts = $keys
|
|
|
+ ->map(fn (string $key): string => substr($key, strlen($keyPrefix)))
|
|
|
+ ->filter(fn (string $key): bool => Str::isUuid($key))
|
|
|
+ ->count();
|
|
|
+
|
|
|
+ if ($keys->isNotEmpty()) {
|
|
|
+ $store->getConnection()->table($table)->whereIn('key', $keys)->delete();
|
|
|
}
|
|
|
|
|
|
- return $keys;
|
|
|
+ return $clearedDrafts;
|
|
|
}
|
|
|
|
|
|
+ //
|
|
|
+
|
|
|
private function ownsAllReservations(array $reservations, string $token): bool
|
|
|
{
|
|
|
foreach ($reservations as $key) {
|
|
|
@@ -231,6 +262,32 @@ private function releaseReservations(array $reservations, string $token): void
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private function registeredTokens(): array
|
|
|
+ {
|
|
|
+ $tokens = Cache::get(self::TOKENS_KEY, []);
|
|
|
+
|
|
|
+ return is_array($tokens) ? $tokens : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function reservationKeys(array $data): array
|
|
|
+ {
|
|
|
+ $keys = [];
|
|
|
+
|
|
|
+ foreach (self::UNIQUE_FIELDS as $field) {
|
|
|
+ $value = $data[$field] ?? null;
|
|
|
+
|
|
|
+ if (! is_string($value) || $value === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $normalizedValue = Str::lower(trim($value));
|
|
|
+
|
|
|
+ $keys[$field] = self::CACHE_PREFIX."reservation:{$field}:".hash('sha256', $normalizedValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $keys;
|
|
|
+ }
|
|
|
+
|
|
|
private function validateDatabaseUniques(array $data): void
|
|
|
{
|
|
|
Validator::make($data, [
|
|
|
@@ -248,15 +305,68 @@ private function uniqueReservationConflict(string $field): never
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
- private function invalidToken(): never
|
|
|
+ //
|
|
|
+
|
|
|
+ private function resolveUnitId(User $user): int
|
|
|
{
|
|
|
- throw ValidationException::withMessages([
|
|
|
- 'registration_draft_token' => __('validation.registration_draft_invalid'),
|
|
|
- ]);
|
|
|
+ $activeUnitId = request()->input('active_unit_id');
|
|
|
+
|
|
|
+ if ($activeUnitId) {
|
|
|
+ $unit = $user->units()->where('units.id', $activeUnitId)->first();
|
|
|
+
|
|
|
+ abort_if(! $unit, 403, 'Unidade não autorizada para este usuário.');
|
|
|
+
|
|
|
+ return $unit->id;
|
|
|
+ }
|
|
|
+
|
|
|
+ $unit = $user->units()->first();
|
|
|
+
|
|
|
+ abort_if(! $unit, 403, 'Usuário sem unidade associada.');
|
|
|
+
|
|
|
+ return $unit->id;
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
+ private function registerToken(string $token): void
|
|
|
+ {
|
|
|
+ $tokens = array_values(array_filter(
|
|
|
+ $this->registeredTokens(),
|
|
|
+ fn (string $registeredToken): bool => Cache::has($this->draftKey($registeredToken)),
|
|
|
+ ));
|
|
|
+
|
|
|
+ $tokens[] = $token;
|
|
|
+
|
|
|
+ Cache::forever(self::TOKENS_KEY, array_values(array_unique($tokens)));
|
|
|
}
|
|
|
|
|
|
+ private function unregisterToken(string $token): void
|
|
|
+ {
|
|
|
+ $tokens = array_values(array_filter(
|
|
|
+ $this->registeredTokens(),
|
|
|
+ fn (string $registeredToken): bool => $registeredToken !== $token,
|
|
|
+ ));
|
|
|
+
|
|
|
+ if ($tokens === []) {
|
|
|
+ Cache::forget(self::TOKENS_KEY);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Cache::forever(self::TOKENS_KEY, $tokens);
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
private function draftKey(string $token): string
|
|
|
{
|
|
|
return self::CACHE_PREFIX.$token;
|
|
|
}
|
|
|
+
|
|
|
+ private function invalidToken(): never
|
|
|
+ {
|
|
|
+ throw ValidationException::withMessages([
|
|
|
+ 'registration_draft_token' => __('validation.registration_draft_invalid'),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|