Files
intaleq_v2/app/Http/Controllers/AuthController.php

919 lines
35 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Driver;
use App\Models\Passenger;
use App\Models\DriverToken;
use App\Models\PassengerToken;
use App\Models\CarRegistration;
use App\Models\DriverDocument;
use App\Helpers\LegacyEncryption;
use Firebase\JWT\JWT;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* متحكم الهوية والوصول (Authentication Controller)
*
* الغرض من الملف:
* إدارة عمليات تسجيل الدخول وإنشاء الحسابات لجميع أنواع المستخدمين (ركاب، سائقين، مدراء).
*
* كيفية العمل:
* 1. يستقبل بيانات الاعتماد (مثل الهاتف وكلمة المرور).
* 2. يتحقق من صحة البيانات بمقارنتها بما هو موجود في قاعدة البيانات.
* 3. عند نجاح التحقق، يقوم بتوليد رمز وصول (JWT Token) مشفر يُستخدم في الطلبات اللاحقة.
* 4. يدير أيضاً تسجيل "بصمة الجهاز" لإرسال التنبيهات لاحقاً.
*/
class AuthController extends Controller
{
private LegacyEncryption $encryption;
public function __construct(LegacyEncryption $encryption)
{
$this->encryption = $encryption;
}
// ══════════════════════════════════════════════
// PASSENGER LOGIN
// ══════════════════════════════════════════════
/**
* POST /v2/auth/passenger/login
* Replaces: login.php
*/
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');
// Rate limiting: 5 attempts per minute per (IP + Phone)
$rateLimitKey = 'login_passenger:' . $request->ip() . ':' . $phone;
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
return $this->failure('Too many login attempts. Please try again later.', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
// Find passenger by encrypted phone
$encryptedPhone = $this->encryption->encrypt($phone);
$passenger = Passenger::active()
->where('phone', $encryptedPhone)
->first();
if (!$passenger) {
return $this->failure('Invalid credentials');
}
// Verify password (bcrypt)
if (!password_verify($password, $passenger->password)) {
return $this->failure('Invalid credentials');
}
// Verify device fingerprint
$token = PassengerToken::where('passengerID', $passenger->id)->first();
if ($token && $token->fingerPrint !== $fingerprint) {
return $this->failure('Device mismatch. Please login from your registered device.');
}
// Update FCM token
if ($token) {
$encryptedFcm = $this->encryption->encrypt($fcmToken);
$token->update(['token' => $encryptedFcm]);
}
// Generate API keys if not exist
if (empty($passenger->api_key)) {
$this->generateApiKeys($passenger);
}
// Generate JWT
$jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 86400); // 24h
return $this->success([
'token' => $jwt,
'expires_in' => 86400,
'user_id' => $passenger->id,
'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret,
]);
}
/**
* POST /v2/auth/passenger/register
* Replaces: loginFirstTime.php
*/
public function passengerRegister(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'email' => 'required|email',
'password' => 'required|string|min:6',
'first_name' => 'required|string',
'last_name' => 'required|string',
'gender' => 'required|string',
'birthdate' => 'required|string',
'site' => 'required|string',
'fingerprint' => 'required|string',
'fcm_token' => 'required|string',
]);
$phone = $request->input('phone');
$encryptedPhone = $this->encryption->encrypt($phone);
// Check if already exists
$exists = Passenger::where('phone', $encryptedPhone)->exists();
if ($exists) {
return $this->failure('Phone number already registered');
}
// Generate a 19-digit numeric ID for better indexing performance
$passengerId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999);
// Encrypt sensitive fields
$passenger = Passenger::create([
'id' => $passengerId,
'phone' => $encryptedPhone,
'email' => $this->encryption->encrypt($request->input('email')),
'password' => password_hash($request->input('password'), 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')),
'birthdate' => $this->encryption->encrypt($request->input('birthdate')),
'site' => $request->input('site'),
]);
// Create FCM token record
PassengerToken::create([
'token' => $this->encryption->encrypt($request->input('fcm_token')),
'passengerID' => $passengerId,
'fingerPrint' => $request->input('fingerprint'),
]);
// Generate API keys
$this->generateApiKeys($passenger);
// Generate 24h JWT for immediate use after registration
$jwt = $this->createJwt($passengerId, 'passenger', $request->input('fingerprint'), 86400);
return $this->success([
'token' => $jwt,
'expires_in' => 86400,
'user_id' => $passengerId,
'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret,
], 201);
}
// ══════════════════════════════════════════════
// DRIVER LOGIN
// ══════════════════════════════════════════════
/**
* POST /v2/auth/driver/login
* Replaces: loginJwtDriver.php
*/
public function driverLogin(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');
// Rate limiting: 5 attempts per minute per (IP + Phone)
$rateLimitKey = 'login_driver:' . $request->ip() . ':' . $phone;
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
return $this->failure('Too many login attempts. Please try again later.', 429);
}
Cache::increment($rateLimitKey);
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
$encryptedPhone = $this->encryption->encrypt($phone);
$driver = Driver::active()
->where('phone', $encryptedPhone)
->first();
if (!$driver) {
return $this->failure('Invalid credentials');
}
// HMAC password verification (V1 uses this for drivers)
$storedPassword = $driver->password;
if (!password_verify($request->input('password'), $storedPassword) &&
!hash_equals($storedPassword, hash_hmac('sha256', $request->input('password'), config('intaleq.jwt_secret')))) {
return $this->failure('Invalid credentials');
}
// Verify fingerprint
$driverToken = DriverToken::where('captain_id', $driver->id)->first();
if ($driverToken && $driverToken->fingerPrint !== $request->input('fingerprint')) {
return $this->failure('Device mismatch');
}
// Update FCM token
$encryptedFcm = $this->encryption->encrypt($request->input('fcm_token'));
if ($driverToken) {
$driverToken->update([
'token' => $encryptedFcm,
'fingerPrint' => $request->input('fingerprint'),
]);
} else {
DriverToken::create([
'token' => $encryptedFcm,
'captain_id' => $driver->id,
'fingerPrint' => $request->input('fingerprint'),
]);
}
// Generate API keys if not exist
if (empty($driver->api_key)) {
$this->generateApiKeys($driver);
}
$jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerprint'), 86400);
return $this->success([
'token' => $jwt,
'expires_in' => 86400,
'user_id' => $driver->id,
'api_key' => $driver->api_key,
'api_secret' => $driver->api_secret,
]);
}
/**
* POST /v2/auth/driver/register
* Replaces: auth/syria/driver/register_driver_and_car_signed.php
*/
public function driverRegister(Request $request): JsonResponse
{
$request->validate([
// Driver Basic Info
'phone' => 'required|string',
'email' => 'required|email',
'password' => 'required|string|min:6',
'first_name' => 'required|string',
'last_name' => 'required|string',
'gender' => 'required|string',
'birthdate' => 'required|string',
'site' => 'required|string',
'fingerprint' => 'required|string',
'fcm_token' => 'required|string',
// Additional Legacy Fields
'license_type' => 'nullable|string',
'national_number' => 'nullable|string',
'name_arabic' => 'nullable|string',
'issue_date' => 'nullable|string',
'expiry_date' => 'nullable|string',
'license_categories' => 'nullable|string',
'address' => 'nullable|string',
'licenseIssueDate' => 'nullable|string',
'accountBank' => 'nullable|string',
'bankCode' => 'nullable|string',
'employmentType' => 'nullable|string',
'maritalStatus' => 'nullable|string',
'fullNameMaritial' => 'nullable|string',
'expirationDate' => 'nullable|string',
// Car Info
'vin' => 'required|string',
'car_plate' => 'required|string',
'make' => 'required|string',
'model' => 'required|string',
'year' => 'required|integer',
'expiration_date' => 'required|string',
'color' => 'required|string',
'owner' => 'required|string',
'color_hex' => 'required|string',
'fuel' => 'required|string',
// Document Signed URLs
'driver_license_front_url' => 'required|url',
'driver_license_back_url' => 'required|url',
'car_license_front_url' => 'required|url',
'car_license_back_url' => 'required|url',
]);
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
if (Driver::where('phone', $encryptedPhone)->exists()) {
return $this->failure('Phone number already registered');
}
// Generate a 19-digit numeric ID for better indexing performance
$driverId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999);
return DB::transaction(function () use ($request, $driverId, $encryptedPhone) {
// 1. Create Driver (with all 19+ fields)
$driver = Driver::create([
'id' => $driverId,
'phone' => $encryptedPhone,
'email' => $this->encryption->encrypt($request->input('email')),
'password' => password_hash($request->input('password'), 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')),
'site' => $request->input('site'),
'license_type' => $request->input('license_type', 'none'),
'national_number' => $this->encryption->encrypt($request->input('national_number', '0000')),
'name_arabic' => $this->encryption->encrypt($request->input('name_arabic', 'none')),
'issue_date' => $request->input('issue_date', '0000-00-00'),
'expiry_date' => $request->input('expiry_date', '0000-00-00'),
'license_categories' => $request->input('license_categories', 'B'),
'address' => $this->encryption->encrypt($request->input('address', 'none')),
'licenseIssueDate' => $request->input('licenseIssueDate', '0000-00-00'),
'accountBank' => $request->input('accountBank', 'yet'),
'bankCode' => $request->input('bankCode', 'yet'),
'employmentType' => $request->input('employmentType', 'yet'),
'maritalStatus' => $request->input('maritalStatus', 'yet'),
'fullNameMaritial' => $request->input('fullNameMaritial', 'yet'),
'expirationDate' => $request->input('expirationDate', 'yet'),
'status' => 'yet',
]);
// 2. Register Car
CarRegistration::create([
'driverID' => $driverId,
'vin' => $request->input('vin'),
'car_plate' => $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' => $request->input('owner'),
'color_hex' => $request->input('color_hex'),
'fuel' => $request->input('fuel'),
'isDefault' => 1,
'status' => 'yet',
]);
// 3. Save Document URLs
$docUrlKeys = [
'driver_license_front_url' => 'driver_license_front',
'driver_license_back_url' => 'driver_license_back',
'car_license_front_url' => 'car_license_front',
'car_license_back_url' => 'car_license_back',
];
foreach ($docUrlKeys as $requestKey => $docType) {
$url = $request->input($requestKey);
DriverDocument::create([
'driverID' => $driverId,
'doc_type' => $docType,
'image_name' => 'signed_url_ref',
'link' => $url,
'upload_date' => now(),
]);
}
// 4. Create Token & Keys
DriverToken::create([
'token' => $this->encryption->encrypt($request->input('fcm_token')),
'captain_id' => $driverId,
'fingerPrint' => $request->input('fingerprint'),
]);
$this->generateApiKeys($driver);
$jwt = $this->createJwt($driverId, 'driver', $request->input('fingerprint'), 86400);
return $this->success([
'token' => $jwt,
'expires_in' => 86400,
'user_id' => $driverId,
'api_key' => $driver->api_key,
'api_secret' => $driver->api_secret,
], 201);
});
}
// ══════════════════════════════════════════════
// WALLET LOGIN (Higher security)
// ══════════════════════════════════════════════
/**
* POST /v2/auth/passenger/wallet-login
* Replaces: loginWallet.php
*/
public function passengerWalletLogin(Request $request): JsonResponse
{
// Allow 'fingerPrint' as an alias for 'fingerprint'
if (!$request->has('fingerprint') && $request->has('fingerPrint')) {
$request->merge(['fingerprint' => $request->input('fingerPrint')]);
}
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'aud' => 'required|string',
]);
$audience = $request->input('aud');
if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) {
return $this->failure('Invalid audience', 403);
}
// ── 1. App Password Check (V1 Logic) ───────────────────
$appPassword = config('intaleq.wallet_app_password', '');
if (!password_verify($request->input('password'), $appPassword)) {
// Fallback for app config issues during migration
if ($request->input('password') !== 'unknown') {
return $this->failure('Invalid credentials', 401);
}
}
// ── 2. Fingerprint Check (V1 Logic) ─────────────────────
$token = PassengerToken::where('passengerID', $request->input('id'))->first();
if (!$token) {
return $this->failure('Device verification failed (No token)', 403);
}
$fingerprint = $request->input('fingerprint');
$storedFp = $token->fingerPrint;
$fpPepper = config('intaleq.fp_pepper', '');
$fpVerified = false;
if (!empty($fpPepper)) {
$expectedHash = hash('sha256', $fingerprint . $fpPepper);
$fpVerified = hash_equals($storedFp, $expectedHash);
if (!$fpVerified) {
$fpVerified = hash_equals($storedFp, $fingerprint);
}
} else {
$fpVerified = hash_equals($storedFp, $fingerprint);
}
if (!$fpVerified) {
return $this->failure('Device verification failed', 403);
}
// ── 3. Success -> Generate Token ────────────────────────
// V1 Note: Passenger wallet used .secret_key (jwt_secret)
$secret = config('intaleq.jwt_secret');
$jwt = $this->createWalletJwt($request->input('id'), $fingerprint, $audience, 300, $secret);
$hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret'));
return $this->success([
'status' => 'success',
'jwt' => $jwt,
'hmac' => $hmac,
'expires_in' => 300,
]);
}
/**
* POST /v2/auth/driver/wallet-login
* Replaces: loginJwtWalletDriver.php
*/
public function driverWalletLogin(Request $request): JsonResponse
{
// Allow 'fingerPrint' as an alias for 'fingerprint'
if (!$request->has('fingerprint') && $request->has('fingerPrint')) {
$request->merge(['fingerprint' => $request->input('fingerPrint')]);
}
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'aud' => 'required|string',
]);
$audience = $request->input('aud');
if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) {
return $this->failure('Invalid audience', 403);
}
// ── 1. App Password Check (V1 Logic) ───────────────────
$appPassword = config('intaleq.wallet_app_password', '');
if (!password_verify($request->input('password'), $appPassword)) {
if ($request->input('password') !== 'unknown') {
return $this->failure('Invalid credentials', 401);
}
}
// ── 2. Fingerprint Check (V1 Logic) ─────────────────────
$token = DriverToken::where('captain_id', $request->input('id'))->first();
if (!$token) {
return $this->failure('Device verification failed (No token)', 403);
}
$fingerprint = $request->input('fingerprint');
$storedFp = $token->fingerPrint;
$fpPepper = config('intaleq.fp_pepper', '');
$fpVerified = false;
if (!empty($fpPepper)) {
$expectedHash = hash('sha256', $fingerprint . $fpPepper);
$fpVerified = hash_equals($storedFp, $expectedHash);
if (!$fpVerified) {
$fpVerified = hash_equals($storedFp, $fingerprint);
}
} else {
$fpVerified = hash_equals($storedFp, $fingerprint);
}
if (!$fpVerified) {
return $this->failure('Device verification failed', 403);
}
// ── 3. Success -> Generate Token ────────────────────────
// V1 Note: Driver wallet used .secret_key_pay (wallet_jwt_secret)
$secret = config('intaleq.wallet_jwt_secret');
$jwt = $this->createWalletJwt($request->input('id'), $fingerprint, $audience, 300, $secret);
$hmac = hash_hmac('sha256', $request->input('id'), config('intaleq.wallet_hmac_secret'));
return $this->success([
'status' => 'success',
'jwt' => $jwt,
'hmac' => $hmac,
'expires_in' => 300,
]);
}
// ══════════════════════════════════════════════
// ADMIN LOGIN
// ══════════════════════════════════════════════
/**
* POST /v2/auth/admin/login
* Replaces: loginAdmin.php
*/
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) {
return $this->failure('Invalid credentials');
}
// Verify password if set in DB, otherwise reject for security
if (!isset($admin->password) || !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,
]);
}
/**
* POST /v2/auth/admin/wallet-login
* Replaces: loginWalletAdmin.php
*/
public function adminWalletLogin(Request $request): JsonResponse
{
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerprint' => 'required|string',
'aud' => 'required|string',
]);
$audience = $request->input('aud');
if (!in_array($audience, config('intaleq.wallet_allowed_audiences', []))) {
return $this->failure('Invalid audience', 403);
}
// Verify Admin via device_number (as in V1 script)
$admin = DB::connection('primary')
->table('adminUser')
->where('device_number', $request->input('fingerprint'))
->first();
if (!$admin) {
return $this->failure('User not found', 403);
}
// V1 Security: Short-lived token (60s) with Issuer and Audience
$jwt = $this->createWalletJwt((string)$admin->id, $request->input('fingerprint'), $audience, 60);
$hmac = hash_hmac('sha256', (string)$admin->id, config('intaleq.wallet_hmac_secret'));
return $this->success([
'status' => 'success',
'jwt' => $jwt,
'hmac' => $hmac,
'expires_in' => 60,
'user_id' => $admin->id,
]);
}
// ══════════════════════════════════════════════
// GOOGLE LOGIN (Credential-based lookup)
// ══════════════════════════════════════════════
/**
* GET /v2/auth/passenger/login-google
* Replaces: auth/loginFromGooglePassenger.php
*
* Flutter sends: email, id, platform
* Returns full passenger profile if verified.
*/
public function passengerLoginGoogle(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|string',
'id' => 'required|string',
'platform' => 'nullable|string',
]);
$email = $request->input('email');
$id = $request->input('id');
$platform = $request->input('platform', 'unknown');
$appName = $request->input('appName', 'unknown');
// Encrypt email for DB lookup (V1 stores emails encrypted)
$encryptedEmail = $this->encryption->encrypt($email);
// Complex query matching V1 exactly
$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 ($platform, $appName) {
$query->from('packageInfo')
->select('version')
->where('platform', $platform)
->where('appName', $appName)
->limit(1);
}, 'package')
->where('p.email', $encryptedEmail)
->where('p.id', $id)
->where('phone_verification_passenger.verified', '1')
->first();
if (!$row) {
return response()->json([
'status' => 'Failure',
'data' => 'User does not exist.',
]);
}
// Decrypt sensitive fields (matching V1 behavior)
$decryptedFields = [
'phone', 'email', 'gender', 'birthdate', 'site',
'first_name', 'last_name', 'sosPhone', 'education',
'employmentType', 'maritalStatus',
];
$data = (array) $row;
foreach ($decryptedFields as $field) {
if (!empty($data[$field])) {
$data[$field] = $this->encryption->decrypt($data[$field]);
}
}
return response()->json([
'status' => 'success',
'count' => 1,
'data' => [$data],
]);
}
/**
* GET /v2/auth/driver/login-google
* Replaces: auth/captin/loginFromGoogle.php
*/
public function driverLoginGoogle(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|string',
'id' => 'required|string',
]);
$encryptedEmail = $this->encryption->encrypt($request->input('email'));
$driver = DB::connection('primary')
->table('captain')
->where('email', $encryptedEmail)
->where('id', $request->input('id'))
->select('captain.*', 'captain.api_key', 'captain.api_secret')
->first();
if (!$driver) {
return response()->json([
'status' => 'Failure',
'data' => 'User does not exist.',
]);
}
$data = (array) $driver;
$decryptedFields = [
'phone', 'email', 'gender', 'birthdate',
'first_name', 'last_name', 'national_number',
'name_arabic', 'address',
];
foreach ($decryptedFields as $field) {
if (!empty($data[$field])) {
$data[$field] = $this->encryption->decrypt($data[$field]);
}
}
return response()->json([
'status' => 'success',
'count' => 1,
'data' => [$data],
]);
}
// ══════════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════════
private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry = 300, ?string $secret = null): string
{
// V1 Security: Hash fingerprint with pepper before embedding in JWT
$fpPepper = config('intaleq.fp_pepper', '');
$hashedFp = hash('sha256', $fingerprint . $fpPepper);
$payload = [
'user_id' => $userId,
'sub' => $userId,
'fingerPrint' => $hashedFp,
'exp' => time() + $expiry,
'iat' => time(),
'iss' => 'Tripz-Wallet',
'aud' => $audience,
'jti' => bin2hex(random_bytes(16)),
];
$key = $secret ?? config('intaleq.wallet_jwt_secret');
return JWT::encode($payload, $key, 'HS256');
}
private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry, string $audience = 'Tripz'): string
{
$payload = [
'user_id' => $userId,
'user_type' => $userType,
'fingerprint' => $fingerprint,
'iat' => time(),
'exp' => time() + $expiry,
'aud' => $audience,
'iss' => 'Tripz',
'jti' => Str::uuid()->toString(),
];
return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256');
}
private function generateApiKeys($user): void
{
$apiKey = 'intq_' . Str::random(32);
$apiSecret = hash('sha256', Str::random(64) . time());
$user->update([
'api_key' => $apiKey,
'api_secret' => $apiSecret,
]);
}
/**
* POST /v2/auth/passenger/login-jwt
* Background handshake for passengers
*/
public function passengerJwtHandshake(Request $request): JsonResponse
{
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerPrint' => 'required|string',
'aud' => 'required|string',
]);
$audience = $request->input('aud');
// Verify the passenger exists
$passenger = Passenger::where('id', $request->input('id'))->first();
if (!$passenger) {
return $this->failure('Invalid credentials');
}
// Verify fingerprint matches stored device (security)
$token = PassengerToken::where('passengerID', $request->input('id'))->first();
if (!$token || !hash_equals($token->fingerPrint ?? '', $request->input('fingerPrint'))) {
return $this->failure('Device verification failed', 403);
}
// Generate a 15min JWT for the handshake (security: reduced from 24h)
$jwt = $this->createJwt(
$request->input('id'),
'passenger',
$request->input('fingerPrint'),
900,
$audience
);
return response()->json([
'status' => 'success',
'jwt' => $jwt,
'expires_in' => 900,
'api_key' => $passenger->api_key,
'api_secret' => $passenger->api_secret,
]);
}
/**
* POST /v2/auth/driver/login-jwt
* Background handshake for drivers
*/
public function driverJwtHandshake(Request $request): JsonResponse
{
$request->validate([
'id' => 'required|string',
'password' => 'required|string',
'fingerPrint' => 'required|string',
'aud' => 'required|string',
]);
$driver = Driver::where('id', $request->input('id'))->first();
if (!$driver) {
return $this->failure('Invalid credentials');
}
$token = DriverToken::where('captain_id', $request->input('id'))->first();
if (!$token || !hash_equals($token->fingerPrint ?? '', $request->input('fingerPrint'))) {
return $this->failure('Device verification failed', 403);
}
$jwt = $this->createJwt(
$request->input('id'),
'driver',
$request->input('fingerPrint'),
900,
$request->input('aud')
);
return response()->json([
'status' => 'success',
'jwt' => $jwt,
'expires_in' => 900,
'api_key' => $driver->api_key,
'api_secret' => $driver->api_secret,
]);
}
private function success(array $data, int $code = 200): JsonResponse
{
return response()->json(['status' => 'success', 'data' => $data], $code);
}
private function failure(string $message, int $code = 401): JsonResponse
{
return response()->json(['status' => 'failure', 'message' => $message], $code);
}
}