Laravel-based OAuth/IAM service with Passport, Vue 3 frontend, and Docker Compose setup for local and production deployment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
171 lines
7.0 KiB
PHP
171 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Models\LoginEvent;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
use Illuminate\Database\Seeder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Passport\Client;
|
|
use Laravel\Passport\ClientRepository;
|
|
|
|
class DatabaseSeeder extends Seeder
|
|
{
|
|
use WithoutModelEvents;
|
|
|
|
public function run(ClientRepository $clients): void
|
|
{
|
|
$this->call(RolesAndPermissionsSeeder::class);
|
|
|
|
$adminUser = User::firstOrCreate(
|
|
['email' => 'admin@example.com'],
|
|
['name' => 'Admin', 'password' => Hash::make('password'), 'approved_at' => now()]
|
|
);
|
|
$adminUser->syncRoles(['admin']);
|
|
|
|
// Usuarios extra solo para que las analíticas (sección "usuarios más activos")
|
|
// tengan algo interesante que mostrar de entrada.
|
|
$extraUsers = collect([
|
|
['name' => 'Carla Gómez', 'email' => 'carla@example.com'],
|
|
['name' => 'Martín Ruiz', 'email' => 'martin@example.com'],
|
|
['name' => 'Bea Fernández', 'email' => 'bea@example.com'],
|
|
])->map(function (array $data) {
|
|
$u = User::firstOrCreate(['email' => $data['email']], ['name' => $data['name'], 'password' => Hash::make('password'), 'approved_at' => now()]);
|
|
$u->syncRoles(['user']);
|
|
|
|
return $u;
|
|
});
|
|
|
|
// Usuario de ejemplo que se autorregistró y todavía nadie aprobó, para
|
|
// que la pestaña "Pendientes" del panel de usuarios no arranque vacía.
|
|
User::firstOrCreate(
|
|
['email' => 'nuevo@example.com'],
|
|
['name' => 'Nuevo Registro', 'password' => Hash::make('password'), 'approved_at' => null]
|
|
)->assignRole('user');
|
|
|
|
// Cliente requerido para poder emitir Personal Access Tokens (grant "personal_access").
|
|
if (! Client::where('personal_access_client', true)->exists()) {
|
|
$clients->createPersonalAccessGrantClient('Personal Access Client');
|
|
}
|
|
|
|
// Cliente de demo para el flujo Authorization Code + PKCE (público, sin secret).
|
|
$pkceClient = $adminUser->oauthApps()->firstOrCreate(
|
|
['name' => 'Demo SPA (Authorization Code + PKCE)'],
|
|
[
|
|
'secret' => null,
|
|
'redirect_uris' => ['http://localhost:8000/callback'],
|
|
'grant_types' => ['authorization_code', 'refresh_token'],
|
|
'revoked' => false,
|
|
]
|
|
);
|
|
|
|
// Cliente de demo para Client Credentials (machine-to-machine).
|
|
$ccClient = $adminUser->oauthApps()->firstOrCreate(
|
|
['name' => 'Demo Service (Client Credentials)'],
|
|
[
|
|
'secret' => Str::random(40),
|
|
'redirect_uris' => [],
|
|
'grant_types' => ['client_credentials'],
|
|
'revoked' => false,
|
|
]
|
|
);
|
|
|
|
// Cliente de demo para Password Grant.
|
|
$pwClient = $adminUser->oauthApps()->firstOrCreate(
|
|
['name' => 'Demo First-Party App (Password Grant)'],
|
|
[
|
|
'secret' => Str::random(40),
|
|
'redirect_uris' => [],
|
|
'grant_types' => ['password', 'refresh_token'],
|
|
'revoked' => false,
|
|
]
|
|
);
|
|
|
|
// Cliente confidencial para probar el botón "Authorize" de Swagger UI
|
|
// (usa /api/oauth2-callback, que ya registra L5-Swagger).
|
|
$swaggerClient = $adminUser->oauthApps()->firstOrCreate(
|
|
['name' => 'Swagger UI'],
|
|
[
|
|
'secret' => Str::random(40),
|
|
'redirect_uris' => [rtrim(config('app.url'), '/').'/api/oauth2-callback'],
|
|
'grant_types' => ['authorization_code', 'client_credentials', 'refresh_token'],
|
|
'revoked' => false,
|
|
]
|
|
);
|
|
|
|
// Rol por-app de ejemplo: carla@example.com es "admin" dentro de la app
|
|
// "Demo SPA", aunque globalmente sea un usuario normal. Muestra que el
|
|
// rol puede variar según qué app pregunte por el usuario.
|
|
DB::table('app_user_roles')->insertOrIgnore([
|
|
'user_id' => $extraUsers->first()->id,
|
|
'client_id' => $pkceClient->id,
|
|
'role' => 'admin',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
if (! LoginEvent::query()->exists()) {
|
|
$this->seedLoginEvents(
|
|
$extraUsers->push($adminUser),
|
|
collect([$pkceClient, $ccClient, $pwClient, $swaggerClient]),
|
|
);
|
|
}
|
|
|
|
$this->command?->info('Admin: admin@example.com / password');
|
|
$this->command?->info('Client Credentials -> client_id: '.$ccClient->id.' secret: '.($ccClient->plainSecret ?? '(ya existía, ver DB)'));
|
|
$this->command?->info('Password Grant -> client_id: '.$pwClient->id.' secret: '.($pwClient->plainSecret ?? '(ya existía, ver DB)'));
|
|
$this->command?->info('Swagger UI -> client_id: '.$swaggerClient->id.' secret: '.($swaggerClient->plainSecret ?? '(ya existía, ver DB)'));
|
|
}
|
|
|
|
/**
|
|
* Historial sintético de logins (últimos 30 días) para que la pantalla de
|
|
* analíticas tenga algo real que graficar apenas se levanta la app.
|
|
*
|
|
* @param \Illuminate\Support\Collection<int, User> $users
|
|
* @param \Illuminate\Support\Collection<int, Client> $clients
|
|
*/
|
|
private function seedLoginEvents($users, $clients): void
|
|
{
|
|
$rows = [];
|
|
|
|
$now = now();
|
|
|
|
for ($daysAgo = 29; $daysAgo >= 0; $daysAgo--) {
|
|
$dayStart = $now->copy()->subDays($daysAgo)->startOfDay();
|
|
$maxMinute = $daysAgo === 0 ? $now->diffInMinutes($dayStart) : 1439;
|
|
$isWeekend = $dayStart->isWeekend();
|
|
$oauthLogins = random_int($isWeekend ? 1 : 4, $isWeekend ? 6 : 14);
|
|
$sessionLogins = random_int($isWeekend ? 0 : 1, $isWeekend ? 2 : 5);
|
|
|
|
for ($i = 0; $i < $oauthLogins; $i++) {
|
|
$rows[] = [
|
|
'user_id' => $users->random()->id,
|
|
'client_id' => $clients->random()->id,
|
|
'type' => 'oauth',
|
|
'ip_address' => '10.0.'.random_int(0, 9).'.'.random_int(2, 254),
|
|
'user_agent' => 'Mozilla/5.0 (demo seed)',
|
|
'created_at' => $dayStart->copy()->addMinutes(random_int(0, max(0, $maxMinute))),
|
|
];
|
|
}
|
|
|
|
for ($i = 0; $i < $sessionLogins; $i++) {
|
|
$rows[] = [
|
|
'user_id' => $users->random()->id,
|
|
'client_id' => null,
|
|
'type' => 'session',
|
|
'ip_address' => '10.0.'.random_int(0, 9).'.'.random_int(2, 254),
|
|
'user_agent' => 'Mozilla/5.0 (demo seed)',
|
|
'created_at' => $dayStart->copy()->addMinutes(random_int(0, max(0, $maxMinute))),
|
|
];
|
|
}
|
|
}
|
|
|
|
foreach (array_chunk($rows, 200) as $chunk) {
|
|
LoginEvent::insert($chunk);
|
|
}
|
|
}
|
|
}
|