From 23c65821ceb5768ae52db0b4a601d8dcab42749e Mon Sep 17 00:00:00 2001 From: danielhe4rt Date: Sat, 21 Mar 2026 16:08:04 -0300 Subject: [PATCH 1/2] refactor(identity): rename provider_id to external_account_id and restructure schema --- .../database/factories/MessageFactory.php | 2 +- .../activity/src/Actions/NewMessage.php | 4 +- .../activity/src/Actions/NewVoiceMessage.php | 4 +- .../activity/src/Actions/PersistMessage.php | 2 +- .../activity/src/DTOs/NewMessageDTO.php | 4 +- .../activity/src/DTOs/NewVoiceMessageDTO.php | 4 +- .../Messages/Schemas/MessageForm.php | 2 +- .../Http/Requests/CreateMessageRequest.php | 2 +- .../Requests/CreateVoiceMessageRequest.php | 2 +- app-modules/activity/src/Models/Message.php | 4 +- app-modules/activity/src/Models/Voice.php | 4 +- .../Admin/Message/CreateMessageTest.php | 4 +- .../Admin/Message/EditMessageTest.php | 2 +- .../activity/tests/Feature/NewMessageTest.php | 12 +- .../tests/Feature/NewVoiceMessageTest.php | 4 +- .../tests/Unit/Actions/NewMessageTest.php | 6 +- .../src/Events/MessageReceivedEvent.php | 4 +- .../bot-discord/src/Events/WelcomeMember.php | 6 +- .../SlashCommands/AbstractSlashCommand.php | 4 +- .../src/SlashCommands/EditProfileCommand.php | 2 +- .../src/SlashCommands/IntroductionCommand.php | 4 +- .../src/SlashCommands/ProfileCommand.php | 2 +- .../Http/Controllers/MeetingController.php | 2 +- .../Meeting/Http/Requests/MeetingRequest.php | 2 +- .../Feature/Feedback/CreateFeedbackTest.php | 6 +- .../Feature/Feedback/ReviewFeedbackTest.php | 4 +- .../Feature/Meeting/StartMeetingTest.php | 4 +- .../Character/ClaimCharacterBadgeTest.php | 6 +- .../Feature/Character/ClaimDailyBonusTest.php | 8 +- .../database/factories/AccessTokenFactory.php | 28 --- .../factories/ExternalIdentityFactory.php | 20 +- .../database/factories/TenantFactory.php | 2 +- ...grate_providers_to_external_identities.php | 185 ++++++++++++++++++ .../src/Auth/Actions/AuthenticateAction.php | 20 +- .../identity/src/Auth/DTOs/OAuthAccessDTO.php | 12 ++ .../identity/src/Auth/DTOs/OAuthUserDTO.php | 2 +- .../CreateAccountByExternalIdentity.php | 10 +- .../Actions/FindExternalIdentity.php | 2 +- .../Actions/LinkExternalIdentity.php | 6 +- .../Actions/ResolveExternalIdentity.php | 16 +- .../ExternalIdentity/Casts/AsCredentials.php | 36 ++++ .../ExternalIdentity/DTOs/NewProviderDTO.php | 4 +- .../DTOs/ResolveUserProviderDTO.php | 4 +- .../Data/ClientAccessManager.php | 91 +++++++++ .../Enums/CredentialsType.php | 23 +++ .../Enums/IdentityProvider.php | 7 + .../ExternalIdentity/Enums/IdentityType.php | 27 +++ .../Http/Controllers/ProvidersController.php | 2 +- .../Http/Requests/CreateProviderRequest.php | 2 +- .../ExternalIdentity/Models/AccessToken.php | 46 ----- .../Models/ExternalIdentity.php | 71 +++++-- .../Admin/Resources/Users/Pages/EditUser.php | 13 +- .../src/User/Actions/UpdateProfile.php | 2 +- .../src/User/DTOs/UpdateProfileDTO.php | 4 +- .../User/Http/Controllers/UsersController.php | 2 +- .../Feature/Auth/AuthenticateActionTest.php | 21 +- .../NewAccountByProviderTest.php | 10 +- .../tests/Feature/FindProfileTest.php | 2 +- app/Console/Commands/AuditMorphColumns.php | 93 +++++++++ .../Commands/GenerateDiscordTenant.php | 9 +- .../VerifyIfHasTenantProviderMiddleware.php | 2 +- tests/TestCase.php | 2 +- 62 files changed, 687 insertions(+), 205 deletions(-) delete mode 100644 app-modules/identity/database/factories/AccessTokenFactory.php create mode 100644 app-modules/identity/database/migrations/2026_03_21_000001_migrate_providers_to_external_identities.php create mode 100644 app-modules/identity/src/ExternalIdentity/Casts/AsCredentials.php create mode 100644 app-modules/identity/src/ExternalIdentity/Data/ClientAccessManager.php create mode 100644 app-modules/identity/src/ExternalIdentity/Enums/CredentialsType.php create mode 100644 app-modules/identity/src/ExternalIdentity/Enums/IdentityType.php delete mode 100644 app-modules/identity/src/ExternalIdentity/Models/AccessToken.php create mode 100644 app/Console/Commands/AuditMorphColumns.php diff --git a/app-modules/activity/database/factories/MessageFactory.php b/app-modules/activity/database/factories/MessageFactory.php index fb2b8bc3..87fda66a 100644 --- a/app-modules/activity/database/factories/MessageFactory.php +++ b/app-modules/activity/database/factories/MessageFactory.php @@ -21,7 +21,7 @@ public function definition(): array return [ 'id' => fake()->uuid(), 'tenant_id' => Tenant::factory(), - 'provider_id' => ExternalIdentity::factory(), + 'external_identity_id' => ExternalIdentity::factory(), 'provider_message_id' => fake()->randomNumber(4), 'channel_id' => fake()->randomNumber(4), 'content' => fake()->sentence(), diff --git a/app-modules/activity/src/Actions/NewMessage.php b/app-modules/activity/src/Actions/NewMessage.php index 8eef6224..f59dbc12 100644 --- a/app-modules/activity/src/Actions/NewMessage.php +++ b/app-modules/activity/src/Actions/NewMessage.php @@ -24,7 +24,7 @@ public function persist(NewMessageDTO $messageDTO): void $userDto = ResolveUserProviderDTO::make([ 'tenant_id' => $messageDTO->tenantId, 'provider' => $messageDTO->provider, - 'provider_id' => $messageDTO->providerId, + 'external_account_id' => $messageDTO->externalAccountId, 'model_type' => User::class, 'username' => $messageDTO->providerUsername, ]); @@ -49,7 +49,7 @@ public function persist(NewMessageDTO $messageDTO): void } catch (Throwable $throwable) { Log::error('NewMessage failed', [ - 'provider_id' => $messageDTO->providerId, + 'external_account_id' => $messageDTO->externalAccountId, 'tenant_id' => $messageDTO->tenantId, 'error' => $throwable->getMessage(), 'trace' => $throwable->getTraceAsString(), diff --git a/app-modules/activity/src/Actions/NewVoiceMessage.php b/app-modules/activity/src/Actions/NewVoiceMessage.php index 139645d0..98753c60 100644 --- a/app-modules/activity/src/Actions/NewVoiceMessage.php +++ b/app-modules/activity/src/Actions/NewVoiceMessage.php @@ -22,7 +22,7 @@ public function persist(array $payload): void $voiceDTO = NewVoiceMessageDTO::make($payload); $externalIdentity = $this->findExternalIdentity->handle( $voiceDTO->provider->value, - $voiceDTO->providerId + $voiceDTO->externalAccountId ); $characterId = Character::query() @@ -37,7 +37,7 @@ public function persist(array $payload): void Voice::query()->create([ 'tenant_id' => request()->tenant_id, - 'provider_id' => $externalIdentity->id, + 'external_identity_id' => $externalIdentity->id, 'channel_name' => $voiceDTO->channelName, 'state' => $voiceDTO->voiceState->value, 'obtained_experience' => $obtainedExperience, diff --git a/app-modules/activity/src/Actions/PersistMessage.php b/app-modules/activity/src/Actions/PersistMessage.php index cf69f553..2cd4d4d9 100644 --- a/app-modules/activity/src/Actions/PersistMessage.php +++ b/app-modules/activity/src/Actions/PersistMessage.php @@ -16,7 +16,7 @@ public function handle( ): Message { return Message::query()->create([ 'tenant_id' => $messageDTO->tenantId, - 'provider_id' => $providerEntity, + 'external_identity_id' => $providerEntity, 'provider_message_id' => $messageDTO->providerMessageId, 'channel_id' => $messageDTO->channelId, 'content' => $messageDTO->content, diff --git a/app-modules/activity/src/DTOs/NewMessageDTO.php b/app-modules/activity/src/DTOs/NewMessageDTO.php index eb5c1436..443e37ca 100644 --- a/app-modules/activity/src/DTOs/NewMessageDTO.php +++ b/app-modules/activity/src/DTOs/NewMessageDTO.php @@ -13,7 +13,7 @@ public function __construct( public int $tenantId, public IdentityProvider $provider, public string $providerUsername, - public string $providerId, + public string $externalAccountId, public string $providerMessageId, public string $channelId, public string $content, @@ -26,7 +26,7 @@ public static function make(array $payload): self tenantId: $payload['tenant_id'], provider: IdentityProvider::from($payload['provider']), providerUsername: $payload['provider_username'], - providerId: $payload['provider_id'], + externalAccountId: $payload['external_account_id'], providerMessageId: $payload['provider_message_id'], channelId: $payload['channel_id'], content: $payload['content'], diff --git a/app-modules/activity/src/DTOs/NewVoiceMessageDTO.php b/app-modules/activity/src/DTOs/NewVoiceMessageDTO.php index 9758994a..5ddba37f 100644 --- a/app-modules/activity/src/DTOs/NewVoiceMessageDTO.php +++ b/app-modules/activity/src/DTOs/NewVoiceMessageDTO.php @@ -11,7 +11,7 @@ { public function __construct( public IdentityProvider $provider, - public string $providerId, + public string $externalAccountId, public VoiceStatesEnum $voiceState, public string $channelName, ) {} @@ -20,7 +20,7 @@ public static function make(array $payload): self { return new self( provider: IdentityProvider::from($payload['provider']), - providerId: $payload['provider_id'], + externalAccountId: $payload['external_account_id'], voiceState: VoiceStatesEnum::from($payload['state']), channelName: $payload['channel_name'] ); diff --git a/app-modules/activity/src/Filament/Admin/Resources/Messages/Schemas/MessageForm.php b/app-modules/activity/src/Filament/Admin/Resources/Messages/Schemas/MessageForm.php index d2ba2f1a..74d04827 100644 --- a/app-modules/activity/src/Filament/Admin/Resources/Messages/Schemas/MessageForm.php +++ b/app-modules/activity/src/Filament/Admin/Resources/Messages/Schemas/MessageForm.php @@ -18,7 +18,7 @@ public static function configure(Schema $schema): Schema { return $schema ->components([ - Select::make('provider_id') + Select::make('external_identity_id') ->label('Provider') ->getOptionLabelFromRecordUsing(fn (ExternalIdentity $record) => $record->provider->getLabel()) ->preload() diff --git a/app-modules/activity/src/Http/Requests/CreateMessageRequest.php b/app-modules/activity/src/Http/Requests/CreateMessageRequest.php index 535bead7..2f247d5e 100644 --- a/app-modules/activity/src/Http/Requests/CreateMessageRequest.php +++ b/app-modules/activity/src/Http/Requests/CreateMessageRequest.php @@ -18,7 +18,7 @@ public function rules(): array return [ 'tenant_id' => ['required'], 'provider' => ['required', 'in:twitch,discord'], - 'provider_id' => ['required'], + 'external_account_id' => ['required'], 'provider_message_id' => ['required'], 'channel_id' => ['required'], 'content' => ['required', 'string'], diff --git a/app-modules/activity/src/Http/Requests/CreateVoiceMessageRequest.php b/app-modules/activity/src/Http/Requests/CreateVoiceMessageRequest.php index 7f6ec60e..5c8d85d0 100644 --- a/app-modules/activity/src/Http/Requests/CreateVoiceMessageRequest.php +++ b/app-modules/activity/src/Http/Requests/CreateVoiceMessageRequest.php @@ -17,7 +17,7 @@ public function rules(): array { return [ 'provider' => ['required', 'in:twitch,discord'], - 'provider_id' => ['required'], + 'external_account_id' => ['required'], 'state' => ['required', 'in:muted,unmuted,disabled'], 'channel_name' => ['required', 'string'], ]; diff --git a/app-modules/activity/src/Models/Message.php b/app-modules/activity/src/Models/Message.php index 981e37f0..62ece1ea 100644 --- a/app-modules/activity/src/Models/Message.php +++ b/app-modules/activity/src/Models/Message.php @@ -22,7 +22,7 @@ final class Message extends Model protected $fillable = [ 'id', 'tenant_id', - 'provider_id', + 'external_identity_id', 'provider_message_id', 'channel_id', 'content', @@ -35,7 +35,7 @@ final class Message extends Model */ public function provider(): BelongsTo { - return $this->belongsTo(ExternalIdentity::class, 'provider_id'); + return $this->belongsTo(ExternalIdentity::class, 'external_identity_id'); } /** diff --git a/app-modules/activity/src/Models/Voice.php b/app-modules/activity/src/Models/Voice.php index 0092f7ff..aad2ec90 100644 --- a/app-modules/activity/src/Models/Voice.php +++ b/app-modules/activity/src/Models/Voice.php @@ -14,7 +14,7 @@ final class Voice extends Model protected $fillable = [ 'tenant_id', - 'provider_id', + 'external_identity_id', 'channel_name', 'state', 'obtained_experience', @@ -25,6 +25,6 @@ final class Voice extends Model */ public function provider(): BelongsTo { - return $this->belongsTo(ExternalIdentity::class); + return $this->belongsTo(ExternalIdentity::class, 'external_identity_id'); } } diff --git a/app-modules/activity/tests/Feature/Filament/Admin/Message/CreateMessageTest.php b/app-modules/activity/tests/Feature/Filament/Admin/Message/CreateMessageTest.php index 59b1461a..e57ea082 100644 --- a/app-modules/activity/tests/Feature/Filament/Admin/Message/CreateMessageTest.php +++ b/app-modules/activity/tests/Feature/Filament/Admin/Message/CreateMessageTest.php @@ -24,7 +24,7 @@ $data = [ 'tenant_id' => $tenant->getKey(), - 'provider_id' => $provider->getKey(), + 'external_identity_id' => $provider->getKey(), 'channel_id' => 1, 'provider_message_id' => 'prov-msg-123', 'content' => 'Mensagem de teste enviada', @@ -40,7 +40,7 @@ assertDatabaseHas(Message::class, [ 'tenant_id' => $tenant->getKey(), - 'provider_id' => $provider->getKey(), + 'external_identity_id' => $provider->getKey(), 'content' => 'Mensagem de teste enviada', ]); }); diff --git a/app-modules/activity/tests/Feature/Filament/Admin/Message/EditMessageTest.php b/app-modules/activity/tests/Feature/Filament/Admin/Message/EditMessageTest.php index cb935665..735a333a 100644 --- a/app-modules/activity/tests/Feature/Filament/Admin/Message/EditMessageTest.php +++ b/app-modules/activity/tests/Feature/Filament/Admin/Message/EditMessageTest.php @@ -41,7 +41,7 @@ ]) ->assertOk() ->assertSchemaStateSet([ - 'provider_id' => $message->provider_id, + 'external_identity_id' => $message->external_identity_id, 'channel_id' => $message->channel_id, 'content' => $message->content, 'provider_message_id' => $message->provider_message_id, diff --git a/app-modules/activity/tests/Feature/NewMessageTest.php b/app-modules/activity/tests/Feature/NewMessageTest.php index 42afa6c6..88f940bd 100644 --- a/app-modules/activity/tests/Feature/NewMessageTest.php +++ b/app-modules/activity/tests/Feature/NewMessageTest.php @@ -18,7 +18,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => IdentityProvider::Discord, - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -31,7 +31,7 @@ $provider = $user->providers[0]; $payload = [ 'provider' => $provider->provider->value, - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, 'provider_message_id' => '12312312', 'channel_id' => '312321', 'content' => '321312', @@ -59,7 +59,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => IdentityProvider::Discord, - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -71,7 +71,7 @@ $provider = $user->providers[0]; $payload = [ 'provider' => $provider->provider->value, - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, 'provider_message_id' => '12312312', 'channel_id' => '312321', 'content' => '321312', @@ -97,7 +97,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => IdentityProvider::Discord, - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -116,7 +116,7 @@ $provider = $user->providers[0]; $payload = [ 'provider' => $provider->provider->value, - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, 'provider_message_id' => '12312312', 'channel_id' => '312321', 'content' => '321312', diff --git a/app-modules/activity/tests/Feature/NewVoiceMessageTest.php b/app-modules/activity/tests/Feature/NewVoiceMessageTest.php index 753cdb44..30d27f51 100644 --- a/app-modules/activity/tests/Feature/NewVoiceMessageTest.php +++ b/app-modules/activity/tests/Feature/NewVoiceMessageTest.php @@ -17,7 +17,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => IdentityProvider::Discord, - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -30,7 +30,7 @@ $provider = $user->providers[0]; $payload = [ 'provider' => $provider->provider->value, - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, 'state' => VoiceStatesEnum::Muted->value, 'channel_name' => 'Estudando', ]; diff --git a/app-modules/activity/tests/Unit/Actions/NewMessageTest.php b/app-modules/activity/tests/Unit/Actions/NewMessageTest.php index 4517d38e..40462a2f 100644 --- a/app-modules/activity/tests/Unit/Actions/NewMessageTest.php +++ b/app-modules/activity/tests/Unit/Actions/NewMessageTest.php @@ -33,12 +33,12 @@ 'id' => '1', 'model_id' => 'id-user-foda', 'provider' => 'twitch', - 'provider_id' => '12312312', + 'external_account_id' => '12312312', ]; $findProviderStub ->shouldReceive('handle') - ->with($provider, $payload['provider_id']) + ->with($provider, $payload['external_account_id']) ->andReturn($providerEntityMock); $findCharacterStub @@ -81,7 +81,7 @@ 'payload' => [ 'provider' => 'twitch', 'tenant_id' => 1, - 'provider_id' => '1234', + 'external_account_id' => '1234', 'provider_message_id' => '78781237', 'channel_id' => '31231267312', 'content' => 'deixa o sub', diff --git a/app-modules/bot-discord/src/Events/MessageReceivedEvent.php b/app-modules/bot-discord/src/Events/MessageReceivedEvent.php index fa0673a7..495a95e7 100644 --- a/app-modules/bot-discord/src/Events/MessageReceivedEvent.php +++ b/app-modules/bot-discord/src/Events/MessageReceivedEvent.php @@ -33,14 +33,14 @@ public function handle(Message $message, Discord $discord): void try { $tenantProvider = ExternalIdentity::query() ->where('model_type', Tenant::class) - ->where('provider_id', (string) $message->guild_id) + ->where('external_account_id', (string) $message->guild_id) ->firstOrFail(); resolve(NewMessage::class)->persist(new NewMessageDTO( tenantId: $tenantProvider->tenant_id, provider: IdentityProvider::Discord, providerUsername: $message->author->username.'#'.$message->author->discriminator, - providerId: $message->user_id, + externalAccountId: $message->user_id, providerMessageId: $message->id, channelId: $message->channel_id, content: $message->content, diff --git a/app-modules/bot-discord/src/Events/WelcomeMember.php b/app-modules/bot-discord/src/Events/WelcomeMember.php index a0052a81..d1f57cbb 100644 --- a/app-modules/bot-discord/src/Events/WelcomeMember.php +++ b/app-modules/bot-discord/src/Events/WelcomeMember.php @@ -26,14 +26,14 @@ public function handle(Member $member, Discord $discord): void $tenantProvider = ExternalIdentity::query() ->where('model_type', Tenant::class) - ->where('provider_id', (string) $member->guild_id) + ->where('external_account_id', (string) $member->guild_id) ->firstOrFail(); try { $userDto = ResolveUserProviderDTO::make([ 'tenant_id' => $tenantProvider->tenant_id, 'provider' => $tenantProvider->provider, - 'provider_id' => $member->user->id, + 'external_account_id' => $member->user->id, 'model_type' => User::class, 'username' => $member->user->username, 'avatar' => $member->user->avatar, @@ -44,7 +44,7 @@ public function handle(Member $member, Discord $discord): void Log::error('Falha ao resolver usuário no evento WelcomeMember', [ 'tenant_id' => $tenantProvider->tenant_id ?? null, 'provider' => $tenantProvider->provider ?? null, - 'provider_id' => $member->user->id ?? null, + 'external_account_id' => $member->user->id ?? null, 'exception' => $throwable, ]); diff --git a/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php b/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php index 8e811a4b..7b84a9ba 100644 --- a/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/AbstractSlashCommand.php @@ -51,14 +51,14 @@ private function beforePipeline(Interaction $interaction): void $this->tenantProvider = ExternalIdentity::query() ->where('model_type', Tenant::class) ->where('provider', IdentityProvider::Discord) - ->where('provider_id', $interaction->guild_id) + ->where('external_account_id', $interaction->guild_id) ->first(); $this->memberProvider = ExternalIdentity::query() ->where('tenant_id', $this->tenantProvider->tenant_id) ->where('model_type', User::class) ->where('provider', IdentityProvider::Discord) - ->where('provider_id', $interaction->user->id) + ->where('external_account_id', $interaction->user->id) ->first(); } } diff --git a/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php b/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php index f9660173..3de315e3 100644 --- a/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/EditProfileCommand.php @@ -130,7 +130,7 @@ private function persistData( $payload = UpdateProfileDTO::fromPayload([ 'tenant_id' => $this->memberProvider->tenant_id, 'provider' => $this->memberProvider->provider, - 'provider_id' => $interaction->user->id, + 'external_account_id' => $interaction->user->id, 'name' => $components->get('custom_id', 'name')?->value, 'nickname' => $components->get('custom_id', 'nickname')?->value, 'linkedin_url' => $components->get('custom_id', 'linkedin_url')?->value, diff --git a/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php b/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php index 7996c6fa..164af2d7 100644 --- a/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/IntroductionCommand.php @@ -116,13 +116,13 @@ private function persistData(Interaction $interaction, Collection $components): { $tenantProvider = ExternalIdentity::query() ->where('model_type', Tenant::class) - ->where('provider_id', (string) $interaction->guild_id) + ->where('external_account_id', (string) $interaction->guild_id) ->firstOrFail(); $userDto = ResolveUserProviderDTO::make([ 'tenant_id' => $tenantProvider->tenant_id, 'provider' => $tenantProvider->provider, - 'provider_id' => $interaction->user->id, + 'external_account_id' => $interaction->user->id, 'model_type' => User::class, 'username' => $interaction->user->username, 'avatar' => $interaction->user->avatar, diff --git a/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php b/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php index 7cdce3be..d0535a7a 100644 --- a/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php +++ b/app-modules/bot-discord/src/SlashCommands/ProfileCommand.php @@ -69,7 +69,7 @@ public function handle(Interaction $interaction): void $mentionedUser = $interaction->user; if ($userId = $this->value('user')) { - $this->memberProvider = $this->getMemberProviderQuery()->where('provider_id', $userId)->first(); + $this->memberProvider = $this->getMemberProviderQuery()->where('external_account_id', $userId)->first(); $mentionedUser = $interaction->data->resolved->users->get('id', $userId); } diff --git a/app-modules/community/src/Meeting/Http/Controllers/MeetingController.php b/app-modules/community/src/Meeting/Http/Controllers/MeetingController.php index c196ccc3..e9164d58 100644 --- a/app-modules/community/src/Meeting/Http/Controllers/MeetingController.php +++ b/app-modules/community/src/Meeting/Http/Controllers/MeetingController.php @@ -29,7 +29,7 @@ public function postMeeting( ): JsonResponse { try { return response()->json( - $startMeeting->handle($provider, $request->input('provider_id'), $request->input('meeting_type_id')), + $startMeeting->handle($provider, $request->input('external_account_id'), $request->input('meeting_type_id')), Response::HTTP_CREATED ); } catch (MeetingException $meetingException) { diff --git a/app-modules/community/src/Meeting/Http/Requests/MeetingRequest.php b/app-modules/community/src/Meeting/Http/Requests/MeetingRequest.php index c17cbfaa..2f8e6433 100644 --- a/app-modules/community/src/Meeting/Http/Requests/MeetingRequest.php +++ b/app-modules/community/src/Meeting/Http/Requests/MeetingRequest.php @@ -17,7 +17,7 @@ public function rules(): array { return [ 'meeting_type_id' => ['required', 'integer'], - 'provider_id' => ['required'], + 'external_account_id' => ['required'], 'provider' => ['required'], ]; } diff --git a/app-modules/community/tests/Feature/Feedback/CreateFeedbackTest.php b/app-modules/community/tests/Feature/Feedback/CreateFeedbackTest.php index 3fcc0bcf..68b5a368 100644 --- a/app-modules/community/tests/Feature/Feedback/CreateFeedbackTest.php +++ b/app-modules/community/tests/Feature/Feedback/CreateFeedbackTest.php @@ -13,7 +13,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => 'discord', - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -22,8 +22,8 @@ $providerTarget = ExternalIdentity::factory()->create(['tenant_id' => $tenant->getKey(), 'provider' => 'discord']); $payload = [ - 'sender_id' => $providerSender->provider_id, - 'target_id' => $providerTarget->provider_id, + 'sender_id' => $providerSender->external_account_id, + 'target_id' => $providerTarget->external_account_id, 'message' => 'mt legal vc', 'type' => 'elogio', ]; diff --git a/app-modules/community/tests/Feature/Feedback/ReviewFeedbackTest.php b/app-modules/community/tests/Feature/Feedback/ReviewFeedbackTest.php index 0355ed54..609e6d86 100644 --- a/app-modules/community/tests/Feature/Feedback/ReviewFeedbackTest.php +++ b/app-modules/community/tests/Feature/Feedback/ReviewFeedbackTest.php @@ -34,7 +34,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => 'discord', - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -42,7 +42,7 @@ $feedback = Feedback::factory()->create(['tenant_id' => $tenant->getKey()]); $staffProvider = ExternalIdentity::factory()->create(['tenant_id' => $tenant->getKey(), 'provider' => 'discord']); - $payload['staff_id'] = $staffProvider->provider_id; + $payload['staff_id'] = $staffProvider->external_account_id; $response = $this ->actingAsAdmin() ->postJson(route('feedbacks.review', [ diff --git a/app-modules/community/tests/Feature/Meeting/StartMeetingTest.php b/app-modules/community/tests/Feature/Meeting/StartMeetingTest.php index 50644000..84206239 100644 --- a/app-modules/community/tests/Feature/Meeting/StartMeetingTest.php +++ b/app-modules/community/tests/Feature/Meeting/StartMeetingTest.php @@ -22,7 +22,7 @@ $meetingType = MeetingType::factory()->create(); $payload = [ 'meeting_type_id' => $meetingType->getKey(), - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, ]; $expectedResponse = [ @@ -52,7 +52,7 @@ $payload = [ 'meeting_type_id' => 12, - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, ]; // Act diff --git a/app-modules/gamification/tests/Feature/Character/ClaimCharacterBadgeTest.php b/app-modules/gamification/tests/Feature/Character/ClaimCharacterBadgeTest.php index ab414f8c..1721289a 100644 --- a/app-modules/gamification/tests/Feature/Character/ClaimCharacterBadgeTest.php +++ b/app-modules/gamification/tests/Feature/Character/ClaimCharacterBadgeTest.php @@ -19,7 +19,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => IdentityProvider::Discord, - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -35,10 +35,10 @@ ->actingAsAdmin() ->postJson(route('characters.claimBadge', [ 'provider' => $provider->provider->value, - 'providerId' => $provider->provider_id, + 'providerId' => $provider->external_account_id, ]), ['redeem_code' => $badge->redeem_code], [ 'X-He4rt-Provider' => $provider->provider->value, - 'X-He4rt-Provider-Id' => $provider->provider_id, + 'X-He4rt-Provider-Id' => $provider->external_account_id, ]); $response->assertStatus(Response::HTTP_NO_CONTENT); diff --git a/app-modules/gamification/tests/Feature/Character/ClaimDailyBonusTest.php b/app-modules/gamification/tests/Feature/Character/ClaimDailyBonusTest.php index c716d9cb..762f899f 100644 --- a/app-modules/gamification/tests/Feature/Character/ClaimDailyBonusTest.php +++ b/app-modules/gamification/tests/Feature/Character/ClaimDailyBonusTest.php @@ -15,7 +15,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => 'discord', - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -28,7 +28,7 @@ $provider = $user->providers[0]; $routeParams = [ 'provider' => $provider->provider, - 'providerId' => $provider->provider_id, + 'providerId' => $provider->external_account_id, ]; $expected = $user->character->daily_bonus_claimed_at; $this->travelTo(now()->addHours(24)->addMinutes(2)); @@ -49,7 +49,7 @@ ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => 'discord', - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); @@ -62,7 +62,7 @@ $provider = $user->providers[0]; $routeParams = [ 'provider' => $provider->provider, - 'providerId' => $provider->provider_id, + 'providerId' => $provider->external_account_id, ]; $this diff --git a/app-modules/identity/database/factories/AccessTokenFactory.php b/app-modules/identity/database/factories/AccessTokenFactory.php deleted file mode 100644 index 607bbcd5..00000000 --- a/app-modules/identity/database/factories/AccessTokenFactory.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ -final class AccessTokenFactory extends Factory -{ - protected $model = AccessToken::class; - - public function definition(): array - { - return [ - 'id' => fake()->uuid(), - 'provider_id' => ExternalIdentity::factory(), - 'access_token' => fake()->uuid(), - 'refresh_token' => fake()->uuid(), - 'expires_in' => fake()->randomNumber(4), - ]; - } -} diff --git a/app-modules/identity/database/factories/ExternalIdentityFactory.php b/app-modules/identity/database/factories/ExternalIdentityFactory.php index eb444388..580622c1 100644 --- a/app-modules/identity/database/factories/ExternalIdentityFactory.php +++ b/app-modules/identity/database/factories/ExternalIdentityFactory.php @@ -4,11 +4,15 @@ namespace He4rt\Identity\Database\Factories; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; +use He4rt\Identity\ExternalIdentity\Enums\CredentialsType; use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; +use He4rt\Identity\ExternalIdentity\Enums\IdentityType; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; use He4rt\Identity\Tenant\Models\Tenant; use He4rt\Identity\User\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Facades\Crypt; /** * @extends Factory @@ -24,9 +28,21 @@ public function definition(): array 'tenant_id' => Tenant::factory(), 'model_type' => User::class, 'model_id' => User::factory(), + 'type' => IdentityType::External, 'provider' => fake()->randomElement(IdentityProvider::cases()), - 'provider_id' => fake()->numerify('######'), - 'email' => fake()->unique()->email(), + 'credentials_type' => CredentialsType::OAuth2, + 'credentials' => ClientAccessManager::make( + accessToken: Crypt::encrypt(fake()->sha256()), + refreshToken: Crypt::encrypt(fake()->sha256()), + expiresIn: Crypt::encrypt((string) 3600), + ), + 'external_account_id' => fake()->numerify('######'), + 'connected_by' => User::factory(), + 'connected_at' => now(), + 'metadata' => [ + 'email' => fake()->unique()->email(), + 'username' => fake()->userName(), + ], ]; } diff --git a/app-modules/identity/database/factories/TenantFactory.php b/app-modules/identity/database/factories/TenantFactory.php index 025fd5fd..dd78cd13 100644 --- a/app-modules/identity/database/factories/TenantFactory.php +++ b/app-modules/identity/database/factories/TenantFactory.php @@ -45,7 +45,7 @@ public function withProvider(IdentityProvider $provider = IdentityProvider::Disc ExternalIdentity::factory()->create([ 'tenant_id' => $tenant->getKey(), 'provider' => $provider, - 'provider_id' => $providerId, + 'external_account_id' => $providerId, ]); }); } diff --git a/app-modules/identity/database/migrations/2026_03_21_000001_migrate_providers_to_external_identities.php b/app-modules/identity/database/migrations/2026_03_21_000001_migrate_providers_to_external_identities.php new file mode 100644 index 00000000..e48fd0c4 --- /dev/null +++ b/app-modules/identity/database/migrations/2026_03_21_000001_migrate_providers_to_external_identities.php @@ -0,0 +1,185 @@ +dropForeign(['provider_id']); + }); + + Schema::table('messages', function (Blueprint $table): void { + $table->dropForeign(['provider_id']); + }); + + Schema::table('voice_messages', function (Blueprint $table): void { + $table->dropForeign(['provider_id']); + }); + + // ────────────────────────────────────────────── + // 2. Add new columns to providers table + // ────────────────────────────────────────────── + Schema::table('providers', function (Blueprint $table): void { + $table->string('type')->default('external')->after('model_id'); + $table->string('credentials_type')->default('oauth2')->after('provider'); + $table->text('credentials')->nullable()->after('credentials_type'); + $table->uuid('connected_by')->nullable()->after('provider_id'); + $table->timestamp('connected_at')->nullable()->after('connected_by'); + $table->timestamp('disconnected_at')->nullable()->after('connected_at'); + $table->json('metadata')->nullable()->after('disconnected_at'); + $table->softDeletes(); + }); + + // ────────────────────────────────────────────── + // 3. Set defaults for existing rows + // ────────────────────────────────────────────── + DB::table('providers')->update([ + 'type' => 'external', + 'credentials_type' => 'oauth2', + ]); + + // Set connected_at = created_at for all existing records + DB::statement('UPDATE providers SET connected_at = created_at'); + + // ────────────────────────────────────────────── + // 4. Migrate provider_tokens → credentials column + // Encrypt each value since ClientAccessManager + // getters call Crypt::decrypt() + // ────────────────────────────────────────────── + $tokens = DB::table('provider_tokens') + ->select('provider_id', 'access_token', 'refresh_token', 'expires_in') + ->get(); + + foreach ($tokens as $token) { + $credentials = json_encode([ + 'client_id' => null, + 'client_secret' => null, + 'access_token' => $token->access_token + ? Crypt::encrypt($token->access_token) + : null, + 'refresh_token' => $token->refresh_token + ? Crypt::encrypt($token->refresh_token) + : null, + 'expires_in' => $token->expires_in !== null + ? Crypt::encrypt((string) $token->expires_in) + : null, + 'username' => null, + 'password' => null, + 'api_key' => null, + ]); + + // provider_tokens.provider_id is a UUID FK to providers.id + DB::table('providers') + ->where('id', $token->provider_id) + ->update(['credentials' => $credentials]); + } + + // Set empty credentials JSON for rows without tokens + DB::table('providers') + ->whereNull('credentials') + ->update([ + 'credentials' => json_encode([ + 'client_id' => null, + 'client_secret' => null, + 'access_token' => null, + 'refresh_token' => null, + 'expires_in' => null, + 'username' => null, + 'password' => null, + 'api_key' => null, + ]), + ]); + + // ────────────────────────────────────────────── + // 5. Migrate email/avatar/username → metadata JSON + // ────────────────────────────────────────────── + $providers = DB::table('providers') + ->select('id', 'email', 'avatar', 'username') + ->get(); + + foreach ($providers as $provider) { + $metadata = json_encode(array_filter([ + 'email' => $provider->email, + 'avatar' => $provider->avatar, + 'username' => $provider->username, + ])); + + DB::table('providers') + ->where('id', $provider->id) + ->update(['metadata' => $metadata]); + } + + // ────────────────────────────────────────────── + // 6. Drop old profile columns + // ────────────────────────────────────────────── + Schema::table('providers', function (Blueprint $table): void { + $table->dropColumn(['email', 'avatar', 'username']); + }); + + // ────────────────────────────────────────────── + // 7. Rename provider_id → external_account_id + // (this is the external platform ID column, + // NOT the UUID FK in other tables) + // ────────────────────────────────────────────── + Schema::table('providers', function (Blueprint $table): void { + $table->renameColumn('provider_id', 'external_account_id'); + }); + + // ────────────────────────────────────────────── + // 8. Rename table: providers → external_identities + // ────────────────────────────────────────────── + Schema::rename('providers', 'external_identities'); + + // ────────────────────────────────────────────── + // 9. Rename FK columns in dependent tables + // (these are UUID FKs, NOT the external ID) + // ────────────────────────────────────────────── + Schema::table('messages', function (Blueprint $table): void { + $table->renameColumn('provider_id', 'external_identity_id'); + }); + + Schema::table('voice_messages', function (Blueprint $table): void { + $table->renameColumn('provider_id', 'external_identity_id'); + }); + + // ────────────────────────────────────────────── + // 10. Re-add FK constraints pointing to new table + // ────────────────────────────────────────────── + Schema::table('messages', function (Blueprint $table): void { + $table->foreign('external_identity_id') + ->references('id') + ->on('external_identities'); + }); + + Schema::table('voice_messages', function (Blueprint $table): void { + $table->foreign('external_identity_id') + ->references('id') + ->on('external_identities'); + }); + + // ────────────────────────────────────────────── + // 11. Add connected_by FK constraint + // ────────────────────────────────────────────── + Schema::table('external_identities', function (Blueprint $table): void { + $table->foreign('connected_by') + ->references('id') + ->on('users'); + }); + + // ────────────────────────────────────────────── + // 12. Drop provider_tokens table + // ────────────────────────────────────────────── + Schema::dropIfExists('provider_tokens'); + } +}; diff --git a/app-modules/identity/src/Auth/Actions/AuthenticateAction.php b/app-modules/identity/src/Auth/Actions/AuthenticateAction.php index a9a3ab7b..4e130725 100644 --- a/app-modules/identity/src/Auth/Actions/AuthenticateAction.php +++ b/app-modules/identity/src/Auth/Actions/AuthenticateAction.php @@ -6,6 +6,7 @@ use He4rt\Identity\Auth\DTOs\OAuthStateDTO; use He4rt\Identity\Auth\DTOs\OAuthUserDTO; +use He4rt\Identity\ExternalIdentity\Enums\CredentialsType; use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; use He4rt\Identity\Tenant\Models\Tenant; @@ -40,7 +41,7 @@ private function authenticateTenant(OAuthStateDTO $state, IdentityProvider $oaut $provider = ExternalIdentity::query() ->where('tenant_id', $tenant->getKey()) ->where('provider', $user->provider) - ->where('provider_id', $user->providerId) + ->where('external_account_id', $user->providerId) ->first(); if (! $provider) { @@ -76,15 +77,20 @@ private function registerNewUser(OAuthUserDTO $userDTO, Tenant $tenant): Externa $provider = $user->providers()->updateOrCreate([ 'tenant_id' => $tenant->getKey(), 'provider' => IdentityProvider::from($userDTO->provider->value), - 'provider_id' => $userDTO->providerId, + 'external_account_id' => $userDTO->providerId, ], [ - 'email' => $userDTO->email, - 'avatar' => $userDTO->avatarUrl, - 'username' => $userDTO->username, + 'type' => $userDTO->provider->getType(), + 'credentials_type' => CredentialsType::OAuth2, + 'credentials' => $userDTO->credentials->toClientAccessManager(), + 'metadata' => [ + 'email' => $userDTO->email, + 'avatar' => $userDTO->avatarUrl, + 'username' => $userDTO->username, + ], + 'connected_at' => now(), + 'connected_by' => $user->id, ]); - $provider->tokens()->create($userDTO->credentials->toDatabase()); - return $provider; } diff --git a/app-modules/identity/src/Auth/DTOs/OAuthAccessDTO.php b/app-modules/identity/src/Auth/DTOs/OAuthAccessDTO.php index 897137bd..ad2c2321 100644 --- a/app-modules/identity/src/Auth/DTOs/OAuthAccessDTO.php +++ b/app-modules/identity/src/Auth/DTOs/OAuthAccessDTO.php @@ -4,6 +4,9 @@ namespace He4rt\Identity\Auth\DTOs; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; +use Illuminate\Support\Facades\Crypt; + abstract class OAuthAccessDTO { public function __construct( @@ -22,4 +25,13 @@ final public function toDatabase(): array 'expires_in' => $this->expiresIn, ]; } + + final public function toClientAccessManager(): ClientAccessManager + { + return ClientAccessManager::make( + accessToken: Crypt::encrypt($this->accessToken), + refreshToken: Crypt::encrypt($this->refreshToken), + expiresIn: $this->expiresIn !== null ? Crypt::encrypt((string) $this->expiresIn) : null, + ); + } } diff --git a/app-modules/identity/src/Auth/DTOs/OAuthUserDTO.php b/app-modules/identity/src/Auth/DTOs/OAuthUserDTO.php index 6607b6bd..73d764a2 100644 --- a/app-modules/identity/src/Auth/DTOs/OAuthUserDTO.php +++ b/app-modules/identity/src/Auth/DTOs/OAuthUserDTO.php @@ -24,7 +24,7 @@ final public function toDatabase(): array { return [ 'provider' => $this->provider, - 'provider_id' => $this->providerId, + 'external_account_id' => $this->providerId, 'email' => $this->email, ]; } diff --git a/app-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.php b/app-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.php index a7c005c9..2ff3596e 100644 --- a/app-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.php +++ b/app-modules/identity/src/ExternalIdentity/Actions/CreateAccountByExternalIdentity.php @@ -4,6 +4,8 @@ namespace He4rt\Identity\ExternalIdentity\Actions; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; +use He4rt\Identity\ExternalIdentity\Enums\CredentialsType; use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; use He4rt\Identity\User\Models\User; @@ -15,7 +17,7 @@ public function handle(int $tenantId, IdentityProvider $provider, string $provid { $existing = ExternalIdentity::query() ->where('provider', $provider->value) - ->where('provider_id', $providerId) + ->where('external_account_id', $providerId) ->first(); if ($existing) { @@ -39,8 +41,12 @@ public function handle(int $tenantId, IdentityProvider $provider, string $provid 'tenant_id' => $tenantId, 'model_type' => User::class, 'model_id' => $user->id, + 'type' => $provider->getType(), 'provider' => $provider->value, - 'provider_id' => $providerId, + 'credentials_type' => CredentialsType::OAuth2, + 'credentials' => ClientAccessManager::make(), + 'external_account_id' => $providerId, + 'connected_at' => now(), ]); } } diff --git a/app-modules/identity/src/ExternalIdentity/Actions/FindExternalIdentity.php b/app-modules/identity/src/ExternalIdentity/Actions/FindExternalIdentity.php index 92310c7a..29771dfb 100644 --- a/app-modules/identity/src/ExternalIdentity/Actions/FindExternalIdentity.php +++ b/app-modules/identity/src/ExternalIdentity/Actions/FindExternalIdentity.php @@ -26,7 +26,7 @@ private function find(string $provider, string $providerId): ExternalIdentity $model = ExternalIdentity::query() ->where('tenant_id', request()->input('tenant_id')) ->where('provider', $provider) - ->where('provider_id', $providerId) + ->where('external_account_id', $providerId) ->first(); throw_unless($model, ExternalIdentityException::notFound($provider, $providerId)); diff --git a/app-modules/identity/src/ExternalIdentity/Actions/LinkExternalIdentity.php b/app-modules/identity/src/ExternalIdentity/Actions/LinkExternalIdentity.php index 8560ada8..2599dbc6 100644 --- a/app-modules/identity/src/ExternalIdentity/Actions/LinkExternalIdentity.php +++ b/app-modules/identity/src/ExternalIdentity/Actions/LinkExternalIdentity.php @@ -16,10 +16,12 @@ public function handle(ExternalIdentity $provider): User return $provider->model; } + $username = $provider->metadata['username'] ?? 'unknown'; + $user = User::query()->create([ 'id' => Uuid::uuid4()->toString(), - 'username' => $provider->username, - 'name' => $provider->username, + 'username' => $username, + 'name' => $username, 'is_donator' => false, ]); diff --git a/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php b/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php index 19aca48a..ba7ed3a5 100644 --- a/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php +++ b/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php @@ -4,7 +4,9 @@ namespace He4rt\Identity\ExternalIdentity\Actions; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; use He4rt\Identity\ExternalIdentity\DTOs\ResolveUserProviderDTO; +use He4rt\Identity\ExternalIdentity\Enums\CredentialsType; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; final class ResolveExternalIdentity @@ -15,13 +17,19 @@ public function handle(ResolveUserProviderDTO $dto): ExternalIdentity [ 'provider' => $dto->provider, 'tenant_id' => $dto->tenantId, - 'provider_id' => $dto->providerId, + 'external_account_id' => $dto->externalAccountId, 'model_type' => $dto->modelType, ], [ - 'username' => $dto->username, - 'email' => $dto->email, - 'avatar' => $dto->avatar, + 'type' => $dto->provider->getType(), + 'credentials_type' => CredentialsType::OAuth2, + 'credentials' => ClientAccessManager::make(), + 'metadata' => array_filter([ + 'username' => $dto->username, + 'email' => $dto->email, + 'avatar' => $dto->avatar, + ]), + 'connected_at' => now(), ] ); } diff --git a/app-modules/identity/src/ExternalIdentity/Casts/AsCredentials.php b/app-modules/identity/src/ExternalIdentity/Casts/AsCredentials.php new file mode 100644 index 00000000..1a73c1a1 --- /dev/null +++ b/app-modules/identity/src/ExternalIdentity/Casts/AsCredentials.php @@ -0,0 +1,36 @@ + + */ +class AsCredentials implements CastsAttributes +{ + public function get(Model $model, string $key, mixed $value, array $attributes): ClientAccessManager + { + $payload = json_decode((string) $value, true); + + return ClientAccessManager::makeFromPayload($payload ?? []); + } + + public function set(Model $model, string $key, mixed $value, array $attributes): string + { + return json_encode([ + 'client_id' => $value->clientId, + 'client_secret' => $value->clientSecret, + 'access_token' => $value->accessToken, + 'refresh_token' => $value->refreshToken, + 'expires_in' => $value->expiresIn, + 'username' => $value->username, + 'password' => $value->password, + 'api_key' => $value->apiKey, + ]); + } +} diff --git a/app-modules/identity/src/ExternalIdentity/DTOs/NewProviderDTO.php b/app-modules/identity/src/ExternalIdentity/DTOs/NewProviderDTO.php index 343fb434..ada0d390 100644 --- a/app-modules/identity/src/ExternalIdentity/DTOs/NewProviderDTO.php +++ b/app-modules/identity/src/ExternalIdentity/DTOs/NewProviderDTO.php @@ -12,7 +12,7 @@ public function __construct( private int $tenantId, private IdentityProvider $provider, - private string $providerId + private string $externalAccountId ) {} public function jsonSerialize(): array @@ -20,7 +20,7 @@ public function jsonSerialize(): array return [ 'tenant_id' => $this->tenantId, 'provider' => $this->provider->value, - 'provider_id' => $this->providerId, + 'external_account_id' => $this->externalAccountId, ]; } } diff --git a/app-modules/identity/src/ExternalIdentity/DTOs/ResolveUserProviderDTO.php b/app-modules/identity/src/ExternalIdentity/DTOs/ResolveUserProviderDTO.php index 3f0b4e6c..ef1b74bf 100644 --- a/app-modules/identity/src/ExternalIdentity/DTOs/ResolveUserProviderDTO.php +++ b/app-modules/identity/src/ExternalIdentity/DTOs/ResolveUserProviderDTO.php @@ -11,7 +11,7 @@ class ResolveUserProviderDTO public function __construct( public int $tenantId, public IdentityProvider $provider, - public string $providerId, + public string $externalAccountId, public string $modelType, public ?string $username = null, public ?string $email = null, @@ -23,7 +23,7 @@ public static function make(array $data): self return new self( tenantId: $data['tenant_id'], provider: $data['provider'], - providerId: $data['provider_id'], + externalAccountId: $data['external_account_id'], modelType: $data['model_type'], username: $data['username'] ?? null, email: $data['email'] ?? null, diff --git a/app-modules/identity/src/ExternalIdentity/Data/ClientAccessManager.php b/app-modules/identity/src/ExternalIdentity/Data/ClientAccessManager.php new file mode 100644 index 00000000..6ab748b3 --- /dev/null +++ b/app-modules/identity/src/ExternalIdentity/Data/ClientAccessManager.php @@ -0,0 +1,91 @@ + $payload + */ + public static function makeFromPayload(array $payload): self + { + return new self( + clientId: $payload['client_id'] ?? null, + clientSecret: $payload['client_secret'] ?? null, + accessToken: $payload['access_token'] ?? null, + refreshToken: $payload['refresh_token'] ?? null, + expiresIn: $payload['expires_in'] ?? null, + username: $payload['username'] ?? null, + password: $payload['password'] ?? null, + apiKey: $payload['api_key'] ?? null, + ); + } + + public function getClientId(): ?string + { + return $this->clientId !== null ? Crypt::decrypt($this->clientId) : null; + } + + public function getAccessToken(): ?string + { + return $this->accessToken !== null ? Crypt::decrypt($this->accessToken) : null; + } + + public function getRefreshToken(): ?string + { + return $this->refreshToken !== null ? Crypt::decrypt($this->refreshToken) : null; + } + + public function getExpiresIn(): ?int + { + return $this->expiresIn !== null ? (int) Crypt::decrypt($this->expiresIn) : null; + } + + public function getClientSecret(): ?string + { + return $this->clientSecret !== null ? Crypt::decrypt($this->clientSecret) : null; + } + + public function getUsername(): ?string + { + return $this->username !== null ? Crypt::decrypt($this->username) : null; + } + + public function getPassword(): ?string + { + return $this->password !== null ? Crypt::decrypt($this->password) : null; + } + + public function getApiKey(): ?string + { + return $this->apiKey !== null ? Crypt::decrypt($this->apiKey) : null; + } +} diff --git a/app-modules/identity/src/ExternalIdentity/Enums/CredentialsType.php b/app-modules/identity/src/ExternalIdentity/Enums/CredentialsType.php new file mode 100644 index 00000000..61397516 --- /dev/null +++ b/app-modules/identity/src/ExternalIdentity/Enums/CredentialsType.php @@ -0,0 +1,23 @@ + 'OAuth 2.0', + self::ApiKey => 'API Key', + self::Basic => 'Basic Auth', + }; + } +} diff --git a/app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php b/app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php index 4ab353a5..79a0b4fe 100644 --- a/app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php +++ b/app-modules/identity/src/ExternalIdentity/Enums/IdentityProvider.php @@ -81,4 +81,11 @@ public function getRedirectUri(?string $tenant = null): string ) ); } + + public function getType(): IdentityType + { + return match ($this) { + self::Discord, self::Twitch => IdentityType::External, + }; + } } diff --git a/app-modules/identity/src/ExternalIdentity/Enums/IdentityType.php b/app-modules/identity/src/ExternalIdentity/Enums/IdentityType.php new file mode 100644 index 00000000..ae40ddc7 --- /dev/null +++ b/app-modules/identity/src/ExternalIdentity/Enums/IdentityType.php @@ -0,0 +1,27 @@ + 'External Platform', + }; + } + + public function getColor(): string + { + return match ($this) { + self::External => 'primary', + }; + } +} diff --git a/app-modules/identity/src/ExternalIdentity/Http/Controllers/ProvidersController.php b/app-modules/identity/src/ExternalIdentity/Http/Controllers/ProvidersController.php index ee569085..dd5df005 100644 --- a/app-modules/identity/src/ExternalIdentity/Http/Controllers/ProvidersController.php +++ b/app-modules/identity/src/ExternalIdentity/Http/Controllers/ProvidersController.php @@ -21,7 +21,7 @@ public function postProvider( $response = $action->handle( $request->input('tenant_id'), IdentityProvider::from($provider), - $request->input('provider_id'), + $request->input('external_account_id'), $request->input('username') ); diff --git a/app-modules/identity/src/ExternalIdentity/Http/Requests/CreateProviderRequest.php b/app-modules/identity/src/ExternalIdentity/Http/Requests/CreateProviderRequest.php index 4fdb5da8..cbe336d1 100644 --- a/app-modules/identity/src/ExternalIdentity/Http/Requests/CreateProviderRequest.php +++ b/app-modules/identity/src/ExternalIdentity/Http/Requests/CreateProviderRequest.php @@ -17,7 +17,7 @@ public function rules(): array { return [ 'provider' => ['required', 'in:discord,twitch'], - 'provider_id' => ['required'], + 'external_account_id' => ['required'], 'username' => ['required'], ]; } diff --git a/app-modules/identity/src/ExternalIdentity/Models/AccessToken.php b/app-modules/identity/src/ExternalIdentity/Models/AccessToken.php deleted file mode 100644 index b2296a90..00000000 --- a/app-modules/identity/src/ExternalIdentity/Models/AccessToken.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ - public function provider(): BelongsTo - { - return $this->belongsTo(ExternalIdentity::class, 'provider_id'); - } - - protected static function newFactory(): AccessTokenFactory - { - return AccessTokenFactory::new(); - } -} diff --git a/app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php b/app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php index 032c4835..1b275084 100644 --- a/app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php +++ b/app-modules/identity/src/ExternalIdentity/Models/ExternalIdentity.php @@ -6,42 +6,62 @@ use He4rt\Activity\Models\Message; use He4rt\Identity\Database\Factories\ExternalIdentityFactory; +use He4rt\Identity\ExternalIdentity\Casts\AsCredentials; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; +use He4rt\Identity\ExternalIdentity\Enums\CredentialsType; use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; +use He4rt\Identity\ExternalIdentity\Enums\IdentityType; use He4rt\Identity\User\Models\User; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; /** * @property string $id - * @property User $user - * @property Collection $tokens - * @property string $user_id * @property int $tenant_id - * @property string $provider_id - * @property string $provider + * @property string $model_type + * @property string $model_id + * @property IdentityType $type + * @property IdentityProvider $provider + * @property CredentialsType $credentials_type + * @property ClientAccessManager $credentials + * @property string|null $external_account_id + * @property string|null $connected_by + * @property Carbon|null $connected_at + * @property Carbon|null $disconnected_at + * @property array|null $metadata + * @property Carbon $created_at + * @property Carbon $updated_at + * @property Carbon|null $deleted_at + * @property-read User|null $connectedByUser */ final class ExternalIdentity extends Model { use HasFactory; use HasUuids; + use SoftDeletes; - protected $table = 'providers'; + protected $table = 'external_identities'; protected $fillable = [ 'id', 'tenant_id', 'model_type', 'model_id', + 'type', 'provider', - 'provider_id', - 'email', - 'avatar', - 'username', + 'credentials_type', + 'credentials', + 'external_account_id', + 'connected_by', + 'connected_at', + 'disconnected_at', + 'metadata', ]; protected $appends = [ @@ -62,11 +82,11 @@ public function user(): BelongsTo } /** - * @return HasMany + * @return BelongsTo */ - public function tokens(): HasMany + public function connectedByUser(): BelongsTo { - return $this->hasMany(AccessToken::class, 'provider_id'); + return $this->belongsTo(User::class, 'connected_by'); } /** @@ -74,7 +94,12 @@ public function tokens(): HasMany */ public function messages(): HasMany { - return $this->hasMany(Message::class, 'provider_id'); + return $this->hasMany(Message::class, 'external_identity_id'); + } + + public function isConnected(): bool + { + return $this->connected_at !== null && $this->disconnected_at === null; } protected static function newFactory(): ExternalIdentityFactory @@ -82,15 +107,21 @@ protected static function newFactory(): ExternalIdentityFactory return ExternalIdentityFactory::new(); } + protected function getMessagesCountAttribute(): int + { + return $this->messages()->count(); + } + protected function casts(): array { return [ + 'type' => IdentityType::class, 'provider' => IdentityProvider::class, + 'credentials_type' => CredentialsType::class, + 'credentials' => AsCredentials::class, + 'connected_at' => 'datetime', + 'disconnected_at' => 'datetime', + 'metadata' => 'array', ]; } - - protected function getMessagesCountAttribute(): int - { - return $this->messages()->count(); - } } diff --git a/app-modules/identity/src/Filament/Admin/Resources/Users/Pages/EditUser.php b/app-modules/identity/src/Filament/Admin/Resources/Users/Pages/EditUser.php index 73b150f1..3e9c14d8 100644 --- a/app-modules/identity/src/Filament/Admin/Resources/Users/Pages/EditUser.php +++ b/app-modules/identity/src/Filament/Admin/Resources/Users/Pages/EditUser.php @@ -76,11 +76,18 @@ public function form(Schema $schema): Schema TextEntry::make('provider') ->label('Provider'), - TextEntry::make('provider_id') - ->label('Provider ID'), + TextEntry::make('external_account_id') + ->label('External Account ID'), - TextEntry::make('email') + TextEntry::make('metadata.email') ->label('Email'), + + TextEntry::make('metadata.username') + ->label('Username'), + + TextEntry::make('connected_at') + ->label('Connected At') + ->dateTime(), ]) ->columns(3) ->addable(false) diff --git a/app-modules/identity/src/User/Actions/UpdateProfile.php b/app-modules/identity/src/User/Actions/UpdateProfile.php index b25bf700..cceaddd6 100644 --- a/app-modules/identity/src/User/Actions/UpdateProfile.php +++ b/app-modules/identity/src/User/Actions/UpdateProfile.php @@ -20,7 +20,7 @@ public function __construct( public function handle(UpdateProfileDTO $profileDTO): void { $providerDto = ResolveUserProviderDTO::make([ - 'provider_id' => $profileDTO->providerId, + 'external_account_id' => $profileDTO->externalAccountId, 'provider' => $profileDTO->provider, 'tenant_id' => $profileDTO->tenantId, 'model_type' => User::class, diff --git a/app-modules/identity/src/User/DTOs/UpdateProfileDTO.php b/app-modules/identity/src/User/DTOs/UpdateProfileDTO.php index 8e90f0cb..d16ee1e2 100644 --- a/app-modules/identity/src/User/DTOs/UpdateProfileDTO.php +++ b/app-modules/identity/src/User/DTOs/UpdateProfileDTO.php @@ -9,7 +9,7 @@ readonly class UpdateProfileDTO { public function __construct( - public string $providerId, + public string $externalAccountId, public IdentityProvider $provider, public int $tenantId, public ?string $name = null, @@ -23,7 +23,7 @@ public function __construct( public static function fromPayload(array $payload): self { return new self( - providerId: $payload['provider_id'], + externalAccountId: $payload['external_account_id'], provider: $payload['provider'], tenantId: $payload['tenant_id'], name: $payload['name'], diff --git a/app-modules/identity/src/User/Http/Controllers/UsersController.php b/app-modules/identity/src/User/Http/Controllers/UsersController.php index b36b1e8f..57fe842b 100644 --- a/app-modules/identity/src/User/Http/Controllers/UsersController.php +++ b/app-modules/identity/src/User/Http/Controllers/UsersController.php @@ -34,7 +34,7 @@ public function getProfile(string $value): JsonResponse $user = User::query()->where('username', $value)->first(); if (! $user) { - $provider = ExternalIdentity::query()->where('provider_id', $value)->first(); + $provider = ExternalIdentity::query()->where('external_account_id', $value)->first(); throw_unless($provider, ProfileException::notFound()); diff --git a/app-modules/identity/tests/Feature/Auth/AuthenticateActionTest.php b/app-modules/identity/tests/Feature/Auth/AuthenticateActionTest.php index ab6fee46..7f094f58 100644 --- a/app-modules/identity/tests/Feature/Auth/AuthenticateActionTest.php +++ b/app-modules/identity/tests/Feature/Auth/AuthenticateActionTest.php @@ -4,6 +4,7 @@ use He4rt\Identity\Auth\Actions\AuthenticateAction; use He4rt\Identity\Auth\DTOs\OAuthStateDTO; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; use He4rt\Identity\ExternalIdentity\Models\ExternalIdentity; use He4rt\Identity\Tenant\Models\Tenant; @@ -67,15 +68,14 @@ function setDiscordConfig(): void $provider = ExternalIdentity::query()->first(); expect($provider)->not->toBeNull(); expect($provider->provider->value)->toBe('discord'); - expect($provider->provider_id)->toBe('1234567890'); + expect($provider->external_account_id)->toBe('1234567890'); expect($provider->user->is($user))->toBeTrue(); - // Token created for new provider + // Credentials stored inline (no more separate tokens table) $provider->refresh(); - expect($provider->tokens()->count())->toBe(1); - $token = $provider->tokens()->first(); - expect($token->access_token)->toBe('discord_access_token_123'); - expect($token->refresh_token)->toBe('discord_refresh_token_456'); + expect($provider->credentials)->toBeInstanceOf(ClientAccessManager::class); + expect($provider->credentials->getAccessToken())->toBe('discord_access_token_123'); + expect($provider->credentials->getRefreshToken())->toBe('discord_refresh_token_456'); }); it('authenticates an existing provider without duplicating records', function (): void { @@ -92,8 +92,8 @@ function setDiscordConfig(): void 'model_type' => User::class, 'model_id' => $existingUser->getKey(), 'provider' => 'discord', - 'provider_id' => '777777', - 'email' => 'existing@example.test', + 'external_account_id' => '777777', + 'metadata' => ['email' => 'existing@example.test'], ]); $initialProviders = ExternalIdentity::query()->count(); @@ -109,8 +109,7 @@ function setDiscordConfig(): void // No duplicate providers expect(ExternalIdentity::query()->count())->toBe($initialProviders); - // By current implementation, tokens are only created on new registrations - // Ensure no new tokens were created for the existing provider + // Credentials should be updated for existing provider (updateOrCreate) $existingProvider->refresh(); - expect($existingProvider->tokens()->count())->toBe(0); + expect($existingProvider->credentials)->toBeInstanceOf(ClientAccessManager::class); }); diff --git a/app-modules/identity/tests/Feature/ExternalIdentity/NewAccountByProviderTest.php b/app-modules/identity/tests/Feature/ExternalIdentity/NewAccountByProviderTest.php index c0ec92dc..a2c9b535 100644 --- a/app-modules/identity/tests/Feature/ExternalIdentity/NewAccountByProviderTest.php +++ b/app-modules/identity/tests/Feature/ExternalIdentity/NewAccountByProviderTest.php @@ -9,7 +9,7 @@ test('can create account by provider', function (): void { $provider = 'discord'; $payload = [ - 'provider_id' => '184789120940244992', + 'external_account_id' => '184789120940244992', 'username' => 'danielhe4rt', ]; @@ -28,9 +28,9 @@ 'username' => $payload['username'], ]); - $this->assertDatabaseHas('providers', [ + $this->assertDatabaseHas('external_identities', [ 'provider' => $provider, - 'provider_id' => $payload['provider_id'], + 'external_account_id' => $payload['external_account_id'], ]); $this->assertDatabaseHas('characters', [ @@ -41,11 +41,11 @@ test('should not create account with a registered provider', function (): void { $provider = ExternalIdentity::factory()->create([ 'provider' => 'discord', - 'provider_id' => '123', + 'external_account_id' => '123', ]); $payload = [ - 'provider_id' => $provider->provider_id, + 'external_account_id' => $provider->external_account_id, 'username' => 'danielhe4rt', ]; diff --git a/app-modules/identity/tests/Feature/FindProfileTest.php b/app-modules/identity/tests/Feature/FindProfileTest.php index 022ac309..fcd1ffe1 100644 --- a/app-modules/identity/tests/Feature/FindProfileTest.php +++ b/app-modules/identity/tests/Feature/FindProfileTest.php @@ -67,7 +67,7 @@ $this ->actingAsAdmin() - ->getJson(route('users.profile', ['value' => $user->providers[0]->provider_id])) + ->getJson(route('users.profile', ['value' => $user->providers[0]->external_account_id])) ->assertStatus(Response::HTTP_OK) ->assertJsonStructure([ 'id', diff --git a/app/Console/Commands/AuditMorphColumns.php b/app/Console/Commands/AuditMorphColumns.php new file mode 100644 index 00000000..82479f9b --- /dev/null +++ b/app/Console/Commands/AuditMorphColumns.php @@ -0,0 +1,93 @@ + 'model_type', + 'wallets' => 'owner_type', + 'transactions' => 'reference_type', + 'events_agenda' => 'schedulable_type', + 'media' => 'model_type', + 'notifications' => 'notifiable_type', + 'personal_access_tokens' => 'tokenable_type', + ]; + protected $signature = 'morph:audit'; + + protected $description = 'Audit all polymorphic *_type columns and report distinct values'; + + public function handle(): void + { + intro('Polymorphic Column Audit'); + + $allRows = []; + + foreach (self::MORPH_TABLES as $tableName => $column) { + if (! Schema::hasTable($tableName)) { + $allRows[] = [$tableName, $column, '(table not found)', '-']; + + continue; + } + + if (! Schema::hasColumn($tableName, $column)) { + $allRows[] = [$tableName, $column, '(column not found)', '-']; + + continue; + } + + $distinctValues = DB::table($tableName) + ->select($column, DB::raw('COUNT(*) as cnt')) + ->groupBy($column) + ->orderByDesc('cnt') + ->get(); + + if ($distinctValues->isEmpty()) { + $allRows[] = [$tableName, $column, '(empty table)', '0']; + + continue; + } + + foreach ($distinctValues as $row) { + $value = $row->{$column} ?? '(NULL)'; + $allRows[] = [$tableName, $column, $value, number_format($row->cnt)]; + } + } + + table( + headers: ['Table', 'Column', 'Distinct Value', 'Count'], + rows: $allRows, + ); + + // Summary + $fqcnCount = 0; + $aliasCount = 0; + foreach ($allRows as $row) { + if (str_contains($row[2], '\\')) { + $fqcnCount++; + } elseif (! str_starts_with($row[2], '(')) { + $aliasCount++; + } + } + + info(sprintf('FQCN entries: %d | Alias entries: %d', $fqcnCount, $aliasCount)); + + if ($fqcnCount > 0) { + warning('Database contains FQCN values that need migration to aliases.'); + } else { + outro('All morph columns use aliases. Ready for enforceMorphMap.'); + } + } +} diff --git a/app/Console/Commands/GenerateDiscordTenant.php b/app/Console/Commands/GenerateDiscordTenant.php index 29532fc9..8dcdaffe 100644 --- a/app/Console/Commands/GenerateDiscordTenant.php +++ b/app/Console/Commands/GenerateDiscordTenant.php @@ -4,7 +4,10 @@ namespace App\Console\Commands; +use He4rt\Identity\ExternalIdentity\Data\ClientAccessManager; +use He4rt\Identity\ExternalIdentity\Enums\CredentialsType; use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider; +use He4rt\Identity\ExternalIdentity\Enums\IdentityType; use He4rt\Identity\Tenant\Models\Tenant; use Illuminate\Console\Command; @@ -35,8 +38,12 @@ public function handle(): void ->providers() ->create([ 'tenant_id' => $tenant->getKey(), + 'type' => IdentityType::External, 'provider' => IdentityProvider::Discord, - 'provider_id' => $this->argument('guildId'), + 'credentials_type' => CredentialsType::OAuth2, + 'credentials' => ClientAccessManager::make(), + 'external_account_id' => $this->argument('guildId'), + 'connected_at' => now(), ]); }) ->create(); diff --git a/app/Http/Middleware/VerifyIfHasTenantProviderMiddleware.php b/app/Http/Middleware/VerifyIfHasTenantProviderMiddleware.php index 69f73489..c30a8950 100644 --- a/app/Http/Middleware/VerifyIfHasTenantProviderMiddleware.php +++ b/app/Http/Middleware/VerifyIfHasTenantProviderMiddleware.php @@ -29,7 +29,7 @@ public function handle(Request $request, Closure $next): mixed $str = sprintf('tenant_provider_%s', $providerId); $providerModel = Cache::flexible($str, [1, 2], fn () => ExternalIdentity::query() - ->where('provider_id', $providerId) + ->where('external_account_id', $providerId) ->where('provider', $provider) ->first() ); diff --git a/tests/TestCase.php b/tests/TestCase.php index 85546d47..37c87ce5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -19,7 +19,7 @@ protected function actingAsAdmin(): self ExternalIdentity::factory([ 'tenant_id' => $tenant->getKey(), 'provider' => 'discord', - 'provider_id' => '123', + 'external_account_id' => '123', ])->create(); }) ->create(); From 4d1f132bd1df976b50b482f8c77e89db385432ea Mon Sep 17 00:00:00 2001 From: danielhe4rt Date: Sat, 21 Mar 2026 16:11:33 -0300 Subject: [PATCH 2/2] fix: rector --- app/Console/Commands/AuditMorphColumns.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Console/Commands/AuditMorphColumns.php b/app/Console/Commands/AuditMorphColumns.php index 82479f9b..fc7d56f6 100644 --- a/app/Console/Commands/AuditMorphColumns.php +++ b/app/Console/Commands/AuditMorphColumns.php @@ -25,6 +25,7 @@ class AuditMorphColumns extends Command 'notifications' => 'notifiable_type', 'personal_access_tokens' => 'tokenable_type', ]; + protected $signature = 'morph:audit'; protected $description = 'Audit all polymorphic *_type columns and report distinct values';