This commit is contained in:
2023-01-24 15:13:03 +10:00
parent decf5c7d39
commit 4c83399d4a
261 changed files with 33538 additions and 1 deletions

32
app/Console/Kernel.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule The schedule.
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}

50
app/Enum/Enum.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
namespace App\Enum;
use ReflectionClass;
class Enum
{
/**
* Caches reflections of enum subclasses.
*
* @var array<class-string<static>, ReflectionClass<static>>
*/
public static $reflectionCache = [];
/**
* Returns a reflection of the enum subclass.
*
* @return ReflectionClass<static>
*/
public static function getReflection(): ReflectionClass
{
$class = static::class;
return static::$reflectionCache[$class] ??= new ReflectionClass($class);
}
/**
* Returns the constants in the enum subclass
*
* @return array<static>
*/
public static function getConstants(): array
{
return static::getReflection()->getConstants();
}
/**
* Returns the constants values in the enum subclass
*
* @return array<static>
*/
public static function getConstantValues(): array
{
return array_values(static::getReflection()->getConstants());
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Enum;
class HttpResponseCodes extends Enum
{
public const HTTP_CONTINUE = 100;
public const HTTP_SWITCHING_PROTOCOLS = 101;
public const HTTP_PROCESSING = 102;
public const HTTP_OK = 200;
public const HTTP_CREATED = 201;
public const HTTP_ACCEPTED = 202;
public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
public const HTTP_NO_CONTENT = 204;
public const HTTP_RESET_CONTENT = 205;
public const HTTP_PARTIAL_CONTENT = 206;
public const HTTP_MULTI_STATUS = 207;
public const HTTP_ALREADY_REPORTED = 208;
public const HTTP_IM_USED = 226;
public const HTTP_MULTIPLE_CHOICES = 300;
public const HTTP_MOVED_PERMANENTLY = 301;
public const HTTP_FOUND = 302;
public const HTTP_SEE_OTHER = 303;
public const HTTP_NOT_MODIFIED = 304;
public const HTTP_USE_PROXY = 305;
public const HTTP_RESERVED = 306;
public const HTTP_TEMPORARY_REDIRECT = 307;
public const HTTP_PERMANENTLY_REDIRECT = 308;
public const HTTP_BAD_REQUEST = 400;
public const HTTP_UNAUTHORIZED = 401;
public const HTTP_PAYMENT_REQUIRED = 402;
public const HTTP_FORBIDDEN = 403;
public const HTTP_NOT_FOUND = 404;
public const HTTP_METHOD_NOT_ALLOWED = 405;
public const HTTP_NOT_ACCEPTABLE = 406;
public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
public const HTTP_REQUEST_TIMEOUT = 408;
public const HTTP_CONFLICT = 409;
public const HTTP_GONE = 410;
public const HTTP_LENGTH_REQUIRED = 411;
public const HTTP_PRECONDITION_FAILED = 412;
public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
public const HTTP_REQUEST_URI_TOO_LONG = 414;
public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
public const HTTP_EXPECTATION_FAILED = 417;
public const HTTP_I_AM_A_TEAPOT = 418;
public const HTTP_MISDIRECTED_REQUEST = 421;
public const HTTP_UNPROCESSABLE_ENTITY = 422;
public const HTTP_LOCKED = 423;
public const HTTP_FAILED_DEPENDENCY = 424;
public const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425;
public const HTTP_UPGRADE_REQUIRED = 426;
public const HTTP_PRECONDITION_REQUIRED = 428;
public const HTTP_TOO_MANY_REQUESTS = 429;
public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
public const HTTP_INTERNAL_SERVER_ERROR = 500;
public const HTTP_NOT_IMPLEMENTED = 501;
public const HTTP_BAD_GATEWAY = 502;
public const HTTP_SERVICE_UNAVAILABLE = 503;
public const HTTP_GATEWAY_TIMEOUT = 504;
public const HTTP_VERSION_NOT_SUPPORTED = 505;
public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;
public const HTTP_INSUFFICIENT_STORAGE = 507;
public const HTTP_LOOP_DETECTED = 508;
public const HTTP_NOT_EXTENDED = 510;
public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
/**
* HTTP Response Messages
* @var string[]
*/
public static $statusTexts = [
100 => 'Continue.',
101 => 'Switching Protocols.',
102 => 'Processing.',
// RFC2518
200 => 'OK.',
201 => 'Created.',
202 => 'Accepted.',
203 => 'Non-Authoritative Information.',
204 => 'No Content.',
205 => 'Reset Content.',
206 => 'Partial Content.',
207 => 'Multi-Status.',
// RFC4918
208 => 'Already Reported.',
// RFC5842
226 => 'IM Used.',
// RFC3229
300 => 'Multiple Choices.',
301 => 'Moved Permanently.',
302 => 'Found.',
303 => 'See Other.',
304 => 'Not Modified.',
305 => 'Use Proxy.',
307 => 'Temporary Redirect.',
308 => 'Permanent Redirect.',
// RFC7238
400 => 'Bad Request.',
401 => 'Unauthorized.',
402 => 'Payment Required.',
403 => 'Forbidden.',
404 => 'Not Found.',
405 => 'Method Not Allowed.',
406 => 'Not Acceptable.',
407 => 'Proxy Authentication Required.',
408 => 'Request Timeout.',
409 => 'Conflict.',
410 => 'Gone.',
411 => 'Length Required.',
412 => 'Precondition Failed.',
413 => 'Payload Too Large.',
414 => 'URI Too Long.',
415 => 'Unsupported Media Type.',
416 => 'Range Not Satisfiable.',
417 => 'Expectation Failed.',
418 => 'I\'m a teapot.',
// RFC2324
421 => 'Misdirected Request.',
// RFC7540
422 => 'Unprocessable Entity.',
// RFC4918
423 => 'Locked.',
// RFC4918
424 => 'Failed Dependency.',
// RFC4918
425 => 'Reserved for WebDAV advanced collections expired proposal.',
// RFC2817
426 => 'Upgrade Required.',
// RFC2817
428 => 'Precondition Required.',
// RFC6585
429 => 'Too Many Requests.',
// RFC6585
431 => 'Request Header Fields Too Large.',
// RFC6585
451 => 'Unavailable For Legal Reasons.',
// RFC7725
500 => 'Internal Server Error.',
501 => 'Not Implemented.',
502 => 'Bad Gateway.',
503 => 'Service Unavailable.',
504 => 'Gateway Timeout.',
505 => 'HTTP Version Not Supported.',
506 => 'Variant Also Negotiates.',
// RFC2295
507 => 'Insufficient Storage.',
// RFC4918
508 => 'Loop Detected.',
// RFC5842
510 => 'Not Extended.',
// RFC2774
511 => 'Network Authentication Required.',
// RFC6585
];
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Exceptions;
use App\Enum\HttpResponseCodes;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use PDOException;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
// $this->renderable(function (HttpException $e, $request) {
// if ($request->is('api/*')) {
// $message = $e->getMessage();
// if ($message === '') {
// $message = HttpResponseCodes::$statusTexts[$e->getStatusCode()];
// }
// return response()->json([
// 'message' => $message
// ], $e->getStatusCode());
// }
// });
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*') === true) {
return response()->json([
'message' => 'Resource not found'
], 404);
}
});
$this->renderable(function (PDOException $e, $request) {
if ($request->is('api/*') === true) {
return response()->json([
'message' => 'The server is currently unavailable'
], 503);
}
});
$this->reportable(function (Throwable $e) {
//
});
}
}

View File

@@ -0,0 +1,29 @@
<?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

@@ -0,0 +1,70 @@
<?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'],
];
// protected $q = [
// 'title',
// 'content'
// ];
/**
* 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

@@ -0,0 +1,589 @@
<?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.
* @return mixed
*/
protected function seeAttributes(array $attributes, mixed $user)
{
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());
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_intersect($attributes, $sortList);
}
/* 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

@@ -0,0 +1,58 @@
<?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

@@ -0,0 +1,47 @@
<?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';
/**
* 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

@@ -0,0 +1,30 @@
<?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.
* @return mixed
*/
protected function seeAttributes(array $attributes, mixed $user)
{
if ($user?->hasPermission('admin/users') !== true) {
return ['id', 'username'];
}
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class ApiController extends Controller
{
/**
* Resource name
* @var string
*/
protected $resourceName = '';
/**
* Return generic json response with the given data.
*
* @param array $data Response data.
* @param integer $respondCode Response status code.
* @param array $headers Response headers.
* @return \Illuminate\Http\JsonResponse
*/
public function respondJson(array $data, int $respondCode = HttpResponseCodes::HTTP_OK, array $headers = [])
{
return response()->json($data, $respondCode, $headers);
}
/**
* Return forbidden message
*
* @param string $message Response message.
* @return \Illuminate\Http\JsonResponse
*/
public function respondForbidden(string $message = 'You do not have permission to access the resource.')
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_FORBIDDEN);
}
/**
* Return forbidden message
*
* @param string $message Response message.
* @return \Illuminate\Http\JsonResponse
*/
public function respondNotFound(string $message = 'The resource was not found.')
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_NOT_FOUND);
}
/**
* Return too large message
*
* @param string $message Response message.
* @return \Illuminate\Http\JsonResponse
*/
public function respondTooLarge(string $message = 'The request entity is too large.')
{
return response()->json(['message' => $message], HttpResponseCodes::HTTP_REQUEST_ENTITY_TOO_LARGE);
}
/**
* Return no content
* @return \Illuminate\Http\JsonResponse
*/
public function respondNoContent()
{
return response()->json([], HttpResponseCodes::HTTP_NO_CONTENT);
}
/**
* Return created
* @return \Illuminate\Http\JsonResponse
*/
public function respondCreated()
{
return response()->json([], HttpResponseCodes::HTTP_CREATED);
}
/**
* Return single error message
*
* @param string $message Error message.
* @param integer $responseCode Resource code.
* @return \Illuminate\Http\JsonResponse
*/
public function respondError(string $message, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY)
{
return response()->json([
'message' => $message
], $responseCode);
}
/**
* Return formatted errors
*
* @param array $errors Error messages.
* @param integer $responseCode Resource code.
* @return \Illuminate\Http\JsonResponse
*/
public function respondWithErrors(array $errors, int $responseCode = HttpResponseCodes::HTTP_UNPROCESSABLE_ENTITY)
{
$keys = array_keys($errors);
$error = $errors[$keys[0]];
if (count($keys) > 1) {
$additional_errors = (count($keys) - 1);
$error .= sprintf(' (and %d more %s', $additional_errors, Str::plural('error', $additional_errors));
}
return response()->json([
'message' => $error,
'errors' => $errors
], $responseCode);
}
/**
* Return resource data
*
* @param array|Model|Collection $data Resource data.
* @param array|null $appendData Data to append to response.
* @param integer $respondCode Resource code.
* @return \Illuminate\Http\JsonResponse
*/
protected function respondAsResource(
mixed $data,
mixed $appendData = null,
int $respondCode = HttpResponseCodes::HTTP_OK
) {
if ($data === null || ($data instanceof Collection && $data->count() === 0)) {
return $this->respondNotFound();
}
$resourceName = $this->resourceName;
if ($this->resourceName === '') {
$resourceName = get_class($this);
$resourceName = substr($resourceName, (strrpos($resourceName, '\\') + 1));
$resourceName = substr($resourceName, 0, strpos($resourceName, 'Controller'));
$resourceName = strtolower($resourceName);
}
$is_multiple = true;
$dataArray = [];
if ($data instanceof Collection) {
$dataArray = $data->toArray();
} elseif (is_array($data) === true) {
$dataArray = $data;
} elseif ($data instanceof Model) {
$is_multiple = false;
$dataArray = $data->toArray();
}
$resource = [];
if ($is_multiple === true) {
$resource = [Str::plural($resourceName) => $dataArray];
} else {
$resource = [Str::singular($resourceName) => $dataArray];
}
if ($appendData !== null) {
$resource += $appendData;
}
return response()->json($resource, $respondCode);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\AuthLoginRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\JsonResponse;
class AuthController extends ApiController
{
/**
* Resource name
* @var string
*/
protected $resourceName = 'user';
/**
* ApplicationController constructor.
*/
public function __construct()
{
// $this->middleware('auth:sanctum')
// ->only(['me']);
}
/**
* Current User details
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function me(Request $request)
{
$user = $request->user()->makeVisible(['permissions']);
return $this->respondAsResource($user);
}
/**
* Login user with supplied creditials
*
* @param App\Http\Controllers\Api\AuthLoginRequest $request Created request data.
* @return JsonResponse|void
*/
public function login(AuthLoginRequest $request)
{
$user = User::where('username', '=', $request->input('username'))->first();
if ($user !== null && Hash::check($request->input('password'), $user->password) === true) {
if ($user->email_verified_at === null) {
return $this->respondWithErrors([
'username' => 'Email address has not been verified.'
]);
}
if ($user->disabled === true) {
return $this->respondWithErrors([
'username' => 'Account has been disabled.'
]);
}
$token = $user->createToken('user_token')->plainTextToken;
$user->logins()->create([
'token' => $token,
'login' => now(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent()
]);
return $this->respondAsResource(
$user->makeVisible(['permissions']),
['token' => $token]
);
}//end if
return $this->respondWithErrors([
'username' => 'Invalid username or password',
'password' => 'Invalid username or password',
]);
}
/**
* Logout current user
*
* @param Request $request Current request data.
* @return JsonResponse
*/
public function logout(Request $request)
{
$user = $request->user();
$user->logins()->where('token', $user->currentAccessToken())->update(['logout' => now()]);
$user->currentAccessToken()->delete();
return $this->respondNoContent();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\ContactSendRequest;
use App\Jobs\SendEmailJob;
use App\Mail\Contact;
class ContactController extends ApiController
{
/**
* Send the request to the site admin by email
*
* @param \App\Http\Requests\User\ContactSendRequest $request Request data.
* @return \Illuminate\Http\Response
*/
public function send(ContactSendRequest $request)
{
dispatch((new SendEmailJob(
config('contact.contact_address'),
new Contact(
$request->input('name'),
$request->input('email'),
$request->input('content')
)
)))->onQueue('mail');
return $this->respondCreated();
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Filters\EventFilter;
use App\Http\Requests\EventRequest;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
}
/**
* Display a listing of the resource.
*
* @param EventFilter $filter The event filter.
* @return \Illuminate\Http\Response
*/
public function index(EventFilter $filter)
{
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
);
}
/**
* 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
);
}
/**
* Display the specified resource.
*
* @param EventFilter $filter The event filter.
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function show(EventFilter $filter, Event $event)
{
return $this->respondAsResource($filter->filter($event));
}
/**
* Update the specified resource in storage.
*
* @param EventRequest $request The event update 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));
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Event $event The specified event.
* @return \Illuminate\Http\Response
*/
public function destroy(Event $event)
{
$event->delete();
return $this->respondNoContent();
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Filters\MediaFilter;
use App\Http\Requests\MediaStoreRequest;
use App\Http\Requests\MediaUpdateRequest;
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
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only(['store','update','destroy']);
}
/**
* Display a listing of the resource.
*
* @param \App\Filters\MediaFilter $filter Created filter object.
* @return \Illuminate\Http\Response
*/
public function index(MediaFilter $filter)
{
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
);
}
/**
* Display the specified resource.
*
* @param MediaFilter $filter The request filter.
* @param Media $medium The request media.
* @return \Illuminate\Http\Response
*/
public function show(MediaFilter $filter, Media $medium)
{
return $this->respondAsResource($filter->filter($medium));
}
/**
* Store a new media resource
*
* @param MediaStoreRequest $request The uploaded media.
* @return \Illuminate\Http\Response
*/
public function store(MediaStoreRequest $request)
{
$file = $request->file('file');
if ($file === null) {
return $this->respondError(['file' => 'An error occurred uploading the file 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->respondError(['file' => 'The file upload was interrupted.']);
default:
return $this->respondError(['file' => 'An error occurred uploading the file to the server.']);
}
}
if ($file->getSize() > Media::maxUploadSize()) {
return $this->respondTooLarge();
}
$title = $file->getClientOriginalName();
$mime = $file->getMimeType();
$fileInfo = Media::store($file, empty($request->input('permission')));
if ($fileInfo === null) {
return $this->respondError(
['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();
$fileInfo = Media::store($file, empty($request->input('permission')));
if ($fileInfo === null) {
return $this->respondError(
['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->respondWithTransformer($file);
}
/**
* Remove the specified resource from storage.
*
* @param Request $request Request instance.
* @param \App\Models\Media $medium Specified media file.
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, Media $medium)
{
if ((new MediaFilter($request))->filter($medium) !== null) {
if (file_exists($medium->path()) === true) {
unlink($medium->path());
}
$medium->delete();
return $this->respondNoContent();
}
return $this->respondNotFound();
}
/**
* Display the specified resource.
*
* @param Request $request Request instance.
* @param \App\Models\Media $medium Specified media.
* @return \Illuminate\Http\Response
*/
public function download(Request $request, Media $medium)
{
$respondJson = in_array('application/json', explode(',', $request->header('Accept', 'application/json')));
$headers = [];
$path = $medium->path();
/* File exists */
if (file_exists($path) === false) {
if ($respondJson === false) {
return redirect('/not-found');
} else {
return $this->respondNotFound();
}
}
$updated_at = Carbon::parse(filemtime($path));
$headerPragma = 'no-cache';
$headerCacheControl = 'max-age=0, must-revalidate';
$headerExpires = $updated_at->toRfc2822String();
if (empty($medium->permission) === true) {
if ($request->user() === null && $request->has('token') === true) {
$accessToken = PersonalAccessToken::findToken(urldecode($request->input('token')));
if (
$accessToken !== null && (config('sanctum.expiration') === null ||
$accessToken->created_at->lte(now()->subMinutes(config('sanctum.expiration'))) === false)
) {
$user = $accessToken->tokenable;
}
}
if ($request->user() === null || $user->hasPermission($medium->permission) === false) {
if ($respondJson === false) {
return redirect('/login?redirect=' . $request->path());
} else {
return $this->respondForbidden();
}
}
} else {
$headerPragma = 'public';
$headerExpires = $updated_at->addMonth()->toRfc2822String();
}//end if
$headerEtag = md5($updated_at->format('U'));
$headerLastModified = $updated_at->toRfc2822String();
$headers = [
'Cache-Control' => $headerCacheControl,
'Content-Disposition' => sprintf('inline; filename="%s"', basename($path)),
'Etag' => $headerEtag,
'Expires' => $headerExpires,
'Last-Modified' => $headerLastModified,
'Pragma' => $headerPragma,
];
$server = request()->server;
$requestModifiedSince = $server->has('HTTP_IF_MODIFIED_SINCE') &&
$server->get('HTTP_IF_MODIFIED_SINCE') === $headerLastModified;
$requestNoneMatch = $server->has('HTTP_IF_NONE_MATCH') &&
$server->get('HTTP_IF_NONE_MATCH') === $headerEtag;
if ($requestModifiedSince === true || $requestNoneMatch === true) {
return response()->make('', 304, $headers);
}
return response()->file($path, $headers);
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Filters\PostFilter;
use App\Http\Requests\PostStoreRequest;
use App\Http\Requests\PostUpdateRequest;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->only([
'store',
'update',
'delete'
]);
}
/**
* Display a listing of the resource.
*
* @param \App\Filters\PostFilter $filter Post filter request.
* @return \Illuminate\Http\Response
*/
public function index(PostFilter $filter)
{
return $this->respondAsResource(
$filter->filter(),
['total' => $filter->foundTotal()]
);
}
/**
* Display the specified resource.
*
* @param PostFilter $filter The filter request.
* @param \App\Models\Post $post The post model.
* @return \Illuminate\Http\Response
*/
public function show(PostFilter $filter, Post $post)
{
return $this->respondAsResource($filter->filter($post));
}
/**
* Store a newly created resource in storage.
*
* @param PostStoreRequest $request The post store request.
* @return \Illuminate\Http\Response
*/
public function store(PostStoreRequest $request)
{
$post = Post::create($request->all());
return $this->respondAsResource(
(new PostFilter($request))->filter($post),
null,
HttpResponseCodes::HTTP_CREATED
);
}
/**
* Update the specified resource in storage.
*
* @param PostUpdateRequest $request The post update request.
* @param \App\Models\Post $post The specified post.
* @return \Illuminate\Http\Response
*/
public function update(PostUpdateRequest $request, Post $post)
{
$post->update($request->all());
return $this->respondAsResource((new PostFilter($request))->filter($post));
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Post $post The specified post.
* @return \Illuminate\Http\Response
*/
public function destroy(Post $post)
{
$post->delete();
return $this->respondNoContent();
}
}

View File

@@ -0,0 +1,126 @@
<?php
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\UserForgotPasswordRequest;
use App\Http\Requests\UserForgotUsernameRequest;
use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest;
use App\Http\Requests\UserVerifyEmailRequest;
use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotUsername;
use App\Mail\ForgotPassword;
use App\Mail\EmailVerify;
use App\Models\User;
use App\Models\UserCode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class SubscriptionController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->except([]);
}
/**
* Display a listing of the resource.
*
* @param \App\Filters\UserFilter $filter Filter object.
* @return \Illuminate\Http\Response
*/
public function index(UserFilter $filter)
{
$collection = $filter->filter();
return $this->respondAsResource(
$collection,
['total' => $filter->foundTotal()]
);
}
/**
* Store a newly created user in the database.
*
* @param UserStoreRequest $request The user update request.
* @return \Illuminate\Http\Response
*/
public function store(UserStoreRequest $request)
{
if ($request->user()->hasPermission('admin/user') !== true) {
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.
* @return \Illuminate\Http\Response
*/
public function show(UserFilter $filter, User $user)
{
return $this->respondAsResource($filter->filter($user));
}
/**
* Update the specified resource in storage.
*
* @param UserUpdateRequest $request The user update request.
* @param User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function update(UserUpdateRequest $request, User $user)
{
$input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password'];
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
} elseif ($request->user()->is($user) !== true) {
return $this->respondForbidden();
}
$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));
}
/**
* Remove the user from the database.
*
* @param User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function destroy(User $user)
{
if ($user->hasPermission('admin/user') === false) {
return $this->respondForbidden();
}
$user->delete();
return $this->respondNoContent();
}
}

View File

@@ -0,0 +1,337 @@
<?php
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\UserForgotPasswordRequest;
use App\Http\Requests\UserForgotUsernameRequest;
use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest;
use App\Http\Requests\UserVerifyEmailRequest;
use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotUsername;
use App\Mail\ForgotPassword;
use App\Mail\EmailVerify;
use App\Models\User;
use App\Models\UserCode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends ApiController
{
/**
* ApplicationController constructor.
*/
public function __construct()
{
$this->middleware('auth:sanctum')
->except([
'index',
'show',
'register',
'exists',
'forgotPassword',
'forgotUsername',
'resetPassword',
'verifyEmail',
'resendVerifyEmailCode'
]);
}
/**
* Display a listing of the resource.
*
* @param \App\Filters\UserFilter $filter Filter object.
* @return \Illuminate\Http\Response
*/
public function index(UserFilter $filter)
{
$collection = $filter->filter();
return $this->respondAsResource(
$collection,
['total' => $filter->foundTotal()]
);
}
/**
* Store a newly created user in the database.
*
* @param UserStoreRequest $request The user update request.
* @return \Illuminate\Http\Response
*/
public function store(UserStoreRequest $request)
{
if ($request->user()->hasPermission('admin/user') !== true) {
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.
* @return \Illuminate\Http\Response
*/
public function show(UserFilter $filter, User $user)
{
return $this->respondAsResource($filter->filter($user));
}
/**
* Update the specified resource in storage.
*
* @param UserUpdateRequest $request The user update request.
* @param User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function update(UserUpdateRequest $request, User $user)
{
$input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password'];
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
} elseif ($request->user()->is($user) !== true) {
return $this->respondForbidden();
}
$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));
}
/**
* Remove the user from the database.
*
* @param User $user The specified user.
* @return \Illuminate\Http\Response
*/
public function destroy(User $user)
{
if ($user->hasPermission('admin/user') === false) {
return $this->respondForbidden();
}
$user->delete();
return $this->respondNoContent();
}
/**
* Register a new user
*
* @param UserRegisterRequest $request The register user request.
* @return \Illuminate\Http\Response
*/
public function register(UserRegisterRequest $request)
{
try {
$user = User::create([
'first_name' => $request->input('first_name'),
'last_name' => $request->input('last_name'),
'username' => $request->input('username'),
'email' => $request->input('email'),
'phone' => $request->input('phone'),
'password' => Hash::make($request->input('password'))
]);
$code = $user->codes()->create([
'action' => 'verify-email',
]);
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
return response()->json([
'message' => 'Check your email for a welcome code.'
]);
} catch (\Exception $e) {
return response()->json([
'message' => 'A server error occurred. Please try again later' . $e
], 500);
}//end try
}
/**
* Sends an email with all the usernames registered at that address
*
* @param UserForgotUsernameRequest $request The forgot username request.
* @return \Illuminate\Http\Response
*/
public function forgotUsername(UserForgotUsernameRequest $request)
{
$users = User::where('email', $request->input('email'))->whereNotNull('email_verified_at')->get();
if ($users->count() > 0) {
dispatch((new SendEmailJob(
$users->first()->email,
new ForgotUsername($users->pluck('username')->toArray())
)))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondJson(['message' => 'Username send to the email address if registered']);
}
/**
* Generates a new reset password code
*
* @param UserForgotPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function forgotPassword(UserForgotPasswordRequest $request)
{
$user = User::where('username', $request->input('username'))->first();
if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([
'action' => 'reset-password'
]);
dispatch((new SendEmailJob($user->email, new ForgotPassword($user, $code->code))))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondNotFound();
}
/**
* Resets a user password
*
* @param UserResetPasswordRequest $request The reset password request.
* @return \Illuminate\Http\Response
*/
public function resetPassword(UserResetPasswordRequest $request)
{
UserCode::clearExpired();
$code = UserCode::where('code', $request->input('code'))->where('action', 'reset-password')->first();
if ($code !== null) {
$user = $code->user()->first();
$code->delete();
$user->codes()->where('action', 'verify-email')->delete();
$user->password = Hash::make($request->input('password'));
if ($user->email_verified_at === null) {
$user->email_verified_at = now();
}
$user->save();
dispatch((new SendEmailJob($user->email, new ChangedPassword($user))))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondError([
'code' => 'The code was not found or has expired'
]);
}
/**
* Verify an email code
*
* @param UserVerifyEmailRequest $request The verify email request.
* @return \Illuminate\Http\Response
*/
public function verifyEmail(UserVerifyEmailRequest $request)
{
UserCode::clearExpired();
$code = UserCode::where('code', $request->input('code'))->where('action', 'verify-email')->first();
if ($code !== null) {
$user = $code->user()->first();
$new_email = $code->data;
if ($new_email === null) {
if ($user->email_verified_at === null) {
$user->email_verified_at = now();
}
} else {
dispatch((new SendEmailJob($user->email, new ChangedEmail($user, $user->email, $new_email))))
->onQueue('mail');
$user->email = $new_email;
$user->email_verified_at = now();
}
$code->delete();
$user->save();
return $this->respondNoContent();
}//end if
return $this->respondWithErrors([
'code' => 'The code was not found or has expired'
]);
}
/**
* Resend a new verify email
*
* @param UserResendVerifyEmailRequest $request The resend verify email request.
* @return \Illuminate\Http\Response
*/
public function resendVerifyEmail(UserResendVerifyEmailRequest $request)
{
UserCode::clearExpired();
$user = User::where('username', $request->input('username'))->first();
if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate();
$code->save();
if ($code->data === null) {
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
} else {
dispatch((new SendEmailJob($user->email, new ChangeEmailVerify($user, $code->code, $code->data))))
->onQueue('mail');
}
}
return response()->json(['message' => 'Verify email sent if user registered and required']);
}
/**
* Resend verification email
*
* @param UserResendVerifyEmailRequest $request The resend user request.
* @return \Illuminate\Http\Response
*/
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{
$user = User::where('username', $request->input('username'))->first();
if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete();
if ($user->email_verified_at === null) {
$code = $user->codes()->create([
'action' => 'verify-email'
]);
dispatch((new SendEmailJob($user->email, new EmailVerify($user, $code->code))))->onQueue('mail');
}
return $this->respondNoContent();
}
return $this->respondNotFound();
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
}

70
app/Http/Kernel.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
// \App\Http\Middleware\TrimStrings::class,
// \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \App\Http\Middleware\ForceJsonResponse::class,
'useSanctumGuard'
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'useSanctumGuard' => \App\Http\Middleware\UseSanctumGuard::class
];
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param mixed $request Request.
* @return string|null
*/
protected function redirectTo(mixed $request)
{
if ($request->expectsJson() === false) {
return route('login');
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param Request $request Request.
* @param Closure(Request): (Response|RedirectResponse) $next Next.
* @param string|null ...$guards Guards.
* @return Response|RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) === true ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check() === true) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var integer
*/
// @codingStandardsIgnoreStart
protected $headers = (Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB);
// @codingStandardsIgnoreEnd
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UseSanctumGuard
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
Auth::shouldUse('sanctum');
return $next($request);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AuthLoginRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'username' => 'required|string|min:6|max:255',
'password' => 'required|string|min:6',
];
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class BaseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
if (method_exists($this, 'postAuthorize') === true && request()->isMethod('post') === true) {
return $this->postAuthorize();
} elseif (method_exists($this, 'putAuthorize') === true && request()->isMethod('put') === true) {
return $this->putAuthorize();
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
$rules = [];
if (method_exists($this, 'baseRules') === true) {
$rules = $this->baseRules();
}
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());
}
return $rules;
}
/**
* Merge two collections of rules.
*
* @param array $collection1 The first collection of rules.
* @param array $collection2 The second collection of rules to merge.
* @return array
*/
private function mergeRules(array $collection1, array $collection2)
{
$rules = [];
foreach ($collection1 as $key => $ruleset) {
if (array_key_exists($key, $collection2) === true) {
if (is_string($collection1[$key]) === true && is_string($collection2[$key]) === true) {
$rules[$key] = $collection1[$key] . '|' . $collection2[$key];
} else {
$key_ruleset = [];
if (is_array($collection1[$key]) === true) {
$key_ruleset = $collection1[$key];
} elseif (is_string($collection1[$key]) === true) {
$key_ruleset = explode('|', $collection1[$key]);
}
if (is_array($collection2[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, $collection2[$key]);
} elseif (is_string($collection1[$key]) === true) {
$key_ruleset = array_merge($key_ruleset, explode('|', $collection1[$key]));
}
if (count($key_ruleset) > 0) {
$rules[$key] = $key_ruleset;
}
}//end if
} else {
$rules[$key] = $ruleset;
}//end if
}//end foreach
foreach ($collection2 as $key => $ruleset) {
if (array_key_exists($key, $rules) === false) {
$rules[$key] = $collection2[$key];
}
}
return $rules;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class ContactSendRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255',
'content' => 'required|max:2000',
'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
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
*
* @return array<string, mixed>
*/
public function baseRules()
{
return [
'title' => 'min:6',
'location' => [
Rule::in(['online', 'physical']),
],
'address' => 'string|nullable',
'start_at' => 'date',
'end_at' => 'date|after:start_date',
'publish_at' => 'date|nullable',
'status' => [
Rule::in(['draft', 'open', 'closed', 'cancelled']),
],
'registration_type' => [
Rule::in(['none', 'email', 'link']),
],
'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')
],
'hero' => 'uuid|exists:media,id',
];
}
/**
* Apply the additional POST base rules to this request
*
* @return array<string, mixed>
*/
protected function postRules()
{
return [
'title' => 'required',
'location' => 'required',
'address' => 'required_if:location,physical',
'start_at' => 'required',
'end_at' => 'required',
'status' => 'required',
'registration_type' => 'required',
'hero' => 'required',
];
}
}

View File

@@ -0,0 +1,20 @@
<?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

@@ -0,0 +1,20 @@
<?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,23 @@
<?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

@@ -0,0 +1,28 @@
<?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

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserForgotPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'username' => 'required|exists:users,username',
'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserForgotUsernameRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'email' => 'required|email|max:255',
'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRegisterRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'username' => 'required|string|min:4|max:255|unique:users',
'password' => 'required|string|min:8',
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserResendVerifyEmailRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'username' => 'required|exists:users,username',
'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserResetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'code' => 'required|digits:6',
'password' => 'required|string|min:8',
'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -0,0 +1,25 @@
<?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

@@ -0,0 +1,25 @@
<?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

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserVerifyEmailRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'code' => 'required|digits:6',
'captcha_token' => [new Recaptcha()],
];
}
}

57
app/Jobs/SendEmailJob.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendEmailJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Mail to receipt
*
* @var string
*/
public $to;
/**
* Mailable item
*
* @var Mailable
*/
public $mailable;
/**
* Create a new job instance.
*
* @param string $to The email receipient.
* @param Mailable $mailable The mailable.
* @return void
*/
public function __construct(string $to, Mailable $mailable)
{
$this->to = $to;
$this->mailable = $mailable;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Mail::to($this->to)->send($this->mailable);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangeEmailVerify extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The registration code.
*
* @var integer
*/
public $code;
/**
* The new email address.
*
* @var string
*/
public $new_email;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @param string $new_email The new email address.
* @return void
*/
public function __construct(User $user, int $code, string $new_email)
{
$this->user = $user;
$this->code = $code;
$this->new_email = $new_email;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '👋🏻 Lets change your email!',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.change_email_verify',
text: 'emails.user.change_email_verify_plain',
);
}
}

79
app/Mail/ChangedEmail.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangedEmail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The old email.
*
* @var string
*/
public $old_email;
/**
* The new email.
*
* @var string
*/
public $new_email;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param string $old_email The previous email address.
* @param string $new_email The new email address.
* @return void
*/
public function __construct(User $user, string $old_email, string $new_email)
{
$this->user = $user;
$this->old_email = $old_email;
$this->new_email = $new_email;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '👍 Your email has been changed!',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.changed_email',
text: 'emails.user.changed_email_plain',
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ChangedPassword extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '👍 Your password has been changed!',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.changed_password',
text: 'emails.user.changed_password_plain',
);
}
}

78
app/Mail/Contact.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class Contact extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The contact name.
*
* @var string
*/
public $name;
/**
* The contact email.
*
* @var string
*/
public $email;
/**
* The contact content.
*
* @var string
*/
public $content;
/**
* Create a new message instance.
*
* @param string $name The contact name.
* @param string $email The contact email.
* @param string $content The contact content.
* @return void
*/
public function __construct(string $name, string $email, string $content)
{
$this->name = $name;
$this->email = $email;
$this->content = $content;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: config('contact.contact_subject'),
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.contact',
text: 'emails.user.contact_plain',
);
}
}

70
app/Mail/EmailVerify.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class EmailVerify extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The registration code.
*
* @var integer
*/
public $code;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @return void
*/
public function __construct(User $user, int $code)
{
$this->user = $user;
$this->code = $code;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '👋🏻 Welcome to STEMMechanics!',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.email_verify',
text: 'emails.user.email_verify_plain',
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ForgotPassword extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The user
*
* @var \App\Models\User
*/
public $user;
/**
* The reset code
*
* @var integer
*/
public $code;
/**
* Create a new message instance.
*
* @param User $user The user the email applies to.
* @param integer $code The action code.
* @return void
*/
public function __construct(User $user, int $code)
{
$this->user = $user;
$this->code = $code;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '🤦 Forgot your password?',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.forgot_password',
text: 'emails.user.forgot_password_plain',
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ForgotUsername extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The list of usernames
*
* @var string[]
*/
public $usernames;
/**
* Create a new message instance.
*
* @param array $usernames The usernames.
* @return void
*/
public function __construct(array $usernames)
{
$this->usernames = $usernames;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '🤦 Forgot your username?',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.forgot_username',
text: 'emails.user.forgot_username_plain',
);
}
}

32
app/Models/Event.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'location',
'address',
'start_at',
'end_at',
'publish_at',
'status',
'registration_type',
'registration_data',
'hero',
'content'
];
}

276
app/Models/Media.php Normal file
View File

@@ -0,0 +1,276 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class Media extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'name',
'mime',
'user_id',
'size',
'permission'
];
/**
* The attributes that are hidden.
*
* @var array<int, string>
*/
protected $hidden = [
'path',
];
/**
* The attributes that are appended.
*
* @var array<string>
*/
protected $appends = [
'url',
];
/**
* Model Boot
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::updating(function ($media) {
if (array_key_exists('permission', $media->getChanges()) === true) {
$origPermission = $media->getOriginal()['permission'];
$newPermission = $media->permission;
$origPath = Storage::disk(Media::getStorageId(empty($origPermission)))->path($media->name);
$newPath = Storage::disk(Media::getStorageId(empty($newPermission)))->path($media->name);
if ($origPath !== $newPath) {
if (file_exists($origPath) === true) {
if (file_exists($newPath) === true) {
$fileParts = pathinfo($newPath);
$newName = '';
// need a new name!
$tmpPath = $newPath;
while (file_exists($tmpPath) === true) {
$newName = uniqid('', true) . $fileParts['extension'];
$tmpPath = $fileParts['dirname'] . '/' . $newName;
}
$media->name = $newName;
}
rename($origPath, $newPath);
}//end if
}//end if
}//end if
});
}
/**
* Return the file URL
*
* @return string
*/
public function getUrlAttribute()
{
$url = config('filesystems.disks.' . Media::getStorageId($this) . '.url');
if (empty($url) === false) {
$replace = [
'id' => $this->id,
'name' => $this->name
];
$url = str_ireplace(array_map(function ($item) {
return '%' . $item . '%';
}, array_keys($replace)), array_values($replace), $url);
return $url;
}//end if
return '';
}
/**
* Return the file owner
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get the file full local path
*
* @return string
*/
public function path()
{
return Storage::disk(Media::getStorageId($this))->path($this->name);
}
/**
* Get Storage ID
*
* @param mixed $mediaOrPublic Media object or if file is public.
* @return string
*/
public static function getStorageId(mixed $mediaOrPublic)
{
$isPublic = true;
if ($mediaOrPublic instanceof Media) {
$isPublic = empty($mediaOrPublic->permission);
} else {
$isPublic = boolval($mediaOrPublic);
}
return $isPublic === true ? 'public' : 'local';
}
/**
* Place uploaded file into storage. Return full path or null
*
* @param UploadedFile $file File to put into storage.
* @param boolean $public Is the file available to the public.
* @return array|null
*/
public static function store(UploadedFile $file, bool $public = true)
{
$storage = Media::getStorageId($public);
$name = $file->store('', ['disk' => $storage]);
if ($name === false) {
return null;
}
$path = Storage::disk($storage)->path($name);
return [
'name' => $name,
'path' => $path
];
}
/**
* Get the server maximum upload size
*
* @return integer
*/
public static function maxUploadSize()
{
$sizes = [
ini_get('upload_max_filesize'),
ini_get('post_max_size'),
ini_get('memory_limit')
];
foreach ($sizes as &$size) {
$size = trim($size);
$last = strtolower($size[(strlen($size) - 1)]);
switch ($last) {
case 'g':
$size = (intval($size) * 1024);
// Size is in MB - fallthrough
case 'm':
$size = (intval($size) * 1024);
// Size is in KB - fallthrough
case 'k':
$size = (intval($size) * 1024);
// Size is in B - fallthrough
}
}
return min($sizes);
}
/**
* Sanitize filename for upload
*
* @param string $filename Filename to sanitize.
* @return string
*/
public static function sanitizeFilename(string $filename)
{
/*
# file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
[<>:"/\\\|?*]|
# control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x00-\x1F]|
# non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[\x7F\xA0\xAD]|
# URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2
[#\[\]@!$&\'()+,;=]|
# URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
[{}^\~`]
*/
$filename = preg_replace(
'~
[<>:"/\\\|?*]|
[\x00-\x1F]|
[\x7F\xA0\xAD]|
[#\[\]@!$&\'()+,;=]|
[{}^\~`]
~x',
'-',
$filename
);
$filename = ltrim($filename, '.-');
$filename = preg_replace([
// "file name.zip" becomes "file-name.zip"
'/ +/',
// "file___name.zip" becomes "file-name.zip"
'/_+/',
// "file---name.zip" becomes "file-name.zip"
'/-+/'
], '-', $filename);
$filename = preg_replace([
// "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/',
// "file...name..zip" becomes "file.name.zip"
'/\.{2,}/'
], '.', $filename);
// lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
// ".file-name.-" becomes "file-name"
$filename = trim($filename, '.-');
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$filename = mb_strcut(
pathinfo($filename, PATHINFO_FILENAME),
0,
(255 - ($ext !== '' ? strlen($ext) + 1 : 0)),
mb_detect_encoding($filename)
) . ($ext !== '' ? '.' . $ext : '');
return $filename;
}
}

34
app/Models/Permission.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Permission extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'permission',
'user',
];
/**
* Get the User associated with this model
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

38
app/Models/Post.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'slug',
'publish_at',
'content',
'user_id',
'hero'
];
/**
* Get the file user
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Subscription extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'email',
];
}

145
app/Models/User.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use OwenIt\Auditing\Contracts\Auditable;
class User extends Authenticatable implements Auditable
{
use HasApiTokens;
use HasFactory;
use Notifiable;
use Uuids;
use \OwenIt\Auditing\Auditable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'username',
'first_name',
'last_name',
'email',
'phone',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'permissions'
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
// protected $hidden = [
// 'permissions'
// ];
/**
* The attributes to append.
*
* @var string[]
*/
protected $appends = [
'permissions'
];
// public function getPermissionsAttribute() {
// return $this->permissions()->pluck('permission')->toArray();
// }
/**
* Get the list of files of the user
*
* @return HasMany
*/
public function permissions()
{
return $this->hasMany(Permission::class);
}
/**
* Get the permission attribute
*
* @return array
*/
public function getPermissionsAttribute()
{
return $this->permissions()->pluck('permission')->toArray();
}
/**
* Test if user has permission
*
* @param string $permission Permission to test.
* @return boolean
*/
public function hasPermission(string $permission)
{
return ($this->permissions()->where('permission', $permission)->first() !== null);
}
/**
* Get the list of files of the user
*
* @return HasMany
*/
public function media()
{
return $this->hasMany(Media::class);
}
/**
* Get the list of files of the user
*
* @return HasMany
*/
public function posts()
{
return $this->hasMany(Post::class);
}
/**
* Get associated user codes
*
* @return HasMany
*/
public function codes()
{
return $this->hasMany(UserCode::class);
}
/**
* Get the list of logins of the user
*
* @return HasMany
*/
public function logins()
{
return $this->hasMany(UserLogins::class);
}
}

82
app/Models/UserCode.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserCode extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'action',
'user_id',
'data',
];
/**
* Boot function from Laravel.
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
UserCode::clearExpired();
if (empty($model->{'code'}) === true) {
while (true) {
$code = random_int(100000, 999999);
if (UserCode::where('code', $code)->count() === 0) {
$model->{'code'} = $code;
break;
}
}
}
});
}
/**
* Generate new code
*
* @return void
*/
public function regenerate()
{
while (true) {
$code = random_int(100000, 999999);
if (UserCode::where('code', $code)->count() === 0) {
$this->code = $code;
break;
}
}
}
/**
* Clear expired user codes
*
* @return void
*/
public static function clearExpired()
{
UserCode::where('updated_at', '<=', now()->subDays(5))->delete();
}
/**
* Get associated user
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

38
app/Models/UserLogins.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserLogins extends Model
{
use HasFactory;
use Uuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'token',
'login',
'logout',
'ip_address',
'user_agent',
];
/**
* Get the file user
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Exception;
use Illuminate\Support\Facades\DB;
use PDOException;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
Queue::after(function (JobProcessed $event) {
// Log::info($event->connectionName);
// Log::info('ID: ' . $event->job->getJobId());
// Log::info('Attempts: ' . $event->job->attempts());
// Log::info('Name: ' . $event->job->getName());
// Log::info('ResolveNAme: ' . $event->job->resolveName());
// Log::info('Queue: ' . $event->job->getQueue());
// Log::info('Body: ' . $event->job->getRawBody());
// Log::info(print_r($event->job->payload(), true));
// $payload = $event->job->payload();
// $data = unserialize($payload['data']['command']);
// Log::info('MAIL: ' . $data->to);
// Log::info('MAIL: ' . get_class($data->mailable));
});
}
/**
* Determine if events and listeners should be automatically discovered.
*
* @return boolean
*/
public function shouldDiscoverEvents()
{
return false;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*
* @return void
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id !== null ?: $request->ip());
});
}
}

52
app/Rules/Recaptcha.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
namespace App\Rules;
use Illuminate\Support\Facades\Http;
use Illuminate\Contracts\Validation\Rule;
class Recaptcha implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param mixed $attribute Attribute name.
* @param mixed $value Attribute value.
* @return boolean
*/
public function passes(mixed $attribute, mixed $value)
{
$endpoint = config('services.google_recaptcha');
$response = Http::asForm()->post($endpoint['url'], [
'secret' => $endpoint['secret_key'],
'response' => $value,
])->json();
if ($response['success'] === true && $response['score'] > 0.5) {
return true;
}
return false;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'Captcha failed. Refresh the page and try again';
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Services;
use ImageIntervention;
class ImageService
{
}

42
app/Traits/Uuids.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait Uuids
{
/**
* Boot function from Laravel.
*
* @return void
*/
protected static function bootUuids()
{
static::creating(function ($model) {
if (empty($model->{$model->getKeyName()}) === true) {
$model->{$model->getKeyName()} = Str::uuid()->toString();
}
});
}
/**
* Get the value indicating whether the IDs are incrementing.
*
* @return boolean
*/
public function getIncrementing()
{
return false;
}
/**
* Get the auto-incrementing key type.
*
* @return string
*/
public function getKeyType()
{
return 'string';
}
}