Use scopes #18

Merged
nomadjimbob merged 39 commits from useScopes into main 2023-03-13 09:49:42 +00:00
54 changed files with 2458 additions and 1296 deletions

59
.env.testing Normal file
View File

@@ -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%"

View File

@@ -0,0 +1,678 @@
<?php
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 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;
/**
* 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;
$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 !== '';
});
}
/**
* 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();
$conductor = new $conductor_class();
$total = 0;
try {
$conductor->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);
if ($request->has('filter') === true) {
$conductor->filterRaw($request->input('filter', ''), $fields);
}
// 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 = explode(',', $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') === true) {
$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];
}
/**
* 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();
$conductor = new $conductor_class();
$fields = $conductor->fields(new $conductor->class());
// Limit fields
$limitFields = $fields;
if ($request !== null && $request->has('fields') === true) {
$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') === true) {
$includes = explode(',', $request->input('includes', ''));
}
$conductor->includes($model, $includes);
// Transform
$model = $conductor->transform($model);
return $model;
}
/**
* 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) {
$this->collection = $collection;
}
return $this->collection;
}
/**
* Return the current conductor collection count.
*
* @return integer The current collection count.
*/
final public function count()
{
if ($this->query !== null) {
return $this->query->count();
}
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) {
$fields = explode(',', $fields);
} elseif ($fields === null) {
$fields = $this->sort;
}
if (is_array($fields) === true) {
foreach ($fields as $orderByField) {
$direction = 'asc';
$directionChar = substr($orderByField, 0, 1);
if (in_array($directionChar, ['-', '+']) === true) {
$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));
}
}
/**
* 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) {
$this->filterField($param, $value);
}
}
/**
* 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
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);
}
/**
* 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) {
$includeMethodName = 'include' . Str::studly($include);
if (method_exists($this, $includeMethodName) === true) {
$attributeName = Str::snake($include);
$attributeValue = $this->{$includeMethodName}($model);
if ($attributeValue !== null) {
$model->$attributeName = $this->{$includeMethodName}($model);
}
}
}
}
/**
* 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) {
$this->query->select($fields);
}
}
/**
* 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;
} else {
$limitFields = array_map('strtolower', $limitFields);
}
$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('/(?<!\\\\)[\'"]/', $item, $matches, PREG_OFFSET_CAPTURE) === 1) {
$glueToken = $matches[0][0];
$item = substr($item, 0, $matches[0][1]) . substr($item, ($matches[0][1] + 1));
$item = str_replace("\\$glueToken", $glueToken, $item);
}
$glued[] = $item;
} else {
// search for ending glue token
if (preg_match('/(?<!\\\\)' . $glueToken . '/', $item, $matches, PREG_OFFSET_CAPTURE) === 1) {
$item = substr($item, 0, $matches[0][1]) . substr($item, ($matches[0][1] + 1));
$glueToken = '';
}
$item = str_replace("\\$glueToken", $glueToken, $item);
$glued[(count($glued) - 1)] .= $item;
}
}//end foreach
$tokens = $glued;
$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);
}
/**
* 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;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Conductors;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class EventConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Event';
/**
* The default sorting field
* @var string
*/
protected $sort = 'start_at';
/**
* 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->hasPermission('admin/events') === false) {
$builder
->where('status', '!=', 'draft')
->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 (strtolower($model->status) === 'draft' || Carbon::parse($model->publish_at)->isFuture() === true) {
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/events') === 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->hasPermission('admin/events') === 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->hasPermission('admin/events') === true);
}
/**
* Return if the current model is destroyable.
*
* @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/events') === true);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class MediaConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Media';
/**
* The default sorting field
* @var string
*/
protected $sort = 'created_at';
/**
* 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)
{
$fields = parent::fields($model);
$user = auth()->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->hasPermission($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->hasPermission('admin/media') === true));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting model.
*/
public static function destroyable(Model $model)
{
$user = auth()->user();
return ($user !== null && ($model->user_id === $user->id || $user->hasPermission('admin/media') === true));
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Conductors;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class PostConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\Post';
/**
* The default sorting field
* @var string
*/
protected $sort = '-publish_at';
/**
* 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->hasPermission('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->hasPermission('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->hasPermission('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->hasPermission('admin/posts') === true);
}
/**
* Return if the current model is destroyable.
*
* @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/posts') === true);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class SubscriptionConductor extends Conductor
{
/**
* The Model Class
* @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->hasPermission('admin/subscriptions') === true));
}
/**
* Return if the current model is destroyable.
*
* @param Model $model The model.
* @return boolean Allow deleting 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->hasPermission('admin/subscriptions') === true));
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Conductors;
use Illuminate\Database\Eloquent\Model;
class UserConductor extends Conductor
{
/**
* The Model Class
* @var string
*/
protected $class = '\App\Models\User';
/**
* Return the visible API fields.
*
* @param Model $model The model.
* @return string[] The fields visible.
*/
public function fields(Model $model)
{
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/users') === false) {
return ['id', 'username'];
}
return parent::fields($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 || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
$fields = ['id', 'username'];
$data = arrayLimitKeys($data, $fields);
}
return $data;
}
/**
* 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);
}
return false;
}
/**
* Return if the current model is destroyable.
*
* @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);
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Filters;
use Illuminate\Support\Collection;
class AuditFilter
{
// public static function filter(Collection $collection): array
// {
// $collection->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();
// }
}

View File

@@ -1,65 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Event;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\Builder;
class EventFilter extends FilterAbstract
{
/**
* Class name of Model
* @var string
*/
protected $class = '\App\Models\Event';
/**
* Default column sorting (prefix with - for descending)
*
* @var string|array
*/
protected $defaultSort = 'start_at';
/**
* Filter columns for q param
*
* @var string|array
*/
protected $q = [
'_' => ['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());
}
}
}

View File

@@ -1,597 +0,0 @@
<?php
namespace App\Filters;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\SchemaException;
use ReflectionClass;
use RuntimeException;
use InvalidArgumentException;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Schema;
abstract class FilterAbstract
{
/**
* The model class to filter
*
* @var mixed
*/
protected $class;
/**
* The filter request
*
* @var \Illuminate\Http\Request
*/
protected $request;
/**
* The models table
*
* @var string
*/
protected $table = '';
/**
* Array of columns that can be filtered by the api
*
* @var array
*/
protected $filterable = null;
/**
* Default column sorting (prefix with - for descending)
*
* @var string|array
*/
protected $defaultSort = 'id';
/**
* Default collection result limit
*
* @var integer
*/
protected $defaultLimit = 50;
/**
* Found records from query
* @var integer
*/
protected $foundTotal = 0;
/**
* Maximum collection result limit
*
* @var integer
*/
protected $maxLimit = 100;
/**
* Only return these attributes in the results
* (minus any excludes)
*
* @var array
*/
protected $only = [];
/**
* Exclude these attributes from the results
*
* @var array
*/
protected $exclude = [];
/**
* Filter columns for q param
*
* @var string|array
*/
protected $q = [];
/**
* Filter constructor.
*
* @param \Illuminate\Http\Request $request Request object.
*/
public function __construct(Request $request)
{
$this->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('/(?<!\\\\)"/', $filterValue, -1, PREG_SPLIT_OFFSET_CAPTURE);
foreach ($matches as $idx => $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('/(?<!\\\\),/', $match_info[0]);
}
foreach ($innerTags as $tag) {
$tag = stripslashes(trim($tag));
if (strlen($tag) > 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;
}
}

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Media;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
class MediaFilter extends FilterAbstract
{
/**
* Class name of Model
* @var string
*/
protected $class = '\App\Models\Media';
/**
* Determine if the user can view the media model
*
* @param Media $media The media instance.
* @param mixed $user The current logged in user.
* @return boolean
*/
protected function viewable(Media $media, mixed $user)
{
if (empty($media->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'));
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Filters;
use App\Models\Post;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\Builder;
class PostFilter extends FilterAbstract
{
/**
* Class name of Model
* @var string
*/
protected $class = '\App\Models\Post';
/**
* Default column sorting (prefix with - for descending)
*
* @var string|array
*/
protected $defaultSort = '-publish_at';
/**
* Determine if the user can view the media model
*
* @param Post $post The post instance.
* @param mixed $user The current logged in user.
* @return boolean
*/
protected function viewable(Post $post, mixed $user)
{
if ($user?->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());
}
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace App\Filters;
use App\Models\User;
class UserFilter extends FilterAbstract
{
/**
* The model class to filter
*
* @var mixed
*/
protected $class = '\App\Models\User';
/**
* 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 $userData User model if single object is requested.
* @return mixed
*/
protected function seeAttributes(array $attributes, mixed $user, ?object $userData = null)
{
if ($user?->hasPermission('admin/users') !== true && ($user === null || $userData === null || $user?->id !== $userData?->id)) {
return ['id', 'username'];
}
}
}

40
app/Helpers/Array.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
/* Array Helper Functions */
/**
* Remove an item from an array.
*
* @param array $arr The array to check.
* @param string|array $item The item or items to remove.
* @return array The filtered array.
*/
function arrayRemoveItem(array $arr, string|array $item): array
{
$filteredArr = $arr;
if (is_string($item) === true) {
$item = [$item];
}
foreach ($item as $str) {
$filteredArr = array_filter($arr, function ($item) use ($str) {
return $item !== $str;
});
}
return $filteredArr;
}
/**
* Return an array with specified the keys
*
* @param array $arr The array to filter.
* @param string|array $keys The keys to keep.
* @return array The filtered array.
*/
function arrayLimitKeys(array $arr, array $keys): array
{
return array_intersect_key($arr, array_flip($keys));
}

View File

@@ -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];

View File

@@ -73,6 +73,7 @@ class AuthController extends ApiController
return $this->respondAsResource(
$user->makeVisible(['permissions']),
false,
['token' => $token]
);
}//end if

View File

@@ -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 App\Http\Requests\EventRequest;
use Illuminate\Http\Request;
class EventController extends ApiController
@@ -22,56 +22,72 @@ class EventController extends ApiController
/**
* Display a listing of the resource.
*
* @param EventFilter $filter The event filter.
* @param \Illuminate\Http\Request $request The endpoint request.
* @return \Illuminate\Http\Response
*/
public function index(EventFilter $filter)
public function index(Request $request)
{
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
);
}
list($collection, $total) = EventConductor::request($request);
/**
* Store a newly created resource in storage.
*
* @param EventRequest $request The event store request.
* @return \Illuminate\Http\Response
*/
public function store(EventRequest $request)
{
$event = Event::create($request->all());
return $this->respondAsResource(
(new EventFilter($request))->filter($event),
null,
HttpResponseCodes::HTTP_CREATED
$collection,
true,
['total' => $total]
);
}
/**
* Display the specified resource.
*
* @param EventFilter $filter The event filter.
* @param \App\Models\Event $event The specified event.
* @param \Illuminate\Http\Request $request The endpoint 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) === 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),
false,
null,
HttpResponseCodes::HTTP_CREATED
);
} else {
return $this->respondForbidden();
}
}
/**
* Update the specified resource in storage.
*
* @param EventRequest $request The event update 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 update(EventRequest $request, Event $event)
{
$event->update($request->all());
return $this->respondAsResource((new EventFilter($request))->filter($event));
if (EventConductor::updatable($event) === true) {
$event->update($request->all());
return $this->respondAsResource(EventConductor::model($request, $event));
}
return $this->respondForbidden();
}
/**
@@ -82,7 +98,11 @@ class EventController extends ApiController
*/
public function destroy(Event $event)
{
$event->delete();
return $this->respondNoContent();
if (EventConductor::destroyable($event) === true) {
$event->delete();
return $this->respondNoContent();
} else {
return $this->respondForbidden();
}
}
}

View File

@@ -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,68 @@ 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,
true,
['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 +94,78 @@ 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),
false,
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 +174,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)

View File

@@ -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,72 @@ 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,
true,
['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),
false,
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 +102,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();
}
}
}

View File

@@ -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,71 @@ 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()]
true,
['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 +118,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();
}
}
/**

View File

@@ -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;
@@ -23,6 +21,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,96 +47,102 @@ class UserController extends ApiController
/**
* Display a listing of the resource.
*
* @param \App\Filters\UserFilter $filter Filter object.
* @param \Illuminate\Http\Request $request The endpoint 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()]
true,
['total' => $total]
);
}
/**
* Store a newly created user in the database.
*
* @param UserStoreRequest $request The user update 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 ($request->user()->hasPermission('admin/user') !== true) {
if (UserConductor::creatable() === true) {
$user = User::create($request->all());
return $this->respondAsResource(UserConductor::model($request, $user), false, [], HttpResponseCodes::HTTP_CREATED);
} else {
return $this->respondForbidden();
}
$user = User::create($request->all());
return $this->respondAsResource((new UserFilter($request))->filter($user), [], HttpResponseCodes::HTTP_CREATED);
}
/**
* 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)
{
return $this->respondAsResource($filter->filter($user));
if (UserConductor::viewable($user) === true) {
return $this->respondAsResource(UserConductor::model($request, $user));
}
return $this->respondForbidden();
}
/**
* Update the specified resource in storage.
*
* @param UserUpdateRequest $request The user update request.
* @param User $user The specified user.
* @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)
{
$input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password'];
if (UserConductor::updatable($user) === true) {
$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));
return $this->respondForbidden();
}
/**
* 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 ($user->hasPermission('admin/user') === false) {
return $this->respondForbidden();
if (UserConductor::destroyable($user) === true) {
$user->delete();
return $this->respondNoContent();
}
$user->delete();
return $this->respondNoContent();
return $this->respondForbidden();
}
/**
* 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)
@@ -171,7 +176,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)
@@ -191,7 +196,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)
@@ -213,7 +218,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)
@@ -247,7 +252,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)
@@ -285,7 +290,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)
@@ -312,7 +317,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)

View File

@@ -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,8 +40,8 @@ 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) {
$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->putRules());
} elseif (method_exists($this, 'destroyRules') === true && request()->isMethod('delete') === true) {
$rules = $this->mergeRules($rules, $this->destroyRules());
}

View File

@@ -2,31 +2,10 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class EventRequest 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
*
@@ -47,11 +26,12 @@ class EventRequest 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',
];

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class MediaRequest extends BaseRequest
{
/* empty */
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MediaStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
//
];
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MediaUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
//
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class PostRequest extends BaseRequest
{
/**
* Get the validation rules that apply to POST requests.
*
* @return array<string, mixed>
*/
public function postRules()
{
return [
'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',
];
}
/**
* Get the validation rules that apply to PUT request.
*
* @return array<string, mixed>
*/
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',
'content' => 'string|min:6',
'hero' => 'uuid|exists:media,id',
];
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PostStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
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',
];
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PostUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
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',
];
}
}

View File

@@ -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',
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
class UserRequest extends BaseRequest
{
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
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<string, mixed>
*/
public function putRules()
{
$user = $this->route('user');
return [
'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',
'phone' => ['nullable','regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => 'string|min:8'
];
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
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'
];
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'username' => 'string|max:255|min:6|unique:users',
'first_name' => 'string|max:255|min:2',
'last_name' => 'string|max:255|min:2',
'email' => 'string|email|max:255',
'phone' => ['nullable','regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => 'string|min:8'
];
}
}

View File

@@ -32,7 +32,6 @@ class Event extends Model
'ages',
];
/**
* Get all of the post's attachments.
*/

View File

@@ -103,6 +103,48 @@ 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];
});
$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
*
* @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
*

View File

@@ -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();
});
}
}
}

View File

@@ -2,7 +2,10 @@
"name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"require": {
"php": "^8.0.2",
@@ -26,6 +29,9 @@
"spatie/laravel-ignition": "^1.0"
},
"autoload": {
"files": [
"app/Helpers/Array.php"
],
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",

View File

@@ -0,0 +1,40 @@
<?php
namespace Database\Factories;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/
class EventFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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+)?'),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/
class MediaFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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,
];
}
}

View File

@@ -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(
@@ -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,
@@ -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;

View File

@@ -65,14 +65,18 @@ const mediaItems: Ref<Media[]> = 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 */
});
};
/**

View File

@@ -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) => {

View File

@@ -66,25 +66,29 @@ const waitForElementRender = (elem: Ref): Promise<HTMLElement> => {
* @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 */
});
};
/**

View File

@@ -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;

View File

@@ -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">
<SMButton
@@ -60,6 +60,15 @@
:block="true"
label="Register for Event"></SMButton>
</div>
<div
v-if="
event.status == 'open' &&
expired == false &&
event.registration_type == 'message'
"
class="sm-workshop-registration sm-workshop-registration-message">
{{ event.registration_data }}
</div>
<div class="sm-workshop-date">
<h4>
<ion-icon
@@ -380,7 +389,8 @@ handleLoad();
}
.sm-workshop-registration-none,
.sm-workshop-registration-soon {
.sm-workshop-registration-soon,
.sm-workshop-registration-message {
border: 1px solid #ffeeba;
background-color: #fff3cd;
color: #856404;

View File

@@ -84,6 +84,7 @@
none: 'None',
email: 'Email',
link: 'Link',
message: 'Message',
}" />
</SMColumn>
<SMColumn>
@@ -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 = "text";
}
return data;

View File

@@ -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']);

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
class AuthApiTest extends TestCase
{
use RefreshDatabase;
public function testLogin()
{
$user = User::factory()->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);
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ContactFormTest extends TestCase
{
use RefreshDatabase;
public function testContactForm()
{
$formData = [
'name' => '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);
}
}

View File

@@ -0,0 +1,136 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use App\Models\Event;
use App\Models\Media;
use Carbon\Carbon;
use Faker\Factory as FakerFactory;
class EventsApiTest extends TestCase
{
use RefreshDatabase;
protected $faker;
public function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function test_the_application_returns_a_successful_response()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}

View File

@@ -0,0 +1,134 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use App\Models\Media;
use App\Models\Post;
use Faker\Factory as FakerFactory;
class PostsApiTest extends TestCase
{
use RefreshDatabase;
protected $faker;
public function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@@ -0,0 +1,235 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\User;
class UsersApiTest extends TestCase
{
use RefreshDatabase;
public function testNonAdminUsersCanOnlyViewBasicUserInfo()
{
// create a non-admin user
$nonAdminUser = User::factory()->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]);
}
}