Implement adequate report filtering for lesser permissioned users

This commit is contained in:
David Bomba 2025-10-29 09:27:48 +11:00
parent b21c182eda
commit 16a380972f
35 changed files with 163 additions and 45 deletions

View File

@ -115,6 +115,8 @@ class ActivityExport extends BaseExport
$query = $this->addDateRange($query, 'activities'); $query = $this->addDateRange($query, 'activities');
$query = $this->filterByUserPermissions($query);
if ($this->input['activity_type_id'] ?? false) { if ($this->input['activity_type_id'] ?? false) {
$query->where('activity_type_id', $this->input['activity_type_id']); $query->where('activity_type_id', $this->input['activity_type_id']);
} }
@ -133,12 +135,10 @@ class ActivityExport extends BaseExport
//insert the header //insert the header
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query->cursor() $query->cursor()
->each(function ($entity) { ->each(function ($entity) {
/** @var \App\Models\Activity $entity */ /** @var \App\Models\Activity $entity */
$this->buildRow($entity); $this->buildRow($entity);
}); });

View File

@ -1710,4 +1710,77 @@ $products = str_getcsv($this->input['product_key'], ',', "'");
return $entity; return $entity;
} }
public function filterByUserPermissions(Builder $query): Builder
{
$user = User::withTrashed()->where('id', $this->input['user_id'])->where('account_id', $this->company->account_id)->first();
if ($user->isAdmin() || $user->hasExactPermission('view_all') || $user->hasExactPermission('edit_all')) { // No State? Do we need to ensure -> isAdmin() binds to the correct company?
return $query;
}
if($user->hasExactPermission('create_all')){
return $query->where('user_id', $user->id);
}
return $this->resolveEntityFilters($user, $query);
}
private function resolveEntityFilters(User $user, Builder $query): Builder
{
$model = get_class($query->getModel());
$column_listing = \Illuminate\Support\Facades\Schema::getColumnListing($query->getModel()->getTable());
$model_string = match($model) {
'App\Models\Client' => 'client',
'App\Models\ClientContact' => 'client',
'App\Models\Invoice' => 'invoice',
'App\Models\Quote' => 'quote',
'App\Models\Credit' => 'credit',
'App\Models\PurchaseOrder' => 'purchase_order',
'App\Models\RecurringInvoice' => 'recurring_invoice',
'App\Models\RecurringExpense' => 'recurring_expense',
'App\Models\Task' => 'task',
'App\Models\Vendor' => 'vendor',
'App\Models\VendorContact' => 'vendor_contact',
'App\Models\Product' => 'product',
'App\Models\Payment' => 'payment',
'App\Models\Expense' => 'expense',
'App\Models\Document' => 'document',
'App\Models\Activity' => 'activity',
'App\Models\Task' => 'task',
'App\Models\Project' => 'project',
default => false,
};
/** If the User can view or edit the entity, then return the query unfiltered */
if($user->hasIntersectPermissions(["view_{$model_string}", "edit_{$model_string}"])){
return $query;
}
//Handle Child Models Like ClientContact or VendorContact
if(in_array($model, ['App\Models\ClientContact', 'App\Models\VendorContact'])){
$query->whereHas($model_string, function ($_q) use ($user){
$_q->where('user_id', $user->id)->orWhere('assigned_user_id', $user->id);
});
return $query;
}
return $query->where(function ($q) use ($user, $column_listing){
if(in_array('user_id', $column_listing)){
$q->where('user_id', $user->id);
}
if(in_array('assigned_user_id', $column_listing)){
$q->orWhere('assigned_user_id', $user->id);
}
});
}
} }

View File

@ -136,6 +136,8 @@ class ClientExport extends BaseExport
$query = $this->addDateRange($query, ' clients'); $query = $this->addDateRange($query, ' clients');
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -65,6 +65,7 @@ class ContactExport extends BaseExport
}); });
$query = $this->addDateRange($query, 'client_contacts'); $query = $this->addDateRange($query, 'client_contacts');
$query = $this->filterByUserPermissions($query);
return $query; return $query;

View File

@ -123,6 +123,8 @@ class CreditExport extends BaseExport
$query = $this->addCreditStatusFilter($query, $this->input['status']); $query = $this->addCreditStatusFilter($query, $this->input['status']);
} }
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -81,6 +81,8 @@ class DocumentExport extends BaseExport
$query = $this->addDateRange($query, 'documents'); $query = $this->addDateRange($query, 'documents');
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -114,6 +114,8 @@ class ExpenseExport extends BaseExport
$query = $this->addCategoryFilter($query, $this->input['categories']); $query = $this->addCategoryFilter($query, $this->input['categories']);
} }
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -82,6 +82,9 @@ class InvoiceExport extends BaseExport
$query = $this->addInvoiceStatusFilter($query, $this->input['status']); $query = $this->addInvoiceStatusFilter($query, $this->input['status']);
} }
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -93,6 +93,8 @@ class InvoiceItemExport extends BaseExport
$query = $this->addInvoiceStatusFilter($query, $this->input['status']); $query = $this->addInvoiceStatusFilter($query, $this->input['status']);
} }
$query = $this->filterByUserPermissions($query);
$query = $this->applyProductFilters($query); $query = $this->applyProductFilters($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {

View File

@ -72,6 +72,7 @@ class PaymentExport extends BaseExport
} }
$query = $this->addPaymentStatusFilters($query, $this->input['status'] ?? ''); $query = $this->addPaymentStatusFilters($query, $this->input['status'] ?? '');
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);

View File

@ -83,6 +83,7 @@ class ProductExport extends BaseExport
} }
$query = $this->addDateRange($query, 'products'); $query = $this->addDateRange($query, 'products');
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);

View File

@ -128,6 +128,8 @@ class ProductSalesExport extends BaseExport
$query = $this->filterByClients($query); $query = $this->filterByClients($query);
$query = $this->filterByUserPermissions($query);
$query = $this->filterByProducts($query); $query = $this->filterByProducts($query);
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());

View File

@ -76,6 +76,7 @@ class PurchaseOrderExport extends BaseExport
if ($clients) { if ($clients) {
$query = $this->addClientFilter($query, $clients); $query = $this->addClientFilter($query, $clients);
} }
$query = $this->filterByUserPermissions($query);
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? ''); $query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');

View File

@ -79,6 +79,7 @@ class PurchaseOrderItemExport extends BaseExport
if ($clients) { if ($clients) {
$query = $this->addClientFilter($query, $clients); $query = $this->addClientFilter($query, $clients);
} }
$query = $this->filterByUserPermissions($query);
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? ''); $query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');

View File

@ -78,6 +78,8 @@ class QuoteExport extends BaseExport
$query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? ''); $query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? '');
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -82,6 +82,7 @@ class QuoteItemExport extends BaseExport
if ($clients) { if ($clients) {
$query = $this->addClientFilter($query, $clients); $query = $this->addClientFilter($query, $clients);
} }
$query = $this->filterByUserPermissions($query);
$query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? ''); $query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? '');

View File

@ -73,6 +73,7 @@ class RecurringInvoiceExport extends BaseExport
if ($clients) { if ($clients) {
$query = $this->addClientFilter($query, $clients); $query = $this->addClientFilter($query, $clients);
} }
$query = $this->filterByUserPermissions($query);
$query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status'] ?? ''); $query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status'] ?? '');

View File

@ -93,6 +93,7 @@ class RecurringInvoiceItemExport extends BaseExport
if ($this->input['status'] ?? false) { if ($this->input['status'] ?? false) {
$query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status']); $query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status']);
} }
$query = $this->filterByUserPermissions($query);
$query = $this->applyProductFilters($query); $query = $this->applyProductFilters($query);

View File

@ -82,6 +82,12 @@ class TaskExport extends BaseExport
$query = $this->addClientFilter($query, $clients); $query = $this->addClientFilter($query, $clients);
} }
if($this->input['status'] ?? false){
$query = $this->addTaskStatusFilter($query, $this->input['status']);
}
$query = $this->filterByUserPermissions($query);
$document_attachments = &$this->input['document_email_attachment']; $document_attachments = &$this->input['document_email_attachment'];
if ($document_attachments) { if ($document_attachments) {
@ -261,6 +267,7 @@ class TaskExport extends BaseExport
*/ */
protected function addTaskStatusFilter(Builder $query, string $status): Builder protected function addTaskStatusFilter(Builder $query, string $status): Builder
{ {
nlog(['addTaskStatusFilter', $status]);
/** @var array $status_parameters */ /** @var array $status_parameters */
$status_parameters = explode(',', $status); $status_parameters = explode(',', $status);
@ -276,6 +283,16 @@ class TaskExport extends BaseExport
$query->whereNull('invoice_id'); $query->whereNull('invoice_id');
} }
$keys = $this->transformKeys($status_parameters);
$keys = collect($keys)->filter(function ($key){
return is_int($key);
})->toArray();
if(count($keys) > 0){
$query->whereIn('status_id', $keys);
}
return $query; return $query;
} }

View File

@ -70,6 +70,7 @@ class VendorExport extends BaseExport
} }
$query = $this->addDateRange($query, 'vendors'); $query = $this->addDateRange($query, 'vendors');
$query = $this->filterByUserPermissions($query);
if ($this->input['document_email_attachment'] ?? false) { if ($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);

View File

@ -1089,7 +1089,7 @@ class BaseController extends Controller
$data = $this->first_load; $data = $this->first_load;
} }
} else { } else {
$included = request()->input('include', ''); $included = request()->input('include') ?? '';
$included = explode(',', $included); $included = explode(',', $included);
foreach ($included as $include) { foreach ($included as $include) {

View File

@ -31,8 +31,6 @@ class ReportPreviewController extends BaseController
$report = Cache::get($hash); $report = Cache::get($hash);
nlog($report);
if (!$report) { if (!$report) {
return response()->json(['message' => 'Still working.....'], 409); return response()->json(['message' => 'Still working.....'], 409);
} }

View File

@ -70,10 +70,12 @@ class ProductSalesReportRequest extends Request
$input['end_date'] = null; $input['end_date'] = null;
} }
if (array_key_exists('client_id', $input) && strlen($input['client_id']) >= 1) { if (array_key_exists('client_id', $input) && strlen($input['client_id'] ?? '') > 1) {
$input['client_id'] = $this->decodePrimaryKey($input['client_id']); $input['client_id'] = $this->decodePrimaryKey($input['client_id']);
} }
$input['user_id'] = auth()->user()->id;
$this->replace($input); $this->replace($input);
} }
@ -89,7 +91,7 @@ class ProductSalesReportRequest extends Request
return false; return false;
} }
return $user->isAdmin() || $user->hasPermission('view_reports'); return $user->isAdmin() || ($user->hasPermission('view_all') && $user->hasPermission('view_reports'));
} }

View File

@ -51,6 +51,9 @@ class ProfitLossRequest extends Request
$input['date_range'] = 'all'; $input['date_range'] = 'all';
} }
$input['user_id'] = auth()->user()->id;
$this->replace($input); $this->replace($input);
} }
@ -66,7 +69,7 @@ class ProfitLossRequest extends Request
return false; return false;
} }
return $user->isAdmin() || $user->hasPermission('view_reports'); return $user->isAdmin() || ($user->hasPermission('view_all') && $user->hasPermission('view_reports'));
} }

View File

@ -69,6 +69,8 @@ class ProjectReportRequest extends Request
$input['end_date'] = null; $input['end_date'] = null;
} }
$input['user_id'] = auth()->user()->id;
$this->replace($input); $this->replace($input);
} }

View File

@ -56,7 +56,7 @@ class PreviewReport implements ShouldQueue
/** /**
* Handle a job failure. * Handle a job failure.
*/ */
public function failed(\Throwable $exception = null) public function failed(?\Throwable $exception)
{ {
if($exception) { if($exception) {
nlog("EXCEPTION:: PreviewReport:: could not preview report for " . $exception->getMessage()); nlog("EXCEPTION:: PreviewReport:: could not preview report for " . $exception->getMessage());

View File

@ -37,25 +37,13 @@ class SendToAdmin implements ShouldQueue
use Queueable; use Queueable;
use SerializesModels; use SerializesModels;
protected Company $company;
protected array $request;
protected string $report_class;
protected string $file_name;
public $tries = 1; public $tries = 1;
/** /**
* Create a new job instance. * Create a new job instance.
*/ */
public function __construct(Company $company, array $request, $report_class, $file_name) public function __construct(protected Company $company, protected array $request, protected string $report_class, protected string $file_name)
{ {
$this->company = $company;
$this->request = $request;
$this->report_class = $report_class;
$this->file_name = $file_name;
} }
public function handle() public function handle()
@ -69,7 +57,6 @@ class SendToAdmin implements ShouldQueue
$file_name = $this->file_name; $file_name = $this->file_name;
$size_mb = round(strlen($csv) / (1024 * 1024), 2); // Size in MB $size_mb = round(strlen($csv) / (1024 * 1024), 2); // Size in MB
nlog("Report Size: MB " . $size_mb);
// If the file is greater than 5MB, we need to zip it to ensure it does not break attachment size limits // If the file is greater than 5MB, we need to zip it to ensure it does not break attachment size limits
if($size_mb > 5){ if($size_mb > 5){
@ -116,12 +103,7 @@ class SendToAdmin implements ShouldQueue
} }
// public function middleware() public function failed(?\Throwable $exception = null)
// {
// return [(new WithoutOverlapping("report-{$this->company->company_key}-{$this->report_class}"))->expireAfter(60)];
// }
public function failed(\Throwable $exception = null)
{ {
if($exception) { if($exception) {
nlog("EXCEPTION:: SendToAdmin:: could not email report for" . $exception->getMessage()); nlog("EXCEPTION:: SendToAdmin:: could not email report for" . $exception->getMessage());

View File

@ -471,10 +471,10 @@ class User extends Authenticatable implements MustVerifyEmail
} }
return $this->isSuperUser() || return $this->isSuperUser() ||
(stripos($this->token()->cu->permissions, $permission) !== false) || (stripos($this->token()->cu->permissions ?? '', $permission) !== false) ||
(stripos($this->token()->cu->permissions, $all_permission) !== false) || (stripos($this->token()->cu->permissions ?? '', $all_permission) !== false) ||
(stripos($this->token()->cu->permissions, $edit_all) !== false) || (stripos($this->token()->cu->permissions ?? '', $edit_all) !== false) ||
(stripos($this->token()->cu->permissions, $edit_entity) !== false); (stripos($this->token()->cu->permissions ?? '', $edit_entity) !== false);
} }
/** /**

View File

@ -103,6 +103,7 @@ class ARDetailReport extends BaseExport
$query = $this->addDateRange($query, 'invoices'); $query = $this->addDateRange($query, 'invoices');
$query = $this->filterByClients($query); $query = $this->filterByClients($query);
$query = $this->filterByUserPermissions($query);
$query->cursor() $query->cursor()
->each(function ($invoice) { ->each(function ($invoice) {

View File

@ -92,10 +92,13 @@ class ARSummaryReport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
Client::query() $query = Client::query()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0);
->orderBy('balance', 'desc')
$query = $this->filterByUserPermissions($query);
$query->orderBy('balance', 'desc')
->cursor() ->cursor()
->each(function ($client) { ->each(function ($client) {

View File

@ -88,13 +88,16 @@ class ClientBalanceReport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
Client::query() $query = Client::query()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0);
->orderBy('balance', 'desc')
$query = $this->filterByUserPermissions($query);
$query->orderBy('balance', 'desc')
->cursor() ->cursor()
->each(function ($client) { ->each(function ($client) {
/** @var \App\Models\Client $client */
$this->csv->insertOne($this->buildRow($client)); $this->csv->insertOne($this->buildRow($client));
}); });

View File

@ -90,13 +90,16 @@ class ClientSalesReport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
Client::query() $query = Client::query()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0);
->orderBy('balance', 'desc')
$query = $this->filterByUserPermissions($query);
$query->orderBy('balance', 'desc')
->cursor() ->cursor()
->each(function ($client) { ->each(function ($client) {
/** @var \App\Models\Client $client */
$this->csv->insertOne($this->buildRow($client)); $this->csv->insertOne($this->buildRow($client));
}); });

View File

@ -66,6 +66,8 @@ class ProjectReport extends BaseExport
$query = \App\Models\Project::with(['invoices','expenses','tasks']) $query = \App\Models\Project::with(['invoices','expenses','tasks'])
->where('company_id', $this->company->id); ->where('company_id', $this->company->id);
$query = $this->filterByUserPermissions($query);
$projects = &$this->input['projects']; $projects = &$this->input['projects'];
if ($projects) { if ($projects) {

View File

@ -89,6 +89,7 @@ class TaxSummaryReport extends BaseExport
->orderBy('balance', 'desc'); ->orderBy('balance', 'desc');
$query = $this->addDateRange($query, 'invoices'); $query = $this->addDateRange($query, 'invoices');
$query = $this->filterByUserPermissions($query);
$this->csv->insertOne([ctrans('texts.tax_summary')]); $this->csv->insertOne([ctrans('texts.tax_summary')]);
$this->csv->insertOne([ctrans('texts.created_on'),' ',$this->translateDate(now()->format('Y-m-d'), $this->company->date_format(), $this->company->locale())]); $this->csv->insertOne([ctrans('texts.created_on'),' ',$this->translateDate(now()->format('Y-m-d'), $this->company->date_format(), $this->company->locale())]);

View File

@ -74,6 +74,8 @@ class UserSalesReport extends BaseExport
$query = $this->filterByClients($query); $query = $this->filterByClients($query);
$query = $this->filterByUserPermissions($query);
$this->csv->insertOne([ctrans('texts.user_sales_report_header', ['client' => $this->client_description, 'start_date' => $this->start_date, 'end_date' => $this->end_date])]); $this->csv->insertOne([ctrans('texts.user_sales_report_header', ['client' => $this->client_description, 'start_date' => $this->start_date, 'end_date' => $this->end_date])]);
if (count($this->input['report_keys']) == 0) { if (count($this->input['report_keys']) == 0) {