Skip to content
Open
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
50 changes: 50 additions & 0 deletions app/Console/Commands/RemoveExpiredGitHubAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Console\Commands;

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

class RemoveExpiredGitHubAccess extends Command
{
protected $signature = 'github:remove-expired-access';

protected $description = 'Remove GitHub repository access for users whose Max licenses have expired';

public function handle(): int
{
$github = GitHubOAuth::make();
$removed = 0;

// Find users with GitHub access granted
$users = User::query()
->whereNotNull('mobile_repo_access_granted_at')
->whereNotNull('github_username')
->get();

foreach ($users as $user) {
// Check if user still has an active Max license
if (! $user->hasActiveMaxLicense()) {
// Remove from repository
$success = $github->removeFromMobileRepo($user->github_username);

if ($success) {
// Clear the access timestamp
$user->update([
'mobile_repo_access_granted_at' => null,
]);

$this->info("Removed access for user: {$user->email} (@{$user->github_username})");
$removed++;
} else {
$this->error("Failed to remove access for user: {$user->email} (@{$user->github_username})");
}
}
}

$this->info("Total users with access removed: {$removed}");

return Command::SUCCESS;
}
}
6 changes: 6 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ protected function schedule(Schedule $schedule): void
->dailyAt('09:00')
->onOneServer()
->runInBackground();

// Remove GitHub access for users with expired Max licenses
$schedule->command('github:remove-expired-access')
->dailyAt('10:00')
->onOneServer()
->runInBackground();
}

/**
Expand Down
92 changes: 92 additions & 0 deletions app/Http/Controllers/GitHubIntegrationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace App\Http\Controllers;

use App\Support\GitHubOAuth;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Laravel\Socialite\Facades\Socialite;

class GitHubIntegrationController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}

public function redirectToGitHub(): RedirectResponse
{
return Socialite::driver('github')
->scopes(['read:user'])
->redirect();
}

public function handleCallback(): RedirectResponse
{
try {
$githubUser = Socialite::driver('github')->user();

$user = Auth::user();
$user->update([
'github_id' => $githubUser->id,
'github_username' => $githubUser->nickname,
]);

return redirect()->route('customer.licenses')
->with('success', 'GitHub account connected successfully!');
} catch (\Exception $e) {
Log::error('GitHub OAuth callback failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);

return redirect()->route('customer.licenses')
->with('error', 'Failed to connect GitHub account. Please try again.');
}
}

public function requestRepoAccess(): RedirectResponse
{
$user = Auth::user();

if (! $user->github_username) {
return back()->with('error', 'Please connect your GitHub account first.');
}

if (! $user->hasActiveMaxLicense()) {
return back()->with('error', 'You need an active Max license to access the mobile repository.');
}

$github = GitHubOAuth::make();
$success = $github->inviteToMobileRepo($user->github_username);

if ($success) {
$user->update([
'mobile_repo_access_granted_at' => now(),
]);

return back()->with('success', 'Repository invitation sent! Please check your GitHub notifications to accept the invitation.');
}

return back()->with('error', 'Failed to send repository invitation. Please try again or contact support.');
}

public function disconnect(): RedirectResponse
{
$user = Auth::user();

if ($user->mobile_repo_access_granted_at && $user->github_username) {
$github = GitHubOAuth::make();
$github->removeFromMobileRepo($user->github_username);
}

$user->update([
'github_id' => null,
'github_username' => null,
'mobile_repo_access_granted_at' => null,
]);

return back()->with('success', 'GitHub account disconnected successfully.');
}
}
10 changes: 10 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class User extends Authenticatable implements FilamentUser
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'mobile_repo_access_granted_at' => 'datetime',
];

public function canAccessPanel(Panel $panel): bool
Expand Down Expand Up @@ -55,6 +56,15 @@ public function wallOfLoveSubmissions(): HasMany
return $this->hasMany(WallOfLoveSubmission::class);
}

public function hasActiveMaxLicense(): bool
{
return $this->licenses()
->where('policy_name', 'max')
->where('is_suspended', false)
->whereActive()
->exists();
}

public function getFirstNameAttribute(): ?string
{
if (empty($this->name)) {
Expand Down
126 changes: 126 additions & 0 deletions app/Support/GitHubOAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace App\Support;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class GitHubOAuth
{
private const ORGANIZATION = 'nativephp';

private const REPOSITORY = 'mobile';

public function __construct(
private ?string $token
) {}

public static function make(): static
{
return new static(config('services.github.token') ?? '');
}

public function inviteToMobileRepo(string $githubUsername): bool
{
$response = Http::withToken($this->token)
->put(
sprintf(
'https://api.github.com/repos/%s/%s/collaborators/%s',
self::ORGANIZATION,
self::REPOSITORY,
$githubUsername
),
[
'permission' => 'pull', // Read-only access
]
);

if ($response->failed()) {
Log::error('Failed to invite user to GitHub repository', [
'username' => $githubUsername,
'status' => $response->status(),
'response' => $response->json(),
]);

return false;
}

return true;
}

public function removeFromMobileRepo(string $githubUsername): bool
{
$response = Http::withToken($this->token)
->delete(
sprintf(
'https://api.github.com/repos/%s/%s/collaborators/%s',
self::ORGANIZATION,
self::REPOSITORY,
$githubUsername
)
);

if ($response->failed()) {
Log::error('Failed to remove user from GitHub repository', [
'username' => $githubUsername,
'status' => $response->status(),
'response' => $response->json(),
]);

return false;
}

return true;
}

public function checkCollaboratorStatus(string $githubUsername): ?string
{
// First check if they're an active collaborator
$response = Http::withToken($this->token)
->get(
sprintf(
'https://api.github.com/repos/%s/%s/collaborators/%s',
self::ORGANIZATION,
self::REPOSITORY,
$githubUsername
)
);

if ($response->status() === 204) {
return 'active';
}

// Check for pending invitation
if ($this->hasPendingInvitation($githubUsername)) {
return 'pending';
}

if ($response->status() === 404) {
return null;
}

return 'unknown';
}

public function hasPendingInvitation(string $githubUsername): bool
{
$response = Http::withToken($this->token)
->get(
sprintf(
'https://api.github.com/repos/%s/%s/invitations',
self::ORGANIZATION,
self::REPOSITORY
)
);

if ($response->failed()) {
return false;
}

$invitations = $response->json();

return collect($invitations)->contains(function ($invitation) use ($githubUsername) {
return strtolower($invitation['invitee']['login'] ?? '') === strtolower($githubUsername);
});
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"laravel/framework": "^10.10",
"laravel/pennant": "^1.18",
"laravel/sanctum": "^3.3",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.8",
"league/commonmark": "^2.4",
"livewire/livewire": "^3.6.4",
Expand Down
Loading