diff --git a/app/Console/Commands/RemoveExpiredDiscordRoles.php b/app/Console/Commands/RemoveExpiredDiscordRoles.php new file mode 100644 index 00000000..c1dbf304 --- /dev/null +++ b/app/Console/Commands/RemoveExpiredDiscordRoles.php @@ -0,0 +1,46 @@ +whereNotNull('discord_role_granted_at') + ->whereNotNull('discord_id') + ->get(); + + foreach ($users as $user) { + if (! $user->hasMaxAccess()) { + $success = $discord->removeMaxRole($user->discord_id); + + if ($success) { + $user->update([ + 'discord_role_granted_at' => null, + ]); + + $this->info("Removed Discord role for user: {$user->email} ({$user->discord_username})"); + $removed++; + } else { + $this->error("Failed to remove Discord role for user: {$user->email} ({$user->discord_username})"); + } + } + } + + $this->info("Total users with Discord role removed: {$removed}"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/RemoveExpiredGitHubAccess.php b/app/Console/Commands/RemoveExpiredGitHubAccess.php index afbb166b..6483f4b5 100644 --- a/app/Console/Commands/RemoveExpiredGitHubAccess.php +++ b/app/Console/Commands/RemoveExpiredGitHubAccess.php @@ -24,8 +24,8 @@ public function handle(): int ->get(); foreach ($users as $user) { - // Check if user still has an active Max license - if (! $user->hasActiveMaxLicense()) { + // Check if user still has Max access (direct or sub-license) + if (! $user->hasMaxAccess()) { // Remove from repository $success = $github->removeFromMobileRepo($user->github_username); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c9e58a9c..50fc8e89 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -23,6 +23,12 @@ protected function schedule(Schedule $schedule): void ->dailyAt('10:00') ->onOneServer() ->runInBackground(); + + // Remove Discord Max role for users with expired Max licenses + $schedule->command('discord:remove-expired-roles') + ->dailyAt('10:30') + ->onOneServer() + ->runInBackground(); } /** diff --git a/app/Http/Controllers/Auth/CustomerAuthController.php b/app/Http/Controllers/Auth/CustomerAuthController.php index cc98b7af..7cb45557 100644 --- a/app/Http/Controllers/Auth/CustomerAuthController.php +++ b/app/Http/Controllers/Auth/CustomerAuthController.php @@ -4,9 +4,11 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Auth\LoginRequest; +use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Illuminate\View\View; class CustomerAuthController extends Controller @@ -16,6 +18,30 @@ public function showLogin(): View return view('auth.login'); } + public function showRegister(): View + { + return view('auth.register'); + } + + public function register(Request $request): RedirectResponse + { + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email:rfc,dns', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + Auth::login($user); + + return redirect()->route('customer.licenses'); + } + public function login(LoginRequest $request): RedirectResponse { $request->authenticate(); diff --git a/app/Http/Controllers/CustomerLicenseController.php b/app/Http/Controllers/CustomerLicenseController.php index 01b3f1dc..a7571ac4 100644 --- a/app/Http/Controllers/CustomerLicenseController.php +++ b/app/Http/Controllers/CustomerLicenseController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Models\SubLicense; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -19,7 +20,17 @@ public function index(): View $user = Auth::user(); $licenses = $user->licenses()->orderBy('created_at', 'desc')->get(); - return view('customer.licenses.index', compact('licenses')); + // Fetch sub-licenses assigned to this user's email (excluding those from licenses they own) + $assignedSubLicenses = SubLicense::query() + ->with('parentLicense') + ->where('assigned_email', $user->email) + ->whereHas('parentLicense', function ($query) use ($user) { + $query->where('user_id', '!=', $user->id); + }) + ->orderBy('created_at', 'desc') + ->get(); + + return view('customer.licenses.index', compact('licenses', 'assignedSubLicenses')); } public function show(string $licenseKey): View diff --git a/app/Http/Controllers/CustomerSubLicenseController.php b/app/Http/Controllers/CustomerSubLicenseController.php index 85c1881f..c103b81a 100644 --- a/app/Http/Controllers/CustomerSubLicenseController.php +++ b/app/Http/Controllers/CustomerSubLicenseController.php @@ -6,6 +6,7 @@ use App\Actions\SubLicenses\SuspendSubLicense; use App\Http\Requests\CreateSubLicenseRequest; use App\Jobs\CreateAnystackSubLicenseJob; +use App\Jobs\RevokeMaxAccessJob; use App\Jobs\UpdateAnystackContactAssociationJob; use App\Models\License; use App\Models\SubLicense; @@ -68,6 +69,11 @@ public function update(Request $request, string $licenseKey, SubLicense $subLice UpdateAnystackContactAssociationJob::dispatch($subLicense, $request->assigned_email); } + // If the email was changed and this is a Max license, revoke access for the old email + if ($oldEmail && $oldEmail !== $request->assigned_email && $license->policy_name === 'max') { + RevokeMaxAccessJob::dispatch($oldEmail); + } + return redirect()->route('customer.licenses.show', $licenseKey) ->with('success', 'Sub-license updated successfully!'); } @@ -82,8 +88,15 @@ public function destroy(string $licenseKey, SubLicense $subLicense): RedirectRes abort(404); } + $assignedEmail = $subLicense->assigned_email; + app(DeleteSubLicense::class)->handle($subLicense); + // If this was a Max license and had an assigned email, revoke access + if ($assignedEmail && $license->policy_name === 'max') { + RevokeMaxAccessJob::dispatch($assignedEmail); + } + return redirect()->route('customer.licenses.show', $licenseKey) ->with('success', 'Sub-license deleted successfully!'); } @@ -98,8 +111,15 @@ public function suspend(string $licenseKey, SubLicense $subLicense): RedirectRes abort(404); } + $assignedEmail = $subLicense->assigned_email; + app(SuspendSubLicense::class)->handle($subLicense); + // If this was a Max license and had an assigned email, revoke access + if ($assignedEmail && $license->policy_name === 'max') { + RevokeMaxAccessJob::dispatch($assignedEmail); + } + return redirect()->route('customer.licenses.show', $licenseKey) ->with('success', 'Sub-license suspended successfully!'); } diff --git a/app/Http/Controllers/DiscordIntegrationController.php b/app/Http/Controllers/DiscordIntegrationController.php new file mode 100644 index 00000000..edbd7323 --- /dev/null +++ b/app/Http/Controllers/DiscordIntegrationController.php @@ -0,0 +1,122 @@ +middleware('auth'); + } + + public function redirectToDiscord(): RedirectResponse + { + $params = http_build_query([ + 'client_id' => config('services.discord.client_id'), + 'redirect_uri' => config('services.discord.redirect'), + 'response_type' => 'code', + 'scope' => 'identify', + ]); + + return redirect('https://discord.com/api/oauth2/authorize?'.$params); + } + + public function handleCallback(): RedirectResponse + { + $code = request('code'); + + if (! $code) { + return redirect()->route('customer.integrations') + ->with('error', 'Discord authorization was cancelled.'); + } + + try { + $tokenResponse = Http::asForm()->post('https://discord.com/api/oauth2/token', [ + 'client_id' => config('services.discord.client_id'), + 'client_secret' => config('services.discord.client_secret'), + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => config('services.discord.redirect'), + ]); + + if ($tokenResponse->failed()) { + throw new \Exception('Failed to exchange code for token'); + } + + $accessToken = $tokenResponse->json('access_token'); + + $userResponse = Http::withToken($accessToken) + ->get('https://discord.com/api/v10/users/@me'); + + if ($userResponse->failed()) { + throw new \Exception('Failed to fetch Discord user'); + } + + $discordUser = $userResponse->json(); + + $user = Auth::user(); + $user->update([ + 'discord_id' => $discordUser['id'], + 'discord_username' => $discordUser['username'], + ]); + + $discord = DiscordApi::make(); + + if (! $discord->isGuildMember($discordUser['id'])) { + return redirect()->route('customer.integrations') + ->with('warning', 'Discord account connected! Please join the NativePHP Discord server to receive the Max role.'); + } + + if ($user->hasMaxAccess()) { + $success = $discord->assignMaxRole($discordUser['id']); + + if ($success) { + $user->update([ + 'discord_role_granted_at' => now(), + ]); + + return redirect()->route('customer.integrations') + ->with('success', 'Discord account connected and Max role assigned!'); + } + + return redirect()->route('customer.integrations') + ->with('warning', 'Discord account connected, but we could not assign the Max role. Please try again later.'); + } + + return redirect()->route('customer.integrations') + ->with('success', 'Discord account connected successfully!'); + } catch (\Exception $e) { + Log::error('Discord OAuth callback failed', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return redirect()->route('customer.integrations') + ->with('error', 'Failed to connect Discord account. Please try again.'); + } + } + + public function disconnect(): RedirectResponse + { + $user = Auth::user(); + + if ($user->discord_role_granted_at && $user->discord_id) { + $discord = DiscordApi::make(); + $discord->removeMaxRole($user->discord_id); + } + + $user->update([ + 'discord_id' => null, + 'discord_username' => null, + 'discord_role_granted_at' => null, + ]); + + return back()->with('success', 'Discord account disconnected successfully.'); + } +} diff --git a/app/Http/Controllers/GitHubIntegrationController.php b/app/Http/Controllers/GitHubIntegrationController.php index 5a6fd20f..da929e9b 100644 --- a/app/Http/Controllers/GitHubIntegrationController.php +++ b/app/Http/Controllers/GitHubIntegrationController.php @@ -54,7 +54,7 @@ public function requestRepoAccess(): RedirectResponse return back()->with('error', 'Please connect your GitHub account first.'); } - if (! $user->hasActiveMaxLicense()) { + if (! $user->hasMaxAccess()) { return back()->with('error', 'You need an active Max license to access the mobile repository.'); } diff --git a/app/Jobs/AssignDiscordMaxRoleJob.php b/app/Jobs/AssignDiscordMaxRoleJob.php new file mode 100644 index 00000000..139dcc25 --- /dev/null +++ b/app/Jobs/AssignDiscordMaxRoleJob.php @@ -0,0 +1,69 @@ +user->discord_id) { + Log::info('Skipping Discord role assignment - user has no Discord connected', [ + 'user_id' => $this->user->id, + ]); + + return; + } + + if (! $this->user->hasMaxAccess()) { + Log::info('Skipping Discord role assignment - user has no Max access', [ + 'user_id' => $this->user->id, + ]); + + return; + } + + $discord = DiscordApi::make(); + + if (! $discord->isGuildMember($this->user->discord_id)) { + Log::info('Skipping Discord role assignment - user is not in guild', [ + 'user_id' => $this->user->id, + 'discord_id' => $this->user->discord_id, + ]); + + return; + } + + $success = $discord->assignMaxRole($this->user->discord_id); + + if ($success) { + $this->user->update(['discord_role_granted_at' => now()]); + + Log::info('Discord Max role assigned successfully', [ + 'user_id' => $this->user->id, + 'discord_id' => $this->user->discord_id, + ]); + } else { + Log::error('Failed to assign Discord Max role', [ + 'user_id' => $this->user->id, + 'discord_id' => $this->user->discord_id, + ]); + } + } +} diff --git a/app/Jobs/RemoveDiscordMaxRoleJob.php b/app/Jobs/RemoveDiscordMaxRoleJob.php new file mode 100644 index 00000000..1cce367d --- /dev/null +++ b/app/Jobs/RemoveDiscordMaxRoleJob.php @@ -0,0 +1,52 @@ +user->discord_id) { + Log::info('Skipping Discord role removal - user has no Discord connected', [ + 'user_id' => $this->user->id, + ]); + + return; + } + + $discord = DiscordApi::make(); + + $success = $discord->removeMaxRole($this->user->discord_id); + + if ($success) { + $this->user->update(['discord_role_granted_at' => null]); + + Log::info('Discord Max role removed successfully', [ + 'user_id' => $this->user->id, + 'discord_id' => $this->user->discord_id, + ]); + } else { + Log::warning('Failed to remove Discord Max role (user may not have role)', [ + 'user_id' => $this->user->id, + 'discord_id' => $this->user->discord_id, + ]); + } + } +} diff --git a/app/Jobs/RevokeMaxAccessJob.php b/app/Jobs/RevokeMaxAccessJob.php new file mode 100644 index 00000000..b08d0686 --- /dev/null +++ b/app/Jobs/RevokeMaxAccessJob.php @@ -0,0 +1,101 @@ +email)->first(); + + if (! $user) { + Log::info('Skipping access revocation - no user found for email', [ + 'email' => $this->email, + ]); + + return; + } + + // Check if user still has Max access through another license or sub-license + if ($user->hasMaxAccess()) { + Log::info('Skipping access revocation - user still has Max access', [ + 'user_id' => $user->id, + 'email' => $this->email, + ]); + + return; + } + + // Revoke GitHub access + $this->revokeGitHubAccess($user); + + // Revoke Discord role + $this->revokeDiscordRole($user); + } + + private function revokeGitHubAccess(User $user): void + { + if (! $user->github_username || ! $user->mobile_repo_access_granted_at) { + return; + } + + $github = GitHubOAuth::make(); + $success = $github->removeFromMobileRepo($user->github_username); + + if ($success) { + $user->update(['mobile_repo_access_granted_at' => null]); + + Log::info('GitHub access revoked for user', [ + 'user_id' => $user->id, + 'github_username' => $user->github_username, + ]); + } else { + Log::warning('Failed to revoke GitHub access for user', [ + 'user_id' => $user->id, + 'github_username' => $user->github_username, + ]); + } + } + + private function revokeDiscordRole(User $user): void + { + if (! $user->discord_id || ! $user->discord_role_granted_at) { + return; + } + + $discord = DiscordApi::make(); + $success = $discord->removeMaxRole($user->discord_id); + + if ($success) { + $user->update(['discord_role_granted_at' => null]); + + Log::info('Discord Max role revoked for user', [ + 'user_id' => $user->id, + 'discord_id' => $user->discord_id, + ]); + } else { + Log::warning('Failed to revoke Discord Max role for user', [ + 'user_id' => $user->id, + 'discord_id' => $user->discord_id, + ]); + } + } +} diff --git a/app/Listeners/StripeWebhookReceivedListener.php b/app/Listeners/StripeWebhookReceivedListener.php index c8048bfe..efc0708e 100644 --- a/app/Listeners/StripeWebhookReceivedListener.php +++ b/app/Listeners/StripeWebhookReceivedListener.php @@ -4,6 +4,8 @@ use App\Jobs\CreateUserFromStripeCustomer; use App\Jobs\HandleInvoicePaidJob; +use App\Jobs\RemoveDiscordMaxRoleJob; +use App\Models\User; use Exception; use Illuminate\Support\Facades\Log; use Laravel\Cashier\Cashier; @@ -19,6 +21,8 @@ public function handle(WebhookReceived $event): void match ($event->payload['type']) { 'invoice.paid' => $this->handleInvoicePaid($event), 'customer.subscription.created' => $this->createUserIfNotExists($event->payload['data']['object']['customer']), + 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($event), + 'customer.subscription.updated' => $this->handleSubscriptionUpdated($event), default => null, }; } @@ -46,4 +50,49 @@ private function handleInvoicePaid(WebhookReceived $event): void dispatch(new HandleInvoicePaidJob($invoice)); } + + private function handleSubscriptionDeleted(WebhookReceived $event): void + { + $subscription = $event->payload['data']['object']; + $customerId = $subscription['customer']; + + $user = Cashier::findBillable($customerId); + + if (! $user instanceof User) { + return; + } + + $this->removeDiscordRoleIfNoMaxLicense($user); + } + + private function handleSubscriptionUpdated(WebhookReceived $event): void + { + $subscription = $event->payload['data']['object']; + $customerId = $subscription['customer']; + + $user = Cashier::findBillable($customerId); + + if (! $user instanceof User) { + return; + } + + $status = $subscription['status']; + + if (in_array($status, ['canceled', 'unpaid', 'past_due', 'incomplete_expired'])) { + $this->removeDiscordRoleIfNoMaxLicense($user); + } + } + + private function removeDiscordRoleIfNoMaxLicense(User $user): void + { + if (! $user->discord_id) { + return; + } + + if ($user->hasMaxAccess()) { + return; + } + + dispatch(new RemoveDiscordMaxRoleJob($user)); + } } diff --git a/app/Livewire/DiscordAccessBanner.php b/app/Livewire/DiscordAccessBanner.php new file mode 100644 index 00000000..dd882880 --- /dev/null +++ b/app/Livewire/DiscordAccessBanner.php @@ -0,0 +1,104 @@ +inline = $inline; + $this->checkRoleStatus(); + } + + public function checkRoleStatus(): void + { + $user = auth()->user(); + + if (! $user || ! $user->discord_id) { + $this->hasMaxRole = false; + $this->isGuildMember = false; + + return; + } + + $cacheKey = "discord_role_status_{$user->id}"; + + $status = Cache::remember($cacheKey, 300, function () use ($user) { + $discord = DiscordApi::make(); + + return [ + 'isGuildMember' => $discord->isGuildMember($user->discord_id), + 'hasMaxRole' => $discord->hasMaxRole($user->discord_id), + ]; + }); + + $this->isGuildMember = $status['isGuildMember']; + $this->hasMaxRole = $status['hasMaxRole']; + + if ($this->hasMaxRole && ! $user->discord_role_granted_at) { + $user->update(['discord_role_granted_at' => now()]); + } + } + + public function refreshStatus(): void + { + $user = auth()->user(); + + if ($user) { + Cache::forget("discord_role_status_{$user->id}"); + } + + $this->checkRoleStatus(); + } + + public function requestMaxRole(): void + { + $user = auth()->user(); + + if (! $user || ! $user->discord_id) { + session()->flash('error', 'Please connect your Discord account first.'); + + return; + } + + if (! $user->hasMaxAccess()) { + session()->flash('error', 'You need an active Max license to receive the Max role.'); + + return; + } + + $discord = DiscordApi::make(); + + if (! $discord->isGuildMember($user->discord_id)) { + session()->flash('error', 'Please join the NativePHP Discord server first.'); + + return; + } + + $success = $discord->assignMaxRole($user->discord_id); + + if ($success) { + $user->update(['discord_role_granted_at' => now()]); + Cache::forget("discord_role_status_{$user->id}"); + $this->checkRoleStatus(); + session()->flash('success', 'Max role assigned successfully!'); + } else { + session()->flash('error', 'Failed to assign Max role. Please try again later.'); + } + } + + public function render() + { + return view('livewire.discord-access-banner'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 4a138df0..bd2d710b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -28,6 +28,7 @@ class User extends Authenticatable implements FilamentUser 'email_verified_at' => 'datetime', 'password' => 'hashed', 'mobile_repo_access_granted_at' => 'datetime', + 'discord_role_granted_at' => 'datetime', ]; public function canAccessPanel(Panel $panel): bool @@ -65,6 +66,35 @@ public function hasActiveMaxLicense(): bool ->exists(); } + public function hasActiveMaxSubLicense(): bool + { + return SubLicense::query() + ->where('assigned_email', $this->email) + ->where('is_suspended', false) + ->whereActive() + ->whereHas('parentLicense', function ($query) { + $query->where('policy_name', 'max') + ->where('is_suspended', false) + ->whereActive(); + }) + ->exists(); + } + + public function hasMaxAccess(): bool + { + return $this->hasActiveMaxLicense() || $this->hasActiveMaxSubLicense(); + } + + public function hasDiscordConnected(): bool + { + return ! empty($this->discord_id); + } + + public function hasActualLicense(): bool + { + return $this->licenses()->exists(); + } + public function getFirstNameAttribute(): ?string { if (empty($this->name)) { diff --git a/app/Support/DiscordApi.php b/app/Support/DiscordApi.php new file mode 100644 index 00000000..367fa1e6 --- /dev/null +++ b/app/Support/DiscordApi.php @@ -0,0 +1,145 @@ +guildId, + $discordUserId + ); + + Log::debug('Checking Discord guild membership', [ + 'url' => $url, + 'guild_id' => $this->guildId, + 'discord_user_id' => $discordUserId, + ]); + + $response = Http::withToken($this->botToken, 'Bot')->get($url); + + if ($response->status() === 404) { + Log::info('Discord user not found in guild', [ + 'discord_user_id' => $discordUserId, + 'guild_id' => $this->guildId, + ]); + + return false; + } + + if ($response->failed()) { + Log::error('Failed to check Discord guild membership', [ + 'discord_user_id' => $discordUserId, + 'guild_id' => $this->guildId, + 'status' => $response->status(), + 'response' => $response->json(), + ]); + + return false; + } + + Log::info('Discord user is guild member', [ + 'discord_user_id' => $discordUserId, + 'guild_id' => $this->guildId, + ]); + + return true; + } + + public function assignMaxRole(string $discordUserId): bool + { + $response = Http::withToken($this->botToken, 'Bot') + ->put(sprintf( + '%s/guilds/%s/members/%s/roles/%s', + self::BASE_URL, + $this->guildId, + $discordUserId, + $this->maxRoleId + )); + + if ($response->failed()) { + Log::error('Failed to assign Discord Max role', [ + 'discord_user_id' => $discordUserId, + 'status' => $response->status(), + 'response' => $response->json(), + ]); + + return false; + } + + return true; + } + + public function removeMaxRole(string $discordUserId): bool + { + $response = Http::withToken($this->botToken, 'Bot') + ->delete(sprintf( + '%s/guilds/%s/members/%s/roles/%s', + self::BASE_URL, + $this->guildId, + $discordUserId, + $this->maxRoleId + )); + + if ($response->failed()) { + Log::error('Failed to remove Discord Max role', [ + 'discord_user_id' => $discordUserId, + 'status' => $response->status(), + 'response' => $response->json(), + ]); + + return false; + } + + return true; + } + + public function hasMaxRole(string $discordUserId): bool + { + $response = Http::withToken($this->botToken, 'Bot') + ->get(sprintf( + '%s/guilds/%s/members/%s', + self::BASE_URL, + $this->guildId, + $discordUserId + )); + + if ($response->failed()) { + Log::error('Failed to check Discord user roles', [ + 'discord_user_id' => $discordUserId, + 'status' => $response->status(), + 'response' => $response->json(), + ]); + + return false; + } + + $member = $response->json(); + $roles = $member['roles'] ?? []; + + return in_array($this->maxRoleId, $roles, true); + } +} diff --git a/config/services.php b/config/services.php index 805e88dd..2f9c0223 100644 --- a/config/services.php +++ b/config/services.php @@ -50,6 +50,15 @@ 'token' => env('GITHUB_TOKEN'), ], + 'discord' => [ + 'client_id' => env('DISCORD_CLIENT_ID'), + 'client_secret' => env('DISCORD_CLIENT_SECRET'), + 'redirect' => env('APP_URL').'/auth/discord/callback', + 'bot_token' => env('DISCORD_BOT_TOKEN'), + 'guild_id' => env('DISCORD_GUILD_ID'), + 'max_role_id' => env('DISCORD_MAX_ROLE_ID'), + ], + 'turnstile' => [ 'site_key' => env('TURNSTILE_SITE_KEY'), 'secret_key' => env('TURNSTILE_SECRET_KEY'), diff --git a/database/migrations/2025_12_21_161702_add_discord_fields_to_users_table.php b/database/migrations/2025_12_21_161702_add_discord_fields_to_users_table.php new file mode 100644 index 00000000..18e33bcd --- /dev/null +++ b/database/migrations/2025_12_21_161702_add_discord_fields_to_users_table.php @@ -0,0 +1,30 @@ +string('discord_id')->nullable()->after('mobile_repo_access_granted_at'); + $table->string('discord_username')->nullable()->after('discord_id'); + $table->timestamp('discord_role_granted_at')->nullable()->after('discord_username'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['discord_id', 'discord_username', 'discord_role_granted_at']); + }); + } +}; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 9364dac6..074f95dc 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -6,7 +6,10 @@ Sign in to your account
- Manage your NativePHP licenses + Or + + create a new account +
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php new file mode 100644 index 00000000..55a06908 --- /dev/null +++ b/resources/views/auth/register.blade.php @@ -0,0 +1,107 @@ ++ Already have an account? + + Sign in + +
++ Connect your accounts to unlock additional features +
+{{ session('success') }}
+{{ session('warning') }}
+{{ session('error') }}
+nativephp/mobile repository.+ Need help? Join our Discord community. +
+ @else ++ These integrations are available to Max license holders. Upgrade to Max to unlock these features. +
+ @endif ++ {{ $subLicense->parentLicense->policy_name ?? 'Sub-License' }} +
++ Sub-license +
++ {{ $subLicense->key }} +
++ @if($subLicense->expires_at) + Expires {{ $subLicense->expires_at->format('M j, Y') }} + @else + No expiration + @endif +
++ Assigned {{ $subLicense->created_at->format('M j, Y') }} +
+Connected as {{ auth()->user()->discord_username }}
+ + @if(!$isGuildMember) ++ + Not in Server + +
+ @elseif($hasMaxRole) ++ + Max Role Active + +
+ @elseif(auth()->user()->hasMaxAccess()) ++ + Eligible + +
+ @endif + @else +Connect your Discord account to receive the Max role.
+ @endif +nativephp/mobile Repo Access
- Connected as {{ '@' . auth()->user()->github_username }}
+nativephp/mobile Repo Access
+ Connected as {{ '@' . auth()->user()->github_username }}
- @if($collaboratorStatus === 'active') -- - Access Granted - -
-You have access to the nativephp/mobile repository.
- @elseif($collaboratorStatus === 'pending') -- - Invitation Pending - -
-Check your GitHub notifications to accept the invitation.
+ @if($collaboratorStatus === 'active') ++ + Access Granted + +
+ @elseif($collaboratorStatus === 'pending') ++ + Invitation Pending + +
+ @endif @else -Request access to the nativephp/mobile repository.
+Connect your GitHub account to access the nativephp/mobile repository.
@endif - @else -Connect your GitHub account to access the nativephp/mobile repository.
- @endif +