From 038ee47fa3285d8c204c603ea6eaabe662d2ef85 Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Thu, 31 Jul 2025 00:56:12 -0400 Subject: [PATCH] Claude be codin --- app/Livewire/ManageAuthenticationTokens.php | 42 +++++++ app/Models/Application.php | 3 + app/Models/AuthenticationToken.php | 8 ++ database/factories/ApplicationFactory.php | 114 ++++++++++++++++++ .../seeders/AuthenticationTokenSeeder.php | 86 +++++++++++++ database/seeders/DatabaseSeeder.php | 17 ++- database/seeders/UserSeeder.php | 16 +++ resources/views/components/card.blade.php | 5 +- resources/views/dashboard.blade.php | 3 + .../manage-authentication-tokens.blade.php | 114 ++++++++++++++++++ 10 files changed, 402 insertions(+), 6 deletions(-) create mode 100644 app/Livewire/ManageAuthenticationTokens.php create mode 100644 database/factories/ApplicationFactory.php create mode 100644 database/seeders/AuthenticationTokenSeeder.php create mode 100644 database/seeders/UserSeeder.php create mode 100644 resources/views/livewire/manage-authentication-tokens.blade.php diff --git a/app/Livewire/ManageAuthenticationTokens.php b/app/Livewire/ManageAuthenticationTokens.php new file mode 100644 index 0000000..a7958c9 --- /dev/null +++ b/app/Livewire/ManageAuthenticationTokens.php @@ -0,0 +1,42 @@ +tokens() + ->with('application') + ->orderBy('issued_at', 'desc') + ->get() + ->groupBy('application.name') + ->sortKeys(); + } + + public function revokeToken($tokenId) + { + $token = AuthenticationToken::where('id', $tokenId) + ->where('user_id', Auth::id()) + ->first(); + + if ($token) { + $token->delete(); + $this->dispatch('token-revoked'); + } + } + + public function render() + { + return view('livewire.manage-authentication-tokens'); + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index 6673f38..56aa481 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -2,10 +2,13 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Application extends Model { + use HasFactory; + protected $guarded = ['id']; public function getIconUrl() diff --git a/app/Models/AuthenticationToken.php b/app/Models/AuthenticationToken.php index d209a2b..0cce833 100644 --- a/app/Models/AuthenticationToken.php +++ b/app/Models/AuthenticationToken.php @@ -9,6 +9,14 @@ class AuthenticationToken extends Model { protected $guarded = ['id']; + protected function casts(): array + { + return [ + 'issued_at' => 'datetime', + 'expires_at' => 'datetime', + ]; + } + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php new file mode 100644 index 0000000..250518c --- /dev/null +++ b/database/factories/ApplicationFactory.php @@ -0,0 +1,114 @@ + + */ +class ApplicationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + // Popular apps that likely have icons in selfh.st/icons + $apps = [ + 'Nextcloud', + 'Plex', + 'Jellyfin', + 'Home Assistant', + 'Grafana', + 'Portainer', + 'Sonarr', + 'Radarr', + 'Lidarr', + 'Bazarr', + 'Overseerr', + 'Tautulli', + 'Ombi', + 'Jackett', + 'qBittorrent', + 'Transmission', + 'SABnzbd', + 'NZBGet', + 'Synology', + 'FreeNAS', + 'UniFi', + 'pfSense', + 'OPNsense', + 'Pi-hole', + 'AdGuard', + 'Bitwarden', + 'Vaultwarden', + 'PhotoPrism', + 'Immich', + 'Paperless', + 'Bookstack', + 'TiddlyWiki', + 'Joplin', + 'Standard Notes', + 'Trilium', + 'Monica', + 'Firefly III', + 'Invoice Ninja', + 'Crater', + 'Akaunting', + 'Ghost', + 'WordPress', + 'Discourse', + 'Flarum', + 'NodeBB', + 'Rocket.Chat', + 'Mattermost', + 'Element', + 'Synapse', + 'Jitsi', + 'BigBlueButton', + 'Jami', + 'Mumble', + 'TeamSpeak', + 'Discord', + 'Gitea', + 'GitLab', + 'Forgejo', + 'Gogs', + 'Sourcehut', + 'Drone', + 'Jenkins', + 'Buildbot', + 'TeamCity', + 'Bamboo', + 'SonarQube', + 'Sentry', + 'Uptime Kuma', + 'Healthchecks', + 'Cachet', + 'Zabbix', + 'Nagios', + 'Icinga', + 'LibreNMS', + 'PRTG', + 'Proxmox', + 'ESXi', + 'XCP-ng', + 'Harvester', + 'Rancher' + ]; + + $appName = $this->faker->randomElement($apps); + $kebabName = str($appName)->kebab()->toString(); + + return [ + 'name' => $appName, + 'client_id' => $this->faker->uuid(), + 'client_secret' => $this->faker->regexify('[A-Za-z0-9]{40}'), + 'redirect_uri' => $this->faker->url() . '/auth/callback', + 'icon' => "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/{$kebabName}.webp", + ]; + } +} diff --git a/database/seeders/AuthenticationTokenSeeder.php b/database/seeders/AuthenticationTokenSeeder.php new file mode 100644 index 0000000..fd8ade5 --- /dev/null +++ b/database/seeders/AuthenticationTokenSeeder.php @@ -0,0 +1,86 @@ +first(); + + if (!$user) { + $this->command->error('Default user not found. Please run the main seeder first.'); + return; + } + + $applications = Application::all(); + + if ($applications->isEmpty()) { + $this->command->error('No applications found. Please run the application seeder first.'); + return; + } + + // Sample user agents from different browsers and devices + $userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1', + 'Mozilla/5.0 (Linux; Android 14; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.43 Mobile Safari/537.36', + ]; + + // Sample IP addresses (using private/local ranges for testing) + $ipAddresses = [ + '192.168.1.101', + '192.168.1.102', + '10.0.0.15', + '10.0.0.23', + '172.16.0.10', + '172.16.0.25', + '127.0.0.1', + '::1', + ]; + + // Create 10-15 tokens for different applications at different times + $tokenCount = rand(10, 15); + $selectedApps = $applications->random(min($tokenCount, $applications->count())); + + foreach ($selectedApps as $index => $app) { + // Create 1-3 tokens per app to simulate multiple sessions + $tokensPerApp = rand(1, 3); + + for ($i = 0; $i < $tokensPerApp; $i++) { + $issuedAt = now()->subDays(rand(0, 30))->subHours(rand(0, 23))->subMinutes(rand(0, 59)); + $expiresAt = $issuedAt->copy()->addDays(rand(7, 90)); // Tokens expire 7-90 days after issue + + AuthenticationToken::create([ + 'user_id' => $user->id, + 'application_id' => $app->id, + 'token' => Str::random(64), // Simulate an OAuth token + 'user_agent' => fake()->randomElement($userAgents), + 'ip' => fake()->randomElement($ipAddresses), + 'issued_at' => $issuedAt, + 'expires_at' => $expiresAt, + 'created_at' => $issuedAt, + 'updated_at' => $issuedAt, + ]); + } + } + + $this->command->info("Created authentication tokens for {$user->email}"); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index d01a0ef..738f52b 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,9 +2,11 @@ namespace Database\Seeders; +use App\Models\Application; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { @@ -13,11 +15,16 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); + // $user = User::create([ + // 'name' => 'Javier Feliz', + // 'email' => 'me@javierfeliz.com', + // 'password' => Hash::make('password') + // ]); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + // // Create 8 sample applications + // Application::factory(8)->create(); + + // Create authentication tokens for testing + $this->call(AuthenticationTokenSeeder::class); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..98acb56 --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,16 @@ + +@props(['class' => '']) + +
merge(['class' => 'bg-white dark:bg-zinc-800 shadow-sm border border-gray-200 dark:border-zinc-700 + rounded-lg ' . $class]) }}> {{ $slot }}
\ No newline at end of file diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 5c644a6..84f64f4 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -3,6 +3,9 @@
+
+ +
diff --git a/resources/views/livewire/manage-authentication-tokens.blade.php b/resources/views/livewire/manage-authentication-tokens.blade.php new file mode 100644 index 0000000..f267b4c --- /dev/null +++ b/resources/views/livewire/manage-authentication-tokens.blade.php @@ -0,0 +1,114 @@ +
+ + Authentication Tokens + + Manage your active authentication tokens for different applications. + + + @if($this->tokens->isEmpty()) +
+ + No tokens + + You haven't authorized any applications yet. + +
+ @else +
+ @foreach($this->tokens as $appName => $appTokens) +
+
+
+ @if($appTokens->first()->application->icon) + {{ $appName }} + + @else +
+ +
+ @endif +
+
+ {{ $appName }} + {{ $appTokens->count() }} {{ $appTokens->count() === 1 ? 'token' : + 'tokens' }} +
+
+ +
+
+ + +
+ @foreach($appTokens as $token) + +
+
+
+ + + Issued: {{ $token->issued_at->format('M j, Y \a\t g:i A') }} + + @if($token->expires_at) + + + Expires: {{ $token->expires_at->format('M j, Y \a\t g:i A') }} + + @endif + @if($token->ip) + + + IP: {{ $token->ip }} + + @endif + @if($token->user_agent) + + + + {{ strlen($token->user_agent) > 60 ? substr($token->user_agent, 0, 60) . + '...' : + $token->user_agent }} + + + @endif +
+
+
+ + Revoke + +
+
+
+ @endforeach +
+
+ @endforeach +
+ @endif +
+ + @script + + @endscript +
\ No newline at end of file