json([ 'empty' => User::query()->count() === 0, ]); } public function register(Request $request) { $data = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); // Instalación nueva (sin usuarios todavía): el primero en registrarse // queda como admin, aprobado y logueado de una — no hay nadie más que // pueda aprobarlo. De ahí en más, todo registro vuelve a requerir // aprobación de un admin como siempre. $isFirstUser = User::query()->count() === 0; $user = User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), 'is_active' => true, 'approved_at' => $isFirstUser ? now() : null, ]); if ($isFirstUser) { (new RolesAndPermissionsSeeder)->run(); $user->assignRole('admin'); Auth::login($user); $request->session()->regenerate(); $this->recordSessionLogin($request, $user); return response()->json([ 'bootstrap' => true, 'user' => $this->present($user), 'intended' => $request->session()->pull('url.intended'), ]); } $user->assignRole('user'); return response()->json([ 'pending' => true, 'message' => __('messages.registration_pending'), ], 201); } public function login(Request $request) { $credentials = $request->validate([ 'email' => ['required', 'string', 'email'], 'password' => ['required', 'string'], ]); // Auth::validate() (no attempt()): solo chequea credenciales, no // loguea todavía — necesitamos poder frenar acá mismo si el usuario // tiene 2FA activo, antes de que exista sesión autenticada. if (! Auth::validate($credentials)) { $this->recordFailedLogin($request); throw ValidationException::withMessages([ 'email' => __('messages.invalid_credentials'), ]); } $user = User::where('email', $credentials['email'])->first(); if (! $user->is_active) { throw ValidationException::withMessages([ 'email' => __('messages.account_deactivated'), ]); } if ($user->isPending()) { throw ValidationException::withMessages([ 'email' => __('messages.account_pending_approval'), ]); } if ($user->hasTwoFactorEnabled()) { $request->session()->put('login.mfa_user_id', $user->id); $request->session()->put('login.mfa_remember', $request->boolean('remember')); return response()->json(['two_factor' => true]); } Auth::login($user, $request->boolean('remember')); $request->session()->regenerate(); $this->recordSessionLogin($request, $user); return response()->json([ 'user' => $this->present($user), 'intended' => $request->session()->pull('url.intended'), ]); } /** * Segundo paso del login cuando el usuario tiene 2FA activo: recibe un * código TOTP o de recuperación y, si es válido, recién ahí crea la sesión. */ public function loginTwoFactor(Request $request) { $data = $request->validate([ 'code' => ['required', 'string'], ]); $userId = $request->session()->get('login.mfa_user_id'); $user = $userId ? User::find($userId) : null; if (! $user || ! TwoFactorVerification::verify($user, $data['code'])) { throw ValidationException::withMessages([ 'code' => __('messages.invalid_two_factor_code'), ]); } $remember = $request->session()->pull('login.mfa_remember', false); $request->session()->forget('login.mfa_user_id'); Auth::login($user, $remember); $request->session()->regenerate(); $this->recordSessionLogin($request, $user); return response()->json([ 'user' => $this->present($user), 'intended' => $request->session()->pull('url.intended'), ]); } public function logout(Request $request) { Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return response()->noContent(); } public function me(Request $request) { return response()->json(['user' => $this->present($request->user())]); } private function recordSessionLogin(Request $request, User $user): void { LoginEvent::create([ 'user_id' => $user->id, 'client_id' => null, 'type' => 'session', 'outcome' => 'success', 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'created_at' => now(), ]); } // Solo credenciales inválidas (la señal relevante para detectar fuerza // bruta) — cuenta desactivada o pendiente de aprobación no cuenta como // "fallo" para esta métrica, son estados de cuenta, no intentos de ataque. private function recordFailedLogin(Request $request): void { LoginEvent::create([ 'user_id' => null, 'client_id' => null, 'type' => 'session', 'outcome' => 'failure', 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), 'created_at' => now(), ]); } private function present(User $user): array { return [ ...$user->toArray(), 'roles' => $user->getRoleNames(), 'permissions' => $user->getAllPermissions()->pluck('name'), 'two_factor_enabled' => $user->hasTwoFactorEnabled(), ]; } }