File: /var/www/api_matriculas/app/Services/AuthService.php
<?php
namespace App\Services;
use App\Models\ResetPassword;
use App\Models\User;
use App\Repositories\ParentRepository;
use App\Repositories\ResetPasswordRepository;
use App\Repositories\UserRepository;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class AuthService
{
public function login(Request $request, $origin = 'web')
{
$errors = $this->validateLogin($request);
if (!empty($errors)) {
throw new Exception($errors, 400);
}
$rut = $this->resolveIdentifier($request, false);
$userRepository = new UserRepository();
$register = $userRepository->getByUsername($rut);
if (!$register) {
throw new Exception("Usuario y/o contraseña incorrecta", 401);
}
if (!$register->status) {
throw new Exception("Usuario ha sido deshabilitado. Pónganse en contacto con el administrador de su empresa", 403);
}
// Bloquear acceso de apoderados si el sitio está en mantenimiento
if (!config('app.open_to_parents') && $register->profile && $register->profile->code === 'parent') {
throw new Exception("El sitio se encuentra en mantenimiento. Intente nuevamente más tarde.", 503);
}
$credentials = ['username' => $rut, 'password' => $request->password, 'deleted' => false];
if (!Auth::attempt($credentials)) {
throw new Exception("Usuario y/o contraseña incorrecta", 401);
}
// Generar token y guardarlo en la BD
$token = bin2hex(random_bytes(40));
$register->api_token = $token;
$register->save();
return [
'token' => $token,
'user' => [
'id' => $register->id,
'name' => $register->name,
'username' => $register->username,
'email' => $register->email,
'role' => $register->profile->code,
'roleName' => $register->profile->profile,
'permissions_users' => $register->getFrontPermissions(),
]
];
}
public function validateSession(Request $request)
{
$user = $request->user();
// Fallback: buscar por Bearer token
if (!$user) {
$bearer = $request->bearerToken();
if ($bearer) {
$user = User::where('api_token', $bearer)->first();
}
}
if (!$user) {
throw new Exception('Acceso no autorizado', 401);
}
if (!$user->status) {
throw new Exception('Usuario ha sido deshabilitado. Pónganse en contacto con el administrador de su empresa', 403);
}
return [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'role' => $user->profile->code,
'roleName' => $user->profile->profile,
'permissions_users' => $user->getFrontPermissions(),
];
}
public function getUser($id)
{
$user = User::find($id);
if (!$user->status) {
$msg = 'Usuario ha sido deshabilitado. Pónganse en contacto con el administrador de su empresa';
$code = 403;
throw new Exception($msg, $code);
}
return [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'roleName' => $user->profile->profile,
'permissions_users' => $user->getFrontPermissions(),
];
}
public function logout(Request $request)
{
$user = $request->user() ?? User::where('api_token', $request->bearerToken())->first();
if ($user) {
$user->api_token = null;
$user->save();
}
return null;
}
public function sendRecoveryCode(Request $request)
{
$errors = null;
$rules = [
'rut' => 'required',
];
$messages = [
'rut.required' => 'Rut es Requerido',
];
// Crear el validador manualmente
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
if (!empty($errors)) {
throw new Exception(json_encode($errors), 400);
}
$rut = $this->resolveIdentifier($request);
$userRepository = new UserRepository();
$userData = $userRepository->getByUsername($rut);
if (!$userData) {
throw new Exception("Documento ingresado no existe en base de datos", 404);
}
if (!$userData->status) {
throw new Exception("Su usuario ha sido deshabilitado. Pónganse en contacto con el administrador del sistema.", 403);
}
$resetPasswordRepository = new ResetPasswordRepository();
$resetPassword = $resetPasswordRepository->getByUserId($userData->id);
$currentDate = now();
if ($resetPassword) {
$expirationDate = $resetPassword->expiration_date;
if (strtotime($expirationDate) >= strtotime($currentDate)) {
throw new Exception(
'Ya existe una solicitud activa para restablecer tu contraseña, la cual expira el ' .
sort_datetimeHuman($resetPassword->expiration_date) .
'. Podrás generar una nueva solicitud una vez que esta haya expirado.',
400
);
}
}
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
ResetPassword::where('user_id', $userData->id)
->where('status', 1)->where('code', '=!', $code)
->update(['status' => 0]);
try {
DB::beginTransaction();
$resetPasswordRepository->create((object)[
'user_id' => $userData->id,
'username' => strLower($userData->username),
'code' => $code,
'change_date' => now(),
'expiration_date' => now()->addMinutes(15),
]);
if (!$resetPasswordRepository) {
throw new Exception("Error al crear usuario.", 500);
}
$mailService = new MailService();
$sendMail = $mailService->recoveryPassword((object)[
'name' => strUpper($userData->name),
'email' => strLower($userData->email),
'code' => $code,
]);
// if (!$sendMail) {
// throw new Exception("Error al enviar correo. Contácte a soporte");
// }
DB::commit();
return [
'name' => $userData->name,
'email' => obfuscateEmail(strLower($userData->email)),
];
} catch (Exception $e) {
if (DB::transactionLevel() > 0) {
DB::rollBack();
}
Log::error("Error al recuperar contraseña: {$e->getMessage()} | {$e->getFile()}:{$e->getLine()}");
throw new Exception("Ocurrió un problema al enviar el correo de recuperación. Recargue e intente nuevamente.", 500);
}
}
public function validateCode(Request $request)
{
$errors = null;
$rules = [
'rut' => 'required',
'code' => 'required|string|min:6',
];
$messages = [
'rut.required' => 'Rut es Requerido',
'code.required' => 'Código es Requerido',
'code.min' => 'Código debe tener 6 caracteres',
];
// Crear el validador manualmente
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
if (!empty($errors)) {
throw new Exception(json_encode($errors), 400);
}
$request->validate([
'rut' => 'required|string',
'code' => 'required|string|min:6',
]);
$rut = $this->resolveIdentifier($request);
$userRepository = new UserRepository();
$userData = $userRepository->getByUsername($rut);
if (!$userData) {
throw new Exception("Lo Sentimos, su solicitud ha expirado.", 404);
}
$resetPasswordRepository = new ResetPasswordRepository();
$resetPassword = $resetPasswordRepository->getByCode($request->code);
$currentDate = now();
if ($resetPassword) {
$expirationDate = $resetPassword->expiration_date;
if (strtotime($expirationDate) < strtotime($currentDate)) {
throw new Exception(
'Lo Sentimos, su solicitud ha expirado. Genere otra y vuelva a intentarlo.',
400
);
}
}
return [
'success' => true,
'message' => 'Código validado correctamente'
];
}
// Restablecer contraseña
public function resetPassword(Request $request)
{
$errors = null;
$rules = [
'rut' => 'required',
'code' => 'required|string|min:6',
'password' => [
'required',
'string',
'min:8',
'regex:/[a-z]/',
'regex:/[A-Z]/',
'regex:/[0-9]/',
// 'regex:/[\W_]/'
],
'password_confirm' => [
'required',
'string',
'min:8',
'regex:/[a-z]/',
'regex:/[A-Z]/',
'regex:/[0-9]/',
// 'regex:/[\W_]/'
],
];
$messages = [
'rut.required' => 'Rut es Requerido',
'code.required' => 'Código es Requerido',
'code.min' => 'Código debe tener 6 caracteres',
'password.required' => 'Contraseña requerida',
'password.string' => 'Contraseña debe ser una cadena de texto',
'password.min' => 'Contraseña debe tener al menos 8 caracteres',
// 'password.regex' => 'Contraseña debe contener al menos una letra mayúscula, una letra minúscula, un número y un carácter especial',
'password.regex' => 'Contraseña debe contener al menos una letra mayúscula, una letra minúscula y un número.',
'password_confirm.required' => 'Confirmar Contraseña requerida',
'password_confirm.string' => 'Confirmar Contraseña debe ser una cadena de texto',
'password_confirm.min' => 'Confirmar Contraseña debe tener al menos 8 caracteres',
'password_confirm.regex' => 'Contraseña debe contener al menos una letra mayúscula, una letra minúscula y un número',
];
// Crear el validador manualmente
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
if (!empty($errors)) {
throw new Exception(json_encode($errors), 400);
}
$rut = $this->resolveIdentifier($request);
$userRepository = new UserRepository();
$userData = $userRepository->getByUsername($rut);
if (!$userData) {
throw new Exception("Lo Sentimos, su solicitud ha expirado.", 404);
}
$resetPasswordRepository = new ResetPasswordRepository();
$resetPassword = $resetPasswordRepository->getByCode($request->code);
$currentDate = now();
if ($resetPassword) {
$expirationDate = $resetPassword->expiration_date;
if (strtotime($expirationDate) < strtotime($currentDate)) {
throw new Exception(
'Lo Sentimos, su solicitud ha expirado. Genere otra y vuelva a intentarlo.',
400
);
}
}
$userData->password = bcrypt($request->password);
$userData->updated_at = now();
if (!$userData->save()) {
throw new Exception("Ocurrió un problema al actualizar contraseña. Intentelo nuevamente.", 404);
}
$resetPassword->status = false;
$resetPassword->save();
$mailService = new MailService();
$sendMail = $mailService->confirmPassword((object)[
'name' => strUpper($userData->name),
'email' => strLower($userData->email),
]);
return [
'success' => true,
'message' => 'Contraseña restablecida correctamente'
];
}
private function resolveIdentifier(Request $request, bool $withDots = true): string
{
$isPassport = strtoupper($request->document_type ?? 'RUT') === 'PASSPORT';
if ($isPassport) {
return strUpper($request->rut);
}
return formatterRut($request->rut);
}
private function validateLogin($request)
{
$errors = null;
$rules = [
'rut' => 'required',
'password' => 'required',
];
$messages = [
'rut.required' => 'Rut es Requerido',
'password.required' => 'Contraseña es Requerida',
];
// Crear el validador manualmente
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
return $errors;
}
public function activateAccount(Request $request)
{
$errors = null;
$rules = [
'rut' => 'required',
];
$messages = [
'rut.required' => 'RUT es requerido',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
if (!empty($errors)) {
throw new Exception(json_encode($errors), 400);
}
$rut = $this->resolveIdentifier($request);
$parentRepository = new ParentRepository();
$parentData = $parentRepository->getByRut($rut);
if (!$parentData) {
throw new Exception("El documento ingresado no pertenece a ningún apoderado registrado.", 404);
}
$userRepository = new UserRepository();
if ($parentData->user_id < 1) {
$profileId = $parentRepository->getProfile();
$isParentPassport = ($parentData->document_type ?? 'RUT') === 'PASSPORT';
$parentUsername = $isParentPassport ? $parentData->rut : (!empty($parentData->rut) ? formatterRut($parentData->rut, false) : null);
$user = $userRepository->create([
'name' => strUpper($parentData->first_name . ' ' . $parentData->last_name),
'username' => $parentUsername,
'rut' => $parentUsername,
'email' => strLower($parentData->email),
'account_confirmed' => false,
'status' => true,
'profile_id' => $profileId,
'created_at' => now(),
'updated_at' => now(),
]);
if (!$user) {
throw new Exception("Ocurrió un error al activar la cuenta. Recarga e intenta nuevamente.", 500);
}
$parentData->user_id = $user->id;
$parentData->save();
}
$userData = $userRepository->getByUsername($rut);
if (!$userData) {
throw new Exception("Documento ingresado no existe en base de datos", 404);
}
if (!$userData->status) {
throw new Exception("Su usuario ha sido deshabilitado. Póngase en contacto con el administrador del sistema.", 403);
}
if ($userData->account_confirmed) {
throw new Exception("Su cuenta ya se encuentra activa. Inicie sesión para continuar con el proceso de matrícula.", 400);
}
$currentDate = now();
$expirationDate = $userData->account_confirmed_at;
// 🔹 Calcular segundos restantes si la solicitud aún no expira
if ($expirationDate && strtotime($expirationDate) >= strtotime($currentDate)) {
$secondsLeft = strtotime($expirationDate) - strtotime($currentDate);
throw new Exception(
'Ya existe una solicitud para activar tu cuenta. <br>'
. '<b>El código actual expira en (' . $secondsLeft . 's)</b>. '
. 'Podrás solicitar un nuevo código una vez que este expire.',
400
);
}
// 🔹 Generar nuevo código de activación (numérico)
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$userData->activation_token = $code;
$userData->account_confirmed_at = now()->addMinutes(1);
$userData->save();
try {
$mailService = new MailService();
$sendMail = $mailService->activeAccount((object)[
'name' => strUpper($userData->name),
'email' => strLower($userData->email),
'code' => $code,
]);
if (!$sendMail) {
throw new Exception("Error al enviar correo. Contacte a soporte.", 500);
}
return [
'name' => $userData->name,
'email' => obfuscateEmail(strLower($userData->email)),
'expire' => $userData->account_confirmed_at,
'ttl' => 60, // segundos de validez total
];
} catch (Exception $e) {
Log::error("Error al activar cuenta: {$e->getMessage()} | {$e->getFile()}:{$e->getLine()}");
throw new Exception("Ocurrió un problema al enviar el correo de activación. Recargue e intente nuevamente.", 500);
}
}
public function validateCodeActivation(Request $request)
{
$errors = null;
$rules = [
'rut' => 'required',
'code' => 'required|string|min:6',
];
$messages = [
'rut.required' => 'Rut es Requerido',
'code.required' => 'Código es Requerido',
'code.min' => 'Código debe tener 6 caracteres',
];
// Crear el validador manualmente
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
if (!empty($errors)) {
throw new Exception(json_encode($errors), 400);
}
$request->validate([
'rut' => 'required|string',
'code' => 'required|string|min:6',
]);
$rut = $this->resolveIdentifier($request);
$userRepository = new UserRepository();
$userData = $userRepository->getByUsername($rut);
if (!$userData) {
throw new Exception("Lo Sentimos, su solicitud ha expirado.", 404);
}
if ($userData->account_confirmed) {
throw new Exception("Su cuenta ya se encuentra activa. Inicie sesión para continuar con el proceso de matrícula.", 400);
}
if ($userData->activation_token != $request->code) {
throw new Exception("Lo Sentimos, su solicitud ha expirado. Genere otra y vuelva a intentarlo.", 404);
}
return [
'success' => true,
'message' => 'Código validado correctamente'
];
}
public function activatePasswordWithCode(Request $request)
{
$errors = null;
$rules = [
'rut' => 'required',
'code' => 'required|string|min:6',
'password' => [
'required',
'string',
'min:8',
'regex:/[a-z]/',
'regex:/[A-Z]/',
'regex:/[0-9]/',
// 'regex:/[\W_]/'
],
'password_confirm' => [
'required',
'string',
'min:8',
'regex:/[a-z]/',
'regex:/[A-Z]/',
'regex:/[0-9]/',
// 'regex:/[\W_]/'
],
];
$messages = [
'rut.required' => 'Rut es Requerido',
'code.required' => 'Código es Requerido',
'code.min' => 'Código debe tener 6 caracteres',
'password.required' => 'Contraseña requerida',
'password.string' => 'Contraseña debe ser una cadena de texto',
'password.min' => 'Contraseña debe tener al menos 8 caracteres',
// 'password.regex' => 'Contraseña debe contener al menos una letra mayúscula, una letra minúscula, un número y un carácter especial',
'password.regex' => 'Contraseña debe contener al menos una letra mayúscula, una letra minúscula y un número.',
'password_confirm.required' => 'Confirmar Contraseña requerida',
'password_confirm.string' => 'Confirmar Contraseña debe ser una cadena de texto',
'password_confirm.min' => 'Confirmar Contraseña debe tener al menos 8 caracteres',
'password_confirm.regex' => 'Contraseña debe contener al menos una letra mayúscula, una letra minúscula y un número',
];
// Crear el validador manualmente
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
$errors = implode(', ', $validator->errors()->all());
}
if (!empty($errors)) {
throw new Exception(json_encode($errors), 400);
}
$rut = $this->resolveIdentifier($request);
$userRepository = new UserRepository();
$userData = $userRepository->getByUsername($rut);
if (!$userData) {
throw new Exception("Lo Sentimos, su solicitud ha expirado.", 404);
}
if ($userData->activation_token != $request->code) {
throw new Exception('Lo Sentimos, su solicitud ha expirado.', 400);
}
$userData->password = bcrypt($request->password);
$userData->updated_at = now();
$userData->activation_token = null;
$userData->account_confirmed = 1;
$userData->account_confirmed_at = now();
if (!$userData->save()) {
throw new Exception("Ocurrió un problema al activar cuenta. Intentelo nuevamente.", 404);
}
$mailService = new MailService();
$sendMail = $mailService->confirmActiveAccount((object)[
'name' => strUpper($userData->name),
'email' => strLower($userData->email),
]);
return [
'success' => true,
'message' => 'Cuenta activada correctamente'
];
}
}