772 lines
33 KiB
PHP
772 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Passenger;
|
|
use App\Models\PassengerToken;
|
|
use App\Models\Driver;
|
|
use App\Models\DriverToken;
|
|
use App\Models\CarRegistration;
|
|
use App\Models\DriverDocument;
|
|
use App\Services\LegacyEncryption;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Str;
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\Key;
|
|
|
|
class AuthController extends Controller
|
|
{
|
|
protected $encryption;
|
|
|
|
public function __construct(LegacyEncryption $encryption)
|
|
{
|
|
$this->encryption = $encryption;
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// PASSENGER LOGIN & REGISTRATION
|
|
// ══════════════════════════════════════════════
|
|
|
|
public function passengerLogin(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'phone' => 'required|string',
|
|
'password' => 'required|string',
|
|
'fingerprint' => 'required|string',
|
|
'fcm_token' => 'required|string',
|
|
]);
|
|
|
|
$phone = $request->input('phone');
|
|
$password = $request->input('password');
|
|
$fingerprint = $request->input('fingerprint');
|
|
$fcmToken = $request->input('fcm_token');
|
|
|
|
$rawPhone = $phone;
|
|
$localPhone = '0' . substr($phone, 3);
|
|
$encRawPhone = $this->encryption->encrypt($rawPhone);
|
|
$encLocalPhone = $this->encryption->encrypt($localPhone);
|
|
|
|
$passenger = Passenger::active()
|
|
->whereIn('phone', [$rawPhone, $localPhone, $encRawPhone, $encLocalPhone])
|
|
->first();
|
|
|
|
if (!$passenger) {
|
|
return $this->failure('Invalid credentials');
|
|
}
|
|
|
|
$storedPassword = $passenger->password;
|
|
if (!password_verify($password, $storedPassword) &&
|
|
!hash_equals($storedPassword, hash_hmac('sha256', $password, config('intaleq.jwt_secret')))) {
|
|
return $this->failure('Invalid credentials');
|
|
}
|
|
|
|
$passengerToken = PassengerToken::where('passengerID', $passenger->id)->first();
|
|
$encryptedFcm = $this->encryption->encrypt($fcmToken);
|
|
if ($passengerToken) {
|
|
$passengerToken->update([
|
|
'token' => $encryptedFcm,
|
|
'fingerPrint' => $fingerprint,
|
|
]);
|
|
} else {
|
|
PassengerToken::create([
|
|
'token' => $encryptedFcm,
|
|
'passengerID' => $passenger->id,
|
|
'fingerPrint' => $fingerprint,
|
|
]);
|
|
}
|
|
|
|
if (empty($passenger->api_key)) {
|
|
$this->generateApiKeys($passenger);
|
|
}
|
|
|
|
$jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 3600); // 1 Hour
|
|
|
|
return $this->success([
|
|
'token' => $jwt,
|
|
'expires_in' => 3600,
|
|
'user_id' => $passenger->id,
|
|
'api_key' => $passenger->api_key,
|
|
'api_secret' => $passenger->api_secret,
|
|
]);
|
|
}
|
|
|
|
public function passengerRegister(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'phone' => 'required|string',
|
|
'email' => 'required|email',
|
|
'first_name' => 'required|string',
|
|
'last_name' => 'required|string',
|
|
'fingerprint' => 'nullable|string',
|
|
'fcm_token' => 'nullable|string',
|
|
]);
|
|
|
|
$phone = $request->input('phone');
|
|
$encryptedPhone = $this->encryption->encrypt($phone);
|
|
|
|
if (Passenger::where('phone', $encryptedPhone)->exists()) {
|
|
return $this->failure('Phone number already registered');
|
|
}
|
|
|
|
$passengerId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999);
|
|
|
|
$passenger = Passenger::create([
|
|
'id' => $passengerId,
|
|
'phone' => $encryptedPhone,
|
|
'email' => $this->encryption->encrypt($request->input('email')),
|
|
'password' => password_hash($request->input('password', '123456'), PASSWORD_BCRYPT),
|
|
'first_name' => $this->encryption->encrypt($request->input('first_name')),
|
|
'last_name' => $this->encryption->encrypt($request->input('last_name')),
|
|
'gender' => $this->encryption->encrypt($request->input('gender', 'male')),
|
|
'birthdate' => $this->encryption->encrypt($request->input('birthdate', '2000-01-01')),
|
|
'site' => $request->input('site', 'Syria'),
|
|
]);
|
|
|
|
if ($request->has('fcm_token')) {
|
|
PassengerToken::create([
|
|
'token' => $this->encryption->encrypt($request->input('fcm_token')),
|
|
'passengerID' => $passengerId,
|
|
'fingerPrint' => $request->input('fingerprint', 'unknown'),
|
|
]);
|
|
}
|
|
|
|
$this->generateApiKeys($passenger);
|
|
$jwt = $this->createJwt($passengerId, 'passenger', $request->input('fingerprint', 'unknown'), 3600);
|
|
|
|
return $this->success([
|
|
'token' => $jwt,
|
|
'expires_in' => 3600,
|
|
'user_id' => $passengerId,
|
|
'api_key' => $passenger->api_key,
|
|
'api_secret' => $passenger->api_secret,
|
|
], 201);
|
|
}
|
|
|
|
public function driverRegister(Request $request): JsonResponse
|
|
{
|
|
// 1. Validate Required Driver Fields
|
|
$request->validate([
|
|
'phone' => 'required|string',
|
|
'password' => 'required|string',
|
|
'first_name' => 'required|string',
|
|
'last_name' => 'required|string',
|
|
// Car Fields
|
|
'vin' => 'required|string',
|
|
'car_plate' => 'required|string',
|
|
'make' => 'required|string',
|
|
'model' => 'required|string',
|
|
'year' => 'required|string',
|
|
'expiration_date' => 'required|string',
|
|
'color' => 'required|string',
|
|
'owner' => 'required|string',
|
|
'color_hex' => 'required|string',
|
|
'fuel' => 'required|string',
|
|
// Documents
|
|
'driver_license_front' => 'required|url',
|
|
'driver_license_back' => 'required|url',
|
|
'car_license_front' => 'required|url',
|
|
'car_license_back' => 'required|url',
|
|
]);
|
|
|
|
$data = $request->all();
|
|
|
|
// 2. Format Phone (Syrian Logic)
|
|
$phone = $data['phone'];
|
|
$phone = preg_replace('/[ \-\(\)\+]/', '', $phone);
|
|
$phone = trim($phone);
|
|
|
|
if (strpos($phone, '00963') === 0) $phone = substr($phone, 2);
|
|
elseif (strpos($phone, '0963') === 0) $phone = substr($phone, 1);
|
|
|
|
if (strpos($phone, '96309') === 0) $phone = '9639' . substr($phone, 5);
|
|
elseif (strpos($phone, '9630') === 0) $phone = '9639' . substr($phone, 4);
|
|
elseif (strpos($phone, '09') === 0) $phone = '963' . substr($phone, 1);
|
|
elseif (strpos($phone, '9') === 0 && strlen($phone) == 9) $phone = '963' . $phone;
|
|
elseif (strpos($phone, '0') === 0 && strlen($phone) == 10) $phone = '963' . substr($phone, 1);
|
|
|
|
if (strpos($phone, '963') === 0 && strlen($phone) > 3) {
|
|
if (strpos($phone, '9639') !== 0) {
|
|
$phone = '9639' . substr($phone, 3);
|
|
}
|
|
}
|
|
$data['phone'] = $phone;
|
|
|
|
// 3. Format Birthdate
|
|
if (!empty($data['birthdate'])) {
|
|
$data['birthdate'] = trim($data['birthdate']) . '-01-01';
|
|
} else {
|
|
$data['birthdate'] = '1970-01-01';
|
|
}
|
|
|
|
// 4. Generate ID & Email
|
|
if (empty($data['id'])) {
|
|
$data['id'] = 'DRV' . date('YmdHis') . random_int(1000, 9999);
|
|
}
|
|
if (empty($data['email'])) {
|
|
$data['email'] = $data['phone'] . '@intaleqapp.com';
|
|
}
|
|
|
|
// 5. Encrypt Driver Fields
|
|
$toEncryptDriver = [
|
|
'phone', 'email', 'first_name', 'last_name', 'name_arabic', 'gender',
|
|
'national_number', 'address', 'site', 'fullNameMaritial', 'birthdate'
|
|
];
|
|
foreach ($toEncryptDriver as $f) {
|
|
if (!empty($data[$f]) && $data[$f] !== 'Not specified') {
|
|
$data[$f] = $this->encryption->encrypt($data[$f]);
|
|
} else {
|
|
$data[$f] = null;
|
|
}
|
|
}
|
|
|
|
// 6. Encrypt Car Fields
|
|
$car = [
|
|
'vin' => $this->encryption->encrypt($request->input('vin')),
|
|
'car_plate' => $this->encryption->encrypt($request->input('car_plate')),
|
|
'make' => $request->input('make'),
|
|
'model' => $request->input('model'),
|
|
'year' => $request->input('year'),
|
|
'expiration_date' => $request->input('expiration_date'),
|
|
'color' => $request->input('color'),
|
|
'owner' => $this->encryption->encrypt($request->input('owner')),
|
|
'color_hex' => $request->input('color_hex'),
|
|
'fuel' => $request->input('fuel'),
|
|
'vehicle_category_id' => $request->input('vehicle_category_id', 1),
|
|
'fuel_type_id' => $request->input('fuel_type_id', 1),
|
|
];
|
|
|
|
// 7. Hash Password (HMAC + password_hash)
|
|
$pepper = config('intaleq.hmac_secret', env('SECRET_KEY_HMAC'));
|
|
$baseParts = [
|
|
$data['id'],
|
|
$phone, // Original formatted phone
|
|
];
|
|
if (!empty($request->input('national_number')) && $request->input('national_number') !== 'Not specified') {
|
|
$baseParts[] = $request->input('national_number');
|
|
} elseif (!empty($request->input('birthdate'))) {
|
|
$year = substr($request->input('birthdate'), 0, 4);
|
|
if (preg_match('/^\d{4}$/', $year)) {
|
|
$baseParts[] = $year;
|
|
}
|
|
}
|
|
$baseString = implode('|', $baseParts);
|
|
$rawSecret = hash_hmac('sha256', $baseString, $pepper, true);
|
|
$pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT);
|
|
|
|
// 8. Check Duplicates
|
|
$dup = DB::connection('primary')->table('driver')
|
|
->where('phone', $data['phone'])
|
|
->orWhere('email', $data['email'])
|
|
->exists();
|
|
|
|
if ($dup) {
|
|
return response()->json(['status' => 'Failure', 'data' => 'Phone or email already registered.']);
|
|
}
|
|
|
|
DB::connection('primary')->beginTransaction();
|
|
try {
|
|
// 9. Insert Driver
|
|
DB::connection('primary')->table('driver')->insert([
|
|
'id' => $data['id'],
|
|
'phone' => $data['phone'],
|
|
'email' => $data['email'],
|
|
'password' => $pwdHashed,
|
|
'gender' => $data['gender'] ?? 'Male',
|
|
'license_type' => $request->input('license_type', 'yet'),
|
|
'national_number' => $data['national_number'],
|
|
'name_arabic' => $data['name_arabic'],
|
|
'issue_date' => $request->input('issue_date', '2020-01-01'),
|
|
'expiry_date' => $request->input('expiry_date', 'yet'),
|
|
'license_categories' => $request->input('license_categories', 'B'),
|
|
'address' => $data['address'],
|
|
'licenseIssueDate' => $request->input('licenseIssueDate', '2020-01-01'),
|
|
'status' => $request->input('status', 'yet'),
|
|
'birthdate' => $data['birthdate'],
|
|
'site' => $data['site'] ?? 'demascus',
|
|
'first_name' => $data['first_name'],
|
|
'last_name' => $data['last_name'],
|
|
'accountBank' => 'yet',
|
|
'bankCode' => 'yet',
|
|
'employmentType' => $request->input('employmentType', 'yet'),
|
|
'maritalStatus' => $request->input('maritalStatus', 'yet'),
|
|
'fullNameMaritial' => $data['fullNameMaritial'] ?? 'yet',
|
|
'expirationDate' => $request->input('expirationDate', 'yet'),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
// 10. Insert Car Registration
|
|
$isDefault = DB::connection('primary')->table('CarRegistration')
|
|
->where('driverID', $data['id'])->exists() ? 0 : 1;
|
|
|
|
$carRegId = DB::connection('primary')->table('CarRegistration')->insertGetId([
|
|
'driverID' => $data['id'],
|
|
'vin' => $car['vin'],
|
|
'car_plate' => $car['car_plate'],
|
|
'make' => $car['make'],
|
|
'model' => $car['model'],
|
|
'year' => $car['year'],
|
|
'expiration_date' => $car['expiration_date'],
|
|
'color' => $car['color'],
|
|
'owner' => $car['owner'],
|
|
'color_hex' => $car['color_hex'],
|
|
'fuel' => $car['fuel'],
|
|
'vehicle_category_id' => $car['vehicle_category_id'],
|
|
'fuel_type_id' => $car['fuel_type_id'],
|
|
'isDefault' => $isDefault,
|
|
'status' => 'yet',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
// 11. Insert Documents
|
|
$docKeys = [
|
|
'driver_license_front', 'driver_license_back',
|
|
'car_license_front', 'car_license_back'
|
|
];
|
|
$docUrls = [];
|
|
foreach ($docKeys as $k) {
|
|
$url = $request->input($k);
|
|
$docUrls[$k] = $url;
|
|
$name = basename(parse_url($url, PHP_URL_PATH) ?? '');
|
|
if ($name === '') { $name = $k . '_' . time() . '.jpg'; }
|
|
|
|
DB::connection('primary')->table('driver_documents')->insert([
|
|
'driverID' => $data['id'],
|
|
'doc_type' => $k,
|
|
'image_name' => $name,
|
|
'link' => $url,
|
|
'upload_date' => now(),
|
|
]);
|
|
}
|
|
|
|
DB::connection('primary')->commit();
|
|
|
|
// 12. FCM Notification to Admin (Fire and Forget)
|
|
try {
|
|
$fcm = resolve(\App\Services\FcmService::class);
|
|
$driverFullName = $request->input('first_name') . ' ' . $request->input('last_name');
|
|
$fcm->sendToTopic(
|
|
'service',
|
|
'تسجيل سائق جديد',
|
|
"سائق جديد ($driverFullName) سجل برقم ID: {$data['id']} وهو بانتظار المراجعة والتفعيل.",
|
|
['category' => 'new_driver_registration']
|
|
);
|
|
} catch (\Exception $e) {
|
|
\Illuminate\Support\Facades\Log::error("FCM Admin notification failed: " . $e->getMessage());
|
|
}
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'driverID' => $data['id'],
|
|
'carRegID' => $carRegId,
|
|
'documents' => $docUrls
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
DB::connection('primary')->rollBack();
|
|
return response()->json(['status' => 'Failure', 'message' => 'Registration failed: ' . $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// DRIVER LOGIN
|
|
// ══════════════════════════════════════════════
|
|
|
|
public function driverLogin(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'email' => 'required|string',
|
|
'password' => 'required|string',
|
|
]);
|
|
|
|
$email = $request->input('email');
|
|
$searchEmail = (str_contains($email, '+') || str_contains($email, '/')) ? $email : $this->encryption->encrypt($email);
|
|
|
|
$row = DB::connection('primary')
|
|
->table('driver as d')
|
|
->leftJoin('phone_verification', 'phone_verification.phone_number', '=', 'd.phone')
|
|
->leftJoin('invites', 'invites.inviterDriverPhone', '=', 'd.phone')
|
|
->leftJoin('shamCash', 'shamCash.driver_id', '=', 'd.id')
|
|
->leftJoin('CarRegistration', 'CarRegistration.driverID', '=', 'd.id')
|
|
->leftJoin('captain_bank', 'captain_bank.captain_id', '=', 'd.id')
|
|
->select([
|
|
'd.id', 'd.phone', 'd.email', 'd.gender', 'd.status',
|
|
'd.first_name', 'd.last_name', 'd.password',
|
|
'd.name_arabic',
|
|
'phone_verification.verified as is_verified',
|
|
'invites.isInstall',
|
|
'shamCash.is_claimed',
|
|
'CarRegistration.make', 'CarRegistration.model', 'CarRegistration.year',
|
|
'captain_bank.bankCode', 'captain_bank.accountBank',
|
|
])
|
|
->where('d.email', $searchEmail)
|
|
->first();
|
|
|
|
if (!$row) {
|
|
return response()->json(['status' => 'failure', 'message' => 'Invalid credentials']);
|
|
}
|
|
|
|
$driver = (array) $row;
|
|
|
|
// Verify password
|
|
if (!password_verify($request->input('password'), $driver['password'])) {
|
|
return response()->json(['status' => 'failure', 'message' => 'Invalid credentials']);
|
|
}
|
|
|
|
// Decrypt necessary fields
|
|
$fieldsToDecrypt = ['email', 'phone', 'first_name', 'last_name', 'gender', 'name_arabic'];
|
|
foreach ($fieldsToDecrypt as $field) {
|
|
if (!empty($driver[$field])) {
|
|
$dec = $this->encryption->decrypt($driver[$field]);
|
|
if ($dec) $driver[$field] = $dec;
|
|
}
|
|
}
|
|
unset($driver['password']); // don't send password
|
|
|
|
// Default values for null fields
|
|
$driver['is_verified'] = $driver['is_verified'] ?? 0;
|
|
$driver['isInstall'] = $driver['isInstall'] ?? 0;
|
|
$driver['is_claimed'] = $driver['is_claimed'] ?? 0;
|
|
$driver['bankCode'] = $driver['bankCode'] ?? '';
|
|
$driver['accountBank'] = $driver['accountBank'] ?? '';
|
|
$driver['make'] = $driver['make'] ?? '';
|
|
$driver['model'] = $driver['model'] ?? '';
|
|
$driver['year'] = $driver['year'] ?? '';
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => [$driver]
|
|
]);
|
|
}
|
|
|
|
|
|
|
|
public function driverLoginGoogle(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['id' => 'required|string']);
|
|
$id = $request->input('id');
|
|
|
|
$row = DB::connection('primary')
|
|
->table('driver as d')
|
|
->leftJoin('phone_verification', 'phone_verification.phone_number', '=', 'd.phone')
|
|
->leftJoin('invites', 'invites.inviterDriverPhone', '=', 'd.phone')
|
|
->leftJoin('driver_gifts', 'driver_gifts.driver_id', '=', 'd.id')
|
|
->leftJoin('CarRegistration', 'CarRegistration.driverID', '=', 'd.id')
|
|
->leftJoin('captain_bank', 'captain_bank.captain_id', '=', 'd.id')
|
|
->select([
|
|
'd.id', 'd.phone', 'd.email', 'd.gender', 'd.status',
|
|
'd.first_name', 'd.last_name',
|
|
'd.name_arabic',
|
|
'd.accountBank', 'd.bankCode',
|
|
'phone_verification.verified as is_verified',
|
|
'invites.isInstall',
|
|
'driver_gifts.is_claimed',
|
|
'CarRegistration.make', 'CarRegistration.model', 'CarRegistration.year',
|
|
'captain_bank.bankCode as bank_code_alt', 'captain_bank.accountBank as account_bank_alt',
|
|
])
|
|
->where('d.id', $id)
|
|
->first();
|
|
|
|
if (!$row) {
|
|
return response()->json(['status' => 'Failure', 'data' => 'User does not exist.']);
|
|
}
|
|
|
|
$driver = (array) $row;
|
|
|
|
// Decrypt necessary fields
|
|
$fieldsToDecrypt = ['email', 'phone', 'first_name', 'last_name', 'gender', 'name_arabic'];
|
|
foreach ($fieldsToDecrypt as $field) {
|
|
if (!empty($driver[$field])) {
|
|
$dec = $this->encryption->decrypt($driver[$field]);
|
|
if ($dec) $driver[$field] = $dec;
|
|
}
|
|
}
|
|
|
|
// Default values for null fields
|
|
$driver['is_verified'] = $driver['is_verified'] ?? 0;
|
|
$driver['isInstall'] = $driver['isInstall'] ?? 0;
|
|
$driver['is_claimed'] = $driver['is_claimed'] ?? 0;
|
|
$driver['bankCode'] = $driver['bankCode'] ?? '';
|
|
$driver['accountBank'] = $driver['accountBank'] ?? '';
|
|
$driver['make'] = $driver['make'] ?? '';
|
|
$driver['model'] = $driver['model'] ?? '';
|
|
$driver['year'] = $driver['year'] ?? '';
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'count' => 1,
|
|
'data' => [$driver]
|
|
]);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// HANDSHAKE (V1 Compatibility)
|
|
// ══════════════════════════════════════════════
|
|
|
|
public function passengerJwtHandshake(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
|
|
|
|
$passenger = Passenger::find($request->input('id'));
|
|
if (!$passenger) return $this->failure('User not found');
|
|
|
|
// Security Check: Verify fingerprint matches stored token
|
|
$storedToken = DB::connection('primary')->table('tokens')
|
|
->where('passengerID', $passenger->id)
|
|
->first();
|
|
|
|
if ($storedToken && !hash_equals((string)$storedToken->fingerPrint, (string)$request->input('fingerPrint'))) {
|
|
return $this->failure('Security mismatch: Invalid device fingerprint', 403);
|
|
}
|
|
|
|
if (empty($passenger->api_key)) {
|
|
$this->generateApiKeys($passenger);
|
|
}
|
|
|
|
$jwt = $this->createJwt($passenger->id, 'passenger', $request->input('fingerPrint'), 3600);
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'jwt' => $jwt,
|
|
'expires_in' => 3600,
|
|
'api_key' => $passenger->api_key,
|
|
'api_secret' => $passenger->api_secret
|
|
]);
|
|
}
|
|
|
|
public function driverJwtHandshake(Request $request): JsonResponse
|
|
{
|
|
// Flutter sends the email in the 'password' field, plus 'id' and 'fingerPrint'
|
|
$request->validate([
|
|
'id' => 'required|string',
|
|
'fingerPrint' => 'required|string',
|
|
'password' => 'required|string',
|
|
'fcm_token' => 'required|string'
|
|
]);
|
|
|
|
$driver = Driver::where('id', $request->input('id'))->first();
|
|
if (!$driver) return $this->failure('User not found');
|
|
|
|
// The Flutter app sends the app-level secret (passnpassenger) in the 'password' field.
|
|
// Since the Flutter app modifies this string locally (e.g., split(Env.addd)[0]),
|
|
// it may not match the raw env('passwordnewpassenger') on the server exactly.
|
|
// We will rely on the fingerprint check below for security, as done in passengerJwtHandshake.
|
|
$appSecret = config('intaleq.wallet_app_password', '');
|
|
if ($appSecret !== '') {
|
|
if ($request->input('password') !== $appSecret && $request->input('password') !== $this->encryption->decrypt($driver->email)) {
|
|
\Log::warning('App verification mismatch, proceeding to fingerprint check', [
|
|
'driver_id' => $driver->id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Security Check: Verify fingerprint matches stored token
|
|
$storedToken = DB::connection('primary')->table('driverToken')
|
|
->where('captain_id', $driver->id)
|
|
->first();
|
|
|
|
// If a fingerprint exists, verify it. Otherwise, save the new one (acts as first time)
|
|
if ($storedToken) {
|
|
if (!empty($storedToken->fingerPrint) && !hash_equals((string)$storedToken->fingerPrint, (string)$request->input('fingerPrint'))) {
|
|
return $this->failure('Security mismatch: Invalid device fingerprint', 403);
|
|
}
|
|
// Update fingerprint and token
|
|
DB::connection('primary')->table('driverToken')->where('captain_id', $driver->id)
|
|
->update([
|
|
'fingerPrint' => $request->input('fingerPrint'),
|
|
'token' => $request->input('fcm_token'),
|
|
]);
|
|
} else {
|
|
DB::connection('primary')->table('driverToken')->insert([
|
|
'captain_id' => $driver->id,
|
|
'fingerPrint' => $request->input('fingerPrint'),
|
|
'token' => $request->input('fcm_token'),
|
|
]);
|
|
}
|
|
|
|
if (empty($driver->api_key)) {
|
|
$this->generateApiKeys($driver);
|
|
}
|
|
|
|
$jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerPrint'), 14400);
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'jwt' => $jwt,
|
|
'expires_in' => 14400,
|
|
'api_key' => $driver->api_key,
|
|
'api_secret' => $driver->api_secret
|
|
]);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// WALLET LOGIN
|
|
// ══════════════════════════════════════════════
|
|
|
|
public function passengerWalletLogin(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
|
|
$jwt = $this->createWalletJwt($request->input('id'), $request->input('fingerPrint'), $request->input('aud', 'TripzWallet'), 60);
|
|
$hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret'));
|
|
|
|
return $this->success(['jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60]);
|
|
}
|
|
|
|
public function driverWalletLogin(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['id' => 'required|string', 'fingerPrint' => 'required|string']);
|
|
$jwt = $this->createWalletJwt($request->input('id'), $request->input('fingerPrint'), $request->input('aud', 'TripzWallet'), 60, config('intaleq.wallet_jwt_secret'));
|
|
$hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret'));
|
|
|
|
return $this->success(['jwt' => $jwt, 'hmac' => $hmac, 'expires_in' => 60]);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// ADMIN LOGIN
|
|
// ══════════════════════════════════════════════
|
|
|
|
public function adminLogin(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['device_number' => 'required|string', 'password' => 'required|string']);
|
|
$admin = DB::connection('primary')->table('adminUser')->where('device_number', $request->input('device_number'))->first();
|
|
|
|
if (!$admin || !password_verify($request->input('password'), $admin->password ?? '')) {
|
|
return $this->failure('Invalid credentials');
|
|
}
|
|
|
|
$jwt = $this->createJwt((string)$admin->id, 'admin', $request->input('device_number'), 900);
|
|
return $this->success(['token' => $jwt, 'expires_in' => 900, 'user_id' => $admin->id]);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// GOOGLE LOGIN
|
|
// ══════════════════════════════════════════════
|
|
|
|
public function passengerLoginGoogle(Request $request): JsonResponse
|
|
{
|
|
$request->validate(['email' => 'required|string', 'id' => 'required|string']);
|
|
$email = $request->input('email');
|
|
$searchEmail = (str_contains($email, '+') || str_contains($email, '/')) ? $email : $this->encryption->encrypt($email);
|
|
|
|
$row = DB::connection('primary')
|
|
->table('passengers as p')
|
|
->leftJoin('phone_verification_passenger', 'phone_verification_passenger.phone_number', '=', 'p.phone')
|
|
->leftJoin('invitesToPassengers', 'invitesToPassengers.inviterPassengerPhone', '=', 'p.phone')
|
|
->leftJoin('promos', 'promos.passengerID', '=', 'p.id')
|
|
->select([
|
|
'p.id', 'p.phone', 'p.email', 'p.gender', 'p.status',
|
|
'p.birthdate', 'p.site', 'p.first_name', 'p.last_name',
|
|
'p.sosPhone', 'p.education', 'p.employmentType', 'p.maritalStatus',
|
|
'p.created_at', 'p.updated_at',
|
|
'phone_verification_passenger.verified',
|
|
'invitesToPassengers.isInstall',
|
|
'invitesToPassengers.inviteCode',
|
|
'invitesToPassengers.isGiftToken',
|
|
'promos.promo_code as promo',
|
|
'promos.amount as discount',
|
|
'promos.validity_end_date as validity',
|
|
'p.api_key',
|
|
'p.api_secret',
|
|
])
|
|
->selectSub(function ($query) use ($request) {
|
|
$query->from('packageInfo')
|
|
->select('version')
|
|
->where('platform', $request->input('platform', 'ios'))
|
|
->limit(1);
|
|
}, 'package')
|
|
->where('p.email', $searchEmail)
|
|
->where('p.id', $request->input('id'))
|
|
->first();
|
|
|
|
if (!$row) return response()->json(['status' => 'Failure', 'data' => 'User does not exist.']);
|
|
|
|
$data = (array) $row;
|
|
$data['package'] = $data['package'] ?? '1.1.33'; // Default to avoid Null error in Flutter
|
|
|
|
// Ensure API keys exist
|
|
if (empty($data['api_key'])) {
|
|
$passenger = Passenger::find($data['id']);
|
|
if ($passenger) {
|
|
$this->generateApiKeys($passenger);
|
|
$data['api_key'] = $passenger->api_key;
|
|
$data['api_secret'] = $passenger->api_secret;
|
|
}
|
|
}
|
|
|
|
foreach ($data as $key => $value) {
|
|
if (is_string($value) && !in_array($key, ['id', 'status', 'created_at', 'updated_at', 'verified', 'isInstall', 'isGiftToken', 'api_key', 'api_secret', 'package'])) {
|
|
$dec = $this->encryption->decrypt($value);
|
|
if ($dec) $data[$key] = $dec;
|
|
}
|
|
}
|
|
|
|
// Fetch Notification Token & Fingerprint
|
|
$tokenRow = DB::connection('primary')->table('tokens')->where('passengerID', $data['id'])->first();
|
|
if ($tokenRow) {
|
|
$data['fcm_token'] = $this->encryption->decrypt($tokenRow->token);
|
|
$data['fingerprint'] = $tokenRow->fingerPrint;
|
|
} else {
|
|
$data['fcm_token'] = null;
|
|
$data['fingerprint'] = null;
|
|
}
|
|
|
|
// Generate JWT using the header fingerprint, or fallback to the stored one
|
|
$clientFp = $request->header('X-Device-FP');
|
|
$jwtFp = !empty($clientFp) ? $clientFp : ($data['fingerprint'] ?? 'unknown');
|
|
$jwt = $this->createJwt($data['id'], 'passenger', $jwtFp, 3600);
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'count' => 1,
|
|
'data' => [$data],
|
|
'jwt' => $jwt,
|
|
'expires_in' => 3600
|
|
]);
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// HELPERS
|
|
// ══════════════════════════════════════════════
|
|
|
|
private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry): string
|
|
{
|
|
$payload = [
|
|
'user_id' => $userId,
|
|
'user_type' => $userType,
|
|
'fingerprint' => $fingerprint,
|
|
'iat' => time(),
|
|
'exp' => time() + $expiry,
|
|
'aud' => 'mobile-app',
|
|
'iss' => 'Tripz',
|
|
'jti' => bin2hex(random_bytes(16)),
|
|
];
|
|
return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256');
|
|
}
|
|
|
|
private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry, ?string $secret = null): string
|
|
{
|
|
$payload = [
|
|
'user_id' => $userId,
|
|
'fingerPrint' => hash('sha256', $fingerprint . config('intaleq.fp_pepper', '')),
|
|
'exp' => time() + $expiry,
|
|
'iat' => time(),
|
|
'iss' => 'Tripz-Wallet',
|
|
'aud' => $audience,
|
|
'jti' => bin2hex(random_bytes(16)),
|
|
];
|
|
return JWT::encode($payload, $secret ?? config('intaleq.jwt_secret'), 'HS256');
|
|
}
|
|
|
|
private function generateApiKeys($model)
|
|
{
|
|
$model->api_key = bin2hex(random_bytes(16));
|
|
$model->api_secret = bin2hex(random_bytes(32));
|
|
DB::connection('primary')->table($model->getTable())->where('id', $model->id)->update([
|
|
'api_key' => $model->api_key,
|
|
'api_secret' => $model->api_secret
|
|
]);
|
|
}
|
|
}
|