attributes->get('earn_api_token'); abort_unless($token instanceof EarnApiToken, 401); $hash = $request->attributes->get('earn_device_hash'); $preview = $request->attributes->get('earn_device_preview'); $result = DB::transaction(function () use ($token, $hash, $preview, $request): array { $locked = EarnApiToken::query()->whereKey($token->id)->lockForUpdate()->firstOrFail(); if ($locked->device_id_hash !== null) { $match = hash_equals($locked->device_id_hash, $hash); if ($match) { $locked->forceFill(['last_ip' => $request->ip(), 'last_seen_at' => now()])->save(); } return ['token' => $locked, 'match' => $match]; } $locked->forceFill(['device_id_hash' => $hash, 'device_id_preview' => $preview, 'device_bound_at' => now(), 'first_ip' => $request->ip(), 'last_ip' => $request->ip(), 'last_seen_at' => now()])->save(); return ['token' => $locked, 'match' => true, 'created' => true]; }, 3); if (! $result['match']) { $failedKey = 'earn-api:failed-device:'.hash('sha256', $result['token']->id.'|'.$request->ip()); $failedLimit = (int) config('earn.api.failed_device_per_minute'); if (RateLimiter::tooManyAttempts($failedKey, $failedLimit)) { $logs->record($request, 'rate_limited', 429, 'RATE_LIMITED', $result['token'], $hash, $preview); return $this->error('RATE_LIMITED', 429)->header('Retry-After', (string) RateLimiter::availableIn($failedKey)); } RateLimiter::hit($failedKey, 60); $result['token']->forceFill(['device_mismatch_seen' => true])->saveQuietly(); $logs->record($request, 'device_mismatch', 409, 'DEVICE_MISMATCH', $result['token'], $hash, $preview); return $this->error('DEVICE_MISMATCH', 409); } $already = ! ($result['created'] ?? false); $logs->record($request, $already ? 'device_bind_repeated' : 'device_bound', 200, null, $result['token'], $hash, $preview); return response()->json(['success' => true, 'data' => ['device_bound' => true, 'already_bound' => $already, 'device_id_preview' => $result['token']->device_id_preview, 'bound_at' => $result['token']->device_bound_at?->toISOString()]]) ->header('Cache-Control', 'no-store'); } private function error(string $code, int $status): JsonResponse { return response()->json(['success' => false, 'error' => ['code' => $code, 'message' => __('earn_api.'.$code)]], $status)->header('Cache-Control', 'no-store'); } }