fix: login con username

This commit is contained in:
Juan Felipe Zapata Moreno 2026-01-19 16:12:46 -06:00
parent ee9b265582
commit 927c46aa2e
9 changed files with 108 additions and 142 deletions

View File

@ -25,16 +25,10 @@ class LoginController extends Controller
{ {
/** /**
* Iniciar sesión * Iniciar sesión
* Permite login con username O email
*/ */
public function login(LoginRequest $request) public function login(LoginRequest $request)
{ {
$credential = $request->get('username'); $user = User::where('username', $request->get('username'))->first();
// Buscar por username o email
$user = User::where('username', $credential)
->orWhere('email', $credential)
->first();
if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) { if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) {
return ApiResponse::UNPROCESSABLE_CONTENT->response([ return ApiResponse::UNPROCESSABLE_CONTENT->response([
@ -62,27 +56,35 @@ public function logout()
/** /**
* Contraseña olvidada * Contraseña olvidada
* Nota: Sin email, el reset se maneja por token directo
*/ */
public function forgotPassword(ForgotRequest $request) public function forgotPassword(ForgotRequest $request)
{ {
$data = $request->validated(); $data = $request->validated();
$user = User::where('email', $data['email'])->first(); $user = User::where('username', $data['username'])->first();
if (!$user) {
return ApiResponse::NOT_FOUND->response([
'username' => ['Usuario no encontrado']
]);
}
try { try {
$token = $this->generateToken($user); $token = $this->generateToken($user);
$user->notify(new ForgotPasswordNotification($token)); // Sin email, retornar el token directamente (para uso administrativo)
return ApiResponse::OK->response([ return ApiResponse::OK->response([
'is_sent' => true 'is_generated' => true,
'token' => $token,
'message' => 'Token generado. Válido por 15 minutos.',
]); ]);
} catch (\Throwable $th) { } catch (\Throwable $th) {
Log::channel('mail')->info("Email: {$data['email']}"); Log::channel('mail')->info("Username: {$data['username']}");
Log::channel('mail')->error($th->getMessage()); Log::channel('mail')->error($th->getMessage());
return ApiResponse::INTERNAL_ERROR->response([ return ApiResponse::INTERNAL_ERROR->response([
'is_sent' => false, 'is_generated' => false,
]); ]);
} }
} }

View File

@ -30,7 +30,7 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'username' => ['required', 'string'], // Acepta username o email 'username' => ['required', 'string'],
'password' => ['required', 'min:8'], 'password' => ['required', 'min:8'],
]; ];
} }
@ -38,7 +38,7 @@ public function rules(): array
public function messages(): array public function messages(): array
{ {
return [ return [
'username.required' => 'El usuario o email es requerido', 'username.required' => 'El usuario es requerido',
'password.required' => 'La contraseña es requerida', 'password.required' => 'La contraseña es requerida',
'password.min' => 'La contraseña debe tener al menos 8 caracteres', 'password.min' => 'La contraseña debe tener al menos 8 caracteres',
]; ];

View File

@ -28,7 +28,15 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
return [ return [
'email' => ['required', 'email', 'exists:users,email'] 'username' => ['required', 'string', 'exists:users,username']
];
}
public function messages(): array
{
return [
'username.required' => 'El nombre de usuario es requerido',
'username.exists' => 'El usuario no existe',
]; ];
} }
} }

View File

@ -3,7 +3,6 @@
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All rights reserved * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All rights reserved
*/ */
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
/** /**
@ -34,27 +33,19 @@ public function rules(): array
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'paternal' => ['required', 'string', 'max:255'], 'paternal' => ['required', 'string', 'max:255'],
'maternal' => ['required', 'string', 'max:255'], 'maternal' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'username' => ['required', 'string', 'max:255', 'unique:users', 'alpha_dash'],
'phone' => ['nullable', 'numeric', 'digits:10'], 'phone' => ['nullable', 'numeric', 'digits:10'],
'password' => ['required', 'string', 'min:8'], 'password' => ['required', 'string', 'min:8'],
'roles' => ['nullable', 'array'] 'roles' => ['nullable', 'array']
]; ];
} }
/** public function messages(): array
* Preparar datos antes de la validación
* Genera el username automáticamente
*/
protected function prepareForValidation(): void
{ {
if ($this->has('name') && $this->has('paternal')) { return [
$this->merge([ 'username.required' => 'El nombre de usuario es requerido',
'username' => User::generateUsername( 'username.unique' => 'El nombre de usuario ya está en uso',
$this->input('name'), 'username.alpha_dash' => 'El usuario solo puede contener letras, números, guiones y guiones bajos',
$this->input('paternal'), ];
$this->input('maternal')
)
]);
}
} }
} }

View File

@ -34,9 +34,18 @@ public function rules(): array
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'paternal' => ['required', 'string', 'max:255'], 'paternal' => ['required', 'string', 'max:255'],
'maternal' => ['required', 'string', 'max:255'], 'maternal' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($this->route('user'))], 'username' => ['required', 'string', 'max:255', 'alpha_dash', Rule::unique('users')->ignore($this->route('user'))],
'phone' => ['nullable', 'numeric', 'digits:10'], 'phone' => ['nullable', 'numeric', 'digits:10'],
'roles' => ['nullable', 'array'] 'roles' => ['nullable', 'array']
]; ];
} }
public function messages(): array
{
return [
'username.required' => 'El nombre de usuario es requerido',
'username.unique' => 'El nombre de usuario ya está en uso',
'username.alpha_dash' => 'El usuario solo puede contener letras, números, guiones y guiones bajos',
];
}
} }

View File

@ -46,7 +46,6 @@ class User extends Authenticatable
'paternal', 'paternal',
'maternal', 'maternal',
'username', 'username',
'email',
'phone', 'phone',
'password', 'password',
'profile_photo_path', 'profile_photo_path',
@ -67,7 +66,6 @@ class User extends Authenticatable
protected function casts(): array protected function casts(): array
{ {
return [ return [
'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
@ -145,90 +143,4 @@ public function responsibleModule()
{ {
return $this->hasOne(Module::class, 'responsible_id'); return $this->hasOne(Module::class, 'responsible_id');
} }
/**
* Generar username automático al crear usuario
* Formato: inicial nombre + inicial segundo nombre + apellido paterno + inicial materno
*/
public static function generateUsername(?string $name, ?string $paternal, ?string $maternal = null): string
{
// Validar que al menos tengamos nombre y apellido paterno
if (empty($name) || empty($paternal)) {
return self::ensureUniqueUsername('user');
}
$name = self::normalizeString($name);
$paternal = self::normalizeString($paternal);
$maternal = $maternal ? self::normalizeString($maternal) : '';
// Separar nombres y obtener iniciales
$nameParts = preg_split('/\s+/', trim($name));
$firstInitial = !empty($nameParts[0]) ? substr($nameParts[0], 0, 1) : '';
$secondInitial = isset($nameParts[1]) && !empty($nameParts[1]) ? substr($nameParts[1], 0, 1) : '';
$maternalInitial = !empty($maternal) ? substr($maternal, 0, 1) : '';
// Construir username
$baseUsername = $firstInitial . $secondInitial . $paternal . $maternalInitial;
// Si el username queda vacío, usar fallback
if (empty($baseUsername)) {
$baseUsername = 'user';
}
return self::ensureUniqueUsername($baseUsername);
}
/**
* Normalizar string: quitar acentos, convertir a minúsculas, solo letras
*/
private static function normalizeString(string $string): string
{
// Convertir a minúsculas
$string = mb_strtolower($string);
// Reemplazar caracteres acentuados
$replacements = [
'á' => 'a',
'é' => 'e',
'í' => 'i',
'ó' => 'o',
'ú' => 'u',
'ä' => 'a',
'ë' => 'e',
'ï' => 'i',
'ö' => 'o',
'ü' => 'u',
'à' => 'a',
'è' => 'e',
'ì' => 'i',
'ò' => 'o',
'ù' => 'u',
'ñ' => 'n',
'ç' => 'c',
];
$string = strtr($string, $replacements);
// Eliminar cualquier caracter que no sea letra o espacio
$string = preg_replace('/[^a-z\s]/', '', $string);
return $string;
}
/**
* Asegurar que el username sea único, agregando número si es necesario
*/
private static function ensureUniqueUsername(string $baseUsername): string
{
$username = $baseUsername;
$counter = 1;
while (self::where('username', $username)->exists()) {
$counter++;
$username = $baseUsername . $counter;
}
return $username;
}
} }

View File

@ -23,7 +23,7 @@ public function created(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
@ -35,7 +35,7 @@ public function updated(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email', key: 'username',
reportChanges: true reportChanges: true
); );
} }
@ -48,7 +48,7 @@ public function deleted(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
@ -60,7 +60,7 @@ public function restored(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
@ -72,7 +72,7 @@ public function forceDeleted(User $user): void
UserEvent::report( UserEvent::report(
model: $user, model: $user,
event: __FUNCTION__, event: __FUNCTION__,
key: 'email' key: 'username'
); );
} }
} }

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Eliminar email si existe
if (Schema::hasColumn('users', 'email')) {
$table->dropUnique(['email']);
$table->dropColumn('email');
}
// Eliminar email_verified_at si existe
if (Schema::hasColumn('users', 'email_verified_at')) {
$table->dropColumn('email_verified_at');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
if (!Schema::hasColumn('users', 'email')) {
$table->string('email')->unique()->after('maternal');
$table->timestamp('email_verified_at')->nullable()->after('email');
}
});
}
};

View File

@ -21,24 +21,28 @@ class UserSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
$developer = UserSecureSupport::create('developer@golsystems.com.mx'); $developer = UserSecureSupport::create('developer');
User::create([ User::updateOrCreate(
'name' => 'Developer', ['username' => 'developer'],
'paternal' => 'golsystems', [
'maternal' => 'Software', 'name' => 'Developer',
'email' => $developer->email, 'paternal' => 'golsystems',
'password' => $developer->hash, 'maternal' => 'Software',
])->assignRole(__('developer')); 'password' => $developer->hash,
]
)->assignRole(__('developer'));
$admin = UserSecureSupport::create('admin@golsystems.com.mx'); $admin = UserSecureSupport::create('admin');
User::create([ User::updateOrCreate(
'name' => 'Admin', ['username' => 'admin'],
'paternal' => 'golsystems', [
'maternal' => 'Software', 'name' => 'Admin',
'email' => $admin->email, 'paternal' => 'golsystems',
'password' => $admin->hash, 'maternal' => 'Software',
])->assignRole(__('admin')); 'password' => $admin->hash,
]
)->assignRole(__('admin'));
} }
} }