From 57092e1b2634a64ecd08221b92d2e2438dbdef81 Mon Sep 17 00:00:00 2001 From: James Collins Date: Tue, 7 Mar 2023 13:03:51 +1000 Subject: [PATCH 01/39] catch promise --- .../js/components/SMInputAttachments.vue | 16 ++++--- resources/js/helpers/transition.ts | 42 ++++++++++--------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/resources/js/components/SMInputAttachments.vue b/resources/js/components/SMInputAttachments.vue index e25fc61..b17b4c1 100644 --- a/resources/js/components/SMInputAttachments.vue +++ b/resources/js/components/SMInputAttachments.vue @@ -65,14 +65,18 @@ const mediaItems: Ref = ref([]); * Handle the user adding a new media item. */ const handleClickAdd = async () => { - openDialog(SMDialogMedia, { mime: "", accepts: "" }).then((result) => { - const media = result as Media; + openDialog(SMDialogMedia, { mime: "", accepts: "" }) + .then((result) => { + const media = result as Media; - mediaItems.value.push(media); - value.value.push(media.id); + mediaItems.value.push(media); + value.value.push(media.id); - emits("update:modelValue", value); - }); + emits("update:modelValue", value); + }) + .catch(() => { + /* empty */ + }); }; /** diff --git a/resources/js/helpers/transition.ts b/resources/js/helpers/transition.ts index 15094bf..7230a89 100644 --- a/resources/js/helpers/transition.ts +++ b/resources/js/helpers/transition.ts @@ -66,25 +66,29 @@ const waitForElementRender = (elem: Ref): Promise => { * @returns {void} */ export const transitionEnter = (elem: Ref, transition: string): void => { - waitForElementRender(elem).then((e: HTMLElement) => { - window.setTimeout(() => { - e.classList.replace( - transition + "-enter-from", - transition + "-enter-active" - ); - const transitionName = transitionEndEventName(); - e.addEventListener( - transitionName, - () => { - e.classList.replace( - transition + "-enter-active", - transition + "-enter-to" - ); - }, - false - ); - }, 1); - }); + waitForElementRender(elem) + .then((e: HTMLElement) => { + window.setTimeout(() => { + e.classList.replace( + transition + "-enter-from", + transition + "-enter-active" + ); + const transitionName = transitionEndEventName(); + e.addEventListener( + transitionName, + () => { + e.classList.replace( + transition + "-enter-active", + transition + "-enter-to" + ); + }, + false + ); + }, 1); + }) + .catch(() => { + /* empty */ + }); }; /** -- 2.49.1 From 44481fe1074cae291f64f2d44d10fd128dd9e4bf Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 12:39:39 +1000 Subject: [PATCH 02/39] added guarded properties --- app/Models/Event.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/Models/Event.php b/app/Models/Event.php index 3924235..c280797 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -32,6 +32,13 @@ class Event extends Model 'ages', ]; + /** + * The attributes that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id']; + /** * Get all of the post's attachments. -- 2.49.1 From d5a703026a0ca40b43c4c21286f6efda35b6aa1d Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 12:40:29 +1000 Subject: [PATCH 03/39] fix attachment event endpoints --- routes/api.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routes/api.php b/routes/api.php index 528c656..03ad37d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -41,9 +41,9 @@ Route::post('posts/{post}/attachments', [PostController::class, 'storeAttachment Route::delete('posts/{post}/attachments/{attachment}', [PostController::class, 'deleteAttachment']); Route::apiResource('events', EventController::class); -Route::get('events/{event}/attachments', [PostController::class, 'getAttachments']); -Route::post('events/{event}/attachments', [PostController::class, 'storeAttachment']); -Route::delete('events/{event}/attachments/{attachment}', [PostController::class, 'deleteAttachment']); +Route::get('events/{event}/attachments', [EventController::class, 'getAttachments']); +Route::post('events/{event}/attachments', [EventController::class, 'storeAttachment']); +Route::delete('events/{event}/attachments/{attachment}', [EventController::class, 'deleteAttachment']); Route::apiResource('subscriptions', SubscriptionController::class); Route::delete('subscriptions', [SubscriptionController::class, 'destroyByEmail']); -- 2.49.1 From e11211fcc7ad8b7579035bd81f69d1b43a06f73e Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 13:46:06 +1000 Subject: [PATCH 04/39] remove unnecessary guarded --- app/Models/Event.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/Models/Event.php b/app/Models/Event.php index c280797..45952e6 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -32,14 +32,6 @@ class Event extends Model 'ages', ]; - /** - * The attributes that are not mass assignable. - * - * @var array - */ - protected $guarded = ['id']; - - /** * Get all of the post's attachments. */ -- 2.49.1 From 6bee6b1ba776176067e4a2de23e1e1fe149dc30c Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 13:46:16 +1000 Subject: [PATCH 05/39] added controllers --- app/Conductors/Conductor.php | 291 ++++++++++++++++++++++++++++++ app/Conductors/EventConductor.php | 37 ++++ app/Conductors/UserConductor.php | 53 ++++++ 3 files changed, 381 insertions(+) create mode 100644 app/Conductors/Conductor.php create mode 100644 app/Conductors/EventConductor.php create mode 100644 app/Conductors/UserConductor.php diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php new file mode 100644 index 0000000..aa0632b --- /dev/null +++ b/app/Conductors/Conductor.php @@ -0,0 +1,291 @@ +query = $conductor->class::query(); + } catch (\Throwable $e) { + throw new \Exception('Failed to create query builder instance for ' . $conductor->class . '.', 0, $e); + } + + // Scope query + $conductor->scope($conductor->query); + + // Filter request + $fields = $conductor->fields(new $conductor->class); + if(is_array($fields) == false) { + $fields = []; + } + + $params = $request->all(); + $filterFields = array_intersect_key($params, array_flip($fields)); + $conductor->filter($filterFields); + + // Sort request + $conductor->sort($request->input('sort', $conductor->sort)); + + // Get total + $total = $conductor->count(); + + // Paginate + $conductor->paginate($request->input('page', 1), $request->input('limit', -1)); + + // Limit fields + $limitFields = $request->input('fields'); + if($limitFields == null) { + $limitFields = $fields; + } else { + $limitFields = array_intersect($limitFields, $fields); + } + $conductor->limitFields($limitFields); + + $conductor->collection = $conductor->query->get(); + + // Transform and Includes + $includes = $conductor->includes; + if($request->has('includes')) { + $includes = explode(',', $request->input('includes')); + } + + $conductor->collection = $conductor->collection->map(function ($model) use($conductor, $includes) { + $conductor->includes($model, $includes); + $model = $conductor->transform($model); + + return $model; + }); + + return [$conductor->collection, $total]; + } + + final public static function model(Request $request, Model $model) { + $conductor_class = get_called_class(); + $conductor = new $conductor_class; + + $fields = $conductor->fields(new $conductor->class); + + // Limit fields + $limitFields = $fields; + if($request != null && $request->has('fields')) { + $requestFields = $request->input('fields'); + if($requestFields != null) { + $limitFields = array_intersect(explode(',', $requestFields), $fields); + } + } + + if(empty($limitFields) === false) { + $modelSubset = new $conductor->class; + foreach($limitFields as $field) { + $modelSubset->setAttribute($field, $model->$field); + } + $model = $modelSubset; + } + + // Includes + $includes = $conductor->includes; + if($request != null && $request->has('includes')) { + $includes = explode(',', $request->input('includes', '')); + } + $conductor->includes($model, $includes); + + // Transform + $model = $conductor->transform($model); + + return $model; + } + + final public function filterField(Builder $builder, string $field, mixed $value) { + // Split by comma, but respect quotation marks + if (is_string($value)) { + $values = preg_split('/(?query->where(function ($query) use ($field, $values) { + foreach ($values as $value) { + $value = trim($value); + + // Check if value has a prefix and remove it if it's a number + if (preg_match('/^([<>]=?)(\d+\.?\d*)$/', $value, $matches)) { + $prefix = $matches[1]; + $value = $matches[2]; + } else { + $prefix = ''; + } + + // If the value starts with '=', exact match + if (strpos($value, '=') === 0) { + $query->where($field, '=', substr($value, 1)); + } else { + // Otherwise, use LIKE with '%value%' + $query->where($field, 'LIKE', "%$value%"); + } + + // Apply the prefix to the query if the value is a number + if (is_numeric($value)) { + switch ($prefix) { + case '>': + $query->where($field, '>', $value); + break; + case '<': + $query->where($field, '<', $value); + break; + case '>=': + $query->where($field, '>=', $value); + break; + case '<=': + $query->where($field, '<=', $value); + break; + } + } + } + }); + } + + final public function collection(Collection $collection = null) { + if($collection != null) { + $this->collection = $collection; + } + + return $this->collection; + } + + final public function count() { + if($this->query != null) { + return $this->query->count(); + } + + return 0; + } + + final public function sort(mixed $fields = null) { + if(is_string($fields)) { + $fields = explode(',', $fields); + } else if($fields == null) { + $fields = $this->sort; + } + + if(is_array($fields)) { + foreach ($fields as $orderByField) { + $direction = 'asc'; + $directionChar = substr($orderByField, 0, 1); + + if(in_array($directionChar, ['-', '+'])) { + $orderByField = substr($orderByField, 1); + if($directionChar == '-') { + $direction = 'desc'; + } + } + + $this->query->orderBy(trim($orderByField), $direction); + } + } else { + throw new \InvalidArgumentException('Expected string or array, got ' . gettype($fields)); + } + } + + final public function filter(array $filters) { + foreach ($filters as $param => $value) { + $this->filterField($this->query, $param, $value); + } + } + + final public function paginate(int $page = 1, int $limit = -1) { + // Limit + if($limit < 1) { + $limit = $this->limit; + } else { + $limit = min($limit, $this->maxLimit); + } + $this->query->limit($limit); + + // Page + if($page < 1) { + $page = 1; + } + $this->query->offset(($page - 1) * $limit); + } + + final public function includes(Model $model, array $includes) { + foreach($includes as $include) { + $includeMethodName = 'include' . Str::studly($include); + if (method_exists($this, $includeMethodName)) { + $attributeName = Str::snake($include); + $attributeValue = $this->{$includeMethodName}($model); + if($attributeValue !== null) { + $model->$attributeName = $this->{$includeMethodName}($model); + } + } + } + } + + final public function limitFields(array $fields) { + if(empty($fields) !== true) { + $this->query->select($fields); + } + } + + /** overrides */ + public function scope(Builder $builder) { + + } + + public function fields(Model $model) { + $visibleFields = $model->getVisible(); + if (empty($visibleFields)) { + $tableColumns = $model->getConnection() + ->getSchemaBuilder() + ->getColumnListing($model->getTable()); + return $tableColumns; + } + + return $visibleFields; + } + + public function transform(Model $model) { + return $model->toArray(); + } + + public static function viewable(Model $model) { + return true; + } + + public static function creatable() { + return true; + } + + public static function updatable(Model $model) { + return true; + } + + public static function destroyable(Model $model) { + return true; + } +} \ No newline at end of file diff --git a/app/Conductors/EventConductor.php b/app/Conductors/EventConductor.php new file mode 100644 index 0000000..68bc48c --- /dev/null +++ b/app/Conductors/EventConductor.php @@ -0,0 +1,37 @@ +location == 'online') { + unset($model['address']); + } + + return $model->toArray(); + } + + public static function viewable(Model $model) { + return true; + } + + public function includeYaw(Model $model) { + $model->yaw = 'YAW!!'; + } +} \ No newline at end of file diff --git a/app/Conductors/UserConductor.php b/app/Conductors/UserConductor.php new file mode 100644 index 0000000..c05bdbc --- /dev/null +++ b/app/Conductors/UserConductor.php @@ -0,0 +1,53 @@ +user(); + + if($user === null || $user->hasPermission('admin/users') === false) { + return ['id', 'username']; + } + + return parent::fields($model); + } + + public function transform(Model $model) { + $user = auth()->user(); + $data = $model->toArray(); + + if($user === null || strcasecmp($user->id, $model->id) !== 0) { + $fields = ['id', 'username']; + $data = array_intersect_key($data, array_flip($fields)); + } + + return $data; + } + + public static function viewable(Model $model) { + return true; + } + + public static function updatable(Model $model) { + $user = auth()->user(); + + if($user !== null) { + return $user->hasPermission('admin/users') === true || strcasecmp($user->id, $model->id) === 0; + } + + return false; + } + + public static function destroyable(Model $model) { + $user = auth()->user(); + return $user !== null && $user->hasPermission('admin/users') === true; + } +} \ No newline at end of file -- 2.49.1 From 3bd5c064c3615688e8515c921826f64e5bb9b84f Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 13:46:30 +1000 Subject: [PATCH 06/39] converted from filters to conductors --- app/Http/Controllers/Api/EventController.php | 62 +++++++++++------- app/Http/Controllers/Api/UserController.php | 69 +++++++++++--------- 2 files changed, 78 insertions(+), 53 deletions(-) diff --git a/app/Http/Controllers/Api/EventController.php b/app/Http/Controllers/Api/EventController.php index 816376a..fbc9364 100644 --- a/app/Http/Controllers/Api/EventController.php +++ b/app/Http/Controllers/Api/EventController.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers\Api; use App\Enum\HttpResponseCodes; -use App\Filters\EventFilter; use App\Http\Requests\EventRequest; use App\Models\Event; +use App\Conductors\EventConductor; use Illuminate\Http\Request; class EventController extends ApiController @@ -22,56 +22,70 @@ class EventController extends ApiController /** * Display a listing of the resource. * - * @param EventFilter $filter The event filter. + * @param Request $request The request. * @return \Illuminate\Http\Response */ - public function index(EventFilter $filter) + public function index(Request $request) { + list($collection, $total) = EventConductor::request($request); + return $this->respondAsResource( - $filter->filter(), - ['total' => $filter->foundTotal()] + $collection, + ['total' => $total] ); } /** * Store a newly created resource in storage. * - * @param EventRequest $request The event store request. + * @param Request $request The request. * @return \Illuminate\Http\Response */ - public function store(EventRequest $request) + public function store(Request $request) { - $event = Event::create($request->all()); - return $this->respondAsResource( - (new EventFilter($request))->filter($event), - null, - HttpResponseCodes::HTTP_CREATED - ); + if(EventConductor::creatable()) { + $event = Event::create($request->all()); + return $this->respondAsResource( + EventConductor::model($request, $event), + null, + HttpResponseCodes::HTTP_CREATED + ); + } else { + return $this->respondForbidden(); + } } /** * Display the specified resource. * - * @param EventFilter $filter The event filter. + * @param Request $request The request. * @param \App\Models\Event $event The specified event. * @return \Illuminate\Http\Response */ - public function show(EventFilter $filter, Event $event) + public function show(Request $request, Event $event) { - return $this->respondAsResource($filter->filter($event)); + if(EventConductor::viewable($event)) { + return $this->respondAsResource(EventConductor::model($request, $event)); + } + + return $this->respondForbidden(); } /** * Update the specified resource in storage. * - * @param EventRequest $request The event update request. + * @param Request $request The request. * @param \App\Models\Event $event The specified event. * @return \Illuminate\Http\Response */ - public function update(EventRequest $request, Event $event) + public function update(Request $request, Event $event) { - $event->update($request->all()); - return $this->respondAsResource((new EventFilter($request))->filter($event)); + if(EventConductor::updatable($event)) { + $event->update($request->all()); + return $this->respondAsResource(EventConductor::model($request, $event)); + } else { + return $this->respondForbidden(); + } } /** @@ -82,7 +96,11 @@ class EventController extends ApiController */ public function destroy(Event $event) { - $event->delete(); - return $this->respondNoContent(); + if(EventConductor::destroyable($event)) { + $event->delete(); + return $this->respondNoContent(); + } else { + return $this->respondForbidden(); + } } } diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index 3e1382b..a163d9b 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -23,6 +23,7 @@ use App\Models\User; use App\Models\UserCode; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; +use App\Conductors\UserConductor; class UserController extends ApiController { @@ -48,32 +49,33 @@ class UserController extends ApiController /** * Display a listing of the resource. * - * @param \App\Filters\UserFilter $filter Filter object. + * @param Request $request The request. * @return \Illuminate\Http\Response */ - public function index(UserFilter $filter) + public function index(Request $request) { - $collection = $filter->filter(); + list($collection, $total) = UserConductor::request($request); + return $this->respondAsResource( $collection, - ['total' => $filter->foundTotal()] + ['total' => $total] ); } /** * Store a newly created user in the database. * - * @param UserStoreRequest $request The user update request. + * @param Request $request The request. * @return \Illuminate\Http\Response */ - public function store(UserStoreRequest $request) + public function store(Request $request) { - if ($request->user()->hasPermission('admin/user') !== true) { + if(UserConductor::creatable()) { + $user = User::create($request->all()); + return $this->respondAsResource(UserConductor::model($request, $user), [], HttpResponseCodes::HTTP_CREATED); + } else { return $this->respondForbidden(); } - - $user = User::create($request->all()); - return $this->respondAsResource((new UserFilter($request))->filter($user), [], HttpResponseCodes::HTTP_CREATED); } @@ -84,9 +86,14 @@ class UserController extends ApiController * @param User $user The user model. * @return \Illuminate\Http\Response */ - public function show(UserFilter $filter, User $user) + // public function show(UserFilter $filter, User $user) + public function show(Request $request, User $user) { - return $this->respondAsResource($filter->filter($user)); + if(UserConductor::viewable($user)) { + return $this->respondAsResource(UserConductor::model($request, $user)); + } + + return $this->respondForbidden(); } /** @@ -98,23 +105,23 @@ class UserController extends ApiController */ public function update(UserUpdateRequest $request, User $user) { - $input = []; - $updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password']; + if(UserConductor::updatable($user)) { + $input = []; + $updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password']; - if ($request->user()->hasPermission('admin/user') === true) { - $updatable = array_merge($updatable, ['email_verified_at']); - } elseif ($request->user()->is($user) !== true) { - return $this->respondForbidden(); + if ($request->user()->hasPermission('admin/user') === true) { + $updatable = array_merge($updatable, ['email_verified_at']); + } + + $input = $request->only($updatable); + if (array_key_exists('password', $input) === true) { + $input['password'] = Hash::make($request->input('password')); + } + + $user->update($input); + + return $this->respondAsResource(UserConductor::model($request, $user)); } - - $input = $request->only($updatable); - if (array_key_exists('password', $input) === true) { - $input['password'] = Hash::make($request->input('password')); - } - - $user->update($input); - - return $this->respondAsResource((new UserFilter($request))->filter($user)); } @@ -126,12 +133,12 @@ class UserController extends ApiController */ public function destroy(User $user) { - if ($user->hasPermission('admin/user') === false) { - return $this->respondForbidden(); + if(UserConductor::destroyable($user)) { + $user->delete(); + return $this->respondNoContent(); } - $user->delete(); - return $this->respondNoContent(); + return $this->respondForbidden(); } /** -- 2.49.1 From a9b480994ae58037e2557d396ed304cd9b276f60 Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 15:35:15 +1000 Subject: [PATCH 07/39] support not equals --- app/Conductors/Conductor.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index aa0632b..44d4a87 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -133,7 +133,7 @@ class Conductor { $value = trim($value); // Check if value has a prefix and remove it if it's a number - if (preg_match('/^([<>]=?)(\d+\.?\d*)$/', $value, $matches)) { + if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches)) { $prefix = $matches[1]; $value = $matches[2]; } else { @@ -143,8 +143,11 @@ class Conductor { // If the value starts with '=', exact match if (strpos($value, '=') === 0) { $query->where($field, '=', substr($value, 1)); + } else if (strpos($value, '!=') === 0) { + $query->where($field, '<>', substr($value, 2)); + } else if (strpos($value, '!') === 0) { + $query->where($field, 'NOT LIKE', '%'.substr($value, 1).'%'); } else { - // Otherwise, use LIKE with '%value%' $query->where($field, 'LIKE', "%$value%"); } @@ -163,6 +166,10 @@ class Conductor { case '<=': $query->where($field, '<=', $value); break; + case '!=': + case '<>': + $query->where($field, '<>', $value); + break; } } } -- 2.49.1 From 0ab92d95eab58d2e21d162dc7faf16df391daac4 Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 17:59:53 +1000 Subject: [PATCH 08/39] cleanup comparitors --- app/Conductors/Conductor.php | 159 ++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 70 deletions(-) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index 44d4a87..0329f32 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -1,4 +1,5 @@ class . '.', 0, $e); } - + // Scope query $conductor->scope($conductor->query); // Filter request - $fields = $conductor->fields(new $conductor->class); - if(is_array($fields) == false) { + $fields = $conductor->fields(new $conductor->class()); + if (is_array($fields) === false) { $fields = []; } @@ -53,7 +57,7 @@ class Conductor { // Limit fields $limitFields = $request->input('fields'); - if($limitFields == null) { + if ($limitFields === null) { $limitFields = $fields; } else { $limitFields = array_intersect($limitFields, $fields); @@ -64,11 +68,11 @@ class Conductor { // Transform and Includes $includes = $conductor->includes; - if($request->has('includes')) { + if ($request->has('includes') === true) { $includes = explode(',', $request->input('includes')); } - - $conductor->collection = $conductor->collection->map(function ($model) use($conductor, $includes) { + + $conductor->collection = $conductor->collection->map(function ($model) use ($conductor, $includes) { $conductor->includes($model, $includes); $model = $conductor->transform($model); @@ -78,50 +82,52 @@ class Conductor { return [$conductor->collection, $total]; } - final public static function model(Request $request, Model $model) { + final public static function model(Request $request, Model $model) + { $conductor_class = get_called_class(); - $conductor = new $conductor_class; - - $fields = $conductor->fields(new $conductor->class); + $conductor = new $conductor_class(); + + $fields = $conductor->fields(new $conductor->class()); // Limit fields $limitFields = $fields; - if($request != null && $request->has('fields')) { + if ($request !== null && $request->has('fields') === true) { $requestFields = $request->input('fields'); - if($requestFields != null) { + if ($requestFields !== null) { $limitFields = array_intersect(explode(',', $requestFields), $fields); } } - if(empty($limitFields) === false) { - $modelSubset = new $conductor->class; - foreach($limitFields as $field) { + if (empty($limitFields) === false) { + $modelSubset = new $conductor->class(); + foreach ($limitFields as $field) { $modelSubset->setAttribute($field, $model->$field); } $model = $modelSubset; } - + // Includes $includes = $conductor->includes; - if($request != null && $request->has('includes')) { + if ($request !== null && $request->has('includes') === true) { $includes = explode(',', $request->input('includes', '')); } $conductor->includes($model, $includes); // Transform $model = $conductor->transform($model); - + return $model; } - final public function filterField(Builder $builder, string $field, mixed $value) { + final public function filterField(Builder $builder, string $field, mixed $value) + { // Split by comma, but respect quotation marks - if (is_string($value)) { + if (is_string($value) === true) { $values = preg_split('/(?query->where(function ($query) use ($field, $values) { foreach ($values as $value) { $value = trim($value); - + // Check if value has a prefix and remove it if it's a number - if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches)) { + if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches) !== false) { $prefix = $matches[1]; $value = $matches[2]; } else { $prefix = ''; } - + // If the value starts with '=', exact match if (strpos($value, '=') === 0) { $query->where($field, '=', substr($value, 1)); - } else if (strpos($value, '!=') === 0) { + } elseif (strpos($value, '!=') === 0) { $query->where($field, '<>', substr($value, 2)); - } else if (strpos($value, '!') === 0) { - $query->where($field, 'NOT LIKE', '%'.substr($value, 1).'%'); + } elseif (strpos($value, '!') === 0) { + $query->where($field, 'NOT LIKE', '%' . substr($value, 1) . '%'); } else { $query->where($field, 'LIKE', "%$value%"); } - + // Apply the prefix to the query if the value is a number - if (is_numeric($value)) { + if (is_numeric($value) === true) { switch ($prefix) { case '>': $query->where($field, '>', $value); @@ -172,41 +178,44 @@ class Conductor { break; } } - } + }//end foreach }); } - final public function collection(Collection $collection = null) { - if($collection != null) { + final public function collection(Collection $collection = null) + { + if ($collection !== null) { $this->collection = $collection; } return $this->collection; } - final public function count() { - if($this->query != null) { + final public function count() + { + if ($this->query !== null) { return $this->query->count(); } return 0; } - - final public function sort(mixed $fields = null) { - if(is_string($fields)) { + + final public function sort(mixed $fields = null) + { + if (is_string($fields) === true) { $fields = explode(',', $fields); - } else if($fields == null) { + } elseif ($fields === null) { $fields = $this->sort; } - if(is_array($fields)) { + if (is_array($fields) === true) { foreach ($fields as $orderByField) { $direction = 'asc'; $directionChar = substr($orderByField, 0, 1); - - if(in_array($directionChar, ['-', '+'])) { + + if (in_array($directionChar, ['-', '+']) === true) { $orderByField = substr($orderByField, 1); - if($directionChar == '-') { + if ($directionChar === '-') { $direction = 'desc'; } } @@ -217,16 +226,18 @@ class Conductor { throw new \InvalidArgumentException('Expected string or array, got ' . gettype($fields)); } } - - final public function filter(array $filters) { + + final public function filter(array $filters) + { foreach ($filters as $param => $value) { $this->filterField($this->query, $param, $value); } } - final public function paginate(int $page = 1, int $limit = -1) { + final public function paginate(int $page = 1, int $limit = -1) + { // Limit - if($limit < 1) { + if ($limit < 1) { $limit = $this->limit; } else { $limit = min($limit, $this->maxLimit); @@ -234,39 +245,42 @@ class Conductor { $this->query->limit($limit); // Page - if($page < 1) { + if ($page < 1) { $page = 1; } $this->query->offset(($page - 1) * $limit); } - final public function includes(Model $model, array $includes) { - foreach($includes as $include) { + final public function includes(Model $model, array $includes) + { + foreach ($includes as $include) { $includeMethodName = 'include' . Str::studly($include); - if (method_exists($this, $includeMethodName)) { + if (method_exists($this, $includeMethodName) === true) { $attributeName = Str::snake($include); $attributeValue = $this->{$includeMethodName}($model); - if($attributeValue !== null) { + if ($attributeValue !== null) { $model->$attributeName = $this->{$includeMethodName}($model); } } } } - final public function limitFields(array $fields) { - if(empty($fields) !== true) { + final public function limitFields(array $fields) + { + if (empty($fields) !== true) { $this->query->select($fields); } } /** overrides */ - public function scope(Builder $builder) { - + public function scope(Builder $builder) + { } - public function fields(Model $model) { + public function fields(Model $model) + { $visibleFields = $model->getVisible(); - if (empty($visibleFields)) { + if (empty($visibleFields) === true) { $tableColumns = $model->getConnection() ->getSchemaBuilder() ->getColumnListing($model->getTable()); @@ -276,23 +290,28 @@ class Conductor { return $visibleFields; } - public function transform(Model $model) { + public function transform(Model $model) + { return $model->toArray(); } - public static function viewable(Model $model) { + public static function viewable(Model $model) + { return true; } - public static function creatable() { - return true; - } - - public static function updatable(Model $model) { + public static function creatable() + { return true; } - public static function destroyable(Model $model) { + public static function updatable(Model $model) + { return true; } -} \ No newline at end of file + + public static function destroyable(Model $model) + { + return true; + } +} -- 2.49.1 From b658e964257c76db27764060f458386b9470b3a7 Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 10 Mar 2023 19:27:49 +1000 Subject: [PATCH 09/39] fix errors in spliting --- app/Conductors/Conductor.php | 85 ++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index 0329f32..9a8c2c4 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -20,6 +20,63 @@ class Conductor private $query = null; + private function splitString($string) + { + $parts = []; + $start = 0; + $len = strlen($string); + + while ($start < $len) { + $commaPos = strpos($string, ',', $start); + $singlePos = strpos($string, '\'', $start); + $doublePos = strpos($string, '"', $start); + + // Find the smallest position that is not false + $minPos = false; + if ($commaPos !== false) { + $minPos = $commaPos; + } + if ($singlePos !== false && ($minPos === false || $singlePos < $minPos)) { + $minPos = $singlePos; + } + if ($doublePos !== false && ($minPos === false || $doublePos < $minPos)) { + $minPos = $doublePos; + } + + if ($minPos === false) { + // No more commas, single quotes, or double quotes found + $part = substr($string, $start); + $parts[] = trim($part); + break; + } else { + // Add the current part to the parts array + $part = substr($string, $start, ($minPos - $start)); + $parts[] = trim($part); + + // Update the start position to the next character after the comma, single quote, or double quote + if ($string[$minPos] === ',') { + $start = ($minPos + 1); + } else { + $quoteChar = $string[$minPos]; + $endPos = strpos($string, $quoteChar, ($minPos + 1)); + if ($endPos === false) { + $part = substr($string, ($minPos + 1)); + $parts[] = trim($part); + break; + } else { + $part = substr($string, ($minPos + 1), ($endPos - $minPos - 1)); + $parts[] = trim($part); + $start = ($endPos + 1); + } + } + }//end if + }//end while + + return array_filter($parts, function ($value) { + return $value !== ''; + }); + } + final public static function request(Request $request) { $conductor_class = get_called_class(); @@ -121,12 +178,11 @@ class Conductor final public function filterField(Builder $builder, string $field, mixed $value) { + $values = []; + // Split by comma, but respect quotation marks if (is_string($value) === true) { - $values = preg_split('/(?splitString($value); } elseif (is_array($value) === true) { $values = $value; } else { @@ -139,7 +195,7 @@ class Conductor $value = trim($value); // Check if value has a prefix and remove it if it's a number - if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches) !== false) { + if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches) > 0) { $prefix = $matches[1]; $value = $matches[2]; } else { @@ -147,34 +203,35 @@ class Conductor } // If the value starts with '=', exact match + if (strpos($value, '=') === 0) { - $query->where($field, '=', substr($value, 1)); + $query->orWhere($field, '=', substr($value, 1)); } elseif (strpos($value, '!=') === 0) { - $query->where($field, '<>', substr($value, 2)); + $query->orWhere($field, '<>', substr($value, 2)); } elseif (strpos($value, '!') === 0) { - $query->where($field, 'NOT LIKE', '%' . substr($value, 1) . '%'); + $query->orWhere($field, 'NOT LIKE', '%' . substr($value, 1) . '%'); } else { - $query->where($field, 'LIKE', "%$value%"); + $query->orWhere($field, 'LIKE', "%$value%"); } // Apply the prefix to the query if the value is a number if (is_numeric($value) === true) { switch ($prefix) { case '>': - $query->where($field, '>', $value); + $query->orWhere($field, '>', $value); break; case '<': - $query->where($field, '<', $value); + $query->orWhere($field, '<', $value); break; case '>=': - $query->where($field, '>=', $value); + $query->orWhere($field, '>=', $value); break; case '<=': - $query->where($field, '<=', $value); + $query->orWhere($field, '<=', $value); break; case '!=': case '<>': - $query->where($field, '<>', $value); + $query->orWhere($field, '<>', $value); break; } } -- 2.49.1 From d1833d7b8ddc2fbcd1d8d833285fa9f657d929ca Mon Sep 17 00:00:00 2001 From: James Collins Date: Sat, 11 Mar 2023 21:38:36 +1000 Subject: [PATCH 10/39] added raw "filter" support as well as <> between --- app/Conductors/Conductor.php | 188 ++++++++++++++++++++++++++++------- 1 file changed, 150 insertions(+), 38 deletions(-) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index 9a8c2c4..0581c56 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; class Conductor @@ -102,6 +103,9 @@ class Conductor $params = $request->all(); $filterFields = array_intersect_key($params, array_flip($fields)); $conductor->filter($filterFields); + if ($request->has('filter') === true) { + $conductor->filterRaw($request->input('filter', ''), $fields); + } // Sort request $conductor->sort($request->input('sort', $conductor->sort)); @@ -176,7 +180,7 @@ class Conductor return $model; } - final public function filterField(Builder $builder, string $field, mixed $value) + private function filterFieldWithBuilder(Builder $builder, string $field, mixed $value, string $boolean = 'AND') { $values = []; @@ -190,53 +194,56 @@ class Conductor } // Add each AND check to the query - $this->query->where(function ($query) use ($field, $values) { + $builder->where(function ($query) use ($field, $values) { foreach ($values as $value) { $value = trim($value); + $prefix = ''; // Check if value has a prefix and remove it if it's a number if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches) > 0) { $prefix = $matches[1]; $value = $matches[2]; - } else { - $prefix = ''; - } - - // If the value starts with '=', exact match - - if (strpos($value, '=') === 0) { - $query->orWhere($field, '=', substr($value, 1)); - } elseif (strpos($value, '!=') === 0) { - $query->orWhere($field, '<>', substr($value, 2)); - } elseif (strpos($value, '!') === 0) { - $query->orWhere($field, 'NOT LIKE', '%' . substr($value, 1) . '%'); - } else { - $query->orWhere($field, 'LIKE', "%$value%"); } // Apply the prefix to the query if the value is a number - if (is_numeric($value) === true) { - switch ($prefix) { - case '>': - $query->orWhere($field, '>', $value); - break; - case '<': - $query->orWhere($field, '<', $value); - break; - case '>=': - $query->orWhere($field, '>=', $value); - break; - case '<=': - $query->orWhere($field, '<=', $value); - break; - case '!=': - case '<>': - $query->orWhere($field, '<>', $value); - break; - } - } + switch ($prefix) { + case '=': + $query->orWhere($field, '=', $value); + break; + case '!': + $query->orWhere($field, 'NOT LIKE', "%$value%"); + break; + case '>': + $query->orWhere($field, '>', $value); + break; + case '<': + $query->orWhere($field, '<', $value); + break; + case '>=': + $query->orWhere($field, '>=', $value); + break; + case '<=': + $query->orWhere($field, '<=', $value); + break; + case '!=': + case '<>': + $query->orWhere($field, '!=', $value); + break; + default: + $betweenPos = strpos($value, '<>'); + if ($betweenPos !== false) { + $query->orWhereBetween($field, [substr($value, 0, $betweenPos), substr($value, ($betweenPos + 2))]); + } else { + $query->orWhere($field, 'LIKE', "%$value%"); + } + }//end switch }//end foreach - }); + }, null, null, $boolean); + } + + final public function filterField(string $field, mixed $value, string $boolean = 'AND') + { + $this->filterFieldWithBuilder($this->query, $field, $value, $boolean); } final public function collection(Collection $collection = null) @@ -287,7 +294,7 @@ class Conductor final public function filter(array $filters) { foreach ($filters as $param => $value) { - $this->filterField($this->query, $param, $value); + $this->filterField($param, $value); } } @@ -371,4 +378,109 @@ class Conductor { return true; } + + public function filterRaw($filterString, $limitFields = null) + { + if (is_array($limitFields) === false || empty($limitFields) === true) { + $limitFields = null; + } else { + $limitFields = array_map('strtolower', $limitFields); + } + + $tokens = preg_split('/([()]|,OR,|,AND,|,)/', $filterString, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)); + + $parseTokens = function ($tokenList, $level, $index, $groupBoolean = null) use ($limitFields, &$parseTokens) { + $tokenGroup = []; + $firstToken = false; + $tokenGroupBoolean = 'AND'; + + if ($groupBoolean !== null) { + $firstToken = true; + $tokenGroupBoolean = $groupBoolean; + } + + while ($index < count($tokenList)) { + $token = $tokenList[$index]; + + ++$index; + if ($token === '(') { + // next group + $nextGroupBoolean = null; + if (count($tokenGroup) > 0 && strlen($tokenGroup[(count($tokenGroup) - 1)]['field']) === 0) { + $nextGroupBoolean = $tokenGroup[(count($tokenGroup) - 1)]['boolean']; + unset($tokenGroup[(count($tokenGroup) - 1)]); + } + + $index = $parseTokens($tokenList, $level + 1, $index, $nextGroupBoolean); + } elseif ($token === ')') { + // end group + break; + } elseif (in_array(strtoupper($token), [',AND,', ',OR,']) === true) { + // update boolean + $boolean = trim(strtoupper($token), ','); + + if ($firstToken === false && $level > 0) { + $tokenGroupBoolean = $boolean; + } else { + $firstToken = true; + $tokenGroup[] = [ + 'field' => '', + 'value' => '', + 'boolean' => $boolean + ]; + } + } elseif (strpos($token, ':') !== false) { + // set tokenGroup + $firstToken = true; + $field = substr($token, 0, strpos($token, ':')); + $value = substr($token, (strpos($token, ':') + 1)); + $boolean = 'AND'; + + if (count($tokenGroup) > 0 && strlen($tokenGroup[(count($tokenGroup) - 1)]['field']) === 0) { + $tokenGroup[(count($tokenGroup) - 1)]['field'] = $field; + $tokenGroup[(count($tokenGroup) - 1)]['value'] = $value; + $boolean = $tokenGroup[(count($tokenGroup) - 1)]['boolean']; + } else { + $tokenGroup[] = [ + 'field' => $field, + 'value' => $value, + 'boolean' => 'AND' + ]; + } + + if ($limitFields === null || in_array(strtolower($field), $limitFields) !== true) { + unset($tokenGroup[(count($tokenGroup) - 1)]); + } + + if ($level === 0) { + $this->filterFieldWithBuilder($this->query, $field, $value, $boolean); + } + }//end if + }//end while + + if ($level > 0) { + if ($tokenGroupBoolean === 'OR') { + $this->query->orWhere(function ($query) use ($tokenGroup) { + foreach ($tokenGroup as $tokenItem) { + if (strlen($tokenItem['field']) > 0) { + $this->filterFieldWithBuilder($query, $tokenItem['field'], $tokenItem['value'], $tokenItem['boolean']); + } + } + }); + } else { + $this->query->where(function ($query) use ($tokenGroup) { + foreach ($tokenGroup as $tokenItem) { + if (strlen($tokenItem['field']) > 0) { + $this->filterFieldWithBuilder($query, $tokenItem['field'], $tokenItem['value'], $tokenItem['boolean']); + } + } + }); + } + }//end if + + return $index; + }; + + $parseTokens($tokens, 0, 0); + } } -- 2.49.1 From 874339348c66e0cfdf2038d3971675efb5af901b Mon Sep 17 00:00:00 2001 From: James Collins Date: Sat, 11 Mar 2023 22:53:16 +1000 Subject: [PATCH 11/39] added docs and cleanup --- app/Conductors/Conductor.php | 382 +++++++++++++++++++++++++---------- 1 file changed, 275 insertions(+), 107 deletions(-) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index 0581c56..05e475d 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -6,22 +6,67 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; class Conductor { + /** + * The Conductors Model class. + * + * @var string|null + */ protected $class = null; + + /** + * The default sorting fields of a collection. Can be an array. Supports - and + prefixes. + * + * @var string|array + */ protected $sort = "id"; + + /** + * The default collection size limit per request. + * + * @var integer + */ protected $limit = 50; + + /** + * The maximum collection size limit per request. + * + * @var integer + */ protected $maxLimit = 100; + + /** + * The default includes to include in a request. + * + * @var array + */ protected $includes = []; + /** + * The conductor collection. + * + * @var Collection + */ private $collection = null; + + /** + * The conductor query. + * + * @var Builder + */ private $query = null; - private function splitString($string) + /** + * Split a string on commas, keeping quotes intact. + * + * @param string $string The string to split. + * @return array The split string. + */ + private function splitString(string $string) { $parts = []; $start = 0; @@ -78,6 +123,85 @@ class Conductor }); } + /** + * Filter a field with a specific Builder object + * + * @param Builder $builder The builder object to append. + * @param string $field The field name. + * @param mixed $value The value or array of values to filter. + * @param string $boolean The comparision boolean (AND or OR). + * @return void + */ + private function filterFieldWithBuilder(Builder $builder, string $field, mixed $value, string $boolean = 'AND') + { + $values = []; + + // Split by comma, but respect quotation marks + if (is_string($value) === true) { + $values = $this->splitString($value); + } elseif (is_array($value) === true) { + $values = $value; + } else { + throw new \InvalidArgumentException('Expected string or array, got ' . gettype($value)); + } + + // Add each AND check to the query + $builder->where(function ($query) use ($field, $values) { + foreach ($values as $value) { + $value = trim($value); + $prefix = ''; + + // Check if value has a prefix and remove it if it's a number + if (preg_match('/^(!?=|[<>]=?|<>|!)([^=!<>].*)$/', $value, $matches) > 0) { + $prefix = $matches[1]; + $value = $matches[2]; + } + + // Apply the prefix to the query if the value is a number + switch ($prefix) { + case '=': + $query->orWhere($field, '=', $value); + break; + case '!': + $query->orWhere($field, 'NOT LIKE', "%$value%"); + break; + case '>': + $query->orWhere($field, '>', $value); + break; + case '<': + $query->orWhere($field, '<', $value); + break; + case '>=': + $query->orWhere($field, '>=', $value); + break; + case '<=': + $query->orWhere($field, '<=', $value); + break; + case '!=': + $query->orWhere($field, '!=', $value); + break; + case '<>': + $seperatorPos = strpos($value, '|'); + if ($seperatorPos !== false) { + $query->orWhereBetween($field, [substr($value, 0, $seperatorPos), substr($value, ($seperatorPos + 1))]); + } else { + $query->orWhere($field, '!=', $value); + } + break; + default: + $query->orWhere($field, 'LIKE', "%$value%"); + break; + }//end switch + }//end foreach + }, null, null, $boolean); + } + + /** + * Run the conductor on a Request to generate a collection and total. + * + * @param Request $request The request data. + * @return array The processed and transformed collection | the total rows found. + */ final public static function request(Request $request) { $conductor_class = get_called_class(); @@ -143,6 +267,13 @@ class Conductor return [$conductor->collection, $total]; } + /** + * Run the conductor on a Model with the data stored in a Request. + * + * @param Request $request The request data. + * @param Model $model The model. + * @return array The processed and transformed model data. + */ final public static function model(Request $request, Model $model) { $conductor_class = get_called_class(); @@ -180,72 +311,25 @@ class Conductor return $model; } - private function filterFieldWithBuilder(Builder $builder, string $field, mixed $value, string $boolean = 'AND') - { - $values = []; - - // Split by comma, but respect quotation marks - if (is_string($value) === true) { - $values = $this->splitString($value); - } elseif (is_array($value) === true) { - $values = $value; - } else { - throw new \InvalidArgumentException('Expected string or array, got ' . gettype($value)); - } - - // Add each AND check to the query - $builder->where(function ($query) use ($field, $values) { - foreach ($values as $value) { - $value = trim($value); - $prefix = ''; - - // Check if value has a prefix and remove it if it's a number - if (preg_match('/^([<>!=]=?)(\d+\.?\d*)$/', $value, $matches) > 0) { - $prefix = $matches[1]; - $value = $matches[2]; - } - - // Apply the prefix to the query if the value is a number - switch ($prefix) { - case '=': - $query->orWhere($field, '=', $value); - break; - case '!': - $query->orWhere($field, 'NOT LIKE', "%$value%"); - break; - case '>': - $query->orWhere($field, '>', $value); - break; - case '<': - $query->orWhere($field, '<', $value); - break; - case '>=': - $query->orWhere($field, '>=', $value); - break; - case '<=': - $query->orWhere($field, '<=', $value); - break; - case '!=': - case '<>': - $query->orWhere($field, '!=', $value); - break; - default: - $betweenPos = strpos($value, '<>'); - if ($betweenPos !== false) { - $query->orWhereBetween($field, [substr($value, 0, $betweenPos), substr($value, ($betweenPos + 2))]); - } else { - $query->orWhere($field, 'LIKE', "%$value%"); - } - }//end switch - }//end foreach - }, null, null, $boolean); - } - + /** + * Filter a single field in the conductor collection. + * + * @param string $field The field name. + * @param mixed $value The value or array of values to filter. + * @param string $boolean The comparision boolean (AND or OR). + * @return void + */ final public function filterField(string $field, mixed $value, string $boolean = 'AND') { $this->filterFieldWithBuilder($this->query, $field, $value, $boolean); } + /** + * Get or Set the conductor collection. + * + * @param Collection $collection If not null, use the passed collection. + * @return Collection The current conductor collection. + */ final public function collection(Collection $collection = null) { if ($collection !== null) { @@ -255,6 +339,11 @@ class Conductor return $this->collection; } + /** + * Return the current conductor collection count. + * + * @return integer The current collection count. + */ final public function count() { if ($this->query !== null) { @@ -264,6 +353,12 @@ class Conductor return 0; } + /** + * Sort the conductor collection. + * + * @param mixed $fields A field name or array of field names to sort. Supports a prefix of + or - to change direction. + * @return void + */ final public function sort(mixed $fields = null) { if (is_string($fields) === true) { @@ -291,6 +386,12 @@ class Conductor } } + /** + * Filter the conductor collection based on an array of field => value. + * + * @param array $filters An array of field => value to filter. + * @return void + */ final public function filter(array $filters) { foreach ($filters as $param => $value) { @@ -298,6 +399,13 @@ class Conductor } } + /** + * Paginate the conductor collection. + * + * @param integer $page The current page to return. + * @param integer $limit The limit of items to include or use default. + * @return void + */ final public function paginate(int $page = 1, int $limit = -1) { // Limit @@ -315,6 +423,13 @@ class Conductor $this->query->offset(($page - 1) * $limit); } + /** + * Append a list of includes to the model. + * + * @param Model $model The model to append. + * @param array $includes The list of includes to include. + * @return void + */ final public function includes(Model $model, array $includes) { foreach ($includes as $include) { @@ -329,6 +444,12 @@ class Conductor } } + /** + * Limit the returned fields in the conductor collection. + * + * @param array $fields An array of field names. + * @return void + */ final public function limitFields(array $fields) { if (empty($fields) !== true) { @@ -336,50 +457,14 @@ class Conductor } } - /** overrides */ - public function scope(Builder $builder) - { - } - - public function fields(Model $model) - { - $visibleFields = $model->getVisible(); - if (empty($visibleFields) === true) { - $tableColumns = $model->getConnection() - ->getSchemaBuilder() - ->getColumnListing($model->getTable()); - return $tableColumns; - } - - return $visibleFields; - } - - public function transform(Model $model) - { - return $model->toArray(); - } - - public static function viewable(Model $model) - { - return true; - } - - public static function creatable() - { - return true; - } - - public static function updatable(Model $model) - { - return true; - } - - public static function destroyable(Model $model) - { - return true; - } - - public function filterRaw($filterString, $limitFields = null) + /** + * Filter the conductor collection using raw data. + * + * @param string $filterString The raw filter string to parse. + * @param array|null $limitFields The fields to ignore in the filter string. + * @return void + */ + final public function filterRaw(string $filterString, array|null $limitFields = null) { if (is_array($limitFields) === false || empty($limitFields) === true) { $limitFields = null; @@ -483,4 +568,87 @@ class Conductor $parseTokens($tokens, 0, 0); } + + /** + * Run a scope query on the collection before anything else. + * + * @param Builder $builder The builder in use. + * @return void + */ + public function scope(Builder $builder) + { + } + + /** + * Return an array of model fields visible to the current user. + * + * @param Model $model The model in question. + * @return array The array of field names. + */ + public function fields(Model $model) + { + $visibleFields = $model->getVisible(); + if (empty($visibleFields) === true) { + $tableColumns = $model->getConnection() + ->getSchemaBuilder() + ->getColumnListing($model->getTable()); + return $tableColumns; + } + + return $visibleFields; + } + + /** + * Transform the passed Model to an array + * + * @param Model $model The model to transform. + * @return array The transformed model. + */ + public function transform(Model $model) + { + return $model->toArray(); + } + + /** + * Is the passed model viewable by the current user? + * + * @param Model $model The model in question. + * @return boolean Is the model viewable. + */ + public static function viewable(Model $model) + { + return true; + } + + /** + * Is the model creatable by the current user? + * + * @return boolean Is the model creatable. + */ + public static function creatable() + { + return true; + } + + /** + * Is the passed model updateable by the current user? + * + * @param Model $model The model in question. + * @return boolean Is the model updateable. + */ + public static function updatable(Model $model) + { + return true; + } + + /** + * Is the passed model destroyable by the current user? + * + * @param Model $model The model in question. + * @return boolean Is the model destroyable. + */ + public static function destroyable(Model $model) + { + return true; + } } -- 2.49.1 From e16ba2d09603563449af468ff26d0446f6a4c396 Mon Sep 17 00:00:00 2001 From: James Collins Date: Sun, 12 Mar 2023 13:50:35 +1000 Subject: [PATCH 12/39] remove filters --- app/Filters/AuditFilter.php | 29 -- app/Filters/EventFilter.php | 65 ---- app/Filters/FilterAbstract.php | 597 ----------------------------- app/Filters/MediaFilter.php | 58 --- app/Filters/PostFilter.php | 54 --- app/Filters/SubscriptionFilter.php | 30 -- app/Filters/UserFilter.php | 31 -- 7 files changed, 864 deletions(-) delete mode 100644 app/Filters/AuditFilter.php delete mode 100644 app/Filters/EventFilter.php delete mode 100644 app/Filters/FilterAbstract.php delete mode 100644 app/Filters/MediaFilter.php delete mode 100644 app/Filters/PostFilter.php delete mode 100644 app/Filters/SubscriptionFilter.php delete mode 100644 app/Filters/UserFilter.php diff --git a/app/Filters/AuditFilter.php b/app/Filters/AuditFilter.php deleted file mode 100644 index 6a94963..0000000 --- a/app/Filters/AuditFilter.php +++ /dev/null @@ -1,29 +0,0 @@ -transform(function ($item, $key) { - // $row = $item->toArray(); - - // unset($row['user_type']); - // unset($row['auditable_type']); - - // if (array_key_exists('password', $row['old_values'])) { - // $row['old_values']['password'] = '###'; - // } - // if (array_key_exists('password', $row['new_values'])) { - // $row['new_values']['password'] = '###'; - // } - - // return $row; - // }); - - // return $collection->toArray(); - // } -} diff --git a/app/Filters/EventFilter.php b/app/Filters/EventFilter.php deleted file mode 100644 index c286a18..0000000 --- a/app/Filters/EventFilter.php +++ /dev/null @@ -1,65 +0,0 @@ - ['title','content'], - 'location' => ['location','address'], - ]; - - - /** - * Determine if the user can view the media model - * - * @param Event $event The event instance. - * @param mixed $user The current logged in user. - * @return boolean - */ - protected function viewable(Event $event, mixed $user) - { - return (strcasecmp($event->status, 'draft') !== 0 && $event->publish_at <= now()) - || $user?->hasPermission('admin/events') === true; - } - - /** - * Determine the prebuild query to limit results - * - * @param EloquentBuilder $builder The builder instance. - * @param mixed $user The current logged in user. - * @return EloquentBuilder|null - */ - protected function prebuild(Builder $builder, mixed $user) - { - if ( - $user?->hasPermission('admin/events') !== true - ) { - return $builder - ->where('status', '!=', 'draft') - ->where('publish_at', '<=', now()); - } - } -} diff --git a/app/Filters/FilterAbstract.php b/app/Filters/FilterAbstract.php deleted file mode 100644 index 13d9e62..0000000 --- a/app/Filters/FilterAbstract.php +++ /dev/null @@ -1,597 +0,0 @@ -request = $request; - } - - /** - * Only include the specified attributes in the results. - * - * @param string|array $only Only return these attributes. - * @return void - */ - public function only(mixed $only) - { - if (is_array($only) === true) { - $this->only = $only; - } else { - $this->only = [$only]; - } - } - - /** - * Exclude the specified attributes in the results. - * - * @param string|array $exclude Attributes to exclude. - * @return void - */ - public function exclude(mixed $exclude) - { - if (is_array($exclude) === true) { - $this->exclude = $exclude; - } else { - $this->exclude = [$exclude]; - } - } - - /** - * Check if the model is viewable by the user - * - * @param mixed $model Model instance. - * @param mixed $user Current user. - * @return boolean - */ - // protected function viewable(mixed $model, mixed $user) - // { - // return true; - // } - - /** - * Prepend action to the builder to limit the results - * - * @param Builder $builder Builder instance. - * @param mixed $user Current user. - * @return Builder|null - */ - // protected function prebuild(Builder $builder, mixed $user) - // { - // return $builder; - // } - - - /** - * Return an array of attributes visible in the results - * - * @param array $attributes Attributes currently visible. - * @param User|null $user Current logged in user or null. - * @param object $modelData Model data if a single object is requested. - * @return mixed - */ - protected function seeAttributes(array $attributes, mixed $user, ?object $modelData = null) - { - return $attributes; - } - - /** - * Apply all the requested filters if available. - * - * @param Model $model Model object to filter. If null create query. - * @return Builder|Model - */ - public function filter(Model $model = null) - { - $this->foundTotal = 0; - - $builder = $this->class::query(); - - /* Get the related model */ - $classModel = $model; - if ($model === null) { - $classModel = $builder->getModel(); - } - - /* Get table name */ - if ($this->table === '') { - if ($model === null) { - $this->table = $classModel->getTable(); - } else { - $this->table = $model->getTable(); - } - } - - /* Run query prebuilder or viewable */ - if ($model === null) { - if (method_exists($this, 'prebuild') === true) { - $prebuilder = $this->prebuild($builder, $this->request->user()); - if ($prebuilder instanceof Builder) { - $builder = $prebuilder; - } - } - } else { - if (method_exists($this, 'viewable') === true) { - if ($this->viewable($model, $this->request->user()) === false) { - return null; - } - } - } - - /* Get attributes from table or use 'only' */ - $attributes = []; - if (is_array($this->only) === true && count($this->only) > 0) { - $attributes = $this->only; - } else { - $attributes = Schema::getColumnListing($this->table); - } - - /* Run attribute modifiers*/ - $modifiedAttribs = $this->seeAttributes($attributes, $this->request->user(), $model); - if (is_array($modifiedAttribs) === true) { - $attributes = $modifiedAttribs; - } - - foreach ($attributes as $key => $column) { - $method = 'see' . Str::studly($column) . 'Attribute'; - if ( - method_exists($this, $method) === true && - $this->$method($this->request->user()) === false - ) { - unset($attributes[$key]); - } - } - - if (is_array($this->exclude) === true && count($this->exclude) > 0) { - $attributes = array_diff($attributes, $this->exclude); - } - - /* Setup attributes and appends */ - // $attributesAppends = array_merge($attributes, $classModel->getAppends()); - - /* Apply ?fields= request to attributes */ - if ($this->request->has('fields') === true) { - $attributes = array_intersect($attributes, explode(',', $this->request->fields)); - } - - /* Hide remaining attributes in model (if present) and return */ - if ($model !== null) { - // TODO: Also show $this->request->fields that are appends - - $model->makeHidden(array_diff(Schema::getColumnListing($this->table), $attributes)); - return $model; - } - - /* Are there attributes left? */ - if (count($attributes) === 0) { - $this->foundTotal = 0; - return new Collection(); - } - - /* apply select! */ - $builder->select($attributes); - - /* Setup filterables if not present */ - if ($this->filterable === null) { - $this->filterable = $attributes; - } - - /* Filter values */ - $filterRequest = array_filter($this->request->only(array_intersect($attributes, $this->filterable))); - $this->builderArrayFilter($builder, $filterRequest); - - if (is_array($this->q) === true && count($this->q) > 0) { - $qQueries = []; - foreach ($this->q as $key => $value) { - if (is_array($value) === true) { - $qKey = $key === '_' ? '' : $key; - foreach ($value as $subvalue) { - $qQueries[$key][$subvalue] = $this->request->get("q" . $qKey); - } - } elseif ($this->request->has("q") === true) { - $qQueries['_'][$value] = $this->request->get("q"); - } - } - - foreach ($qQueries as $key => $value) { - $builder->where(function ($query) use ($value) { - $this->builderArrayFilter($query, $value, 'or'); - }); - } - }//end if - - /* Apply sorting */ - $sortList = $this->defaultSort; - if ($this->request->has('sort') === true) { - $sortList = explode(',', $this->request->sort); - } - - /* Transform sort list to array */ - if (is_array($sortList) === false) { - if (strlen($sortList) > 0) { - $sortList = [$sortList]; - } else { - $sortList = []; - } - } - - /* Remove non-viewable attributes from sort list */ - if (count($sortList) > 0) { - $sortList = array_filter($sortList, function ($item) use ($attributes) { - $parsedItem = $item; - if (substr($parsedItem, 0, 1) === '-') { - $parsedItem = substr($parsedItem, 1); - } - - return in_array($parsedItem, $attributes); - }); - } - - /* Do we have any sort element left? */ - if (count($sortList) > 0) { - foreach ($sortList as $sortAttribute) { - $prefix = substr($sortAttribute, 0, 1); - $direction = 'asc'; - - if (in_array($prefix, ['-', '+']) === true) { - $sortAttribute = substr($sortAttribute, 1); - if ($prefix === '-') { - $direction = 'desc'; - } - } - - $builder->orderBy($sortAttribute, $direction); - }//end foreach - }//end if - - /* save found count */ - $this->foundTotal = $builder->count(); - - /* Apply result limit */ - $limit = $this->defaultLimit; - if ($this->request->has('limit') === true) { - $limit = intval($this->request->limit); - } - if ($limit < 1) { - $limit = 1; - } - if ($limit > $this->maxLimit && $this->maxLimit !== 0) { - $limit = $this->maxLimit; - } - - $builder->limit($limit); - - /* Apply page offset */ - if ($this->request->has('page') === true) { - $page = intval($this->request->page); - if ($page < 1) { - $page = 1; - } - - $builder->offset((intval($this->request->page) - 1) * $limit); - } - - /* run spot run */ - $collection = $builder->get(); - - return $collection; - } - - /** - * Filter content based on the filterRequest - * @param mixed $builder Builder object - * @param array $filterRequest Filter key/value - * @param string $defaultBoolean Default where boolean - * @return void - */ - protected function builderArrayFilter(mixed $builder, array $filterRequest, string $defaultBoolean = 'and') - { - foreach ($filterRequest as $filterAttribute => $filterValue) { - $tags = []; - $boolean = $defaultBoolean; - - $matches = preg_split('/(? $match_info) { - if (($idx % 2) === true) { - if (substr($filterValue, ($match_info[1] - 2), 1) === ',') { - $tags[] = ['operator' => '', 'tag' => stripslashes(trim($match_info[0]))]; - } else { - $tags[(count($tags) - 1)]['tag'] .= stripslashes(trim($match_info[0])); - } - } else { - $innerTags = [$match_info[0]]; - if (strpos($match_info[0], ',') !== false) { - $innerTags = preg_split('/(? 0) { - $operator = '='; - - $single = substr($tag, 0, 1); - $double = substr($tag . ' ', 0, 2); // add empty space incase len $tag < 2 - - // check for operators at start - if (in_array($double, ['!=', '<>', '><', '>=', '<=', '=>', '=<']) === true) { - if ($double === '<>' || $double === '><') { - $double = '!='; - } elseif ($double === '=>') { - $double = '>='; - } elseif ($double === '=<') { - $double == '>='; - } - - $operator = $double; - $tag = substr($tag, 2); - } else { - if (in_array($single, ['=', '!', '>', '<', '~', '%']) === true) { - if ($single === '=') { - $single = '=='; // a single '=' is actually a double '==' - } - - $operator = $single; - $tag = substr($tag, 1); - } - }//end if - - $tags[] = ['operator' => $operator, 'tag' => $tag]; - }//end if - }//end foreach - }//end if - }//end foreach - - if (count($tags) > 1) { - $boolean = 'or'; - } - - foreach ($tags as $tag_data) { - $operator = $tag_data['operator']; - $value = $tag_data['tag']; - $table = $this->table; - $column = $filterAttribute; - - if (($dotPos = strpos($filterAttribute, '.')) !== false) { - $table = substr($filterAttribute, 0, $dotPos); - $column = substr($filterAttribute, ($dotPos + 1)); - } - - $columnType = DB::getSchemaBuilder()->getColumnType($table, $column); - - if ( - in_array($columnType, ['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint', - 'decimal', 'float', 'double', 'real', 'double precision' - ]) === true - ) { - if (in_array($operator, ['=', '>', '<', '>=', '<=', '%', '!']) === false) { - continue; - } - - $columnType = 'numeric'; - } elseif (in_array($columnType, ['date', 'time', 'datetime', 'timestamp', 'year']) === true) { - if (in_array($operator, ['=', '>', '<', '>=', '<=', '!']) === false) { - continue; - } - - $columnType = 'datetime'; - } elseif ( - in_array($columnType, ['string', 'char', 'varchar', 'timeblob', 'blob', 'mediumblob', - 'longblob', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum' - ]) === true - ) { - if (in_array($operator, ['=', '==', '!', '!=', '~']) === false) { - continue; - } - - $columnType = 'text'; - - if ($value === "''" || $value === '""') { - $value = ''; - } elseif (strcasecmp($value, 'null') !== 0) { - if ($operator === '!') { - $operator = 'NOT LIKE'; - $value = '%' . $value . '%'; - } elseif ($operator === '=') { - $operator = 'LIKE'; - $value = '%' . $value . '%'; - } elseif ($operator === '~') { - $operator = 'SOUNDS LIKE'; - } elseif ($operator === '==') { - $operator = '='; - } - } - } elseif ($columnType === 'boolean') { - if (in_array($operator, ['=', '!']) === false) { - continue; - } - - if (strtolower($value) === 'true') { - $value = 1; - } elseif (strtolower($value) === 'false') { - $value = 0; - } - }//end if - - $betweenSeperator = strpos($value, '<>'); - if ( - $operator === '=' && $betweenSeperator !== false && in_array($columnType, ['numeric', - 'datetime' - ]) === true - ) { - $value = explode('<>', $value); - $operator = '<>'; - } - - if ($operator !== '') { - $this->builderWhere($builder, $table, $column, $operator, $value, $boolean); - } - }//end foreach - }//end foreach - } - - /** - * Insert a where statement into the builder, taking the filter map into consideration - * - * @param Builder $builder Builder instance. - * @param string $table Table name. - * @param string $column Column name. - * @param string $operator Where operator. - * @param mixed $value Value to test. - * @param string $boolean Use Or comparison. - * @return void - * @throws RuntimeException Error applying statement. - * @throws InvalidArgumentException Error applying statement. - */ - protected function builderWhere( - Builder &$builder, - string $table, - string $column, - string $operator, - mixed $value, - string $boolean - ) { - if ( - (is_string($value) === true && $operator !== '<>') || (is_array($value) === true && count($value) === 2 && - $operator === '<>') - ) { - if ($table !== '' && $table !== $this->table) { - $builder->whereHas($table, function ($query) use ($column, $operator, $value, $boolean) { - if ($operator !== '<>') { - if (strcasecmp($value, 'null') === 0) { - if ($operator === '!') { - $query->whereNotNull($column, $boolean); - } else { - $query->whereNull($column, $boolean); - } - } else { - $query->where($column, $operator, $value, $boolean); - } - } else { - $query->whereBetween($column, $value, $boolean); - } - }); - } else { - if ($operator !== '<>') { - if (strcasecmp($value, 'null') === 0) { - if ($operator === '!') { - $builder->whereNotNull($column, $boolean); - } else { - $builder->whereNull($column, $boolean); - } - } else { - $builder->where($column, $operator, $value, $boolean); - } - } else { - $builder->whereBetween($column, $value, $boolean); - } - }//end if - }//end if - } - - /** - * Return the found total of items - * @return integer - */ - public function foundTotal() - { - return $this->foundTotal; - } -} diff --git a/app/Filters/MediaFilter.php b/app/Filters/MediaFilter.php deleted file mode 100644 index 0e704d5..0000000 --- a/app/Filters/MediaFilter.php +++ /dev/null @@ -1,58 +0,0 @@ -permission) === false) { - return ($user?->hasPermission('admin/media') || $user?->hasPermission($media->permission)); - } - - return true; - } - - /** - * Determine the prebuild query to limit results - * - * @param EloquentBuilder $builder The builder instance. - * @param mixed $user The current logged in user. - * @return EloquentBuilder|null - */ - protected function prebuild(Builder $builder, mixed $user) - { - if ($user === null) { - return $builder->whereNull('permission'); - } - } - - /** - * Show the permission attribute in the results - * - * @param User|null $user Current logged in user or null. - * @return boolean - */ - protected function seePermissionAttribute(mixed $user) - { - return ($user?->hasPermission('admin/media')); - } -} diff --git a/app/Filters/PostFilter.php b/app/Filters/PostFilter.php deleted file mode 100644 index 030b5c5..0000000 --- a/app/Filters/PostFilter.php +++ /dev/null @@ -1,54 +0,0 @@ -hasPermission('admin/posts') !== true) { - return ($post->publish_at <= now()); - } - - return true; - } - - /** - * Determine the prebuild query to limit results - * - * @param EloquentBuilder $builder The builder instance. - * @param mixed $user The current logged in user. - * @return EloquentBuilder|null - */ - protected function prebuild(Builder $builder, mixed $user) - { - if ($user?->hasPermission('admin/posts') !== true) { - return $builder->where('publish_at', '<=', Carbon::now()); - } - } -} diff --git a/app/Filters/SubscriptionFilter.php b/app/Filters/SubscriptionFilter.php deleted file mode 100644 index 04d0d8d..0000000 --- a/app/Filters/SubscriptionFilter.php +++ /dev/null @@ -1,30 +0,0 @@ -hasPermission('admin/users') !== true) { - return ['id', 'email', 'confirmed_at']; - } - } -} diff --git a/app/Filters/UserFilter.php b/app/Filters/UserFilter.php deleted file mode 100644 index 398969d..0000000 --- a/app/Filters/UserFilter.php +++ /dev/null @@ -1,31 +0,0 @@ -hasPermission('admin/users') !== true && ($user === null || $userData === null || $user?->id !== $userData?->id)) { - return ['id', 'username']; - } - } -} -- 2.49.1 From 3f48f11838e4b15c504be194644ff95dde32d875 Mon Sep 17 00:00:00 2001 From: James Collins Date: Sun, 12 Mar 2023 13:50:51 +1000 Subject: [PATCH 13/39] support quotes and embed quotes in filter --- app/Conductors/Conductor.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index 05e475d..2a2379f 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -473,6 +473,30 @@ class Conductor } $tokens = preg_split('/([()]|,OR,|,AND,|,)/', $filterString, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)); + $glued = []; + $glueToken = ''; + foreach ($tokens as $item) { + if ($glueToken === '') { + if (preg_match('/(? Date: Sun, 12 Mar 2023 13:51:01 +1000 Subject: [PATCH 14/39] added array helper functions --- app/Helpers/Array.php | 40 ++++++++++++++++++++++++++++++++++++++++ composer.json | 8 +++++++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 app/Helpers/Array.php diff --git a/app/Helpers/Array.php b/app/Helpers/Array.php new file mode 100644 index 0000000..f58662f --- /dev/null +++ b/app/Helpers/Array.php @@ -0,0 +1,40 @@ + Date: Sun, 12 Mar 2023 13:51:12 +1000 Subject: [PATCH 15/39] use new filter option --- resources/js/views/EventList.vue | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/resources/js/views/EventList.vue b/resources/js/views/EventList.vue index 699ec3c..cbaac9f 100644 --- a/resources/js/views/EventList.vue +++ b/resources/js/views/EventList.vue @@ -95,11 +95,21 @@ const handleLoad = async () => { try { let query = {}; + /* + cats, dogs + (title:"cats, dogs",OR,content:"cats, dogs") + + "cats, dogs", mice + (title:""cats, dogs", mice",OR,content:"\"cats, dogs\", mice") + */ + if (filterKeywords.value && filterKeywords.value.length > 0) { - query["q"] = filterKeywords.value; + let value = filterKeywords.value.replace(/"/g, '\\"'); + + query["filter"] = `(title:"${value}",OR,content:"${value}")`; } if (filterLocation.value && filterLocation.value.length > 0) { - query["qlocation"] = filterLocation.value; + query["location"] = filterLocation.value; } if (filterDateRange.value && filterDateRange.value.length > 0) { let error = false; -- 2.49.1 From 615abcc8e310af4caba46de974e866732acea3f2 Mon Sep 17 00:00:00 2001 From: James Collins Date: Sun, 12 Mar 2023 13:51:23 +1000 Subject: [PATCH 16/39] added new conductors --- app/Conductors/EventConductor.php | 95 +++++++++++++++----- app/Conductors/MediaConductor.php | 109 +++++++++++++++++++++++ app/Conductors/PostConductor.php | 91 +++++++++++++++++++ app/Conductors/SubscriptionConductor.php | 12 +++ app/Conductors/UserConductor.php | 67 ++++++++++---- 5 files changed, 335 insertions(+), 39 deletions(-) create mode 100644 app/Conductors/MediaConductor.php create mode 100644 app/Conductors/PostConductor.php create mode 100644 app/Conductors/SubscriptionConductor.php diff --git a/app/Conductors/EventConductor.php b/app/Conductors/EventConductor.php index 68bc48c..a47bc9b 100644 --- a/app/Conductors/EventConductor.php +++ b/app/Conductors/EventConductor.php @@ -1,37 +1,92 @@ location == 'online') { - unset($model['address']); + /** + * Run a scope query on the collection before anything else. + * + * @param Builder $builder The builder in use. + * @return void + */ + public function scope(Builder $builder) + { + $user = auth()->user(); + if ($user === null || $user->has_permission('admin/events') === false) { + $builder + ->where('status', '!=', 'draft') + ->where('publish_at', '<=', now()); } - - return $model->toArray(); } - public static function viewable(Model $model) { + /** + * Return if the current model is visible. + * + * @param Model $model The model. + * @return boolean Allow model to be visible. + */ + public static function viewable(Model $model) + { + if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) { + $user = auth()->user(); + if ($user === null || $user->has_permission('admin/events') === false) { + return false; + } + } + return true; } - public function includeYaw(Model $model) { - $model->yaw = 'YAW!!'; + /** + * Return if the current model is creatable. + * + * @return boolean Allow creating model. + */ + public static function creatable() + { + $user = auth()->user(); + return ($user !== null && $user->has_permission('admin/events') === true); } -} \ No newline at end of file + + /** + * Return if the current model is updatable. + * + * @param Model $model The model. + * @return boolean Allow updating model. + */ + public static function updatable(Model $model) + { + $user = auth()->user(); + return ($user !== null && $user->has_permission('admin/events') === true); + } + + /** + * Return if the current model is deletable. + * + * @param Model $model The model. + * @return boolean Allow deleting model. + */ + public static function deletable(Model $model) + { + $user = auth()->user(); + return ($user !== null && $user->has_permission('admin/events') === true); + } +} diff --git a/app/Conductors/MediaConductor.php b/app/Conductors/MediaConductor.php new file mode 100644 index 0000000..6ad3de8 --- /dev/null +++ b/app/Conductors/MediaConductor.php @@ -0,0 +1,109 @@ +user(); + if ($user === null || $user->hasPermission('admin/media') === false) { + $fields = arrayRemoveItem($fields, 'permission'); + } + + return $fields; + } + + /** + * Run a scope query on the collection before anything else. + * + * @param Builder $builder The builder in use. + * @return void + */ + public function scope(Builder $builder) + { + $user = auth()->user(); + if ($user === null) { + $builder->whereNull('permission'); + } else { + $builder->whereNull('permission')->orWhereIn('permission', $user->permissions); + } + } + + /** + * Return if the current model is visible. + * + * @param Model $model The model. + * @return boolean Allow model to be visible. + */ + public static function viewable(Model $model) + { + if ($model->permission !== null) { + $user = auth()->user(); + if ($user === null || $user->has_permission($model->permission) === false) { + return false; + } + } + + return true; + } + + /** + * Return if the current model is creatable. + * + * @return boolean Allow creating model. + */ + public static function creatable() + { + $user = auth()->user(); + return ($user !== null); + } + + /** + * Return if the current model is updatable. + * + * @param Model $model The model. + * @return boolean Allow updating model. + */ + public static function updatable(Model $model) + { + $user = auth()->user(); + return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 || $user->has_permission('admin/media') === true)); + } + + /** + * Return if the current model is deletable. + * + * @param Model $model The model. + * @return boolean Allow deleting model. + */ + public static function deletable(Model $model) + { + $user = auth()->user(); + return ($user !== null && ($model->user_id === $user->id || $user->has_permission('admin/media') === true)); + } +} diff --git a/app/Conductors/PostConductor.php b/app/Conductors/PostConductor.php new file mode 100644 index 0000000..63b4988 --- /dev/null +++ b/app/Conductors/PostConductor.php @@ -0,0 +1,91 @@ +user(); + if ($user === null || $user->has_permission('admin/posts') === false) { + $builder + ->where('publish_at', '<=', now()); + } + } + + /** + * Return if the current model is visible. + * + * @param Model $model The model. + * @return boolean Allow model to be visible. + */ + public static function viewable(Model $model) + { + if (Carbon::parse($model->publish_at)->isFuture() === true) { + $user = auth()->user(); + if ($user === null || $user->has_permission('admin/posts') === false) { + return false; + } + } + + return true; + } + + /** + * Return if the current model is creatable. + * + * @return boolean Allow creating model. + */ + public static function creatable() + { + $user = auth()->user(); + return ($user !== null && $user->has_permission('admin/posts') === true); + } + + /** + * Return if the current model is updatable. + * + * @param Model $model The model. + * @return boolean Allow updating model. + */ + public static function updatable(Model $model) + { + $user = auth()->user(); + return ($user !== null && $user->has_permission('admin/posts') === true); + } + + /** + * Return if the current model is deletable. + * + * @param Model $model The model. + * @return boolean Allow deleting model. + */ + public static function deletable(Model $model) + { + $user = auth()->user(); + return ($user !== null && $user->has_permission('admin/posts') === true); + } +} diff --git a/app/Conductors/SubscriptionConductor.php b/app/Conductors/SubscriptionConductor.php new file mode 100644 index 0000000..d121a4f --- /dev/null +++ b/app/Conductors/SubscriptionConductor.php @@ -0,0 +1,12 @@ +user(); - - if($user === null || $user->hasPermission('admin/users') === false) { + if ($user === null || $user->hasPermission('admin/users') === false) { return ['id', 'username']; } return parent::fields($model); } - public function transform(Model $model) { + /** + * Transform the passed Model to an array + * + * @param Model $model The model to transform. + * @return array The transformed model. + */ + public function transform(Model $model) + { $user = auth()->user(); $data = $model->toArray(); - if($user === null || strcasecmp($user->id, $model->id) !== 0) { + if ($user === null || strcasecmp($user->id, $model->id) !== 0) { $fields = ['id', 'username']; - $data = array_intersect_key($data, array_flip($fields)); + $data = arrayOnlyItems($data, $fields); } - + return $data; } - public static function viewable(Model $model) { - return true; - } - - public static function updatable(Model $model) { + /** + * Return if the current model is updatable. + * + * @param Model $model The model. + * @return boolean Allow updating model. + */ + public static function updatable(Model $model) + { $user = auth()->user(); - - if($user !== null) { - return $user->hasPermission('admin/users') === true || strcasecmp($user->id, $model->id) === 0; + if ($user !== null) { + return ($user->hasPermission('admin/users') === true || strcasecmp($user->id, $model->id) === 0); } return false; } - public static function destroyable(Model $model) { + /** + * Return if the current model is deletable. + * + * @param Model $model The model. + * @return boolean Allow deleting model. + */ + public static function destroyable(Model $model) + { $user = auth()->user(); - return $user !== null && $user->hasPermission('admin/users') === true; + return ($user !== null && $user->hasPermission('admin/users') === true); } -} \ No newline at end of file +} -- 2.49.1 From c18b740f4603666b63f7ef431214ef8a3ef70346 Mon Sep 17 00:00:00 2001 From: James Collins Date: Sun, 12 Mar 2023 15:39:43 +1000 Subject: [PATCH 17/39] cleanup --- app/Conductors/SubscriptionConductor.php | 27 +++ app/Filters/SubscriptionFilter.php | 30 +++ app/Http/Controllers/Api/EventController.php | 58 +++--- app/Http/Controllers/Api/MediaController.php | 177 +++++++++--------- app/Http/Controllers/Api/PostController.php | 69 ++++--- .../Api/SubscriptionController.php | 84 +++++---- app/Http/Controllers/Api/UserController.php | 41 ++-- app/Http/Requests/BaseRequest.php | 8 +- app/Http/Requests/EventRequest.php | 22 +-- app/Http/Requests/MediaRequest.php | 8 + app/Http/Requests/MediaStoreRequest.php | 20 -- app/Http/Requests/MediaUpdateRequest.php | 20 -- app/Http/Requests/PostRequest.php | 42 +++++ app/Http/Requests/PostStoreRequest.php | 23 --- app/Http/Requests/PostUpdateRequest.php | 28 --- app/Http/Requests/SubscriptionRequest.php | 14 +- 16 files changed, 358 insertions(+), 313 deletions(-) create mode 100644 app/Filters/SubscriptionFilter.php create mode 100644 app/Http/Requests/MediaRequest.php delete mode 100644 app/Http/Requests/MediaStoreRequest.php delete mode 100644 app/Http/Requests/MediaUpdateRequest.php create mode 100644 app/Http/Requests/PostRequest.php delete mode 100644 app/Http/Requests/PostStoreRequest.php delete mode 100644 app/Http/Requests/PostUpdateRequest.php diff --git a/app/Conductors/SubscriptionConductor.php b/app/Conductors/SubscriptionConductor.php index d121a4f..3f447d2 100644 --- a/app/Conductors/SubscriptionConductor.php +++ b/app/Conductors/SubscriptionConductor.php @@ -2,6 +2,8 @@ namespace App\Conductors; +use Illuminate\Database\Eloquent\Model; + class SubscriptionConductor extends Conductor { /** @@ -9,4 +11,29 @@ class SubscriptionConductor extends Conductor * @var string */ protected $class = '\App\Models\Subscription'; + + + /** + * Return if the current model is updatable. + * + * @param Model $model The model. + * @return boolean Allow updating model. + */ + public static function updatable(Model $model) + { + $user = auth()->user(); + return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->has_permission('admin/subscriptions') === true)); + } + + /** + * Return if the current model is deletable. + * + * @param Model $model The model. + * @return boolean Allow deleting model. + */ + public static function deletable(Model $model) + { + $user = auth()->user(); + return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->has_permission('admin/subscriptions') === true)); + } } diff --git a/app/Filters/SubscriptionFilter.php b/app/Filters/SubscriptionFilter.php new file mode 100644 index 0000000..04d0d8d --- /dev/null +++ b/app/Filters/SubscriptionFilter.php @@ -0,0 +1,30 @@ +hasPermission('admin/users') !== true) { + return ['id', 'email', 'confirmed_at']; + } + } +} diff --git a/app/Http/Controllers/Api/EventController.php b/app/Http/Controllers/Api/EventController.php index fbc9364..2f4febb 100644 --- a/app/Http/Controllers/Api/EventController.php +++ b/app/Http/Controllers/Api/EventController.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers\Api; use App\Enum\HttpResponseCodes; -use App\Http\Requests\EventRequest; use App\Models\Event; use App\Conductors\EventConductor; +use App\Http\Requests\EventRequest; use Illuminate\Http\Request; class EventController extends ApiController @@ -22,7 +22,7 @@ class EventController extends ApiController /** * Display a listing of the resource. * - * @param Request $request The request. + * @param \Illuminate\Http\Request $request The endpoint request. * @return \Illuminate\Http\Response */ public function index(Request $request) @@ -36,14 +36,30 @@ class EventController extends ApiController } /** - * Store a newly created resource in storage. + * Display the specified resource. * - * @param Request $request The request. + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\Event $event The specified event. * @return \Illuminate\Http\Response */ - public function store(Request $request) + public function show(Request $request, Event $event) { - if(EventConductor::creatable()) { + if (EventConductor::viewable($event) === true) { + return $this->respondAsResource(EventConductor::model($request, $event)); + } + + return $this->respondForbidden(); + } + + /** + * Store a newly created resource in storage. + * + * @param \App\Http\Requests\EventRequest $request The request. + * @return \Illuminate\Http\Response + */ + public function store(EventRequest $request) + { + if (EventConductor::creatable() === true) { $event = Event::create($request->all()); return $this->respondAsResource( EventConductor::model($request, $event), @@ -56,38 +72,22 @@ class EventController extends ApiController } /** - * Display the specified resource. + * Update the specified resource in storage. * - * @param Request $request The request. - * @param \App\Models\Event $event The specified event. + * @param \App\Http\Requests\EventRequest $request The endpoint request. + * @param \App\Models\Event $event The specified event. * @return \Illuminate\Http\Response */ - public function show(Request $request, Event $event) + public function update(EventRequest $request, Event $event) { - if(EventConductor::viewable($event)) { + if (EventConductor::updatable($event) === true) { + $event->update($request->all()); return $this->respondAsResource(EventConductor::model($request, $event)); } return $this->respondForbidden(); } - /** - * Update the specified resource in storage. - * - * @param Request $request The request. - * @param \App\Models\Event $event The specified event. - * @return \Illuminate\Http\Response - */ - public function update(Request $request, Event $event) - { - if(EventConductor::updatable($event)) { - $event->update($request->all()); - return $this->respondAsResource(EventConductor::model($request, $event)); - } else { - return $this->respondForbidden(); - } - } - /** * Remove the specified resource from storage. * @@ -96,7 +96,7 @@ class EventController extends ApiController */ public function destroy(Event $event) { - if(EventConductor::destroyable($event)) { + if (EventConductor::destroyable($event) === true) { $event->delete(); return $this->respondNoContent(); } else { diff --git a/app/Http/Controllers/Api/MediaController.php b/app/Http/Controllers/Api/MediaController.php index 9fdf11fa..5fde469 100644 --- a/app/Http/Controllers/Api/MediaController.php +++ b/app/Http/Controllers/Api/MediaController.php @@ -2,14 +2,12 @@ namespace App\Http\Controllers\Api; +use App\Conductors\MediaConductor; use App\Enum\HttpResponseCodes; -use App\Filters\MediaFilter; -use App\Http\Requests\MediaStoreRequest; -use App\Http\Requests\MediaUpdateRequest; +use App\Http\Requests\MediaRequest; use App\Models\Media; use Illuminate\Http\Request; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Storage; use Laravel\Sanctum\PersonalAccessToken; class MediaController extends ApiController @@ -26,99 +24,67 @@ class MediaController extends ApiController /** * Display a listing of the resource. * - * @param \App\Filters\MediaFilter $filter Created filter object. + * @param \Illuminate\Http\Request $request The endpoint request. * @return \Illuminate\Http\Response */ - public function index(MediaFilter $filter) + public function index(Request $request) { + list($collection, $total) = MediaConductor::request($request); + return $this->respondAsResource( - $filter->filter(), - ['total' => $filter->foundTotal()] + $collection, + ['total' => $total] ); } /** * Display the specified resource. * - * @param MediaFilter $filter The request filter. - * @param Media $medium The request media. + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\Media $medium The request media. * @return \Illuminate\Http\Response */ - public function show(MediaFilter $filter, Media $medium) + public function show(Request $request, Media $medium) { - return $this->respondAsResource($filter->filter($medium)); + if (MediaConductor::viewable($medium) === true) { + return $this->respondAsResource(MediaConductor::model($request, $medium)); + } + + return $this->respondForbidden(); } /** * Store a new media resource * - * @param MediaStoreRequest $request The uploaded media. + * @param \App\Http\Requests\MediaRequest $request The uploaded media. * @return \Illuminate\Http\Response */ - public function store(MediaStoreRequest $request) + public function store(MediaRequest $request) { - $file = $request->file('file'); - if ($file === null) { - return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']); - } - - if ($file->isValid() !== true) { - switch ($file->getError()) { - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - return $this->respondTooLarge(); - case UPLOAD_ERR_PARTIAL: - return $this->respondWithErrors(['file' => 'The file upload was interrupted.']); - default: - return $this->respondWithErrors(['file' => 'An error occurred uploading the file to the server.']); + if (MediaConductor::creatable() === true) { + $file = $request->file('file'); + if ($file === null) { + return $this->respondWithErrors(['file' => 'The browser did not upload the file correctly to the server.']); } - } - if ($file->getSize() > Media::maxUploadSize()) { - return $this->respondTooLarge(); - } + if ($file->isValid() !== true) { + switch ($file->getError()) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + return $this->respondTooLarge(); + case UPLOAD_ERR_PARTIAL: + return $this->respondWithErrors(['file' => 'The file upload was interrupted.']); + default: + return $this->respondWithErrors(['file' => 'An error occurred uploading the file to the server.']); + } + } - $title = $file->getClientOriginalName(); - $mime = $file->getMimeType(); - $fileInfo = Media::store($file, empty($request->input('permission'))); - if ($fileInfo === null) { - return $this->respondWithErrors( - ['file' => 'The file could not be stored on the server'], - HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR - ); - } - - $request->merge([ - 'title' => $title, - 'mime' => $mime, - 'name' => $fileInfo['name'], - 'size' => filesize($fileInfo['path']) - ]); - - $media = $request->user()->media()->create($request->all()); - return $this->respondAsResource((new MediaFilter($request))->filter($media)); - } - - /** - * Update the media resource in storage. - * - * @param MediaUpdateRequest $request The update request. - * @param \App\Models\Media $medium The specified media. - * @return \Illuminate\Http\Response - */ - public function update(MediaUpdateRequest $request, Media $medium) - { - if ((new MediaFilter($request))->filter($medium) === null) { - return $this->respondNotFound(); - } - - $file = $request->file('file'); - if ($file !== null) { if ($file->getSize() > Media::maxUploadSize()) { return $this->respondTooLarge(); } - $oldPath = $medium->path(); + $title = $file->getClientOriginalName(); + $mime = $file->getMimeType(); $fileInfo = Media::store($file, empty($request->input('permission'))); if ($fileInfo === null) { return $this->respondWithErrors( @@ -127,34 +93,77 @@ class MediaController extends ApiController ); } - if (file_exists($oldPath) === true) { - unlink($oldPath); - } - $request->merge([ - 'title' => $file->getClientOriginalName(), - 'mime' => $file->getMimeType(), + 'title' => $title, + 'mime' => $mime, 'name' => $fileInfo['name'], 'size' => filesize($fileInfo['path']) ]); + + $media = $request->user()->media()->create($request->all()); + return $this->respondAsResource( + MediaConductor::model($request, $media), + null, + HttpResponseCodes::HTTP_CREATED + ); }//end if - $medium->update($request->all()); - return $this->respondWithTransformer($file); + return $this->respondForbidden(); } + /** + * Update the media resource in storage. + * + * @param \App\Http\Requests\MediaRequest $request The update request. + * @param \App\Models\Media $medium The specified media. + * @return \Illuminate\Http\Response + */ + public function update(MediaRequest $request, Media $medium) + { + if (MediaConductor::updatable($medium) === true) { + $file = $request->file('file'); + if ($file !== null) { + if ($file->getSize() > Media::maxUploadSize()) { + return $this->respondTooLarge(); + } + $oldPath = $medium->path(); + $fileInfo = Media::store($file, empty($request->input('permission'))); + if ($fileInfo === null) { + return $this->respondWithErrors( + ['file' => 'The file could not be stored on the server'], + HttpResponseCodes::HTTP_INTERNAL_SERVER_ERROR + ); + } + + if (file_exists($oldPath) === true) { + unlink($oldPath); + } + + $request->merge([ + 'title' => $file->getClientOriginalName(), + 'mime' => $file->getMimeType(), + 'name' => $fileInfo['name'], + 'size' => filesize($fileInfo['path']) + ]); + }//end if + + $medium->update($request->all()); + return $this->respondAsResource(MediaConductor::model($request, $medium)); + }//end if + + return $this->respondForbidden(); + } /** * Remove the specified resource from storage. * - * @param Request $request Request instance. - * @param \App\Models\Media $medium Specified media file. + * @param \App\Models\Media $medium Specified media file. * @return \Illuminate\Http\Response */ - public function destroy(Request $request, Media $medium) + public function destroy(Media $medium) { - if ((new MediaFilter($request))->filter($medium) !== null) { + if (MediaConductor::destroyable($medium) === true) { if (file_exists($medium->path()) === true) { unlink($medium->path()); } @@ -163,14 +172,14 @@ class MediaController extends ApiController return $this->respondNoContent(); } - return $this->respondNotFound(); + return $this->respondForbidden(); } /** * Display the specified resource. * - * @param Request $request Request instance. - * @param \App\Models\Media $medium Specified media. + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\Media $medium Specified media. * @return \Illuminate\Http\Response */ public function download(Request $request, Media $medium) diff --git a/app/Http/Controllers/Api/PostController.php b/app/Http/Controllers/Api/PostController.php index 4f092c5..717bfab 100644 --- a/app/Http/Controllers/Api/PostController.php +++ b/app/Http/Controllers/Api/PostController.php @@ -2,10 +2,9 @@ namespace App\Http\Controllers\Api; +use App\Conductors\PostConductor; use App\Enum\HttpResponseCodes; -use App\Filters\PostFilter; -use App\Http\Requests\PostStoreRequest; -use App\Http\Requests\PostUpdateRequest; +use App\Http\Requests\PostRequest; use App\Models\Post; use Illuminate\Http\Request; @@ -27,56 +26,70 @@ class PostController extends ApiController /** * Display a listing of the resource. * - * @param \App\Filters\PostFilter $filter Post filter request. + * @param \Illuminate\Http\Request $request The endpoint request. * @return \Illuminate\Http\Response */ - public function index(PostFilter $filter) + public function index(Request $request) { + list($collection, $total) = PostConductor::request($request); + return $this->respondAsResource( - $filter->filter(), - ['total' => $filter->foundTotal()] + $collection, + ['total' => $total] ); } /** * Display the specified resource. * - * @param PostFilter $filter The filter request. - * @param \App\Models\Post $post The post model. + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\Post $post The post model. * @return \Illuminate\Http\Response */ - public function show(PostFilter $filter, Post $post) + public function show(Request $request, Post $post) { - return $this->respondAsResource($filter->filter($post)); + if (PostConductor::viewable($post) === true) { + return $this->respondAsResource(PostConductor::model($request, $post)); + } + + return $this->respondForbidden(); } /** * Store a newly created resource in storage. * - * @param PostStoreRequest $request The post store request. + * @param \App\Http\Requests\PostRequest $request The user request. * @return \Illuminate\Http\Response */ - public function store(PostStoreRequest $request) + public function store(PostRequest $request) { - $post = Post::create($request->all()); - return $this->respondAsResource( - (new PostFilter($request))->filter($post), - null, - HttpResponseCodes::HTTP_CREATED - ); + if (PostConductor::creatable() === true) { + $post = Post::create($request->all()); + return $this->respondAsResource( + PostConductor::model($request, $post), + null, + HttpResponseCodes::HTTP_CREATED + ); + } else { + return $this->respondForbidden(); + } } /** * Update the specified resource in storage. * - * @param PostUpdateRequest $request The post update request. - * @param \App\Models\Post $post The specified post. + * @param \App\Http\Requests\PostRequest $request The post update request. + * @param \App\Models\Post $post The specified post. * @return \Illuminate\Http\Response */ - public function update(PostUpdateRequest $request, Post $post) + public function update(PostRequest $request, Post $post) { - $post->update($request->all()); - return $this->respondAsResource((new PostFilter($request))->filter($post)); + if (PostConductor::updatable($post) === true) { + $post->update($request->all()); + return $this->respondAsResource(PostConductor::model($request, $post)); + } + + return $this->respondForbidden(); } /** @@ -87,7 +100,11 @@ class PostController extends ApiController */ public function destroy(Post $post) { - $post->delete(); - return $this->respondNoContent(); + if (PostConductor::destroyable($post) === true) { + $post->delete(); + return $this->respondNoContent(); + } else { + return $this->respondForbidden(); + } } } diff --git a/app/Http/Controllers/Api/SubscriptionController.php b/app/Http/Controllers/Api/SubscriptionController.php index 45f138f..49451cd 100644 --- a/app/Http/Controllers/Api/SubscriptionController.php +++ b/app/Http/Controllers/Api/SubscriptionController.php @@ -2,12 +2,14 @@ namespace App\Http\Controllers\Api; +use App\Conductors\SubscriptionConductor; +use App\Enum\HttpResponseCodes; use App\Models\Subscription; -use App\Filters\SubscriptionFilter; use App\Http\Requests\SubscriptionRequest; use App\Jobs\SendEmailJob; use App\Mail\SubscriptionConfirm; use App\Mail\SubscriptionUnsubscribed; +use Illuminate\Http\Request; class SubscriptionController extends ApiController { @@ -23,58 +25,70 @@ class SubscriptionController extends ApiController /** * Display a listing of subscribers. * - * @param \App\Filters\SubscriptionFilter $filter Filter object. + * @param \Illuminate\Http\Request $request The endpoint request. * @return \Illuminate\Http\Response */ - public function index(SubscriptionFilter $filter) + public function index(Request $request) { - $collection = $filter->filter(); + list($collection, $total) = SubscriptionConductor::request($request); + return $this->respondAsResource( $collection, - ['total' => $filter->foundTotal()] + ['total' => $total] ); } + /** + * Display the specified user. + * + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\Subscription $subscription The subscription model. + * @return \Illuminate\Http\Response + */ + public function show(Request $request, Subscription $subscription) + { + if (SubscriptionConductor::viewable($subscription) === true) { + return $this->respondAsResource(SubscriptionConductor::model($request, $subscription)); + } + + return $this->respondForbidden(); + } + /** * Store a subscriber email in the database. * - * @param SubscriptionRequest $request The subscriber update request. + * @param \App\Http\Requests\SubscriptionRequest $request The subscriber update request. * @return \Illuminate\Http\Response */ public function store(SubscriptionRequest $request) { - if (Subscription::where('email', $request->email)->first() !== null) { - return $this->respondWithErrors(['email' => 'This email address has already subscribed']); + if (SubscriptionConductor::creatable() === true) { + Subscription::create($request->all()); + dispatch((new SendEmailJob($request->email, new SubscriptionConfirm($request->email))))->onQueue('mail'); + + return $this->respondCreated(); + } else { + return $this->respondForbidden(); } - - Subscription::create($request->all()); - dispatch((new SendEmailJob($request->email, new SubscriptionConfirm($request->email))))->onQueue('mail'); - - return $this->respondCreated(); - } - - - /** - * Display the specified user. - * - * @param SubscriptionFilter $filter The subscription filter. - * @param Subscription $subscription The subscription model. - * @return \Illuminate\Http\Response - */ - public function show(SubscriptionFilter $filter, Subscription $subscription) - { - return $this->respondAsResource($filter->filter($subscription)); } /** * Update the specified resource in storage. * - * @param SubscriptionRequest $request The subscription update request. - * @param Subscription $subscription The specified subscription. + * @param \App\Http\Requests\SubscriptionRequest $request The subscription update request. + * @param \App\Models\Subscription $subscription The specified subscription. * @return \Illuminate\Http\Response */ public function update(SubscriptionRequest $request, Subscription $subscription) { + // if (EventConductor::updatable($event) === true) { + // $event->update($request->all()); + // return $this->respondAsResource(EventConductor::model($request, $event)); + // } + + // return $this->respondForbidden(); + + // $input = []; // $updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password']; @@ -103,14 +117,12 @@ class SubscriptionController extends ApiController */ public function destroy(Subscription $subscription) { - // if ($user->hasPermission('admin/user') === false) { - // return $this->respondForbidden(); - // } - - $email = $subscription->email; - - $subscription->delete(); - return $this->respondNoContent(); + if (SubscriptionConductor::destroyable($subscription) === true) { + $subscription->delete(); + return $this->respondNoContent(); + } else { + return $this->respondForbidden(); + } } /** diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index a163d9b..ef08a3b 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -49,7 +49,7 @@ class UserController extends ApiController /** * Display a listing of the resource. * - * @param Request $request The request. + * @param \Illuminate\Http\Request $request The endpoint request. * @return \Illuminate\Http\Response */ public function index(Request $request) @@ -65,12 +65,12 @@ class UserController extends ApiController /** * Store a newly created user in the database. * - * @param Request $request The request. + * @param \App\Http\Requests\UserStoreRequest $request The endpoint request. * @return \Illuminate\Http\Response */ - public function store(Request $request) + public function store(UserStoreRequest $request) { - if(UserConductor::creatable()) { + if (UserConductor::creatable() === true) { $user = User::create($request->all()); return $this->respondAsResource(UserConductor::model($request, $user), [], HttpResponseCodes::HTTP_CREATED); } else { @@ -78,18 +78,16 @@ class UserController extends ApiController } } - /** * Display the specified user. * - * @param UserFilter $filter The user filter. - * @param User $user The user model. + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\User $user The user model. * @return \Illuminate\Http\Response */ - // public function show(UserFilter $filter, User $user) public function show(Request $request, User $user) { - if(UserConductor::viewable($user)) { + if (UserConductor::viewable($user) === true) { return $this->respondAsResource(UserConductor::model($request, $user)); } @@ -99,13 +97,13 @@ class UserController extends ApiController /** * Update the specified resource in storage. * - * @param UserUpdateRequest $request The user update request. - * @param User $user The specified user. + * @param \App\Http\Requests\UserUpdateRequest $request The user update request. + * @param \App\Models\User $user The specified user. * @return \Illuminate\Http\Response */ public function update(UserUpdateRequest $request, User $user) { - if(UserConductor::updatable($user)) { + if (UserConductor::updatable($user) === true) { $input = []; $updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password']; @@ -124,16 +122,15 @@ class UserController extends ApiController } } - /** * Remove the user from the database. * - * @param User $user The specified user. + * @param \App\Models\User $user The specified user. * @return \Illuminate\Http\Response */ public function destroy(User $user) { - if(UserConductor::destroyable($user)) { + if (UserConductor::destroyable($user) === true) { $user->delete(); return $this->respondNoContent(); } @@ -144,7 +141,7 @@ class UserController extends ApiController /** * Register a new user * - * @param UserRegisterRequest $request The register user request. + * @param \App\Http\Requests\UserRegisterRequest $request The register user request. * @return \Illuminate\Http\Response */ public function register(UserRegisterRequest $request) @@ -178,7 +175,7 @@ class UserController extends ApiController /** * Sends an email with all the usernames registered at that address * - * @param UserForgotUsernameRequest $request The forgot username request. + * @param \App\Http\Requests\UserForgotUsernameRequest $request The forgot username request. * @return \Illuminate\Http\Response */ public function forgotUsername(UserForgotUsernameRequest $request) @@ -198,7 +195,7 @@ class UserController extends ApiController /** * Generates a new reset password code * - * @param UserForgotPasswordRequest $request The reset password request. + * @param \App\Http\Requests\UserForgotPasswordRequest $request The reset password request. * @return \Illuminate\Http\Response */ public function forgotPassword(UserForgotPasswordRequest $request) @@ -220,7 +217,7 @@ class UserController extends ApiController /** * Resets a user password * - * @param UserResetPasswordRequest $request The reset password request. + * @param \App\Http\Requests\UserResetPasswordRequest $request The reset password request. * @return \Illuminate\Http\Response */ public function resetPassword(UserResetPasswordRequest $request) @@ -254,7 +251,7 @@ class UserController extends ApiController /** * Verify an email code * - * @param UserVerifyEmailRequest $request The verify email request. + * @param \App\Http\Requests\UserVerifyEmailRequest $request The verify email request. * @return \Illuminate\Http\Response */ public function verifyEmail(UserVerifyEmailRequest $request) @@ -292,7 +289,7 @@ class UserController extends ApiController /** * Resend a new verify email * - * @param UserResendVerifyEmailRequest $request The resend verify email request. + * @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend verify email request. * @return \Illuminate\Http\Response */ public function resendVerifyEmail(UserResendVerifyEmailRequest $request) @@ -319,7 +316,7 @@ class UserController extends ApiController /** * Resend verification email * - * @param UserResendVerifyEmailRequest $request The resend user request. + * @param \App\Http\Requests\UserResendVerifyEmailRequest $request The resend user request. * @return \Illuminate\Http\Response */ public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request) diff --git a/app/Http/Requests/BaseRequest.php b/app/Http/Requests/BaseRequest.php index 4d41794..aa8a5ca 100644 --- a/app/Http/Requests/BaseRequest.php +++ b/app/Http/Requests/BaseRequest.php @@ -14,10 +14,12 @@ class BaseRequest extends FormRequest */ public function authorize() { - if (method_exists($this, 'postAuthorize') === true && request()->isMethod('post') === true) { + if (request()->isMethod('post') === true && method_exists($this, 'postAuthorize') === true) { return $this->postAuthorize(); - } elseif (method_exists($this, 'putAuthorize') === true && request()->isMethod('put') === true) { + } elseif ((request()->isMethod('put') === true || request()->isMethod('patch') === true) && method_exists($this, 'putAuthorize') === true) { return $this->putAuthorize(); + } elseif (request()->isMethod('delete') === true && method_exists($this, 'destroyAuthorize') === true) { + return $this->deleteAuthorize(); } return true; @@ -38,7 +40,7 @@ class BaseRequest extends FormRequest if (method_exists($this, 'postRules') === true && request()->isMethod('post') === true) { $rules = $this->mergeRules($rules, $this->postRules()); - } elseif (method_exists($this, 'putRules') === true && request()->isMethod('put') === true) { + } elseif (method_exists($this, 'putRules') === true && (request()->isMethod('put') === true || request()->isMethod('patch') === true)) { $rules = $this->mergeRules($rules, $this->postRules()); } elseif (method_exists($this, 'destroyRules') === true && request()->isMethod('delete') === true) { $rules = $this->mergeRules($rules, $this->destroyRules()); diff --git a/app/Http/Requests/EventRequest.php b/app/Http/Requests/EventRequest.php index a591b63..537a1c6 100644 --- a/app/Http/Requests/EventRequest.php +++ b/app/Http/Requests/EventRequest.php @@ -5,28 +5,8 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -class EventRequest extends BaseRequest +class EventStoreRequest extends BaseRequest { - /** - * Determine if the user is authorized to make this request. - * - * @return boolean - */ - public function postAuthorize() - { - return $this->user()?->hasPermission('admin/events'); - } - - /** - * Determine if the user is authorized to make this request. - * - * @return boolean - */ - public function putAuthorize() - { - return $this->user()?->hasPermission('admin/events'); - } - /** * Apply the base rules to this request * diff --git a/app/Http/Requests/MediaRequest.php b/app/Http/Requests/MediaRequest.php new file mode 100644 index 0000000..a737fce --- /dev/null +++ b/app/Http/Requests/MediaRequest.php @@ -0,0 +1,8 @@ + - */ - public function rules() - { - return [ - // - ]; - } -} diff --git a/app/Http/Requests/MediaUpdateRequest.php b/app/Http/Requests/MediaUpdateRequest.php deleted file mode 100644 index d6b61c7..0000000 --- a/app/Http/Requests/MediaUpdateRequest.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public function rules() - { - return [ - // - ]; - } -} diff --git a/app/Http/Requests/PostRequest.php b/app/Http/Requests/PostRequest.php new file mode 100644 index 0000000..dd15f51 --- /dev/null +++ b/app/Http/Requests/PostRequest.php @@ -0,0 +1,42 @@ + + */ + public function postRules() + { + return [ + 'slug' => 'string|min:6|unique:posts', + 'title' => 'string|min:6|max:255', + 'publish_at' => 'date', + 'user_id' => 'uuid|exists:users,id', + ]; + } + + /** + * Get the validation rules that apply to PUT request. + * + * @return array + */ + public function putRules() + { + return [ + 'slug' => [ + 'string', + 'min:6', + Rule::unique('posts')->ignoreModel($this->post), + ], + 'title' => 'string|min:6|max:255', + 'publish_at' => 'date', + 'user_id' => 'uuid|exists:users,id', + ]; + } +} diff --git a/app/Http/Requests/PostStoreRequest.php b/app/Http/Requests/PostStoreRequest.php deleted file mode 100644 index 6d4d21b..0000000 --- a/app/Http/Requests/PostStoreRequest.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - public function rules() - { - return [ - 'slug' => 'string|min:6|unique:posts', - 'title' => 'string|min:6|max:255', - 'publish_at' => 'date', - 'user_id' => 'uuid|exists:users,id', - ]; - } -} diff --git a/app/Http/Requests/PostUpdateRequest.php b/app/Http/Requests/PostUpdateRequest.php deleted file mode 100644 index c0d01c7..0000000 --- a/app/Http/Requests/PostUpdateRequest.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - public function rules() - { - return [ - 'slug' => [ - 'string', - 'min:6', - Rule::unique('posts')->ignoreModel($this->post), - ], - 'title' => 'string|min:6|max:255', - 'publish_at' => 'date', - 'user_id' => 'uuid|exists:users,id', - ]; - } -} diff --git a/app/Http/Requests/SubscriptionRequest.php b/app/Http/Requests/SubscriptionRequest.php index b34374e..fbfafff 100644 --- a/app/Http/Requests/SubscriptionRequest.php +++ b/app/Http/Requests/SubscriptionRequest.php @@ -14,7 +14,7 @@ class SubscriptionRequest extends BaseRequest public function postRules() { return [ - 'email' => 'required|email', + 'email' => 'required|email|unique:subscriptions', 'captcha_token' => [new Recaptcha()], ]; } @@ -31,4 +31,16 @@ class SubscriptionRequest extends BaseRequest 'captcha_token' => [new Recaptcha()], ]; } + + /** + * Get the custom error messages. + * + * @return array + */ + public function messages() + { + return [ + 'email.unique' => 'This email address has already subscribed', + ]; + } } -- 2.49.1 From 0c2ac5d0a54bb7b6357df4c6616da689ca434afb Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 10:38:50 +1000 Subject: [PATCH 18/39] added give and revoke helper methods --- app/Models/User.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/Models/User.php b/app/Models/User.php index 8de62ae..9457eab 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -103,6 +103,42 @@ class User extends Authenticatable implements Auditable return ($this->permissions()->where('permission', $permission)->first() !== null); } + /** + * Give permissions to the user + * + * @param string|array $permissions The permission(s) to give. + * @return Collection + */ + public function givePermission($permissions) + { + if (!is_array($permissions)) { + $permissions = [$permissions]; + } + + $permissions = collect($permissions)->map(function ($permission) { + return ['permission' => $permission]; + }); + + return $this->permissions()->firstOrCreateMany($permissions->toArray()); + } + + /** + * Revoke permissions from the user + * + * @param string|array $permissions The permission(s) to revoke. + * @return int + */ + public function revokePermission($permissions) + { + if (!is_array($permissions)) { + $permissions = [$permissions]; + } + + return $this->permissions() + ->whereIn('permission', $permissions) + ->delete(); + } + /** * Get the list of files of the user * -- 2.49.1 From 46de5cc0c9beded3a3bf394e47b99f61a135c17a Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 11:24:51 +1000 Subject: [PATCH 19/39] fix transform to show all fields for admin users --- app/Conductors/UserConductor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Conductors/UserConductor.php b/app/Conductors/UserConductor.php index f27e3e0..ee25977 100644 --- a/app/Conductors/UserConductor.php +++ b/app/Conductors/UserConductor.php @@ -44,9 +44,9 @@ class UserConductor extends Conductor $user = auth()->user(); $data = $model->toArray(); - if ($user === null || strcasecmp($user->id, $model->id) !== 0) { + if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) { $fields = ['id', 'username']; - $data = arrayOnlyItems($data, $fields); + $data = arrayLimitKeys($data, $fields); } return $data; -- 2.49.1 From 8a3d9eec035d9193644deedb8229aa517028c5c5 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 11:25:13 +1000 Subject: [PATCH 20/39] rename arrayOnlyItems to arrayLimitKeys --- app/Helpers/Array.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helpers/Array.php b/app/Helpers/Array.php index f58662f..17a169b 100644 --- a/app/Helpers/Array.php +++ b/app/Helpers/Array.php @@ -34,7 +34,7 @@ function arrayRemoveItem(array $arr, string|array $item): array * @param string|array $keys The keys to keep. * @return array The filtered array. */ -function arrayOnlyItems(array $arr, array $keys): array +function arrayLimitKeys(array $arr, array $keys): array { return array_intersect_key($arr, array_flip($keys)); } -- 2.49.1 From 970618f56127d9f40e92d64580c391e643215b5a Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 11:25:25 +1000 Subject: [PATCH 21/39] fix givePermission relationship --- app/Models/User.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Models/User.php b/app/Models/User.php index 9457eab..9bc63d1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -119,9 +119,15 @@ class User extends Authenticatable implements Auditable return ['permission' => $permission]; }); - return $this->permissions()->firstOrCreateMany($permissions->toArray()); + $existingPermissions = $this->permissions()->whereIn('permission', $permissions->pluck('permission'))->get(); + $newPermissions = $permissions->reject(function ($permission) use ($existingPermissions) { + return $existingPermissions->contains('permission', $permission['permission']); + }); + + return $this->permissions()->createMany($newPermissions->toArray()); } + /** * Revoke permissions from the user * -- 2.49.1 From be8ccbd41a23efbd8ab95766232508ab54078d02 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:12:24 +1000 Subject: [PATCH 22/39] remove obsolete uses --- app/Conductors/UserConductor.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/Conductors/UserConductor.php b/app/Conductors/UserConductor.php index ee25977..7c3252e 100644 --- a/app/Conductors/UserConductor.php +++ b/app/Conductors/UserConductor.php @@ -2,11 +2,7 @@ namespace App\Conductors; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\Request; -use Illuminate\Support\Str; class UserConductor extends Conductor { -- 2.49.1 From 7a1499a0b32bb859e6736a4e1d8b3abc7eb7b412 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:12:32 +1000 Subject: [PATCH 23/39] fix missing response --- app/Http/Controllers/Api/UserController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index ef08a3b..fb4306f 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -120,6 +120,8 @@ class UserController extends ApiController return $this->respondAsResource(UserConductor::model($request, $user)); } + + return $this->respondForbidden(); } /** -- 2.49.1 From 14dd2bb336c192463d32caff0274f50eaff18402 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:12:55 +1000 Subject: [PATCH 24/39] allow same username on update --- app/Http/Requests/UserUpdateRequest.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/UserUpdateRequest.php b/app/Http/Requests/UserUpdateRequest.php index 00ff97b..bc5837c 100644 --- a/app/Http/Requests/UserUpdateRequest.php +++ b/app/Http/Requests/UserUpdateRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class UserUpdateRequest extends FormRequest { @@ -13,8 +14,20 @@ class UserUpdateRequest extends FormRequest */ public function rules() { + $user = $this->route('user'); + return [ - 'username' => 'string|max:255|min:6|unique:users', + 'username' => [ + 'string', + 'max:255', + 'min:4', + Rule::unique('users')->ignore($user->id)->when( + $this->username !== $user->username, + function ($query) { + return $query->where('username', $this->username); + } + ), + ], 'first_name' => 'string|max:255|min:2', 'last_name' => 'string|max:255|min:2', 'email' => 'string|email|max:255', -- 2.49.1 From dc56edf486bcef585054291bc9a6710ae63fb24d Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:13:02 +1000 Subject: [PATCH 25/39] added tests --- tests/Feature/UsersEndpointTest.php | 235 ++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 tests/Feature/UsersEndpointTest.php diff --git a/tests/Feature/UsersEndpointTest.php b/tests/Feature/UsersEndpointTest.php new file mode 100644 index 0000000..7fdeaf6 --- /dev/null +++ b/tests/Feature/UsersEndpointTest.php @@ -0,0 +1,235 @@ +create(); + $nonAdminUser->revokePermission('admin/users'); + + // create an admin user + $adminUser = User::factory()->create(); + $adminUser->givePermission('admin/users'); + + // ensure the non-admin user can access the endpoint and see basic user info only + $response = $this->actingAs($nonAdminUser)->get('/api/users'); + $response->assertStatus(200); + $response->assertJsonStructure([ + 'users' => [ + '*' => [ + 'id', + 'username' + ] + ], + 'total' + ]); + + $response->assertJsonMissing([ + 'users' => [ + '*' => [ + 'email', + 'password' + ] + ], + ]); + $response->assertJsonFragment([ + 'id' => $nonAdminUser->id, + 'username' => $nonAdminUser->username + ]); + + // ensure the admin user can access the endpoint and see additional user info + $response = $this->actingAs($adminUser)->get('/api/users'); + $response->assertStatus(200); + $response->assertJsonStructure([ + 'users' => [ + '*' => [ + 'id', + 'username', + 'email' + ] + ], + 'total' + ]); + $response->assertJsonMissing([ + 'users' => [ + '*' => [ + 'password' + ] + ] + ]); + $response->assertJsonFragment([ + 'id' => $nonAdminUser->id, + 'username' => $nonAdminUser->username + ]); + } + + public function testGuestCannotCreateUser() + { + $userData = [ + 'username' => 'johndoe', + 'email' => 'johndoe@example.com', + 'password' => 'password', + ]; + + $response = $this->postJson('/api/users', $userData); + $response->assertStatus(401); + $this->assertDatabaseMissing('users', [ + 'username' => $userData['username'], + 'email' => $userData['email'], + ]); + } + + public function testGuestCanRegisterUser() + { + $userData = [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'username' => 'johndoe', + 'email' => 'johndoe@example.com', + 'password' => 'password', + ]; + + $response = $this->postJson('/api/register', $userData); + $response->assertStatus(200); + $this->assertDatabaseHas('users', [ + 'username' => $userData['username'], + 'email' => $userData['email'], + ]); + } + + public function testCannotCreateDuplicateUsername() + { + $userData = [ + 'first_name' => 'Jack', + 'last_name' => 'Doe', + 'username' => 'jackdoe', + 'email' => 'jackdoe@example.com', + 'password' => 'password', + ]; + + // Test creating user + $response = $this->postJson('/api/register', $userData); + $response->assertStatus(200); + $this->assertDatabaseHas('users', [ + 'username' => 'jackdoe', + 'email' => 'jackdoe@example.com', + ]); + + // Test creating duplicate user + $response = $this->postJson('/api/register', $userData); + $response->assertStatus(422); + $response->assertJsonValidationErrors('username'); + } + + public function testUserCanOnlyUpdateOwnUser() + { + $user = User::factory()->create(); + + $userData = [ + 'username' => 'raffi', + 'email' => 'raffi@example.com', + 'password' => 'password', + ]; + + // Test updating own user + $response = $this->actingAs($user)->putJson('/api/users/' . $user->id, $userData); + $response->assertStatus(200); + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'username' => 'raffi', + 'email' => 'raffi@example.com', + ]); + + // Test updating another user + $otherUser = User::factory()->create(); + $otherUserData = [ + 'username' => 'otherraffi', + 'email' => 'otherraffi@example.com', + 'password' => 'password', + ]; + + $response = $this->actingAs($user)->putJson('/api/users/' . $otherUser->id, $otherUserData); + $response->assertStatus(403); + } + + public function testUserCannotDeleteUsers() + { + $user = User::factory()->create(); + + // Test deleting own user + $response = $this->actingAs($user)->deleteJson('/api/users/' . $user->id); + $response->assertStatus(403); + $this->assertDatabaseHas('users', ['id' => $user->id]); + + // Test deleting another user + $otherUser = User::factory()->create(); + $response = $this->actingAs($user)->deleteJson('/api/users/' . $otherUser->id); + $response->assertStatus(403); + $this->assertDatabaseHas('users', ['id' => $otherUser->id]); + } + + public function testAdminCanUpdateAnyUser() + { + $admin = User::factory()->create(); + $admin->givePermission('admin/users'); + + $user = User::factory()->create(); + + $userData = [ + 'username' => 'Todd Doe', + 'email' => 'todddoe@example.com', + 'password' => 'password', + ]; + + // Test updating own user + $response = $this->actingAs($admin)->putJson('/api/users/' . $user->id, $userData); + $response->assertStatus(200); + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'username' => 'Todd Doe', + 'email' => 'todddoe@example.com' + ]); + + // Test updating another user + $otherUser = User::factory()->create(); + $otherUserData = [ + 'username' => 'Kim Doe', + 'email' => 'kimdoe@example.com', + 'password' => 'password', + ]; + + $response = $this->actingAs($admin)->putJson('/api/users/' . $otherUser->id, $otherUserData); + $response->assertStatus(200); + $this->assertDatabaseHas('users', [ + 'id' => $otherUser->id, + 'username' => 'Kim Doe', + 'email' => 'kimdoe@example.com', + ]); + } + + public function testAdminCanDeleteAnyUser() + { + $admin = User::factory()->create(); + $admin->givePermission('admin/users'); + + $user = User::factory()->create(); + + // Test deleting own user + $response = $this->actingAs($admin)->deleteJson('/api/users/' . $user->id); + $response->assertStatus(204); + $this->assertDatabaseMissing('users', ['id' => $user->id]); + + // Test deleting another user + $otherUser = User::factory()->create(); + $response = $this->actingAs($admin)->deleteJson('/api/users/' . $otherUser->id); + $response->assertStatus(204); + $this->assertDatabaseMissing('users', ['id' => $otherUser->id]); + } +} -- 2.49.1 From af4b9b95e7788a9035d1d0d342251699aeb72a94 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:19:39 +1000 Subject: [PATCH 26/39] combine UserRequest to BaseRequest --- app/Http/Controllers/Api/UserController.php | 12 ++++----- ...{UserUpdateRequest.php => UserRequest.php} | 24 +++++++++++++++--- app/Http/Requests/UserStoreRequest.php | 25 ------------------- 3 files changed, 25 insertions(+), 36 deletions(-) rename app/Http/Requests/{UserUpdateRequest.php => UserRequest.php} (56%) delete mode 100644 app/Http/Requests/UserStoreRequest.php diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index fb4306f..85a05d7 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -3,9 +3,7 @@ namespace App\Http\Controllers\Api; use App\Enum\HttpResponseCodes; -use App\Filters\UserFilter; -use App\Http\Requests\UserUpdateRequest; -use App\Http\Requests\UserStoreRequest; +use App\Http\Requests\UserRequest; use App\Http\Requests\UserForgotPasswordRequest; use App\Http\Requests\UserForgotUsernameRequest; use App\Http\Requests\UserRegisterRequest; @@ -65,10 +63,10 @@ class UserController extends ApiController /** * Store a newly created user in the database. * - * @param \App\Http\Requests\UserStoreRequest $request The endpoint request. + * @param \App\Http\Requests\UserRequest $request The endpoint request. * @return \Illuminate\Http\Response */ - public function store(UserStoreRequest $request) + public function store(UserRequest $request) { if (UserConductor::creatable() === true) { $user = User::create($request->all()); @@ -97,11 +95,11 @@ class UserController extends ApiController /** * Update the specified resource in storage. * - * @param \App\Http\Requests\UserUpdateRequest $request The user update request. + * @param \App\Http\Requests\UserRequest $request The user update request. * @param \App\Models\User $user The specified user. * @return \Illuminate\Http\Response */ - public function update(UserUpdateRequest $request, User $user) + public function update(UserRequest $request, User $user) { if (UserConductor::updatable($user) === true) { $input = []; diff --git a/app/Http/Requests/UserUpdateRequest.php b/app/Http/Requests/UserRequest.php similarity index 56% rename from app/Http/Requests/UserUpdateRequest.php rename to app/Http/Requests/UserRequest.php index bc5837c..2cfd74a 100644 --- a/app/Http/Requests/UserUpdateRequest.php +++ b/app/Http/Requests/UserRequest.php @@ -2,17 +2,33 @@ namespace App\Http\Requests; -use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -class UserUpdateRequest extends FormRequest +class UserRequest extends BaseRequest { /** - * Get the validation rules that apply to the request. + * Apply the additional POST base rules to this request * * @return array */ - public function rules() + public function postRules() + { + return [ + 'username' => 'required|string|max:255|min:4|unique:users', + 'first_name' => 'required|string|max:255|min:2', + 'last_name' => 'required|string|max:255|min:2', + 'email' => 'required|string|email|max:255', + 'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'], + 'email_verified_at' => 'date' + ]; + } + + /** + * Get the validation rules that apply to PUT request. + * + * @return array + */ + public function putRules() { $user = $this->route('user'); diff --git a/app/Http/Requests/UserStoreRequest.php b/app/Http/Requests/UserStoreRequest.php deleted file mode 100644 index 43bf17f..0000000 --- a/app/Http/Requests/UserStoreRequest.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function rules() - { - return [ - 'username' => 'required|string|max:255|min:4|unique:users', - 'first_name' => 'required|string|max:255|min:2', - 'last_name' => 'required|string|max:255|min:2', - 'email' => 'required|string|email|max:255', - 'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'], - 'email_verified_at' => 'date' - ]; - } -} -- 2.49.1 From 58d302fc383447fef8f1187df30908815d443546 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:23:25 +1000 Subject: [PATCH 27/39] using postRules instead of putRules on PUT request --- app/Http/Requests/BaseRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/BaseRequest.php b/app/Http/Requests/BaseRequest.php index aa8a5ca..675719a 100644 --- a/app/Http/Requests/BaseRequest.php +++ b/app/Http/Requests/BaseRequest.php @@ -41,7 +41,7 @@ class BaseRequest extends FormRequest if (method_exists($this, 'postRules') === true && request()->isMethod('post') === true) { $rules = $this->mergeRules($rules, $this->postRules()); } elseif (method_exists($this, 'putRules') === true && (request()->isMethod('put') === true || request()->isMethod('patch') === true)) { - $rules = $this->mergeRules($rules, $this->postRules()); + $rules = $this->mergeRules($rules, $this->putRules()); } elseif (method_exists($this, 'destroyRules') === true && request()->isMethod('delete') === true) { $rules = $this->mergeRules($rules, $this->destroyRules()); } -- 2.49.1 From 7b58303cde4857cfd33cb71e998f1ff19bb89554 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 12:31:20 +1000 Subject: [PATCH 28/39] added test --- tests/Feature/AuthEndpointTest.php | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/Feature/AuthEndpointTest.php diff --git a/tests/Feature/AuthEndpointTest.php b/tests/Feature/AuthEndpointTest.php new file mode 100644 index 0000000..e4dcc5f --- /dev/null +++ b/tests/Feature/AuthEndpointTest.php @@ -0,0 +1,52 @@ +create([ + 'password' => bcrypt('password'), + ]); + + // Test successful login + $response = $this->postJson('/api/login', [ + 'username' => $user->username, + 'password' => 'password', + ]); + $response->assertStatus(200); + $response->assertJsonStructure([ + 'token', + ]); + $token = $response->json('token'); + + // Test getting authenticated user + $response = $this->withHeaders([ + 'Authorization' => "Bearer $token", + ])->get('/api/me'); + $response->assertStatus(200); + $response->assertJson([ + 'user' => [ + 'id' => $user->id, + 'username' => $user->username, + ] + ]); + + // Test logout + $response = $this->withHeaders([ + 'Authorization' => "Bearer $token", + ])->postJson('/api/logout'); + $response->assertStatus(204); + + // Test failed login + $response = $this->postJson('/api/login', [ + 'username' => $user->username, + 'password' => 'wrongpassword', + ]); + $response->assertStatus(422); + } +} -- 2.49.1 From 3126991e8f9fa56470a0d29e1eec5952c83a3aa1 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 13:06:52 +1000 Subject: [PATCH 29/39] added testing env --- .env.testing | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .env.testing diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..999d7c8 --- /dev/null +++ b/.env.testing @@ -0,0 +1,59 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= +APP_DEBUG=true +APP_URL=http://127.0.0.1 +APP_URL_API="${APP_URL}/api/" + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +DB_DATABASE=:memory: + +BROADCAST_DRIVER=log +CACHE_DRIVER=array +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +MEMCACHED_HOST=127.0.0.1 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_HOST=null +MAIL_PORT=null +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_HOST= +PUSHER_PORT=443 +PUSHER_SCHEME=https +PUSHER_APP_CLUSTER=mt1 + +VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +VITE_PUSHER_HOST="${PUSHER_HOST}" +VITE_PUSHER_PORT="${PUSHER_PORT}" +VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +CONTACT_ADDRESS="hello@stemmechanics.com.au" +CONTACT_SUBJECT="Contact from website" + +STORAGE_LOCAL_URL="${APP_URL}/api/media/%ID%/download" +STORAGE_PUBLIC_URL="${APP_URL}/uploads/%NAME%" \ No newline at end of file -- 2.49.1 From 7605a826d6b0fdb72ef25d219ec2f3b3d4fd82d6 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 13:06:59 +1000 Subject: [PATCH 30/39] added test --- tests/Feature/ContactFormTest.php | 28 ++++++++++++++++++++++++++++ tests/Feature/ExampleTest.php | 21 --------------------- 2 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 tests/Feature/ContactFormTest.php delete mode 100644 tests/Feature/ExampleTest.php diff --git a/tests/Feature/ContactFormTest.php b/tests/Feature/ContactFormTest.php new file mode 100644 index 0000000..736bcf2 --- /dev/null +++ b/tests/Feature/ContactFormTest.php @@ -0,0 +1,28 @@ + 'John Doe', + 'email' => 'johndoe@example.com', + 'content' => 'Hello, this is a test message.', + ]; + + $response = $this->postJson('/api/contact', $formData); + $response->assertStatus(201); + + $formData = [ + 'name' => 'John Doe', + 'content' => 'Hello, this is a test message.', + ]; + + $response = $this->postJson('/api/contact', $formData); + $response->assertStatus(422); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 1eafba6..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,21 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} -- 2.49.1 From 7ecec705200f5dce09ec8102424b9f69eaede947 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 13:13:40 +1000 Subject: [PATCH 31/39] Added registration type of message --- app/Http/Requests/EventRequest.php | 5 +++-- resources/js/views/EventView.vue | 12 +++++++++++- resources/js/views/dashboard/EventEdit.vue | 5 +++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/Http/Requests/EventRequest.php b/app/Http/Requests/EventRequest.php index 537a1c6..e2fe02b 100644 --- a/app/Http/Requests/EventRequest.php +++ b/app/Http/Requests/EventRequest.php @@ -27,11 +27,12 @@ class EventStoreRequest extends BaseRequest Rule::in(['draft', 'soon', 'open', 'closed', 'cancelled']), ], 'registration_type' => [ - Rule::in(['none', 'email', 'link']), + Rule::in(['none', 'email', 'link', 'message']), ], 'registration_data' => [ Rule::when(strcasecmp('email', $this->attributes->get('registration_type')) == 0, 'required|email'), - Rule::when(strcasecmp('link', $this->attributes->get('registration_type')) == 0, 'required|url') + Rule::when(strcasecmp('link', $this->attributes->get('registration_type')) == 0, 'required|url'), + Rule::when(strcasecmp('message', $this->attributes->get('registration_type')) == 0, 'required|message'), ], 'hero' => 'uuid|exists:media,id', ]; diff --git a/resources/js/views/EventView.vue b/resources/js/views/EventView.vue index 891ada5..3b89f85 100644 --- a/resources/js/views/EventView.vue +++ b/resources/js/views/EventView.vue @@ -60,6 +60,15 @@ :block="true" label="Register for Event"> +
+ {{ event.registration_data }} +

@@ -186,6 +187,10 @@ const registration_data = computed(() => { data.visible = true; data.title = "Registration URL"; data.type = "url"; + } else if (form?.controls.registration_type.value === "message") { + data.visible = true; + data.title = "Registration message"; + data.type = "test"; } return data; -- 2.49.1 From 44b123307a05796071e0c538ff6dc6d0609e6cb2 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 13:14:45 +1000 Subject: [PATCH 32/39] fix class name --- app/Http/Requests/EventRequest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Http/Requests/EventRequest.php b/app/Http/Requests/EventRequest.php index e2fe02b..f1bd1f1 100644 --- a/app/Http/Requests/EventRequest.php +++ b/app/Http/Requests/EventRequest.php @@ -2,10 +2,9 @@ namespace App\Http\Requests; -use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -class EventStoreRequest extends BaseRequest +class EventRequest extends BaseRequest { /** * Apply the base rules to this request -- 2.49.1 From 2cea90c2c8a7a87bc5d0ddb251341ecf67647a2f Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 16:03:32 +1000 Subject: [PATCH 33/39] added tests, bug fixes and cleanup --- app/Conductors/EventConductor.php | 14 +- app/Conductors/MediaConductor.php | 10 +- app/Conductors/PostConductor.php | 14 +- app/Conductors/SubscriptionConductor.php | 8 +- app/Conductors/UserConductor.php | 2 +- app/Http/Controllers/Api/ApiController.php | 12 +- app/Http/Controllers/Api/AuthController.php | 1 + app/Http/Controllers/Api/EventController.php | 2 + app/Http/Controllers/Api/MediaController.php | 2 + app/Http/Controllers/Api/PostController.php | 2 + .../Api/SubscriptionController.php | 1 + app/Http/Controllers/Api/UserController.php | 3 +- app/Http/Requests/PostRequest.php | 12 +- app/Providers/RouteServiceProvider.php | 26 +++- database/factories/EventFactory.php | 40 ++++++ database/factories/MediaFactory.php | 29 ++++ database/factories/PostFactory.php | 31 ++++ .../{AuthEndpointTest.php => AuthApiTest.php} | 2 +- tests/Feature/EventsApiTest.php | 136 ++++++++++++++++++ tests/Feature/PostsApiTest.php | 134 +++++++++++++++++ ...UsersEndpointTest.php => UsersApiTest.php} | 2 +- 21 files changed, 443 insertions(+), 40 deletions(-) create mode 100644 database/factories/EventFactory.php create mode 100644 database/factories/MediaFactory.php create mode 100644 database/factories/PostFactory.php rename tests/Feature/{AuthEndpointTest.php => AuthApiTest.php} (97%) create mode 100644 tests/Feature/EventsApiTest.php create mode 100644 tests/Feature/PostsApiTest.php rename tests/Feature/{UsersEndpointTest.php => UsersApiTest.php} (99%) diff --git a/app/Conductors/EventConductor.php b/app/Conductors/EventConductor.php index a47bc9b..4cc6982 100644 --- a/app/Conductors/EventConductor.php +++ b/app/Conductors/EventConductor.php @@ -30,7 +30,7 @@ class EventConductor extends Conductor public function scope(Builder $builder) { $user = auth()->user(); - if ($user === null || $user->has_permission('admin/events') === false) { + if ($user === null || $user->hasPermission('admin/events') === false) { $builder ->where('status', '!=', 'draft') ->where('publish_at', '<=', now()); @@ -47,7 +47,7 @@ class EventConductor extends Conductor { if (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) { $user = auth()->user(); - if ($user === null || $user->has_permission('admin/events') === false) { + if ($user === null || $user->hasPermission('admin/events') === false) { return false; } } @@ -63,7 +63,7 @@ class EventConductor extends Conductor public static function creatable() { $user = auth()->user(); - return ($user !== null && $user->has_permission('admin/events') === true); + return ($user !== null && $user->hasPermission('admin/events') === true); } /** @@ -75,18 +75,18 @@ class EventConductor extends Conductor public static function updatable(Model $model) { $user = auth()->user(); - return ($user !== null && $user->has_permission('admin/events') === true); + return ($user !== null && $user->hasPermission('admin/events') === true); } /** - * Return if the current model is deletable. + * Return if the current model is destroyable. * * @param Model $model The model. * @return boolean Allow deleting model. */ - public static function deletable(Model $model) + public static function destroyable(Model $model) { $user = auth()->user(); - return ($user !== null && $user->has_permission('admin/events') === true); + return ($user !== null && $user->hasPermission('admin/events') === true); } } diff --git a/app/Conductors/MediaConductor.php b/app/Conductors/MediaConductor.php index 6ad3de8..dd88d5d 100644 --- a/app/Conductors/MediaConductor.php +++ b/app/Conductors/MediaConductor.php @@ -64,7 +64,7 @@ class MediaConductor extends Conductor { if ($model->permission !== null) { $user = auth()->user(); - if ($user === null || $user->has_permission($model->permission) === false) { + if ($user === null || $user->hasPermission($model->permission) === false) { return false; } } @@ -92,18 +92,18 @@ class MediaConductor extends Conductor public static function updatable(Model $model) { $user = auth()->user(); - return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 || $user->has_permission('admin/media') === true)); + return ($user !== null && (strcasecmp($model->user_id, $user->id) === 0 || $user->hasPermission('admin/media') === true)); } /** - * Return if the current model is deletable. + * Return if the current model is destroyable. * * @param Model $model The model. * @return boolean Allow deleting model. */ - public static function deletable(Model $model) + public static function destroyable(Model $model) { $user = auth()->user(); - return ($user !== null && ($model->user_id === $user->id || $user->has_permission('admin/media') === true)); + return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true)); } } diff --git a/app/Conductors/PostConductor.php b/app/Conductors/PostConductor.php index 63b4988..bfaa40f 100644 --- a/app/Conductors/PostConductor.php +++ b/app/Conductors/PostConductor.php @@ -30,7 +30,7 @@ class PostConductor extends Conductor public function scope(Builder $builder) { $user = auth()->user(); - if ($user === null || $user->has_permission('admin/posts') === false) { + if ($user === null || $user->hasPermission('admin/posts') === false) { $builder ->where('publish_at', '<=', now()); } @@ -46,7 +46,7 @@ class PostConductor extends Conductor { if (Carbon::parse($model->publish_at)->isFuture() === true) { $user = auth()->user(); - if ($user === null || $user->has_permission('admin/posts') === false) { + if ($user === null || $user->hasPermission('admin/posts') === false) { return false; } } @@ -62,7 +62,7 @@ class PostConductor extends Conductor public static function creatable() { $user = auth()->user(); - return ($user !== null && $user->has_permission('admin/posts') === true); + return ($user !== null && $user->hasPermission('admin/posts') === true); } /** @@ -74,18 +74,18 @@ class PostConductor extends Conductor public static function updatable(Model $model) { $user = auth()->user(); - return ($user !== null && $user->has_permission('admin/posts') === true); + return ($user !== null && $user->hasPermission('admin/posts') === true); } /** - * Return if the current model is deletable. + * Return if the current model is destroyable. * * @param Model $model The model. * @return boolean Allow deleting model. */ - public static function deletable(Model $model) + public static function destroyable(Model $model) { $user = auth()->user(); - return ($user !== null && $user->has_permission('admin/posts') === true); + return ($user !== null && $user->hasPermission('admin/posts') === true); } } diff --git a/app/Conductors/SubscriptionConductor.php b/app/Conductors/SubscriptionConductor.php index 3f447d2..191eed6 100644 --- a/app/Conductors/SubscriptionConductor.php +++ b/app/Conductors/SubscriptionConductor.php @@ -22,18 +22,18 @@ class SubscriptionConductor extends Conductor public static function updatable(Model $model) { $user = auth()->user(); - return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->has_permission('admin/subscriptions') === true)); + return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true)); } /** - * Return if the current model is deletable. + * Return if the current model is destroyable. * * @param Model $model The model. * @return boolean Allow deleting model. */ - public static function deletable(Model $model) + public static function destroyable(Model $model) { $user = auth()->user(); - return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->has_permission('admin/subscriptions') === true)); + return ($user !== null && ((strcasecmp($model->email, $user->email) === 0 && $user->email_verified_at !== null) || $user->hasPermission('admin/subscriptions') === true)); } } diff --git a/app/Conductors/UserConductor.php b/app/Conductors/UserConductor.php index 7c3252e..cc8d125 100644 --- a/app/Conductors/UserConductor.php +++ b/app/Conductors/UserConductor.php @@ -65,7 +65,7 @@ class UserConductor extends Conductor } /** - * Return if the current model is deletable. + * Return if the current model is destroyable. * * @param Model $model The model. * @return boolean Allow deleting model. diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 27b1e4d..9cb5269 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -121,13 +121,15 @@ class ApiController extends Controller /** * Return resource data * - * @param array|Model|Collection $data Resource data. - * @param array|null $appendData Data to append to response. - * @param integer $respondCode Resource code. + * @param array|Model|Collection $data Resource data. + * @param boolean $isCollection If the data is a group of items. + * @param array|null $appendData Data to append to response. + * @param integer $respondCode Resource code. * @return \Illuminate\Http\JsonResponse */ protected function respondAsResource( mixed $data, + bool $isCollection = false, mixed $appendData = null, int $respondCode = HttpResponseCodes::HTTP_OK ) { @@ -144,8 +146,6 @@ class ApiController extends Controller $resourceName = strtolower($resourceName); } - $is_multiple = true; - $dataArray = []; if ($data instanceof Collection) { $dataArray = $data->toArray(); @@ -157,7 +157,7 @@ class ApiController extends Controller } $resource = []; - if ($is_multiple === true) { + if ($isCollection === true) { $resource = [Str::plural($resourceName) => $dataArray]; } else { $resource = [Str::singular($resourceName) => $dataArray]; diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index cd1b8d1..463e01d 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -73,6 +73,7 @@ class AuthController extends ApiController return $this->respondAsResource( $user->makeVisible(['permissions']), + false, ['token' => $token] ); }//end if diff --git a/app/Http/Controllers/Api/EventController.php b/app/Http/Controllers/Api/EventController.php index 2f4febb..ba3a7a5 100644 --- a/app/Http/Controllers/Api/EventController.php +++ b/app/Http/Controllers/Api/EventController.php @@ -31,6 +31,7 @@ class EventController extends ApiController return $this->respondAsResource( $collection, + true, ['total' => $total] ); } @@ -63,6 +64,7 @@ class EventController extends ApiController $event = Event::create($request->all()); return $this->respondAsResource( EventConductor::model($request, $event), + false, null, HttpResponseCodes::HTTP_CREATED ); diff --git a/app/Http/Controllers/Api/MediaController.php b/app/Http/Controllers/Api/MediaController.php index 5fde469..a381aae 100644 --- a/app/Http/Controllers/Api/MediaController.php +++ b/app/Http/Controllers/Api/MediaController.php @@ -33,6 +33,7 @@ class MediaController extends ApiController return $this->respondAsResource( $collection, + true, ['total' => $total] ); } @@ -103,6 +104,7 @@ class MediaController extends ApiController $media = $request->user()->media()->create($request->all()); return $this->respondAsResource( MediaConductor::model($request, $media), + false, null, HttpResponseCodes::HTTP_CREATED ); diff --git a/app/Http/Controllers/Api/PostController.php b/app/Http/Controllers/Api/PostController.php index 717bfab..239dce2 100644 --- a/app/Http/Controllers/Api/PostController.php +++ b/app/Http/Controllers/Api/PostController.php @@ -35,6 +35,7 @@ class PostController extends ApiController return $this->respondAsResource( $collection, + true, ['total' => $total] ); } @@ -67,6 +68,7 @@ class PostController extends ApiController $post = Post::create($request->all()); return $this->respondAsResource( PostConductor::model($request, $post), + false, null, HttpResponseCodes::HTTP_CREATED ); diff --git a/app/Http/Controllers/Api/SubscriptionController.php b/app/Http/Controllers/Api/SubscriptionController.php index 49451cd..0593032 100644 --- a/app/Http/Controllers/Api/SubscriptionController.php +++ b/app/Http/Controllers/Api/SubscriptionController.php @@ -34,6 +34,7 @@ class SubscriptionController extends ApiController return $this->respondAsResource( $collection, + true, ['total' => $total] ); } diff --git a/app/Http/Controllers/Api/UserController.php b/app/Http/Controllers/Api/UserController.php index 85a05d7..024d93b 100644 --- a/app/Http/Controllers/Api/UserController.php +++ b/app/Http/Controllers/Api/UserController.php @@ -56,6 +56,7 @@ class UserController extends ApiController return $this->respondAsResource( $collection, + true, ['total' => $total] ); } @@ -70,7 +71,7 @@ class UserController extends ApiController { if (UserConductor::creatable() === true) { $user = User::create($request->all()); - return $this->respondAsResource(UserConductor::model($request, $user), [], HttpResponseCodes::HTTP_CREATED); + return $this->respondAsResource(UserConductor::model($request, $user), false, [], HttpResponseCodes::HTTP_CREATED); } else { return $this->respondForbidden(); } diff --git a/app/Http/Requests/PostRequest.php b/app/Http/Requests/PostRequest.php index dd15f51..b8593c5 100644 --- a/app/Http/Requests/PostRequest.php +++ b/app/Http/Requests/PostRequest.php @@ -14,10 +14,12 @@ class PostRequest extends BaseRequest public function postRules() { return [ - 'slug' => 'string|min:6|unique:posts', - 'title' => 'string|min:6|max:255', - 'publish_at' => 'date', - 'user_id' => 'uuid|exists:users,id', + 'slug' => 'required|string|min:6|unique:posts', + 'title' => 'required|string|min:6|max:255', + 'publish_at' => 'required|date', + 'user_id' => 'required|uuid|exists:users,id', + 'content' => 'required|string|min:6', + 'hero' => 'required|uuid|exists:media,id', ]; } @@ -37,6 +39,8 @@ class PostRequest extends BaseRequest 'title' => 'string|min:6|max:255', 'publish_at' => 'date', 'user_id' => 'uuid|exists:users,id', + 'content' => 'string|min:6', + 'hero' => 'uuid|exists:media,id', ]; } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 55eb40e..a23540f 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -46,8 +46,28 @@ class RouteServiceProvider extends ServiceProvider */ protected function configureRateLimiting() { - RateLimiter::for('api', function (Request $request) { - return Limit::perMinute(60)->by($request->user()?->id !== null ?: $request->ip()); - }); + // RateLimiter::for('api', function (Request $request) { + // return Limit::perMinute(60)->by($request->user()?->id !== null ?: $request->ip()); + // }); + + $rateLimitEnabled = true; + $user = auth()->user(); + + if (app()->environment('testing')) { + $rateLimitEnabled = false; + } elseif ($user !== null && $user->hasPermission('admin/ratelimit') === true) { + // Admin users with the "admin/ratelimit" permission are not rate limited + $rateLimitEnabled = false; + } + + if ($rateLimitEnabled === true) { + RateLimiter::for('api', function (Request $request) { + return Limit::perMinute(180)->by($request->user()?->id ?: $request->ip()); + }); + } else { + RateLimiter::for('api', function () { + return Limit::none(); + }); + } } } diff --git a/database/factories/EventFactory.php b/database/factories/EventFactory.php new file mode 100644 index 0000000..d4b1976 --- /dev/null +++ b/database/factories/EventFactory.php @@ -0,0 +1,40 @@ + + */ +class EventFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + $startDate = Carbon::parse($this->faker->dateTimeBetween('now', '+1 year')); + $endDate = Carbon::parse($this->faker->dateTimeBetween($startDate, '+1 year')); + $publishDate = Carbon::parse($this->faker->dateTimeBetween('-1 month', '+1 month')); + + return [ + 'title' => $this->faker->sentence(), + 'location' => $this->faker->randomElement(['online', 'physical']), + 'address' => $this->faker->address, + 'start_at' => $startDate, + 'end_at' => $endDate, + 'publish_at' => $publishDate, + 'status' => $this->faker->randomElement(['draft', 'soon', 'open', 'closed', 'cancelled']), + 'registration_type' => $this->faker->randomElement(['none', 'email', 'link', 'message']), + 'registration_data' => $this->faker->sentence(), + 'hero' => $this->faker->uuid, + 'content' => $this->faker->paragraphs(3, true), + 'price' => $this->faker->numberBetween(0, 150), + 'ages' => $this->faker->regexify('\d+(\+|\-\d+)?'), + ]; + } +} diff --git a/database/factories/MediaFactory.php b/database/factories/MediaFactory.php new file mode 100644 index 0000000..c3c257a --- /dev/null +++ b/database/factories/MediaFactory.php @@ -0,0 +1,29 @@ + + */ +class MediaFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'title' => $this->faker->sentence(), + 'name' => storage_path('app/public/') . $this->faker->slug() . '.' . $this->faker->fileExtension, + 'mime' => $this->faker->mimeType, + 'user_id' => $this->faker->uuid, + 'size' => $this->faker->numberBetween(1000, 1000000), + 'permission' => null + ]; + } +} diff --git a/database/factories/PostFactory.php b/database/factories/PostFactory.php new file mode 100644 index 0000000..80405e2 --- /dev/null +++ b/database/factories/PostFactory.php @@ -0,0 +1,31 @@ + + */ +class PostFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + $publishDate = Carbon::parse($this->faker->dateTimeBetween('-1 month', '+1 month')); + + return [ + 'title' => $this->faker->sentence(), + 'slug' => $this->faker->slug(), + 'publish_at' => $publishDate, + 'content' => $this->faker->paragraphs(3, true), + 'user_id' => $this->faker->uuid, + 'hero' => $this->faker->uuid, + ]; + } +} diff --git a/tests/Feature/AuthEndpointTest.php b/tests/Feature/AuthApiTest.php similarity index 97% rename from tests/Feature/AuthEndpointTest.php rename to tests/Feature/AuthApiTest.php index e4dcc5f..e8a0e98 100644 --- a/tests/Feature/AuthEndpointTest.php +++ b/tests/Feature/AuthApiTest.php @@ -3,7 +3,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\User; -class AuthEndpointTest extends TestCase +class AuthApiTest extends TestCase { use RefreshDatabase; diff --git a/tests/Feature/EventsApiTest.php b/tests/Feature/EventsApiTest.php new file mode 100644 index 0000000..9e6a7a1 --- /dev/null +++ b/tests/Feature/EventsApiTest.php @@ -0,0 +1,136 @@ +faker = FakerFactory::create(); + } + + public function testAnyUserCanViewEvent() + { + // Create an event + $event = Event::factory()->create([ + 'publish_at' => Carbon::parse($this->faker->dateTimeBetween('-2 months', '-1 month')), + ]); + + // Create a future event + $futureEvent = Event::factory()->create([ + 'publish_at' => Carbon::parse($this->faker->dateTimeBetween('+1 month', '+2 months')), + ]); + + // Send GET request to the /api/events endpoint + $response = $this->getJson('/api/events'); + $response->assertStatus(200); + + // Assert that the event is in the response data + $response->assertJsonCount(1, 'events'); + $response->assertJsonFragment([ + 'id' => $event->id, + 'title' => $event->title, + ]); + + $response->assertJsonMissing([ + 'id' => $futureEvent->id, + 'title' => $futureEvent->title, + ]); + } + + public function testAdminCanCreateUpdateDeleteEvent() + { + // Create a user with the admin/events permission + $adminUser = User::factory()->create(); + $adminUser->givePermission('admin/events'); + + // Create media data + $media = Media::factory()->create(['user_id' => $adminUser->id]); + + // Create event data + $eventData = Event::factory()->make([ + 'start_at' => now()->addDays(7), + 'end_at' => now()->addDays(7)->addHours(2), + 'hero' => $media->id, + ])->toArray(); + + // Test creating event + $response = $this->actingAs($adminUser)->postJson('/api/events', $eventData); + $response->assertStatus(201); + $this->assertDatabaseHas('events', [ + 'title' => $eventData['title'], + 'content' => $eventData['content'], + ]); + + // Test viewing event + $event = Event::where('title', $eventData['title'])->first(); + $response = $this->get("/api/events/$event->id"); + $response->assertStatus(200); + $response->assertJsonStructure([ + 'event' => [ + 'id', + 'title', + 'content', + 'start_at', + 'end_at', + ] + ]); + + // Test updating event + $eventData['title'] = 'Updated Event'; + $response = $this->actingAs($adminUser)->putJson("/api/events/$event->id", $eventData); + $response->assertStatus(200); + $this->assertDatabaseHas('events', [ + 'title' => 'Updated Event', + ]); + + // Test deleting event + $response = $this->actingAs($adminUser)->delete("/api/events/$event->id"); + $response->assertStatus(204); + $this->assertDatabaseMissing('events', [ + 'title' => 'Updated Event', + ]); + } + + public function testNonAdminCannotCreateUpdateDeleteEvent() + { + // Create a user without admin/events permission + $user = User::factory()->create(); + + // Authenticate as the user + $this->actingAs($user); + + // Try to create a new event + $media = Media::factory()->create(['user_id' => $user->id]); + + $newEventData = Event::factory()->make(['hero' => $media->id])->toArray(); + + $response = $this->postJson('/api/events', $newEventData); + $response->assertStatus(403); + + // Try to update an event + $event = Event::factory()->create(); + $updatedEventData = [ + 'title' => 'Updated Event', + 'content' => 'This is an updated event.', + // Add more fields as needed + ]; + $response = $this->putJson('/api/events/' . $event->id, $updatedEventData); + $response->assertStatus(403); + + // Try to delete an event + $event = Event::factory()->create(); + $response = $this->deleteJson('/api/events/' . $event->id); + $response->assertStatus(403); + } +} diff --git a/tests/Feature/PostsApiTest.php b/tests/Feature/PostsApiTest.php new file mode 100644 index 0000000..c77a086 --- /dev/null +++ b/tests/Feature/PostsApiTest.php @@ -0,0 +1,134 @@ +faker = FakerFactory::create(); + } + + public function testAnyUserCanViewPost() + { + // Create an event + $post = Post::factory()->create([ + 'publish_at' => $this->faker->dateTimeBetween('-2 months', '-1 month'), + ]); + + // Create a future event + $futurePost = Post::factory()->create([ + 'publish_at' => $this->faker->dateTimeBetween('+1 month', '+2 months'), + ]); + + // Send GET request to the /api/posts endpoint + $response = $this->getJson('/api/posts'); + $response->assertStatus(200); + + // Assert that the event is in the response data + $response->assertJsonCount(1, 'posts'); + $response->assertJsonFragment([ + 'id' => $post->id, + 'title' => $post->title, + 'content' => $post->content, + ]); + + $response->assertJsonMissing([ + 'id' => $futurePost->id, + 'title' => $futurePost->title, + 'content' => $futurePost->content, + ]); + } + + public function testAdminCanCreateUpdateDeletePost() + { + // Create a user with the admin/events permission + $adminUser = User::factory()->create(); + $adminUser->givePermission('admin/posts'); + + // Create media data + $media = Media::factory()->create(['user_id' => $adminUser->id]); + + // Create event data + $postData = Post::factory()->make([ + 'user_id' => $adminUser->id, + 'hero' => $media->id, + ])->toArray(); + + // Test creating event + $response = $this->actingAs($adminUser)->postJson('/api/posts', $postData); + $response->assertStatus(201); + $this->assertDatabaseHas('posts', [ + 'title' => $postData['title'], + 'content' => $postData['content'], + ]); + + // Test viewing event + $post = Post::where('title', $postData['title'])->first(); + $response = $this->get("/api/posts/$post->id"); + $response->assertStatus(200); + $response->assertJsonStructure([ + 'post' => [ + 'id', + 'title', + 'content', + ] + ]); + + // Test updating event + $postData['title'] = 'Updated Post'; + $response = $this->actingAs($adminUser)->putJson("/api/posts/$post->id", $postData); + $response->assertStatus(200); + $this->assertDatabaseHas('posts', [ + 'title' => 'Updated Post', + ]); + + // Test deleting event + $response = $this->actingAs($adminUser)->delete("/api/posts/$post->id"); + $response->assertStatus(204); + $this->assertDatabaseMissing('posts', [ + 'title' => 'Updated Post', + ]); + } + + public function testNonAdminCannotCreateUpdateDeletePost() + { + // Create a user without admin/events permission + $user = User::factory()->create(); + + // Authenticate as the user + $this->actingAs($user); + + // Try to create a new post + $media = Media::factory()->create(['user_id' => $user->id]); + + $newPostData = Post::factory()->make(['user_id' => $user->id, 'hero' => $media->id])->toArray(); + + $response = $this->postJson('/api/posts', $newPostData); + $response->assertStatus(403); + + // Try to update an event + $post = Post::factory()->create(); + $updatedPostData = [ + 'title' => 'Updated Event', + 'content' => 'This is an updated event.', + // Add more fields as needed + ]; + $response = $this->putJson('/api/posts/' . $post->id, $updatedPostData); + $response->assertStatus(403); + + // Try to delete an event + $post = Post::factory()->create(); + $response = $this->deleteJson('/api/posts/' . $post->id); + $response->assertStatus(403); + } +} diff --git a/tests/Feature/UsersEndpointTest.php b/tests/Feature/UsersApiTest.php similarity index 99% rename from tests/Feature/UsersEndpointTest.php rename to tests/Feature/UsersApiTest.php index 7fdeaf6..65a1d63 100644 --- a/tests/Feature/UsersEndpointTest.php +++ b/tests/Feature/UsersApiTest.php @@ -4,7 +4,7 @@ use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase; use App\Models\User; -class UsersEndpointTest extends TestCase +class UsersApiTest extends TestCase { use RefreshDatabase; -- 2.49.1 From 154dffeee401bcc1a3cd8ed35df12d0fe9cf7dd2 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 19:02:00 +1000 Subject: [PATCH 34/39] use API path in .env --- resources/js/helpers/api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/js/helpers/api.ts b/resources/js/helpers/api.ts index cafc0f7..81f5741 100644 --- a/resources/js/helpers/api.ts +++ b/resources/js/helpers/api.ts @@ -1,5 +1,7 @@ import { useProgressStore } from "../store/ProgressStore"; import { useUserStore } from "../store/UserStore"; +import { ImportMetaExtras } from "../../../import-meta"; + interface ApiProgressData { loaded: number; total: number; @@ -31,7 +33,8 @@ const apiDefaultHeaders = { export const api = { timeout: 8000, - baseUrl: "https://www.stemmechanics.com.au/api", + baseUrl: (import.meta as ImportMetaExtras).env.APP_URL_API, + // baseUrl: "https://www.stemmechanics.com.au/api", send: function (options: ApiOptions) { return new Promise((resolve, reject) => { -- 2.49.1 From 85c37ba748121e8f009bf2080c606986360440e0 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 19:02:14 +1000 Subject: [PATCH 35/39] fix input fields not being used as an array --- app/Conductors/Conductor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Conductors/Conductor.php b/app/Conductors/Conductor.php index 2a2379f..f2f3c50 100644 --- a/app/Conductors/Conductor.php +++ b/app/Conductors/Conductor.php @@ -241,7 +241,7 @@ class Conductor $conductor->paginate($request->input('page', 1), $request->input('limit', -1)); // Limit fields - $limitFields = $request->input('fields'); + $limitFields = explode(',', $request->input('fields')); if ($limitFields === null) { $limitFields = $fields; } else { -- 2.49.1 From cf3c35ffa3e999aaabebf745d2d763f4f3ef7574 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 19:18:16 +1000 Subject: [PATCH 36/39] cleanup before deamalgmation --- resources/js/components/SMInput.vue | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/resources/js/components/SMInput.vue b/resources/js/components/SMInput.vue index 5773b5c..a023c14 100644 --- a/resources/js/components/SMInput.vue +++ b/resources/js/components/SMInput.vue @@ -166,7 +166,7 @@ const inputActive = ref(value.value.length > 0 || props.type == "select"); * Return the classname based on type */ const computedClassType = computed(() => { - return `sm-input-${props.type}`; + return `sm-input-type-${props.type}`; }); watch( @@ -434,15 +434,28 @@ const handleMediaSelect = async (event) => { background-size: 24px 18px; } - &.sm-input-media { + &.sm-input-type-media { label { position: relative; transform: none; } + + .sm-input-help { + text-align: center; + } + + &.sm-feedback-invalid .sm-input-media .sm-input-media-item ion-icon { + border: 2px solid $danger-color; + } + + &.sm-feedback-invalid .sm-invalid-icon { + // position: relative; + } } .sm-input-media { text-align: center; + margin-bottom: map-get($spacer, 2); .sm-input-media-item { display: block; -- 2.49.1 From 23288e15e0375d01690849a1075b1731edf2aed2 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 19:48:01 +1000 Subject: [PATCH 37/39] support value = null --- resources/js/components/SMInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/SMInput.vue b/resources/js/components/SMInput.vue index a023c14..b77c4a5 100644 --- a/resources/js/components/SMInput.vue +++ b/resources/js/components/SMInput.vue @@ -187,7 +187,7 @@ if (objControl) { label.value = toTitleCase(props.control); } - inputActive.value = value.value.length > 0 || props.type == "select"; + inputActive.value = value.value?.length > 0 || props.type == "select"; watch( () => objControl.validation.result.valid, -- 2.49.1 From 655c003969a3765d41f09f62cf3cfd04be4abc6d Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 19:48:10 +1000 Subject: [PATCH 38/39] typo --- resources/js/views/dashboard/EventEdit.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/views/dashboard/EventEdit.vue b/resources/js/views/dashboard/EventEdit.vue index c8d31a9..371137b 100644 --- a/resources/js/views/dashboard/EventEdit.vue +++ b/resources/js/views/dashboard/EventEdit.vue @@ -190,7 +190,7 @@ const registration_data = computed(() => { } else if (form?.controls.registration_type.value === "message") { data.visible = true; data.title = "Registration message"; - data.type = "test"; + data.type = "text"; } return data; -- 2.49.1 From d21d1b699392e075133d98d6130bffcbef2e624a Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 13 Mar 2023 19:48:39 +1000 Subject: [PATCH 39/39] fix registration_type case check --- resources/js/views/EventView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/views/EventView.vue b/resources/js/views/EventView.vue index 3b89f85..3d5b1d9 100644 --- a/resources/js/views/EventView.vue +++ b/resources/js/views/EventView.vue @@ -52,7 +52,7 @@ v-if=" event.status == 'open' && expired == false && - event.registration_type != 'none' + event.registration_type == 'url' " class="sm-workshop-registration sm-workshop-registration-url">