refactor: update OTP system to support user-specific verification tables with legacy encryption and multi-role authentication.
This commit is contained in:
@@ -6,6 +6,8 @@ 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;
|
||||
@@ -52,19 +54,19 @@ class AuthController extends Controller
|
||||
'fcm_token' => 'required|string',
|
||||
]);
|
||||
|
||||
// Rate limiting: 5 attempts per minute per IP
|
||||
$rateLimitKey = 'login_passenger:' . $request->ip();
|
||||
$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));
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$password = $request->input('password');
|
||||
$fingerprint = $request->input('fingerprint');
|
||||
$fcmToken = $request->input('fcm_token');
|
||||
|
||||
// Find passenger by encrypted phone
|
||||
$encryptedPhone = $this->encryption->encrypt($phone);
|
||||
$passenger = Passenger::active()
|
||||
@@ -137,7 +139,8 @@ class AuthController extends Controller
|
||||
return $this->failure('Phone number already registered');
|
||||
}
|
||||
|
||||
$passengerId = Str::uuid()->toString();
|
||||
// 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([
|
||||
@@ -162,12 +165,12 @@ class AuthController extends Controller
|
||||
// Generate API keys
|
||||
$this->generateApiKeys($passenger);
|
||||
|
||||
// Generate temporary JWT (5 min — for registration flow)
|
||||
$jwt = $this->createJwt($passengerId, 'passenger_temp', $request->input('fingerprint'), 300);
|
||||
// Generate 24h JWT for immediate use after registration
|
||||
$jwt = $this->createJwt($passengerId, 'passenger', $request->input('fingerprint'), 86400);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 300,
|
||||
'expires_in' => 86400,
|
||||
'user_id' => $passengerId,
|
||||
'api_key' => $passenger->api_key,
|
||||
'api_secret' => $passenger->api_secret,
|
||||
@@ -191,15 +194,19 @@ class AuthController extends Controller
|
||||
'fcm_token' => 'required|string',
|
||||
]);
|
||||
|
||||
// Rate limiting
|
||||
$rateLimitKey = 'login_driver:' . $request->ip();
|
||||
$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', 429);
|
||||
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));
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$encryptedPhone = $this->encryption->encrypt($phone);
|
||||
|
||||
$driver = Driver::active()
|
||||
@@ -256,11 +263,12 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* POST /v2/auth/driver/register
|
||||
* Replaces: loginFirstTimeDriver.php
|
||||
* 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',
|
||||
@@ -271,6 +279,40 @@ class AuthController extends Controller
|
||||
'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'));
|
||||
@@ -279,37 +321,93 @@ class AuthController extends Controller
|
||||
return $this->failure('Phone number already registered');
|
||||
}
|
||||
|
||||
$driverId = Str::uuid()->toString();
|
||||
// Generate a 19-digit numeric ID for better indexing performance
|
||||
$driverId = (string) mt_rand(1000000000, 9999999999) . mt_rand(100000000, 999999999);
|
||||
|
||||
$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')),
|
||||
'birthdate' => $this->encryption->encrypt($request->input('birthdate')),
|
||||
'site' => $request->input('site'),
|
||||
]);
|
||||
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',
|
||||
]);
|
||||
|
||||
DriverToken::create([
|
||||
'token' => $this->encryption->encrypt($request->input('fcm_token')),
|
||||
'captain_id' => $driverId,
|
||||
'fingerPrint' => $request->input('fingerprint'),
|
||||
]);
|
||||
// 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',
|
||||
]);
|
||||
|
||||
$this->generateApiKeys($driver);
|
||||
// 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',
|
||||
];
|
||||
|
||||
$jwt = $this->createJwt($driverId, 'driver_temp', $request->input('fingerprint'), 300);
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 300,
|
||||
'user_id' => $driverId,
|
||||
'api_key' => $driver->api_key,
|
||||
'api_secret' => $driver->api_secret,
|
||||
], 201);
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -326,8 +424,14 @@ class AuthController extends Controller
|
||||
'phone' => '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);
|
||||
}
|
||||
|
||||
// Stricter rate limit for wallet
|
||||
$rateLimitKey = 'wallet_login:' . $request->ip();
|
||||
if (Cache::get($rateLimitKey, 0) >= 3) {
|
||||
@@ -343,12 +447,21 @@ class AuthController extends Controller
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// Short-lived token for wallet operations (5 min)
|
||||
$jwt = $this->createJwt($passenger->id, 'passenger_wallet', $request->input('fingerprint'), 300);
|
||||
// V1 Security: Verify device fingerprint
|
||||
$token = PassengerToken::where('passengerID', $passenger->id)->first();
|
||||
if (!$token || $token->fingerPrint !== $request->input('fingerprint')) {
|
||||
return $this->failure('Device fingerprint verification failed', 403);
|
||||
}
|
||||
|
||||
// V1 Security: Short-lived token (60s) with Issuer and Audience
|
||||
$jwt = $this->createWalletJwt($passenger->id, $request->input('fingerprint'), $audience, 60);
|
||||
$hmac = hash_hmac('sha256', $passenger->id, config('intaleq.wallet_hmac_secret'));
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 300,
|
||||
'status' => 'success',
|
||||
'jwt' => $jwt,
|
||||
'hmac' => $hmac,
|
||||
'expires_in' => 60,
|
||||
'user_id' => $passenger->id,
|
||||
]);
|
||||
}
|
||||
@@ -363,8 +476,15 @@ class AuthController extends Controller
|
||||
'phone' => '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);
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
$rateLimitKey = 'wallet_login_driver:' . $request->ip();
|
||||
if (Cache::get($rateLimitKey, 0) >= 3) {
|
||||
return $this->failure('Too many attempts', 429);
|
||||
@@ -379,10 +499,20 @@ class AuthController extends Controller
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
$jwt = $this->createJwt($driver->id, 'driver_wallet', $request->input('fingerprint'), 60);
|
||||
// V1 Security: Verify device fingerprint
|
||||
$token = DriverToken::where('captain_id', $driver->id)->first();
|
||||
if (!$token || $token->fingerPrint !== $request->input('fingerprint')) {
|
||||
return $this->failure('Device fingerprint verification failed', 403);
|
||||
}
|
||||
|
||||
// V1 Security: Short-lived token (60s) with Issuer and Audience
|
||||
$jwt = $this->createWalletJwt($driver->id, $request->input('fingerprint'), $audience, 60);
|
||||
$hmac = hash_hmac('sha256', $driver->id, config('intaleq.wallet_hmac_secret'));
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'status' => 'success',
|
||||
'jwt' => $jwt,
|
||||
'hmac' => $hmac,
|
||||
'expires_in' => 60,
|
||||
'user_id' => $driver->id,
|
||||
]);
|
||||
@@ -394,7 +524,7 @@ class AuthController extends Controller
|
||||
|
||||
/**
|
||||
* POST /v2/auth/admin/login
|
||||
* Replaces: loginAdmin.php (NOW WITH ACTUAL PASSWORD CHECK!)
|
||||
* Replaces: loginAdmin.php
|
||||
*/
|
||||
public function adminLogin(Request $request): JsonResponse
|
||||
{
|
||||
@@ -412,10 +542,7 @@ class AuthController extends Controller
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// TODO: Add password field to adminUser table and verify with password_verify
|
||||
// For now, this is a placeholder — must be implemented before production
|
||||
|
||||
$jwt = $this->createJwt($admin->id, 'admin', $request->input('device_number'), 900);
|
||||
$jwt = $this->createJwt((string)$admin->id, 'admin', $request->input('device_number'), 900);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
@@ -424,10 +551,69 @@ class AuthController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// HELPERS
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
private function createWalletJwt(string $userId, string $fingerprint, string $audience, int $expiry = 60): 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,
|
||||
'fingerPrint' => $hashedFp,
|
||||
'exp' => time() + $expiry,
|
||||
'iat' => time(),
|
||||
'iss' => 'Tripz-Wallet',
|
||||
'aud' => $audience
|
||||
];
|
||||
|
||||
return JWT::encode($payload, config('intaleq.wallet_jwt_secret'), 'HS256');
|
||||
}
|
||||
|
||||
private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry): string
|
||||
{
|
||||
$payload = [
|
||||
|
||||
Reference in New Issue
Block a user