From 0ce8dc1030a50267fef46fc8a8310a0ad4f12d03 Mon Sep 17 00:00:00 2001 From: amirghasempoor Date: Tue, 29 Apr 2025 13:35:25 +0330 Subject: [PATCH] add permission, datatable and file services --- laravel/app/Facades/DataTable/DataTable.php | 67 ++++++ .../app/Facades/DataTable/DataTableFacade.php | 13 ++ laravel/app/Facades/File/File.php | 112 ++++++++++ laravel/app/Facades/File/FileFacade.php | 13 ++ laravel/app/Models/Permission.php | 12 ++ laravel/app/Models/User.php | 3 +- .../Providers/DataTableServiceProvider.php | 27 +++ laravel/app/Providers/FileServiceProvider.php | 27 +++ .../app/Services/DataTable/DataTableInput.php | 83 +++++++ .../Services/DataTable/DataTableService.php | 128 +++++++++++ .../app/Services/DataTable/Enums/DataType.php | 15 ++ .../Services/DataTable/Enums/SearchType.php | 19 ++ .../Exceptions/InvalidFilterException.php | 21 ++ .../Exceptions/InvalidParameterInterface.php | 7 + .../Exceptions/InvalidRelationException.php | 21 ++ .../Exceptions/InvalidSortingException.php | 21 ++ .../Services/DataTable/Filter/ApplyFilter.php | 84 ++++++++ .../app/Services/DataTable/Filter/Filter.php | 73 +++++++ .../Filter/SearchFunctions/FilterBetween.php | 33 +++ .../Filter/SearchFunctions/FilterContains.php | 31 +++ .../Filter/SearchFunctions/FilterEquals.php | 30 +++ .../SearchFunctions/FilterGreaterThan.php | 18 ++ .../FilterGreaterThanOrEqual.php | 18 ++ .../Filter/SearchFunctions/FilterLessThan.php | 18 ++ .../SearchFunctions/FilterLessThanOrEqual.php | 18 ++ .../SearchFunctions/FilterNotEquals.php | 17 ++ .../Filter/SearchFunctions/SearchFilter.php | 22 ++ .../app/Services/DataTable/Sort/ApplySort.php | 31 +++ laravel/app/Services/DataTable/Sort/Sort.php | 39 ++++ .../DataTable/Validators/FilterValidator.php | 69 ++++++ .../Validators/RelationValidator.php | 25 +++ .../DataTable/Validators/SortingValidator.php | 39 ++++ laravel/bootstrap/providers.php | 2 + laravel/composer.json | 3 +- laravel/composer.lock | 85 +++++++- laravel/config/permission.php | 202 ++++++++++++++++++ .../database/factories/PermissionFactory.php | 23 ++ ..._04_29_095539_create_permission_tables.php | 140 ++++++++++++ laravel/database/seeders/PermissionSeeder.php | 17 ++ 39 files changed, 1623 insertions(+), 3 deletions(-) create mode 100644 laravel/app/Facades/DataTable/DataTable.php create mode 100644 laravel/app/Facades/DataTable/DataTableFacade.php create mode 100644 laravel/app/Facades/File/File.php create mode 100644 laravel/app/Facades/File/FileFacade.php create mode 100644 laravel/app/Models/Permission.php create mode 100644 laravel/app/Providers/DataTableServiceProvider.php create mode 100644 laravel/app/Providers/FileServiceProvider.php create mode 100644 laravel/app/Services/DataTable/DataTableInput.php create mode 100644 laravel/app/Services/DataTable/DataTableService.php create mode 100644 laravel/app/Services/DataTable/Enums/DataType.php create mode 100644 laravel/app/Services/DataTable/Enums/SearchType.php create mode 100644 laravel/app/Services/DataTable/Exceptions/InvalidFilterException.php create mode 100644 laravel/app/Services/DataTable/Exceptions/InvalidParameterInterface.php create mode 100644 laravel/app/Services/DataTable/Exceptions/InvalidRelationException.php create mode 100644 laravel/app/Services/DataTable/Exceptions/InvalidSortingException.php create mode 100644 laravel/app/Services/DataTable/Filter/ApplyFilter.php create mode 100644 laravel/app/Services/DataTable/Filter/Filter.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php create mode 100644 laravel/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php create mode 100644 laravel/app/Services/DataTable/Sort/ApplySort.php create mode 100644 laravel/app/Services/DataTable/Sort/Sort.php create mode 100644 laravel/app/Services/DataTable/Validators/FilterValidator.php create mode 100644 laravel/app/Services/DataTable/Validators/RelationValidator.php create mode 100644 laravel/app/Services/DataTable/Validators/SortingValidator.php create mode 100644 laravel/config/permission.php create mode 100644 laravel/database/factories/PermissionFactory.php create mode 100644 laravel/database/migrations/2025_04_29_095539_create_permission_tables.php create mode 100644 laravel/database/seeders/PermissionSeeder.php diff --git a/laravel/app/Facades/DataTable/DataTable.php b/laravel/app/Facades/DataTable/DataTable.php new file mode 100644 index 0000000..6369950 --- /dev/null +++ b/laravel/app/Facades/DataTable/DataTable.php @@ -0,0 +1,67 @@ +filters); + $sorting = json_decode($request->sorting); + $rels = $request->rels ?: array(); + + $dataTableInput = new DataTableInput( + $request->start, + $request->size, + $filters, + $sorting, + $rels, + $allowedFilters, + $allowedSortings, + $allowedGroupBy + ); + + $query = $this->makeQueryFromModel($mixed); + + $dataTableService = (new DataTableService($query, $dataTableInput)) + ->setAllowedFilters($allowedFilters) + ->setAllowedRelations($allowedRelations) + ->setAllowedSortings($allowedSortings) + ->setAllowedSelects($allowedSelects) + ->setAllowedGroupBy($allowedGroupBy); + + return $dataTableService->getData(); + } + + protected function makeQueryFromModel(Model|Builder $mixed): Builder + { + return ($mixed instanceof Model) ? $mixed->query() : $mixed; + } +} diff --git a/laravel/app/Facades/DataTable/DataTableFacade.php b/laravel/app/Facades/DataTable/DataTableFacade.php new file mode 100644 index 0000000..0e94ab5 --- /dev/null +++ b/laravel/app/Facades/DataTable/DataTableFacade.php @@ -0,0 +1,13 @@ +putFileAs( + $path, + $file, + $name ?? Str::uuid() . '.' . $file->extension() + ); + } + + /** + * @param string $filePath + * @param bool $url_is_absolute + * @param string $disk + * @return void + * @throws \InvalidArgumentException when filePath is empty. + */ + public function delete(string $filePath, bool $url_is_absolute = false, string $disk = 'public'): void + { + if (!$filePath){ + throw new \InvalidArgumentException('File path is invalid.'); + } + + if ($url_is_absolute) { + $needle = 'storage/'; + $pos = strpos($filePath, $needle); + $filePath = substr($filePath, $pos + strlen($needle)); + } + + if (Storage::disk($disk)->exists($filePath)) { + Storage::disk($disk)->delete($filePath); + } + } + + public function deleteContent($file_path): bool + { + if (file_exists($file_path)) { + file_put_contents($file_path, ''); + + return true; + } + return false; + } + + public function tail($file_path, $lines = 100, $buffer = 2048): bool|string + { + // Open file + $file = @fopen($file_path, "rb"); + if ($file === false) { + return false; + } + + // Jump to last character + fseek($file, -1, SEEK_END); + + // Read it and adjust line number if necessary + // (Otherwise the result would be wrong if file doesn't end with a blank line) + if (fread($file, 1) != "\n") { + $lines -= 1; + } + + // Start reading + $output = ''; + $chunk = ''; + + // While we would like more + while (ftell($file) > 0 && $lines >= 0) { + + // Figure out how far back we should jump + $seek = min(ftell($file), $buffer); + + // Do the jump (backwards, relative to where we are) + fseek($file, -$seek, SEEK_CUR); + + // Read a chunk and prepend it to our output + $output = ($chunk = fread($file, $seek)) . $output; + + // Jump back to where we started reading + fseek($file, -mb_strlen($chunk, '8bit'), SEEK_CUR); + + // Decrease our line counter + $lines -= substr_count($chunk, "\n"); + } + + // While we have too many lines + // (Because of buffer size we might have read too many) + while ($lines++ < 0) { + + // Find first newline and remove all text before that + $output = substr($output, strpos($output, "\n") + 1); + } + + // Close file and return + fclose($file); + return trim($output); + } + + public function deleteDirectory(string $path, string $disk = 'public'): void + { + Storage::disk($disk)->deleteDirectory($path); + } +} diff --git a/laravel/app/Facades/File/FileFacade.php b/laravel/app/Facades/File/FileFacade.php new file mode 100644 index 0000000..1c193f8 --- /dev/null +++ b/laravel/app/Facades/File/FileFacade.php @@ -0,0 +1,13 @@ +app->bind('datatable', function () { + return new DataTable(); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/laravel/app/Providers/FileServiceProvider.php b/laravel/app/Providers/FileServiceProvider.php new file mode 100644 index 0000000..3174c6a --- /dev/null +++ b/laravel/app/Providers/FileServiceProvider.php @@ -0,0 +1,27 @@ +app->bind('file', function () { + return new File(); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/laravel/app/Services/DataTable/DataTableInput.php b/laravel/app/Services/DataTable/DataTableInput.php new file mode 100644 index 0000000..1bbe520 --- /dev/null +++ b/laravel/app/Services/DataTable/DataTableInput.php @@ -0,0 +1,83 @@ +start; + } + + public function getSize(): ?int + { + return $this->size; + } + + /** + * @return array returns an array of Filter objects + */ + public function getFilters(): array + { + $filters = array(); + + foreach ($this->filters as $filter) { + $filters[] = new Filter( + $filter->id, + $filter->value, + $filter->fn, + $filter->datatype, + $this->allowedFilters + ); + } + + return $filters; + } + + /** + * @return array + */ + public function getSorting(): ?array + { + $sorts = []; + if (!empty($this->sorting)){ + foreach ($this->sorting as $sort) { + $sorts[] = new Sort($sort->id, $sort->desc, $this->allowedSortings); + } + } + return $sorts; + } + + public function getRelations(): array + { + return $this->rels; + } + + public function getGroupBy(): ?array + { + return $this->GroupBy; + } +} diff --git a/laravel/app/Services/DataTable/DataTableService.php b/laravel/app/Services/DataTable/DataTableService.php new file mode 100644 index 0000000..5755ad1 --- /dev/null +++ b/laravel/app/Services/DataTable/DataTableService.php @@ -0,0 +1,128 @@ +allowedFilters = $allowedFilters; + return $this; + } + + public function setAllowedRelations(array $allowedRelations): DataTableService + { + $this->allowedRelations = $allowedRelations; + return $this; + } + + public function setAllowedSortings(array $allowedSortings): DataTableService + { + $this->allowedSortings = $allowedSortings; + return $this; + } + + public function setAllowedSelects(array $allowedSelects): DataTableService + { + $this->allowedSelects = $allowedSelects; + return $this; + } + + public function setAllowedGroupBy(array $allowedGroupBy): DataTableService + { + $this->allowedGroupBy = $allowedGroupBy; + return $this; + } + + /** + * Handle 'getData' operations + * @return array + */ + public function getData(): array + { + $query = $this->buildQuery(); + $data = $query->get(); + + return array( + 'data' => $data, + 'meta' => [ + 'totalRowCount' => $this->totalRowCount + ] + ); + } + + protected function buildQuery(): Builder + { + $query = $this->query; + + foreach ($this->dataTableInput->getFilters() as $filter) { + $query = (new ApplyFilter($query, $filter))->apply(); + } + + $query = $this->applySelect($query, $this->allowedSelects); + $query = $this->includeRelationsInQuery($query, $this->allowedRelations); + $query = $this->applyGroupBy($query, $this->allowedGroupBy); + + $this->totalRowCount = $query->count(); + + if (!is_null($this->dataTableInput->getStart())) { + $query->offset($this->dataTableInput->getStart()); + } + + if(!is_null($this->dataTableInput->getSize())){ + $query->limit($this->dataTableInput->getSize()); + } + + $sorting = $this->dataTableInput->getSorting(); + foreach ($sorting as $sort){ + $query = (new ApplySort($query, $sort))->apply(); + } + + return $query; + } + + protected function applySelect(Builder $query, array $selectedFields): Builder + { + if (!empty($selectedFields)) { + $query->select($selectedFields); + } + + return $query; + } + + protected function applyGroupBy(Builder $query, array $groupBy): Builder + { + return empty($groupBy) ? $query : $query->groupBy($groupBy); + } + + protected function includeRelationsInQuery(Builder $query, array $rels): Builder + { + if (!empty($rels)) { + $query->with($rels); + } + + return $query; + } + + // (later) define mapping of relation names to prevent relation name expose. + // (later) define mapping of column names to prevent column name expose. +} diff --git a/laravel/app/Services/DataTable/Enums/DataType.php b/laravel/app/Services/DataTable/Enums/DataType.php new file mode 100644 index 0000000..e16e98c --- /dev/null +++ b/laravel/app/Services/DataTable/Enums/DataType.php @@ -0,0 +1,15 @@ +fieldName = $fieldName; + parent::__construct($message, $code, $previous); + } + + public function getFieldName() + { + return $this->fieldName; + } +} diff --git a/laravel/app/Services/DataTable/Exceptions/InvalidParameterInterface.php b/laravel/app/Services/DataTable/Exceptions/InvalidParameterInterface.php new file mode 100644 index 0000000..3e7b3c7 --- /dev/null +++ b/laravel/app/Services/DataTable/Exceptions/InvalidParameterInterface.php @@ -0,0 +1,7 @@ +fieldName = $fieldName; + parent::__construct($message, $code, $previous); + } + + public function getFieldName() + { + return $this->fieldName; + } +} diff --git a/laravel/app/Services/DataTable/Exceptions/InvalidSortingException.php b/laravel/app/Services/DataTable/Exceptions/InvalidSortingException.php new file mode 100644 index 0000000..d170469 --- /dev/null +++ b/laravel/app/Services/DataTable/Exceptions/InvalidSortingException.php @@ -0,0 +1,21 @@ +fieldName = $fieldName; + parent::__construct($message, $code, $previous); + } + + public function getFieldName() + { + return $this->fieldName; + } +} diff --git a/laravel/app/Services/DataTable/Filter/ApplyFilter.php b/laravel/app/Services/DataTable/Filter/ApplyFilter.php new file mode 100644 index 0000000..60ac88e --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/ApplyFilter.php @@ -0,0 +1,84 @@ +filter; + $query = $this->query; + + $searchType = SearchType::from($filter->getFn()); + switch ($searchType) { + case SearchType::CONTAINS: + $this->searchFilter = new FilterContains($query, $filter); + break; + + case SearchType::EQUALS: + $this->searchFilter = new FilterEquals($query, $filter); + break; + + case SearchType::NOT_EQUALS: + $this->searchFilter = new FilterNotEquals($query, $filter); + break; + + case SearchType::BETWEEN: + $this->searchFilter = new FilterBetween($query, $filter); + break; + + case SearchType::GREATER_THAN: + $this->searchFilter = new FilterGreaterThan($query, $filter); + break; + + case SearchType::LESS_THAN: + $this->searchFilter = new FilterLessThan($query, $filter); + break; + + default: + $searchFunction = $filter->getFn(); + throw new InvalidFilterException($searchFunction, "search function `$searchFunction` is invalid."); + + } + + $relation = $this->filter->getRelation(); + return method_exists($this->query->getModel(), $relation) ? $this->applyFilterToRelation($relation) : $this->searchFilter->apply(); + } + + protected function applyFilterToRelation(string $relation): Builder + { + return $this->query->whereHas($relation, function (Builder $query) { + $this->filter->removeRelationFromId(); + $this->applyFilter($query, $this->filter); + }); + } + + private function applyFilter(Builder $query, Filter $filter): Builder + { + return (new ApplyFilter($query, $filter))->apply(); + } +} diff --git a/laravel/app/Services/DataTable/Filter/Filter.php b/laravel/app/Services/DataTable/Filter/Filter.php new file mode 100644 index 0000000..22362a1 --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/Filter.php @@ -0,0 +1,73 @@ +isValid($this, $this->allowedFilters); + } + + public function getId(): string + { + return $this->id; + } + + public function getValue(): array|int|string + { + return $this->value; + } + + public function getFn(): string + { + return $this->fn; + } + + public function getDatatype(): string + { + return $this->datatype; + } + + public function getRelation(): string + { + $fieldArray = explode('.', $this->id); + return count($fieldArray) > 1 ? $fieldArray[0] : ''; + } + + public function getColumn(): string + { + $fieldArray = explode('.', $this->id); + return array_pop($fieldArray); + } + + public function removeRelationFromId(): void + { + $this->id = $this->getColumn(); + } + + public function setValue(int|array|string $value): void + { + $this->value = $value; + } + +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php new file mode 100644 index 0000000..7a6292d --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php @@ -0,0 +1,33 @@ +query; + [$minVal, $maxVal] = $this->filter->getValue(); + + if ($minVal) { + if ($this->filter->getDatatype() == "date") { + $minVal = $minVal . " 00:00:00"; + } + $this->filter->setValue($minVal); + $query = (new FilterGreaterThanOrEqual($this->query, $this->filter))->apply(); + } + + if ($maxVal) { + if ($this->filter->getDatatype() == "date") { + $maxVal = $maxVal . " 23:59:59"; + } + $this->filter->setValue($maxVal); + $query = (new FilterLessThanOrEqual($this->query, $this->filter))->apply(); + } + + return $query; + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php new file mode 100644 index 0000000..686fe9f --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php @@ -0,0 +1,31 @@ +filter->getId(); + $value = '%' . $this->filter->getValue() . '%'; + + if ($this->filter->getDatatype() == DataType::TEXT->value) { + $query = $this->searchIgnoreCase($column, $value); + + } else { + $query = $this->query->where($column, 'LIKE', $value); + } + + return $query; + } + + private function searchIgnoreCase(string $column, string $value): Builder + { + $value = strtolower($value); + return $this->query->whereRaw("LOWER($column) LIKE ?", [$value]); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php new file mode 100644 index 0000000..594ce87 --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php @@ -0,0 +1,30 @@ +filter->getId(); + $value = $this->filter->getValue(); + + if ($this->filter->getDatatype() == DataType::TEXT->value) { + $query = $this->searchIgnoreCase($column, $value); + } else { + $query = $this->query->where($column, $value); + } + + return $query; + } + + private function searchIgnoreCase(string $column, string $value): Builder + { + $value = strtolower($value); + return $this->query->whereRaw("LOWER($column) = ?", [$value]); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php new file mode 100644 index 0000000..30da5d3 --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '>', $value); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php new file mode 100644 index 0000000..8e97248 --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '>=', $value); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php new file mode 100644 index 0000000..4069b9e --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '<', $value); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php new file mode 100644 index 0000000..8440905 --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '<=', $value); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php new file mode 100644 index 0000000..e525f2b --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php @@ -0,0 +1,17 @@ +filter->getId(); + $value = $this->filter->getValue(); + + return $this->query->whereNot($column, $value); + } +} diff --git a/laravel/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php b/laravel/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php new file mode 100644 index 0000000..d4dcbf6 --- /dev/null +++ b/laravel/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php @@ -0,0 +1,22 @@ +query; + + if (!is_null($this->sort)) { + $query->orderBy($this->sort->getId(), $this->sort->getDirection()); + } + + return $query; + } +} diff --git a/laravel/app/Services/DataTable/Sort/Sort.php b/laravel/app/Services/DataTable/Sort/Sort.php new file mode 100644 index 0000000..5196797 --- /dev/null +++ b/laravel/app/Services/DataTable/Sort/Sort.php @@ -0,0 +1,39 @@ +isValid($this, $this->allowedSortings); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + public function getDirection(): string + { + return $this->desc === true ? 'desc' : 'asc'; + } +} diff --git a/laravel/app/Services/DataTable/Validators/FilterValidator.php b/laravel/app/Services/DataTable/Validators/FilterValidator.php new file mode 100644 index 0000000..ea837f9 --- /dev/null +++ b/laravel/app/Services/DataTable/Validators/FilterValidator.php @@ -0,0 +1,69 @@ +isAllowed($filter, $allowedFilters)) { + $filterId = $filter->getId(); + throw new InvalidFilterException($filter->getId(), "filtering field `$filterId` is not allowed."); + } + + if (!$this->isValidSearchFunction($filter)) { + $searchFunction = $filter->getFn(); + throw new InvalidFilterException($searchFunction, "search function `$searchFunction` is invalid."); + } + + if ($this->isValidDataType($filter) == -1) { + throw new InvalidFilterException(null, "datatype property is not set in `filters` array."); + } + + if (!$this->isValidDataType($filter)) { + $datatype = $filter->getDatatype(); + throw new InvalidFilterException($datatype, "datatype `$datatype` is invalid."); + } + + return true; + } + + protected function isAllowed(Filter $filter, array $allowedFilters): bool + { + return $allowedFilters == ['*'] || in_array($filter->getId(), $allowedFilters); + } + + protected function isValidSearchFunction(Filter $filter): bool + { + $searchFunction = $filter->getFn(); + return isset($searchFunction) && in_array($searchFunction, SearchType::values()); + } + + protected function isValidDataType(Filter $filter): int + { + if (!property_exists($filter, 'datatype')) + return -1; + + return $filter->getDatatype() && in_array($filter->getDatatype(), DataType::values()) ? 1 : 0; + } +} diff --git a/laravel/app/Services/DataTable/Validators/RelationValidator.php b/laravel/app/Services/DataTable/Validators/RelationValidator.php new file mode 100644 index 0000000..ab87382 --- /dev/null +++ b/laravel/app/Services/DataTable/Validators/RelationValidator.php @@ -0,0 +1,25 @@ +isAllowed($sorting, $allowedSortings)) { + $sortId = $sorting->getId(); + throw new InvalidSortingException($sortId, "sorting field `$sortId` is not allowed."); + } + + return true; + } + + protected function isAllowed(Sort $sorting, array $allowedSortings): bool + { + return $allowedSortings == ['*'] || in_array($sorting->getId(), $allowedSortings); + } +} diff --git a/laravel/bootstrap/providers.php b/laravel/bootstrap/providers.php index 38b258d..9a7a919 100644 --- a/laravel/bootstrap/providers.php +++ b/laravel/bootstrap/providers.php @@ -2,4 +2,6 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\DataTableServiceProvider::class, + App\Providers\FileServiceProvider::class, ]; diff --git a/laravel/composer.json b/laravel/composer.json index 83f714b..147bce5 100644 --- a/laravel/composer.json +++ b/laravel/composer.json @@ -12,7 +12,8 @@ "php": "^8.2", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "spatie/laravel-permission": "^6.17" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/laravel/composer.lock b/laravel/composer.lock index 17d705c..cab5c94 100644 --- a/laravel/composer.lock +++ b/laravel/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "85c1d2065f70e38b0d6bf66559fb13c5", + "content-hash": "c6435e4e604560a7d58c57b5d1bd2d63", "packages": [ { "name": "brick/math", @@ -3350,6 +3350,89 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "spatie/laravel-permission", + "version": "6.17.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "02ada8f638b643713fa2fb543384738e27346ddb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/02ada8f638b643713fa2fb543384738e27346ddb", + "reference": "02ada8f638b643713fa2fb543384738e27346ddb", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.4|^10.1|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 8.0 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/6.17.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-04-08T15:06:14+00:00" + }, { "name": "symfony/clock", "version": "v7.2.0", diff --git a/laravel/config/permission.php b/laravel/config/permission.php new file mode 100644 index 0000000..8e84e9d --- /dev/null +++ b/laravel/config/permission.php @@ -0,0 +1,202 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/laravel/database/factories/PermissionFactory.php b/laravel/database/factories/PermissionFactory.php new file mode 100644 index 0000000..d2cec7f --- /dev/null +++ b/laravel/database/factories/PermissionFactory.php @@ -0,0 +1,23 @@ + + */ +class PermissionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/laravel/database/migrations/2025_04_29_095539_create_permission_tables.php b/laravel/database/migrations/2025_04_29_095539_create_permission_tables.php new file mode 100644 index 0000000..70a120f --- /dev/null +++ b/laravel/database/migrations/2025_04_29_095539_create_permission_tables.php @@ -0,0 +1,140 @@ +engine('InnoDB'); + $table->bigIncrements('id'); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + // $table->engine('InnoDB'); + $table->bigIncrements('id'); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + if (empty($tableNames)) { + throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + } + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/laravel/database/seeders/PermissionSeeder.php b/laravel/database/seeders/PermissionSeeder.php new file mode 100644 index 0000000..1e866db --- /dev/null +++ b/laravel/database/seeders/PermissionSeeder.php @@ -0,0 +1,17 @@ +