enforce(RateLimiter::identifier(), 'api'); try { $transit_con = Database::get('transit'); } catch (Exception $e) { jsonError('Transit service unavailable', 503); } requireTransitFields(['phone', 'otp']); $phone = normalizePhone(filterRequest('phone')); $otp = preg_replace('/\D/', '', filterRequest('otp')); if (!transitVerifyAdminOtp($phone, $otp)) jsonError('Invalid or expired OTP', 401); $phoneEnc = $encryptionHelper->encryptData($phone); $st = $transit_con->prepare( "SELECT a.id, a.org_id, a.name, a.role, a.permissions, o.name_ar, o.name_en, o.type, o.contract_status, o.logo_url FROM transit_org_admins a JOIN transit_orgs o ON o.id = a.org_id WHERE a.phone = ? AND a.is_active = 1 LIMIT 1" ); $st->execute([$phoneEnc]); $admin = $st->fetch(); if (!$admin) jsonError('Admin not found', 401); $token = transitCreateSession((int)$admin['id'], (int)$admin['org_id']); jsonSuccess([ 'token' => $token, 'expires_in' => 86400, 'admin' => [ 'id' => (int)$admin['id'], 'name' => $admin['name'], 'role' => $admin['role'], 'permissions' => json_decode($admin['permissions'] ?? '{}', true), ], 'org' => [ 'id' => (int)$admin['org_id'], 'name_ar' => $admin['name_ar'], 'name_en' => $admin['name_en'], 'type' => $admin['type'], 'contract_status' => $admin['contract_status'], 'logo_url' => $admin['logo_url'], ], ], 'Login successful');