generated from thegrind/laravel-dockerized
Claude be codin
This commit is contained in:
parent
392d14e0e1
commit
038ee47fa3
42
app/Livewire/ManageAuthenticationTokens.php
Normal file
42
app/Livewire/ManageAuthenticationTokens.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\AuthenticationToken;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
||||
class ManageAuthenticationTokens extends Component
|
||||
{
|
||||
#[Computed]
|
||||
public function tokens()
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
return $user->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');
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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);
|
||||
|
114
database/factories/ApplicationFactory.php
Normal file
114
database/factories/ApplicationFactory.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Application>
|
||||
*/
|
||||
class ApplicationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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",
|
||||
];
|
||||
}
|
||||
}
|
86
database/seeders/AuthenticationTokenSeeder.php
Normal file
86
database/seeders/AuthenticationTokenSeeder.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\AuthenticationToken;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AuthenticationTokenSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$user = User::where('email', 'me@javierfeliz.com')->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}");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
16
database/seeders/UserSeeder.php
Normal file
16
database/seeders/UserSeeder.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void {}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
<div class="flex w-full flex-col gap-2 bg-white dark:bg-zinc-800 p-4">
|
||||
@props(['class' => ''])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'bg-white dark:bg-zinc-800 shadow-sm border border-gray-200 dark:border-zinc-700
|
||||
rounded-lg ' . $class]) }}>
|
||||
{{ $slot }}
|
||||
</div>
|
@ -3,6 +3,9 @@
|
||||
<div class="grid grid-cols-2">
|
||||
<livewire:forms.user-profile />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<livewire:manage-authentication-tokens />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<livewire:app-container />
|
||||
</div>
|
||||
|
114
resources/views/livewire/manage-authentication-tokens.blade.php
Normal file
114
resources/views/livewire/manage-authentication-tokens.blade.php
Normal file
@ -0,0 +1,114 @@
|
||||
<div>
|
||||
<x-card class="space-y-6 p-6">
|
||||
<flux:heading size="lg">Authentication Tokens</flux:heading>
|
||||
<flux:subheading>
|
||||
Manage your active authentication tokens for different applications.
|
||||
</flux:subheading>
|
||||
|
||||
@if($this->tokens->isEmpty())
|
||||
<div class="text-center py-12">
|
||||
<flux:icon.shield-check variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<flux:heading size="md" class="mt-4">No tokens</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
You haven't authorized any applications yet.
|
||||
</flux:subheading>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-6">
|
||||
@foreach($this->tokens as $appName => $appTokens)
|
||||
<div class="space-y-3" x-data="{ expanded: false }"
|
||||
wire:key="app-{{ $appTokens->first()->application_id }}">
|
||||
<div class="flex items-center space-x-3 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/50 p-2 rounded-lg transition-colors"
|
||||
x-on:click="expanded = !expanded">
|
||||
<div class="flex-shrink-0">
|
||||
@if($appTokens->first()->application->icon)
|
||||
<img class="h-8 w-8 rounded-lg object-cover"
|
||||
src="{{ $appTokens->first()->application->getIconUrl() }}" alt="{{ $appName }}"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="h-8 w-8 rounded-lg bg-zinc-100 dark:bg-zinc-700 flex items-center justify-center"
|
||||
style="display: none;">
|
||||
<flux:icon.computer-desktop class="h-5 w-5 text-zinc-500" />
|
||||
</div>
|
||||
@else
|
||||
<div class="h-8 w-8 rounded-lg bg-zinc-100 dark:bg-zinc-700 flex items-center justify-center">
|
||||
<flux:icon.computer-desktop class="h-5 w-5 text-zinc-500" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<flux:heading size="sm">{{ $appName }}</flux:heading>
|
||||
<flux:subheading size="xs">{{ $appTokens->count() }} {{ $appTokens->count() === 1 ? 'token' :
|
||||
'tokens' }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<flux:icon.chevron-down class="h-5 w-5 text-zinc-400 transition-transform duration-200"
|
||||
x-bind:class="expanded ? 'rotate-180' : ''" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="ml-11 space-y-2" x-show="expanded" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 transform translate-y-0"
|
||||
x-transition:leave-end="opacity-0 transform -translate-y-2">
|
||||
@foreach($appTokens as $token)
|
||||
<x-card class="p-4" wire:key="token-{{ $token->id }}">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="space-y-1">
|
||||
<flux:subheading size="xs">
|
||||
<flux:icon.calendar class="inline h-3 w-3 mr-1" />
|
||||
Issued: {{ $token->issued_at->format('M j, Y \a\t g:i A') }}
|
||||
</flux:subheading>
|
||||
@if($token->expires_at)
|
||||
<flux:subheading size="xs">
|
||||
<flux:icon.clock class="inline h-3 w-3 mr-1" />
|
||||
Expires: {{ $token->expires_at->format('M j, Y \a\t g:i A') }}
|
||||
</flux:subheading>
|
||||
@endif
|
||||
@if($token->ip)
|
||||
<flux:subheading size="xs">
|
||||
<flux:icon.globe-alt class="inline h-3 w-3 mr-1" />
|
||||
IP: {{ $token->ip }}
|
||||
</flux:subheading>
|
||||
@endif
|
||||
@if($token->user_agent)
|
||||
<flux:subheading size="xs" class="truncate">
|
||||
<flux:icon.device-phone-mobile class="inline h-3 w-3 mr-1" />
|
||||
<span title="{{ $token->user_agent }}">
|
||||
{{ strlen($token->user_agent) > 60 ? substr($token->user_agent, 0, 60) .
|
||||
'...' :
|
||||
$token->user_agent }}
|
||||
</span>
|
||||
</flux:subheading>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ml-4">
|
||||
<flux:button wire:click="revokeToken({{ $token->id }})" icon="trash"
|
||||
wire:confirm="Are you sure you want to revoke this token? The application will need to be re-authorized."
|
||||
variant="danger" size="sm">
|
||||
Revoke
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</x-card>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</x-card>
|
||||
|
||||
@script
|
||||
<script>
|
||||
$wire.on('token-revoked', () => {
|
||||
// You can add a toast notification here if you have one
|
||||
console.log('Token revoked successfully');
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user