diff --git a/app/Http/Controllers/System/LoginController.php b/app/Http/Controllers/System/LoginController.php index 6471d9b..0fc4df9 100644 --- a/app/Http/Controllers/System/LoginController.php +++ b/app/Http/Controllers/System/LoginController.php @@ -25,16 +25,10 @@ class LoginController extends Controller { /** * Iniciar sesión - * Permite login con username O email */ public function login(LoginRequest $request) { - $credential = $request->get('username'); - - // Buscar por username o email - $user = User::where('username', $credential) - ->orWhere('email', $credential) - ->first(); + $user = User::where('username', $request->get('username'))->first(); if (!$user || !$user->validateForPassportPasswordGrant($request->get('password'))) { return ApiResponse::UNPROCESSABLE_CONTENT->response([ @@ -62,27 +56,35 @@ public function logout() /** * Contraseña olvidada + * Nota: Sin email, el reset se maneja por token directo */ public function forgotPassword(ForgotRequest $request) { $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 { $token = $this->generateToken($user); - $user->notify(new ForgotPasswordNotification($token)); - + // Sin email, retornar el token directamente (para uso administrativo) return ApiResponse::OK->response([ - 'is_sent' => true + 'is_generated' => true, + 'token' => $token, + 'message' => 'Token generado. Válido por 15 minutos.', ]); } catch (\Throwable $th) { - Log::channel('mail')->info("Email: {$data['email']}"); + Log::channel('mail')->info("Username: {$data['username']}"); Log::channel('mail')->error($th->getMessage()); return ApiResponse::INTERNAL_ERROR->response([ - 'is_sent' => false, + 'is_generated' => false, ]); } } diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 8a742b8..ff2f380 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -30,7 +30,7 @@ public function authorize(): bool public function rules(): array { return [ - 'username' => ['required', 'string'], // Acepta username o email + 'username' => ['required', 'string'], 'password' => ['required', 'min:8'], ]; } @@ -38,7 +38,7 @@ public function rules(): array public function messages(): array { return [ - 'username.required' => 'El usuario o email es requerido', + 'username.required' => 'El usuario es requerido', 'password.required' => 'La contraseña es requerida', 'password.min' => 'La contraseña debe tener al menos 8 caracteres', ]; diff --git a/app/Http/Requests/User/ForgotRequest.php b/app/Http/Requests/User/ForgotRequest.php index 8f308cb..9101be7 100644 --- a/app/Http/Requests/User/ForgotRequest.php +++ b/app/Http/Requests/User/ForgotRequest.php @@ -28,7 +28,15 @@ public function authorize(): bool public function rules(): array { 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', ]; } } diff --git a/app/Http/Requests/Users/UserStoreRequest.php b/app/Http/Requests/Users/UserStoreRequest.php index b61ae94..1d17425 100644 --- a/app/Http/Requests/Users/UserStoreRequest.php +++ b/app/Http/Requests/Users/UserStoreRequest.php @@ -3,7 +3,6 @@ * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All rights reserved */ -use App\Models\User; use Illuminate\Foundation\Http\FormRequest; /** @@ -34,27 +33,19 @@ public function rules(): array 'name' => ['required', 'string', 'max:255'], 'paternal' => ['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'], 'password' => ['required', 'string', 'min:8'], 'roles' => ['nullable', 'array'] ]; } - /** - * Preparar datos antes de la validación - * Genera el username automáticamente - */ - protected function prepareForValidation(): void + public function messages(): array { - if ($this->has('name') && $this->has('paternal')) { - $this->merge([ - 'username' => User::generateUsername( - $this->input('name'), - $this->input('paternal'), - $this->input('maternal') - ) - ]); - } + 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', + ]; } } diff --git a/app/Http/Requests/Users/UserUpdateRequest.php b/app/Http/Requests/Users/UserUpdateRequest.php index 32b7d8b..d2b9e0e 100644 --- a/app/Http/Requests/Users/UserUpdateRequest.php +++ b/app/Http/Requests/Users/UserUpdateRequest.php @@ -34,9 +34,18 @@ public function rules(): array 'name' => ['required', 'string', 'max:255'], 'paternal' => ['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'], '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', + ]; + } } diff --git a/app/Models/User.php b/app/Models/User.php index ee0c17f..299a572 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -46,7 +46,6 @@ class User extends Authenticatable 'paternal', 'maternal', 'username', - 'email', 'phone', 'password', 'profile_photo_path', @@ -67,7 +66,6 @@ class User extends Authenticatable protected function casts(): array { return [ - 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; } @@ -145,90 +143,4 @@ public function responsibleModule() { 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; - } } diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index 3e2488f..e34bb3a 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -23,7 +23,7 @@ public function created(User $user): void UserEvent::report( model: $user, event: __FUNCTION__, - key: 'email' + key: 'username' ); } @@ -35,7 +35,7 @@ public function updated(User $user): void UserEvent::report( model: $user, event: __FUNCTION__, - key: 'email', + key: 'username', reportChanges: true ); } @@ -48,7 +48,7 @@ public function deleted(User $user): void UserEvent::report( model: $user, event: __FUNCTION__, - key: 'email' + key: 'username' ); } @@ -60,7 +60,7 @@ public function restored(User $user): void UserEvent::report( model: $user, event: __FUNCTION__, - key: 'email' + key: 'username' ); } @@ -72,7 +72,7 @@ public function forceDeleted(User $user): void UserEvent::report( model: $user, event: __FUNCTION__, - key: 'email' + key: 'username' ); } } diff --git a/database/migrations/2026_01_19_160736_remove_email_from_users_table.php b/database/migrations/2026_01_19_160736_remove_email_from_users_table.php new file mode 100644 index 0000000..db5ffe7 --- /dev/null +++ b/database/migrations/2026_01_19_160736_remove_email_from_users_table.php @@ -0,0 +1,40 @@ +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'); + } + }); + } +}; diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index a430d5d..4c8a670 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -21,24 +21,28 @@ class UserSeeder extends Seeder */ public function run(): void { - $developer = UserSecureSupport::create('developer@golsystems.com.mx'); + $developer = UserSecureSupport::create('developer'); - User::create([ - 'name' => 'Developer', - 'paternal' => 'golsystems', - 'maternal' => 'Software', - 'email' => $developer->email, - 'password' => $developer->hash, - ])->assignRole(__('developer')); + User::updateOrCreate( + ['username' => 'developer'], + [ + 'name' => 'Developer', + 'paternal' => 'golsystems', + 'maternal' => 'Software', + 'password' => $developer->hash, + ] + )->assignRole(__('developer')); - $admin = UserSecureSupport::create('admin@golsystems.com.mx'); + $admin = UserSecureSupport::create('admin'); - User::create([ - 'name' => 'Admin', - 'paternal' => 'golsystems', - 'maternal' => 'Software', - 'email' => $admin->email, - 'password' => $admin->hash, - ])->assignRole(__('admin')); + User::updateOrCreate( + ['username' => 'admin'], + [ + 'name' => 'Admin', + 'paternal' => 'golsystems', + 'maternal' => 'Software', + 'password' => $admin->hash, + ] + )->assignRole(__('admin')); } }