Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/Console/Commands/RemoveExpiredDiscordRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Support\DiscordApi;
use Illuminate\Console\Command;

class RemoveExpiredDiscordRoles extends Command
{
protected $signature = 'discord:remove-expired-roles';

protected $description = 'Remove Discord Max role for users whose Max licenses have expired';

public function handle(): int
{
$discord = DiscordApi::make();
$removed = 0;

$users = User::query()
->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;
}
}
4 changes: 2 additions & 2 deletions app/Console/Commands/RemoveExpiredGitHubAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 6 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down
26 changes: 26 additions & 0 deletions app/Http/Controllers/Auth/CustomerAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
13 changes: 12 additions & 1 deletion app/Http/Controllers/CustomerLicenseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions app/Http/Controllers/CustomerSubLicenseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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!');
}
Expand All @@ -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!');
}
Expand All @@ -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!');
}
Expand Down
122 changes: 122 additions & 0 deletions app/Http/Controllers/DiscordIntegrationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

namespace App\Http\Controllers;

use App\Support\DiscordApi;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class DiscordIntegrationController extends Controller
{
public function __construct()
{
$this->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.');
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/GitHubIntegrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}

Expand Down
69 changes: 69 additions & 0 deletions app/Jobs/AssignDiscordMaxRoleJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace App\Jobs;

use App\Models\User;
use App\Support\DiscordApi;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class AssignDiscordMaxRoleJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public int $tries = 3;

public int $backoff = 60;

public function __construct(public User $user) {}

public function handle(): void
{
if (! $this->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,
]);
}
}
}
Loading