Skip to content

Commit 4906b27

Browse files
committed
release reg service
1 parent 19d3298 commit 4906b27

4 files changed

Lines changed: 353 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Badge\Badge;
6+
use App\Services\RegistrationApiService;
7+
use Illuminate\Console\Command;
8+
9+
class BadgeAttendeeStatusReportCommand extends Command
10+
{
11+
protected $signature = 'badge:attendee-status-report';
12+
13+
protected $description = 'Report badge attendee statuses by checking against the registration service API using encrypted bearer tokens';
14+
15+
public function handle(RegistrationApiService $apiService): void
16+
{
17+
$this->info('🎭 Badge Attendee Status Report');
18+
$this->info('====================================');
19+
20+
// Get all badges that have attendee IDs
21+
$badges = Badge::whereNotNull('custom_id')
22+
->whereHas('fursuit.user.eventUsers', function ($query) {
23+
$query->whereNotNull('attendee_id');
24+
})
25+
->with(['fursuit.user.eventUsers'])
26+
->get();
27+
28+
if ($badges->isEmpty()) {
29+
$this->warn('No badges found with attendee IDs.');
30+
return;
31+
}
32+
33+
$this->info("Found {$badges->count()} badges with attendee IDs");
34+
$this->newLine();
35+
36+
// Group attendee IDs and get their statuses from registration service
37+
$attendeeIds = [];
38+
$badgesByAttendeeId = [];
39+
40+
foreach ($badges as $badge) {
41+
foreach ($badge->fursuit->user->eventUsers as $eventUser) {
42+
if ($eventUser->attendee_id) {
43+
$attendeeIds[] = (int) $eventUser->attendee_id;
44+
$badgesByAttendeeId[$eventUser->attendee_id][] = $badge;
45+
}
46+
}
47+
}
48+
49+
$uniqueAttendeeIds = array_unique($attendeeIds);
50+
$this->info("Checking status for " . count($uniqueAttendeeIds) . " unique attendee IDs");
51+
$this->newLine();
52+
53+
// Initialize counters
54+
$statusCounts = [
55+
'approved' => 0,
56+
'cancelled' => 0,
57+
'deleted' => 0,
58+
'new' => 0,
59+
'partially paid' => 0,
60+
'paid' => 0,
61+
'checked in' => 0,
62+
'waiting' => 0,
63+
'api_error' => 0,
64+
'not_found' => 0
65+
];
66+
67+
// Use batch operation for better performance
68+
$this->info('Fetching attendee statuses from registration service...');
69+
70+
try {
71+
$attendeeStatuses = $apiService->getAttendeeStatuses($uniqueAttendeeIds);
72+
73+
foreach ($uniqueAttendeeIds as $attendeeId) {
74+
$status = $attendeeStatuses[$attendeeId] ?? 'not_found';
75+
76+
if (isset($statusCounts[$status])) {
77+
$statusCounts[$status]++;
78+
} else if ($status === 'not_found') {
79+
$statusCounts['not_found']++;
80+
} else if ($status === 'api_error') {
81+
$statusCounts['api_error']++;
82+
} else {
83+
$this->warn("Unknown status: {$status} for attendee {$attendeeId}");
84+
$statusCounts['api_error']++;
85+
}
86+
}
87+
88+
} catch (\Exception $e) {
89+
$this->error("Failed to fetch attendee statuses: " . $e->getMessage());
90+
$statusCounts['api_error'] = count($uniqueAttendeeIds);
91+
}
92+
$this->newLine(2);
93+
94+
// Display results
95+
$this->info('📊 Badge Attendee Status Report');
96+
$this->info('================================');
97+
98+
$this->table(
99+
['Status', 'Count', 'Percentage'],
100+
collect($statusCounts)->map(function ($count, $status) use ($uniqueAttendeeIds) {
101+
$percentage = count($uniqueAttendeeIds) > 0
102+
? number_format(($count / count($uniqueAttendeeIds)) * 100, 1)
103+
: '0.0';
104+
105+
return [
106+
ucfirst(str_replace('_', ' ', $status)),
107+
$count,
108+
$percentage . '%'
109+
];
110+
})->toArray()
111+
);
112+
113+
$this->newLine();
114+
$this->info("Total attendee IDs checked: " . count($uniqueAttendeeIds));
115+
$this->info("Total badges affected: {$badges->count()}");
116+
117+
// Summary for key statuses
118+
$this->newLine();
119+
$this->info('🎯 Key Status Summary:');
120+
$this->line("✅ Approved: {$statusCounts['approved']}");
121+
$this->line("❌ Cancelled: {$statusCounts['cancelled']}");
122+
$this->line("🗑️ Deleted: {$statusCounts['deleted']}");
123+
124+
if ($statusCounts['api_error'] > 0) {
125+
$this->newLine();
126+
$this->warn("⚠️ API Errors: {$statusCounts['api_error']} attendees could not be checked");
127+
}
128+
129+
if ($statusCounts['not_found'] > 0) {
130+
$this->newLine();
131+
$this->warn("❓ Not Found: {$statusCounts['not_found']} attendee IDs were not found in registration system");
132+
}
133+
}
134+
}

app/Console/Commands/RefreshTokensCommand.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,19 @@ class RefreshTokensCommand extends Command
1414

1515
public function handle(): void
1616
{
17+
// CRITICAL: Never attempt to use refresh tokens on non-production environments
18+
if (! app()->isProduction()) {
19+
$this->error('Refresh tokens are not allowed in non-production environments');
20+
$this->error('This command can only be run in production');
21+
return;
22+
}
23+
24+
$this->info('Starting token refresh for expired tokens...');
25+
1726
User::where('refresh_token_expires_at', '<', now())
1827
->chunk(100,
1928
fn ($chunk) => $chunk->each(fn (User $user) => (new TokenRefreshService($user))->refreshToken()));
29+
30+
$this->info('Token refresh completed');
2031
}
2132
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use App\Models\User;
6+
use Illuminate\Http\Client\Response;
7+
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Support\Facades\Http;
9+
use Illuminate\Support\Facades\Log;
10+
11+
class RegistrationApiService
12+
{
13+
private string $baseUrl;
14+
15+
public function __construct()
16+
{
17+
$this->baseUrl = rtrim(config('services.attsrv.url'), '/');
18+
}
19+
20+
/**
21+
* Get the status of a single attendee
22+
*/
23+
public function getAttendeeStatus(int $attendeeId): ?string
24+
{
25+
$cacheKey = "attendee_status_{$attendeeId}";
26+
27+
// Cache results for 5 minutes to avoid hammering the API
28+
return Cache::remember($cacheKey, 300, function () use ($attendeeId) {
29+
try {
30+
$token = $this->getValidBearerToken();
31+
32+
if (!$token) {
33+
throw new \Exception('No valid bearer token available');
34+
}
35+
36+
$response = Http::withHeaders([
37+
'Authorization' => "Bearer {$token}",
38+
'Accept' => 'application/json',
39+
'Content-Type' => 'application/json'
40+
])
41+
->timeout(30)
42+
->get("{$this->baseUrl}/attendees/{$attendeeId}/status");
43+
44+
if ($response->successful()) {
45+
$data = $response->json();
46+
return $data['status'] ?? null;
47+
}
48+
49+
if ($response->status() === 404) {
50+
return null; // Attendee not found
51+
}
52+
53+
Log::warning("Registration API error for attendee {$attendeeId}", [
54+
'status' => $response->status(),
55+
'body' => $response->body()
56+
]);
57+
58+
return null;
59+
} catch (\Exception $e) {
60+
Log::error("Failed to check attendee {$attendeeId} status", [
61+
'error' => $e->getMessage()
62+
]);
63+
throw $e;
64+
}
65+
});
66+
}
67+
68+
/**
69+
* Search for attendees by criteria and get their statuses
70+
*/
71+
public function searchAttendees(array $criteria): array
72+
{
73+
try {
74+
$token = $this->getValidBearerToken();
75+
76+
if (!$token) {
77+
throw new \Exception('No valid bearer token available');
78+
}
79+
80+
$response = Http::withHeaders([
81+
'Authorization' => "Bearer {$token}",
82+
'Accept' => 'application/json',
83+
'Content-Type' => 'application/json'
84+
])
85+
->timeout(60) // Longer timeout for search
86+
->post("{$this->baseUrl}/attendees/find", [
87+
'match_any' => $criteria,
88+
'fill_fields' => ['id', 'status']
89+
]);
90+
91+
if ($response->successful()) {
92+
$data = $response->json();
93+
return $data['attendees'] ?? [];
94+
}
95+
96+
Log::warning("Registration API search error", [
97+
'status' => $response->status(),
98+
'body' => $response->body(),
99+
'criteria' => $criteria
100+
]);
101+
102+
return [];
103+
} catch (\Exception $e) {
104+
Log::error("Failed to search attendees", [
105+
'error' => $e->getMessage(),
106+
'criteria' => $criteria
107+
]);
108+
throw $e;
109+
}
110+
}
111+
112+
/**
113+
* Bulk get attendee statuses by IDs
114+
*/
115+
public function getAttendeeStatuses(array $attendeeIds): array
116+
{
117+
$batchSize = 50; // API limit or reasonable batch size
118+
$results = [];
119+
120+
$batches = array_chunk($attendeeIds, $batchSize);
121+
122+
foreach ($batches as $batch) {
123+
try {
124+
$batchResults = $this->searchAttendees([
125+
[
126+
'ids' => $batch
127+
]
128+
]);
129+
130+
foreach ($batchResults as $attendee) {
131+
$results[$attendee['id']] = $attendee['status'] ?? 'unknown';
132+
}
133+
} catch (\Exception $e) {
134+
Log::error("Failed to get batch attendee statuses", [
135+
'error' => $e->getMessage(),
136+
'batch' => $batch
137+
]);
138+
139+
// Mark all in this batch as error
140+
foreach ($batch as $attendeeId) {
141+
$results[$attendeeId] = 'api_error';
142+
}
143+
}
144+
}
145+
146+
return $results;
147+
}
148+
149+
/**
150+
* Get a valid bearer token from any user
151+
* We need admin permissions to search attendees
152+
*/
153+
private function getValidBearerToken(): ?string
154+
{
155+
// Try to find a user with admin permissions and valid token
156+
// This assumes admin users have been properly set up with tokens
157+
$adminUsers = User::whereNotNull('token')
158+
->whereNotNull('refresh_token')
159+
->where(function ($query) {
160+
$query->where('token_expires_at', '>', now())
161+
->orWhereNull('token_expires_at');
162+
})
163+
->get();
164+
165+
foreach ($adminUsers as $user) {
166+
try {
167+
$tokenService = new TokenRefreshService($user);
168+
$token = $tokenService->getValidAccessToken();
169+
170+
if ($token) {
171+
// Test if this token has admin permissions by trying a simple API call
172+
if ($this->testTokenPermissions($token)) {
173+
return $token;
174+
}
175+
}
176+
} catch (\Exception $e) {
177+
Log::warning("Failed to get valid token for user {$user->id}: " . $e->getMessage());
178+
continue;
179+
}
180+
}
181+
182+
return null;
183+
}
184+
185+
/**
186+
* Test if a token has the required permissions for attendee searches
187+
*/
188+
private function testTokenPermissions(string $token): bool
189+
{
190+
try {
191+
$response = Http::withHeaders([
192+
'Authorization' => "Bearer {$token}",
193+
'Accept' => 'application/json'
194+
])
195+
->timeout(10)
196+
->get("{$this->baseUrl}/countdown");
197+
198+
return $response->successful();
199+
} catch (\Exception $e) {
200+
return false;
201+
}
202+
}
203+
}

app/Services/TokenRefreshService.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public function renewRefreshTokenIfExpired()
3838

3939
public function refreshToken()
4040
{
41+
// CRITICAL: Never attempt to use refresh tokens on non-production environments
42+
if (! app()->isProduction()) {
43+
throw new \Exception('Refresh tokens are not allowed in non-production environments');
44+
}
45+
4146
try {
4247
/** @var Token $token */
4348
$token = Socialite::driver('identity')

0 commit comments

Comments
 (0)