Fix CORS, timezone y configuraciones para producción

- Actualizar middleware CORS para permitir orígenes de producción y apps móviles
- Cambiar timezone de America/Tijuana a America/Mexico_City en PostulationController
- Agregar configuración de Google Maps
- Agregar servicio de notificaciones push

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 08:59:17 +00:00
parent 689e456fe5
commit 21260b645f
6 changed files with 762 additions and 25 deletions

View File

@@ -42,3 +42,8 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# OneSignal Push Notifications
ONESIGNAL_APP_ID=
ONESIGNAL_REST_API_KEY=
ONESIGNAL_USER_AUTH_KEY=

View File

@@ -124,7 +124,7 @@ class PostulationController extends Controller
// Parsear fecha y hora del formato ISO que envía el frontend
$dateStr = substr(strip_tags($request->setdate), 0, 10); // "2026-01-28"
$timeStr = substr(strip_tags($request->sethour), 11, 8); // "19:47:00"
$postulation->appointment = Carbon::createFromFormat('Y-m-d H:i:s', $dateStr . ' ' . $timeStr, 'America/Tijuana')->tz('UTC');
$postulation->appointment = Carbon::createFromFormat('Y-m-d H:i:s', $dateStr . ' ' . $timeStr, 'America/Mexico_City')->tz('UTC');
$postulation->amount = 5000;
$postulation->details = preg_replace('/\d+/', '', strip_tags($request->details));
$postulation->save();

View File

@@ -12,15 +12,27 @@ class Cors
*/
public function handle($request, Closure $next)
{
$allowedOrigins = [
'http://localhost',
'https://localhost',
'ionic://localhost',
'https://jobhero.consultoria-as.com',
'https://jobhero-api.consultoria-as.com',
'capacitor://localhost',
'http://localhost:8100'
];
$allowedOrigins = ['http://localhost', 'ionic://localhost'];
$origin = $request->server('HTTP_ORIGIN');
if (in_array($request->server('HTTP_ORIGIN'), $allowedOrigins)) {
return $next($request)
->header('Access-Control-Allow-Origin', $request->server('HTTP_ORIGIN'))
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-XSRF-TOKEN');
}
// Allow requests from mobile apps (no origin or capacitor/ionic)
if (empty($origin) || in_array($origin, $allowedOrigins)) {
$response = $next($request);
$allowOrigin = empty($origin) ? '*' : $origin;
return $response
->header('Access-Control-Allow-Origin', $allowOrigin)
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-XSRF-TOKEN');
}
return $next($request);
}

View File

@@ -0,0 +1,207 @@
<?php
namespace App\Services;
use OneSignal;
class PushNotificationService
{
/**
* Send notification to a specific user by their ID
*
* @param int $userId
* @param string $message
* @param string $heading
* @param array $data
* @return mixed
*/
public function sendToUser(int $userId, string $message, string $heading = 'JobHero', array $data = [])
{
return OneSignal::sendNotificationUsingTags(
$message,
[
['field' => 'tag', 'key' => 'iChamba_ID', 'relation' => '=', 'value' => (string) $userId]
],
null,
null,
null,
null,
$heading,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
$data
);
}
/**
* Send notification to multiple users
*
* @param array $userIds
* @param string $message
* @param string $heading
* @param array $data
* @return array
*/
public function sendToUsers(array $userIds, string $message, string $heading = 'JobHero', array $data = [])
{
$results = [];
foreach ($userIds as $userId) {
$results[$userId] = $this->sendToUser($userId, $message, $heading, $data);
}
return $results;
}
/**
* Send scheduled notification to a user
*
* @param int $userId
* @param string $message
* @param string $sendAt UTC datetime string
* @param string $heading
* @param array $data
* @return mixed
*/
public function sendScheduledToUser(int $userId, string $message, string $sendAt, string $heading = 'JobHero', array $data = [])
{
return OneSignal::sendNotificationUsingTags(
$message,
[
['field' => 'tag', 'key' => 'iChamba_ID', 'relation' => '=', 'value' => (string) $userId]
],
null,
null,
null,
null,
$heading,
null,
null,
null,
null,
null,
null,
null,
$sendAt,
null,
null,
null,
null,
null,
null,
null,
null,
$data
);
}
/**
* Send notification to all users with a specific role
*
* @param int $roleId
* @param string $message
* @param string $heading
* @param array $data
* @return mixed
*/
public function sendToRole(int $roleId, string $message, string $heading = 'JobHero', array $data = [])
{
return OneSignal::sendNotificationUsingTags(
$message,
[
['field' => 'tag', 'key' => 'iChamba_Role', 'relation' => '=', 'value' => (string) $roleId]
],
null,
null,
null,
null,
$heading,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
$data
);
}
/**
* Notification types for contracts
*/
public function notifyContractHired(int $supplierId, string $address)
{
return $this->sendToUser(
$supplierId,
"Has sido contratado para un servicio. Dirígete a la sección de postulaciones contratadas para más detalles.",
"¡Nuevo Contrato!"
);
}
public function notifyContractStarted(int $userId, string $address, string $date)
{
return $this->sendToUser(
$userId,
"El servicio en {$address} el día {$date} ha sido iniciado. Dirígete a la sección de servicios contratados para más detalles.",
"Servicio Iniciado"
);
}
public function notifyContractCancelled(int $supplierId, string $address, string $date)
{
return $this->sendToUser(
$supplierId,
"El servicio en {$address} el día {$date} ha sido cancelado. Dirígete a la sección de servicios contratados para más detalles.",
"Servicio Cancelado"
);
}
public function notifySupplierArrived(int $clientId, string $address)
{
return $this->sendToUser(
$clientId,
"El proveedor para el servicio en {$address} ha llegado. Dirígete a la sección de contratos confirmados para más detalles.",
"Proveedor ha llegado"
);
}
public function notifyAppointmentReminder(int $userId, string $address, int $minutesBefore = 30)
{
return $this->sendToUser(
$userId,
"Tienes un servicio agendado hoy en {$address} en {$minutesBefore} minutos. Dirígete a la sección de contratos confirmados para más detalles.",
"Recordatorio de Cita"
);
}
public function notifyNewPostulation(int $supplierId)
{
return $this->sendToUser(
$supplierId,
"Hay una nueva postulación disponible en tu área. Dirígete a la sección de postulaciones para ver más detalles.",
"Nueva Postulación"
);
}
}

503
config/googlemaps.php Normal file
View File

@@ -0,0 +1,503 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| API Key
|--------------------------------------------------------------------------
|
| Will be used for all web services,
| unless overwritten bellow using 'key' parameter
|
|
*/
'key' => env('GOOGLE_MAPS_API_KEY', ''),
/*
|--------------------------------------------------------------------------
| Verify SSL Peer
|--------------------------------------------------------------------------
|
| Will be used for all web services to verify
| SSL peer (SSL certificate validation)
|
*/
'ssl_verify_peer' => FALSE,
/*
|--------------------------------------------------------------------------
| CURL's connection timeout
|--------------------------------------------------------------------------
|
| Will be used for all web services to limit
| the maximum time tha connection can take in seconds
|
*/
'connection_timeout' => 5,
/*
|--------------------------------------------------------------------------
| CURL's request timeout
|--------------------------------------------------------------------------
|
| Will be used for all web services to limit
| the maximum time a request can take
|
*/
'request_timeout' => 30,
/*
|--------------------------------------------------------------------------
| CURL's CURLOPT_ENCODING
|--------------------------------------------------------------------------
|
| Will be used for all web services to use compression on requests.
|
| Sets the contents of the "Accept-Encoding:" header a containing all
| supported encoding types.
|
*/
'request_use_compression' => false,
/*
|--------------------------------------------------------------------------
| Service URL
|--------------------------------------------------------------------------
| url - web service URL
| type - request type POST or GET
| key - API key, if different to API key above
| endpoint - boolean, indicates whenever output parameter to be used in the request or not
| responseDefaultKey - specify default field value to be returned when calling getByKey()
| param - accepted request parameters
|
*/
'service' => [
'geocoding' => [
'url' => 'https://maps.googleapis.com/maps/api/geocode/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'place_id',
'param' => [
'address' => null,
'bounds' => null,
'key' => null,
'region' => null,
'language' => null,
'result_type' => null,
'location_type' => null,
'latlng' => null,
'place_id' => null,
'components' => [
'route' => null,
'locality' => null,
'administrative_area' => null,
'postal_code' => null,
'country' => null,
]
]
],
// https://developers.google.com/maps/documentation/routes/reference/rest
'routes' => [
'url' => 'https://routes.googleapis.com/directions/v2:computeRoutes',
'type' => 'POST',
'key' => null,
'endpoint' => true,
'decodePolyline' => true, // true = decode overview_polyline.points to an array of points
'param' => [
'origin' => null, // required
'destination' => null, //required
'intermediates' => null,
'travelMode' => null,
'transitRoutingPreference' => null,
'polylineQuality' => null,
'polylineEncoding' => null,
'departureTime' => null,
'arrivalTime' => null,
'computeAlternativeRoutes' => null,
'routeModifiers' => null,
'languageCode' => null,
'regionCode' => null,
'units' => null,
'optimizeWaypointOrder' => false,
'requestedReferenceRoutes' => null,
'extraComputations' => null,
'trafficModel' => null,
'transitPreferences' => null,
]
],
// https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRouteMatrix
'routematrix' => [
'url' => 'https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix',
'type' => 'POST',
'key' => null,
'endpoint' => true,
'param' => [
'origins' => null, // required
'destinations' => null, //required
'travelMode' => null,
'routingPreference' => null,
'departureTime' => null,
'arrivalTime' => null,
'languageCode' => null,
'regionCode' => null,
'units' => null,
'extraComputations' => null,
'trafficModel' => null,
'transitPreferences' => null,
]
],
// Deprecated
'directions' => [
'url' => 'https://maps.googleapis.com/maps/api/directions/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'geocoded_waypoints',
'decodePolyline' => true, // true = decode overview_polyline.points to an array of points
'param' => [
'origin' => null, // required
'destination' => null, //required
'mode' => null,
'waypoints' => null,
'place_id' => null,
'alternatives' => null,
'avoid' => null,
'language' => null,
'units' => null,
'region' => null,
'departure_time' => null,
'arrival_time' => null,
'transit_mode' => null,
'transit_routing_preference' => null,
]
],
// Deprecated
'distancematrix' => [
'url' => 'https://maps.googleapis.com/maps/api/distancematrix/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'origin_addresses',
'param' => [
'origins' => null,
'destinations' => null,
'key' => null,
'mode' => null,
'language' => null,
'avoid' => null,
'units' => null,
'departure_time' => null,
'arrival_time' => null,
'transit_mode' => null,
'transit_routing_preference' => null,
]
],
'elevation' => [
'url' => 'https://maps.googleapis.com/maps/api/elevation/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'elevation',
'param' => [
'locations' => null,
'path' => null,
'samples' => null,
'key' => null,
]
],
'geolocate' => [
'url' => 'https://www.googleapis.com/geolocation/v1/geolocate?',
'type' => 'POST',
'key' => null,
'endpoint' => false,
'responseDefaultKey' => 'location',
'param' => [
'homeMobileCountryCode' => null,
'homeMobileNetworkCode' => null,
'radioType' => null,
'carrier' => null,
'considerIp' => null,
'cellTowers' => [
'cellId' => null,
'locationAreaCode' => null,
'mobileCountryCode' => null,
'mobileNetworkCode' => null,
'age' => null,
'signalStrength' => null,
'timingAdvance' => null,
],
'wifiAccessPoints' => [
'macAddress' => null,
'signalStrength' => null,
'age' => null,
'channel' => null,
'signalToNoiseRatio' => null,
],
]
],
'snapToRoads' => [
'url' => 'https://roads.googleapis.com/v1/snapToRoads?',
'type' => 'GET',
'key' => null,
'endpoint' => false,
'responseDefaultKey' => 'snappedPoints',
'param' => [
'locations' => null,
'path' => null,
'samples' => null,
'key' => null,
]
],
'speedLimits' => [
'url' => 'https://roads.googleapis.com/v1/speedLimits?',
'type' => 'GET',
'key' => null,
'endpoint' => false,
'responseDefaultKey' => 'speedLimits',
'param' => [
'path' => null,
'placeId' => null,
'units' => null,
'key' => null,
]
],
'timezone' => [
'url' => 'https://maps.googleapis.com/maps/api/timezone/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'dstOffset',
'param' => [
'location' => null,
'timestamp' => null,
'key' => null,
'language' => null,
]
],
'nearbysearch' => [
'url' => 'https://maps.googleapis.com/maps/api/place/nearbysearch/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'results',
'param' => [
'key' => null,
'location' => null,
'radius' => null,
'keyword' => null,
'language' => null,
'minprice' => null,
'maxprice' => null,
'name' => null,
'opennow' => null,
'rankby' => null,
'type' => null, // types depricated, one type may be specified
'pagetoken' => null,
'zagatselected' => null,
]
],
'textsearch' => [
'url' => 'https://maps.googleapis.com/maps/api/place/textsearch/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'results',
'param' => [
'key' => null,
'query' => null,
'location' => null,
'radius' => null,
'language' => null,
'minprice' => null,
'maxprice' => null,
'opennow' => null,
'type' => null, // types deprecated, one type may be specified
'pagetoken' => null,
'zagatselected' => null,
]
],
'radarsearch' => [
'url' => 'https://maps.googleapis.com/maps/api/place/radarsearch/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'geometry',
'param' => [
'key' => null,
'radius' => null,
'location' => null,
'keyword' => null,
'minprice' => null,
'maxprice' => null,
'opennow' => null,
'name' => null,
'type' => null, // types depricated, one type may be specified
'zagatselected' => null,
]
],
'placedetails' => [
'url' => 'https://maps.googleapis.com/maps/api/place/details/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'result',
'param' => [
'key' => null,
'placeid' => null,
'extensions' => null,
'language' => null,
]
],
'placeadd' => [
'url' => 'https://maps.googleapis.com/maps/api/place/add/',
'type' => 'POST',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'place_id',
'param' => [
'key' => null,
'accuracy' => null,
'address' => null,
'language' => null,
'location' => null,
'name' => null,
'phone_number' => null,
'types' => null, // according to docs types still required as string parameter
'type' => null, // types deprecated, one type may be specified
'website' => null,
]
],
'placedelete' => [
'url' => 'https://maps.googleapis.com/maps/api/place/delete/',
'type' => 'POST',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'status',
'param' => [
'key' => null,
'place_id' => null,
]
],
'placephoto' => [
'url' => 'https://maps.googleapis.com/maps/api/place/photo?',
'type' => 'GET',
'key' => null,
'endpoint' => false,
'responseDefaultKey' => 'image',
'param' => [
'key' => null,
'photoreference' => null,
'maxheight' => null,
'maxwidth' => null,
]
],
'placeautocomplete' => [
'url' => 'https://maps.googleapis.com/maps/api/place/autocomplete/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'predictions',
'param' => [
'key' => null,
'input' => null,
'offset' => null,
'location' => null,
'radius' => null,
'language' => null,
'types' => null, // use string as parameter
'type' => null, // types deprecated, one type may be specified
'components' => null,
]
],
'placequeryautocomplete' => [
'url' => 'https://maps.googleapis.com/maps/api/place/queryautocomplete/',
'type' => 'GET',
'key' => null,
'endpoint' => true,
'responseDefaultKey' => 'predictions',
'param' => [
'key' => null,
'input' => null,
'offset' => null,
'location' => null,
'radius' => null,
'language' => null,
]
],
],
/*
|--------------------------------------------------------------------------
| End point
|--------------------------------------------------------------------------
|
|
*/
'endpoint' => [
'xml' => 'xml?',
'json' => 'json?',
],
];

View File

@@ -1,23 +1,33 @@
<?php
return array(
return [
/*
|--------------------------------------------------------------------------
| One Signal App Id
|--------------------------------------------------------------------------
|
|
*/
'app_id' => '00d23dae-1209-42cc-bea7-e1f17cee27fa',
|--------------------------------------------------------------------------
| One Signal App Id
|--------------------------------------------------------------------------
|
| Your OneSignal App ID from the OneSignal dashboard
|
*/
'app_id' => env('ONESIGNAL_APP_ID'),
/*
|--------------------------------------------------------------------------
| Rest API Key
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
| Rest API Key
|--------------------------------------------------------------------------
|
|
*/
'rest_api_key' => 'NTQwMDY4ZjUtODlmMy00NzAzLTg1ZDItMWNhMDgyOGVkYzRk',
'user_auth_key' => 'YOUR-USER-AUTH-KEY'
);
| Your OneSignal REST API Key from the OneSignal dashboard
|
*/
'rest_api_key' => env('ONESIGNAL_REST_API_KEY'),
/*
|--------------------------------------------------------------------------
| User Auth Key
|--------------------------------------------------------------------------
|
| Your OneSignal User Auth Key (optional, for user-level API access)
|
*/
'user_auth_key' => env('ONESIGNAL_USER_AUTH_KEY'),
];