Skip to content

Commit 04f0bcc

Browse files
committed
begin ticketing system
1 parent 14c5ac2 commit 04f0bcc

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Support;
4+
5+
use App\Models\Support\SupportCase;
6+
use App\Services\Support\SupportJson;
7+
use App\User;
8+
use Illuminate\Console\Command;
9+
use Illuminate\Support\Facades\DB;
10+
11+
class UserUpdateEmailCommand extends Command
12+
{
13+
protected $signature = 'support:user-update-email {from} {to} {--dry-run} {--json}';
14+
15+
protected $description = 'Support tool: update a user email address (dry-run supported)';
16+
17+
public function handle(): int
18+
{
19+
$from = $this->normalizeEmail((string) $this->argument('from'));
20+
$to = $this->normalizeEmail((string) $this->argument('to'));
21+
$dryRun = (bool) $this->option('dry-run');
22+
23+
$input = [
24+
'from' => $from,
25+
'to' => $to,
26+
'dry_run' => $dryRun,
27+
];
28+
29+
$case = SupportCase::create([
30+
'source_channel' => 'manual',
31+
'processing_mode' => 'manual',
32+
'subject' => 'CLI: support:user-update-email',
33+
'raw_message' => 'CLI invocation',
34+
'normalized_message' => null,
35+
'status' => 'investigating',
36+
'risk_level' => 'high',
37+
'correlation_id' => SupportJson::correlationId(),
38+
]);
39+
40+
try {
41+
if (!$this->isValidEmail($from)) {
42+
throw new \InvalidArgumentException('Invalid FROM email.');
43+
}
44+
if (!$this->isValidEmail($to)) {
45+
throw new \InvalidArgumentException('Invalid TO email.');
46+
}
47+
if ($from === $to) {
48+
throw new \InvalidArgumentException('FROM and TO emails are identical.');
49+
}
50+
51+
/** @var \Illuminate\Database\Eloquent\Collection<int, User> $matches */
52+
$matches = User::withTrashed()
53+
->whereRaw('LOWER(email) = ?', [$from])
54+
->orWhereRaw('LOWER(email_display) = ?', [$from])
55+
->get();
56+
57+
if ($matches->count() === 0) {
58+
$payload = SupportJson::fail('user_update_email', $input, 'No user found for FROM email (email or email_display).');
59+
$this->output->writeln(json_encode($payload, JSON_UNESCAPED_SLASHES));
60+
return self::FAILURE;
61+
}
62+
63+
if ($matches->count() > 1) {
64+
$payload = SupportJson::fail('user_update_email', $input, [
65+
'Multiple users match FROM email; refusing to update.',
66+
'Matches: '.implode(', ', $matches->map(fn (User $u) => (string) $u->id)->all()),
67+
]);
68+
$this->output->writeln(json_encode($payload, JSON_UNESCAPED_SLASHES));
69+
return self::FAILURE;
70+
}
71+
72+
$user = $matches->first();
73+
if (!$user) {
74+
throw new \RuntimeException('Unexpected: missing matched user.');
75+
}
76+
77+
$conflict = User::withTrashed()
78+
->where('id', '<>', $user->id)
79+
->where(function ($q) use ($to) {
80+
$q->whereRaw('LOWER(email) = ?', [$to])
81+
->orWhereRaw('LOWER(email_display) = ?', [$to]);
82+
})
83+
->exists();
84+
85+
if ($conflict) {
86+
$payload = SupportJson::fail('user_update_email', $input, 'TO email already exists on another user (email or email_display).');
87+
$this->output->writeln(json_encode($payload, JSON_UNESCAPED_SLASHES));
88+
return self::FAILURE;
89+
}
90+
91+
$before = [
92+
'id' => $user->id,
93+
'email' => $user->email,
94+
'email_display' => $user->email_display,
95+
'deleted_at' => $user->deleted_at?->toISOString(),
96+
'email_verified_at' => optional($user->email_verified_at)->toISOString(),
97+
];
98+
99+
$wouldUpdateEmailDisplay = ($this->normalizeEmail((string) ($user->email_display ?? '')) === $from);
100+
101+
if (!$dryRun) {
102+
DB::transaction(function () use ($user, $to, $wouldUpdateEmailDisplay) {
103+
$user->email = $to;
104+
if ($wouldUpdateEmailDisplay) {
105+
$user->email_display = $to;
106+
}
107+
108+
// Email changed: require re-verification in case this is used for auth flows.
109+
if (property_exists($user, 'email_verified_at')) {
110+
$user->email_verified_at = null;
111+
}
112+
113+
$user->save();
114+
});
115+
116+
$user->refresh();
117+
}
118+
119+
$after = [
120+
'id' => $user->id,
121+
'email' => $dryRun ? $to : $user->email,
122+
'email_display' => $dryRun
123+
? ($wouldUpdateEmailDisplay ? $to : $user->email_display)
124+
: $user->email_display,
125+
'email_verified_at' => $dryRun ? null : optional($user->email_verified_at)->toISOString(),
126+
];
127+
128+
$result = [
129+
'support_case_id' => $case->id,
130+
'updated' => !$dryRun,
131+
'would_update_email_display' => $wouldUpdateEmailDisplay,
132+
'before' => $before,
133+
'after' => $after,
134+
];
135+
136+
$payload = SupportJson::ok('user_update_email', $input, $result);
137+
} catch (\Throwable $e) {
138+
$payload = SupportJson::fail('user_update_email', $input, $e->getMessage());
139+
}
140+
141+
$this->output->writeln(json_encode($payload, JSON_UNESCAPED_SLASHES));
142+
143+
return $payload['ok'] ? self::SUCCESS : self::FAILURE;
144+
}
145+
146+
private function normalizeEmail(string $email): string
147+
{
148+
return strtolower(trim($email));
149+
}
150+
151+
private function isValidEmail(string $email): bool
152+
{
153+
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
154+
}
155+
}
156+

0 commit comments

Comments
 (0)