Merge branch 'develop' into trunk

This commit is contained in:
Jérémy Munsch 2021-07-27 12:13:25 +02:00
commit 05da11e9ed
148 changed files with 10722 additions and 2937 deletions

View file

@ -3,7 +3,7 @@
namespace App\Console\Commands;
use App\Enums\Role;
use App\Models\Project;
use App\Models\Site;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
class CreateUser extends Command
{
/**
* The name and signature of the console command.
*
@ -25,16 +26,6 @@ class CreateUser extends Command
*/
protected $description = 'Create a user';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@ -52,16 +43,6 @@ class CreateUser extends Command
$user->role = Role::admin();
} else {
$user->role = Role::client();
try {
$project = Project::where(
'id',
$this->ask("What's the id of the project (s)he's working on?")
)->firstOrFail();
$user->projects()->sync([$project->id]);
} catch (ModelNotFoundException $e) {
$this->error('The project does not exists');
return -1;
}
}
$user->password = Hash::make($pwd);
$user->save();

View file

@ -7,14 +7,13 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
protected $commands = [];
/**
* Define the application's command schedule.
@ -24,7 +23,6 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
}
/**
@ -34,8 +32,7 @@ class Kernel extends ConsoleKernel
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}

View file

@ -1,49 +0,0 @@
<?php
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
/**
*
* https://laravel.com/docs/8.x/authorization
* Controller Method Policy Method
* index viewAny
* show view
* create create
* store create
* edit update
* update update
* destroy delete
*
* @method static self viewAny()
* @method static self view()
* @method static self create()
* @method static self update()
* @method static self delete()
*/
class CrudAction extends BaseEnum
{
protected static function values(): array
{
return [
'viewAny' => 0,
'view' => 1,
'create' => 2,
'update' => 3,
'delete' => 4,
];
}
protected static function labels(): array
{
return [
'viewAny' => trans('Liste'),
'view' => trans('Voir'),
'create' => trans('Créer'),
'update' => trans('Éditer'),
'delete' => trans('Supprimer'),
];
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
/**
* @method static self boolean()
* @method static self int()
* @method static self string()
* @method static self text()
* @method static self json()
* @method static self image()
* @method static self email()
* @method static self password()
* @method static self float()
* @method static self enum()
* @method static self belongsTo()
* @method static self belongsToMany()
*/
class CrudType extends BaseEnum
{
protected static function values(): array
{
return [
'boolean' => 0,
'int' => 1,
'string' => 2,
'text' => 3,
'json' => 4,
'image' => 5,
'email' => 6,
'password' => 7,
'float' => 8,
'enum' => 9,
'belongsTo' => 10,
'belongsToMany' => 11
];
}
protected static function labels(): array
{
return [
'boolean' => trans('Booléen'),
'int' => trans('Nombre'),
'string' => trans('Chaîne de charactères simple'),
'text' => trans('Texte'),
'json' => trans('Json'),
'image' => trans('Image'),
'email' => trans('Chaîne de charactères email'),
'password' => trans('Mot de passe'),
'float' => trans('Nombre à virgule flotante'),
'enum' => trans('Énumération'),
'belongsTo' => trans('Clé étrangère'),
'belongsToMany' => trans('Relation Belongs To Many')
];
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
/**
* @method static self collectionNeeds()
* @method static self specifications()
* @method static self quotation()
* @method static self tests()
* @method static self informations()
* @method static self feedback()
*/
class FileCategory extends BaseEnum
{
protected static function values(): array
{
return [
'collectionNeeds' => 0,
'specifications' => 1,
'quotation' => 2,
'tests' => 3,
'informations' => 4,
'feedback' => 5
];
}
protected static function labels(): array
{
return [
'collectionNeeds' => trans('Recueil des besoins'),
'specifications' => trans('Cahier des charges'),
'quotation' => trans('Devis'),
'tests' => trans('Recettage'),
'informations' => trans('Informations'),
'feedback' => trans('Retours client'),
];
}
}

29
app/Enums/FileType.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
/**
* @method static self stringlink()
* @method static self binfile()
*/
class FileType extends BaseEnum
{
protected static function values(): array
{
return [
'stringlink' => 0,
'binfile' => 1
];
}
protected static function labels(): array
{
return [
'stringlink' => trans('Lien uri'),
'binfile' => trans('Fichier hebergé')
];
}
}

View file

@ -5,6 +5,7 @@ namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
/**
* @method static self developper()
* @method static self admin()
* @method static self client()
*/
@ -14,6 +15,7 @@ class Role extends BaseEnum
protected static function values(): array
{
return [
'developper' => 0,
'admin' => 1,
'client' => 2
];
@ -22,6 +24,7 @@ class Role extends BaseEnum
protected static function labels(): array
{
return [
'developper' => trans('Developpeur'),
'admin' => trans('Administrateur'),
'client' => trans('Client')
];

View file

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Models\File;
use App\Models\User;
class FileCreated extends Loggable
{
/** @var File */
public $file;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(File $file, User $user)
{
parent::__construct($user);
$this->file = $file;
$this->message = trans(':username à crée le fichier :filename', [
'username' => $user->name,
'filename' => $file->name
]);
}
}

31
app/Events/Loggable.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
abstract class Loggable
{
use Dispatchable;
use SerializesModels;
/** @var User */
public $user;
public $message = '';
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
$this->message = trans(':username did someting', [
'username' => $user->name
]);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Models\Project;
use App\Models\User;
class ProjectUpdated extends Loggable
{
/** @var Project */
public $project;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Project $project, User $user)
{
parent::__construct($user);
$this->project = $project;
$this->message = trans(':username à mis à jour projet :projectname', [
'username' => $user->name,
'projectname' => $project->name
]);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Models\Site;
use App\Models\User;
class SiteUpdated extends Loggable
{
/** @var Site */
public $site;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Site $site, User $user)
{
parent::__construct($user);
$this->site = $site;
$this->message = trans(':username à mis à jour site :sitename', [
'username' => $user->name,
'sitename' => $site->name
]);
}
}

View file

@ -1,9 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class CrudException extends Exception
{
}

View file

@ -9,5 +9,7 @@ use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
}

View file

@ -1,262 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Enums\CrudType;
use App\Exceptions\CrudException;
use App\Http\Requests\CrudIndexRequest;
use App\Http\Requests\StoreCrudModel;
use App\Http\Requests\UpdateCrudModel;
use App\Http\Resources\CrudResource;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
class CrudController extends Controller
{
/** @var \Illuminate\Database\Eloquent\Model */
protected static $modelClass = null;
// Eloquent callbacks
public function indexQuery(Builder $query): Builder
{
return $query;
}
public function storeModel(Model &$model)
{
}
public function storedModel(Model &$model)
{
}
public function updateModel(Model &$model)
{
}
public function updatedModel(Model &$model)
{
}
public function __construct()
{
// AUTH CONTROL TO ROOT POLICY
$this->authorizeResource(static::$modelClass);
$modelClass = static::$modelClass;
if (is_null($modelClass) or !class_exists($modelClass)) {
throw new CrudException(sprintf(
'%s needs the modelClass prop to be a class, %s %s given',
__CLASS__,
gettype($modelClass),
$modelClass
));
}
if (!is_subclass_of($modelClass, Model::class)) {
throw new CrudException(sprintf(
'%s needs the model prop to be a class the extends %s',
__CLASS__,
Model::class
));
}
if (!request()->ajax() and !app()->runningInConsole()) {
self::shareCrudModelsClassesToView();
$this->shareModelClassToView();
$this->shareModelHasImageToView();
$this->shareModelTableToView();
$this->shareModelPropsToView();
}
}
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\Database\Eloquent\Builder */
$models = static::$modelClass::query();
// Search on crud Label column
if ($request->search) {
$model = static::$modelClass;
$col = (new $model())->crudLabelColumn;
$models = $models->where($col, 'LIKE', "%$request->search%");
}
// Sort columns
$tableName = (new static::$modelClass())->getTable();
if ($request->rst) {
Session::remove("crud.$tableName.sort_col");
Session::remove("crud.$tableName.sort_way");
}
if ($request->sort_col) {
Session::put("crud.$tableName.sort_col", $request->sort_col);
Session::save();
}
if ($request->sort_way) {
Session::put("crud.$tableName.sort_way", $request->sort_way);
Session::save();
}
if (
Session::has("crud.$tableName.sort_col") and
Session::has("crud.$tableName.sort_way") and
Schema::hasColumn($tableName, Session::get("crud.$tableName.sort_col"))
) {
$models = $models->orderBy(
Session::get("crud.$tableName.sort_col"),
Session::get("crud.$tableName.sort_way")
);
}
$models = $this->indexQuery($models);
/** @var \Illuminate\Pagination\AbstractPaginator */
$models = $models->paginate(config('crud.paginate', 15));
if ($request->ajax()) {
return response()->json(CrudResource::collection($models));
} else {
$models = $models->withQueryString();
return view('crud.index', compact('models'));
}
}
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
public function show(Model $model)
{
return view('crud.show', compact('model'));
}
public function create()
{
return view('crud.create');
}
public function store(StoreCrudModel $request)
{
$modelClass = static::$modelClass;
$model = new $modelClass();
$model->fill($request->validated());
$this->storeModel($model);
$model->saveOrFail();
$model->saveRelations($request);
$this->storedModel($model);
return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model)
->with('success', trans(':model a bien été enregisté.', ['model' => $model->getModelName()]));
}
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
public function edit(Model $model)
{
return view('crud.edit', compact('model'));
}
public function update(UpdateCrudModel $request, Model $model)
{
$model->fill($request->validated());
$this->updateModel($model);
$model->saveOrFail();
$model->saveRelations($request);
$this->updatedModel($model);
return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model)
->with('success', trans(':model a bien été enregisté.', ['model' => $model->getModelName()]));
}
public function destroy(Model $model)
{
if (
!in_array(SoftDeletes::class, class_uses_recursive($model), true) and
$restrict = $model->gotHasManyRelationWithRestrictOnDelete()
) {
return redirect()->back()
->with('error', trans(
':model n\'as pas pu être supprimé car il est encore associé avec des :associated',
[
'model' => $model->getModelName(),
'associated' => Str::plural($restrict)
]
));
}
if ($model->delete()) {
return \redirect()->route(sprintf('%s.index', $model->getTable()))
->with('warning', trans(':model a bien été supprimé.', ['model' => $model->getModelName()]));
} else {
return redirect()->back()
->with('error', trans(':model n\'as pas pu être supprimé', ['model' => $model->getModelName()]));
}
}
public function getModelClass()
{
return static::$modelClass;
}
/**
* Get the current model class
*
* @return void
*/
public static function shareCrudModelsClassesToView(): void
{
$models = [];
foreach (File::allFiles(\app_path('Http/Controllers')) as $file) {
$class = sprintf('App\Http\Controllers\%s', \substr($file->getFilename(), 0, -4));
if (is_subclass_of($class, CrudController::class) and $class::$modelClass) {
$models[] = $class::$modelClass;
}
}
View::share('modelsClasses', $models);
}
/**
* Get the current model class
*
* @return void
*/
private function shareModelClassToView(): void
{
View::share('modelClass', static::$modelClass);
}
/**
* Get the current model class
*
* @return void
*/
private function shareModelHasImageToView(): void
{
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = static::$modelClass;
View::share('hasImage', (bool)collect((new $model())->getEditableProperties())->filter(function ($prop) {
return $prop['type']->equals(CrudType::image());
})->count());
}
/**
* Get the current model table
*
* @return void
*/
private function shareModelTableToView(): void
{
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = static::$modelClass;
View::share('modelTable', (new $model())->getTable());
}
/**
* Get the current model properties
*
* @return void
*/
private function shareModelPropsToView(): void
{
$model = static::$modelClass;
View::share('props', (new $model())->getEditableProperties());
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use App\Models\File;
use Illuminate\Database\Eloquent\Builder;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class FileController extends Controller
{
use CrudController {
index as crudIndex;
create as crudCreate;
}
/** @var File */
public static $modelClass = File::class;
public static $layout = 'layout.app';
public function indexQuery(Builder $query): Builder
{
$parent = request()->site ?? request()->project;
if ($parent) {
$query->whereHasMorph('parent', [get_class($parent)], function ($q) use ($parent) {
return $q->whereId($parent->id);
});
}
return $query;
}
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('files.index', $view->getData());
}
public function create(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudCreate($request);
return view('files.create', $view->getData());
}
public function storeModel(File &$file)
{
$file->parent()->associate(request()->site ?? request()->project);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers;
use App\Models\Log;
use Illuminate\Database\Eloquent\Builder;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class LogController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Log */
public static $modelClass = Log::class;
public static $layout = 'layout.app';
public function indexQuery(Builder $query): Builder
{
$parent = request()->site ?? request()->project;
if ($parent) {
$query->whereHasMorph('parent', [get_class($parent)], function ($q) use ($parent) {
return $q->whereId($parent->id);
});
}
return $query;
}
public function index(CrudIndexRequest $request)
{
// Autosort
if (!$request->sort_col and !$request->sort_way) {
$request->merge([
'sort_col' => 'created_at',
'sort_way' => 'desc',
]);
}
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('logs.index', $view->getData());
}
public function storeModel(Log &$log)
{
$log->parent()->associate(request()->site ?? request()->project);
}
}

View file

@ -7,19 +7,27 @@ use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function index() {
public function index()
{
return view('login');
}
public function authenticate(Request $request) {
if (Auth::attempt(["email" => $request->email, "password" => $request->password], $request->remember !== null ? true : false)) {
public function authenticate(Request $request)
{
if (
Auth::attempt([
"email" => $request->email, "password" => $request->password
], $request->remember !== null ? true : false)
) {
return redirect()->route('home');
} else {
return back()->withInput($request->except("password"))->withErrors("Les identifiants entrés sont incorrects, veuillez réessayer.");
return back()->withInput($request->except("password"))
->withErrors("Les identifiants entrés sont incorrects, veuillez réessayer.");
}
}
public function logout() {
public function logout()
{
Auth::logout();
return redirect()->route('home');
}

View file

@ -3,10 +3,27 @@
namespace App\Http\Controllers;
use App\Models\Project;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class ProjectController extends CrudController
class ProjectController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Project */
protected static $modelClass = Project::class;
public static $modelClass = Project::class;
public static $layout = 'layout.app';
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('projects.index', $view->getData());
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Site;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class SiteController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Site */
public static $modelClass = Site::class;
public static $layout = 'layout.app';
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('sites.index', $view->getData());
}
}

View file

@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use App\Enums\Role;
use App\Enums\TicketStatus;
use App\Enums\TicketType;
use App\Http\Requests\ChangeTicketStatus;
@ -12,7 +11,7 @@ use App\Http\Requests\StoreTicket;
use App\Http\Requests\UpdateTicket;
use App\Mail\NewTicket;
use App\Models\Message;
use App\Models\Project;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
use Exception;
@ -35,9 +34,9 @@ class TicketController extends Controller
{
/** @var \App\Models\User */
$user = auth()->user();
$tickets = Ticket::with('project')->has('project');
if (!$user->role->equals(Role::admin())) {
$tickets = $tickets->whereHas('project', function ($q) use ($user) {
$tickets = Ticket::with('site')->has('site');
if (!$user->isAdmin()) {
$tickets = $tickets->whereHas('site', function ($q) use ($user) {
$q->whereHas('users', function ($q) use ($user) {
$q->whereId($user->id);
});
@ -50,9 +49,9 @@ class TicketController extends Controller
$tickets = $this->filterTickets($tickets, $request);
$tickets = $tickets->paginate();
$projects = Project::select('id', 'name')->orderBy('name')->get();
$sites = Site::select('id', 'name')->orderBy('name')->get();
return view('ticket.index', compact('tickets', 'projects'));
return view('ticket.index', compact('tickets', 'sites'));
}
private function filterTickets(Builder $tickets, Request $request): Builder
@ -77,14 +76,14 @@ class TicketController extends Controller
Session::save();
if (Session::has('tickets.pr')) {
$tickets = $tickets->where('project_id', Session::get('tickets.pr'));
$tickets = $tickets->where('site_id', Session::get('tickets.pr'));
}
if (
Session::has('tickets.sort_col') and
Session::has('tickets.sort_way') and
in_array(Session::get('tickets.sort_col'), [
'id', 'project_id', 'created_at', 'created_by',
'id', 'site_id', 'created_at', 'created_by',
'type', 'status'
])
) {
@ -137,31 +136,32 @@ class TicketController extends Controller
public function create()
{
$user = Auth::user();
if ($user->role->value === 1) {
$projects = Project::orderBy('name')->get();
/** @var \App\Models\User */
$user = auth()->user();
if ($user->isAdmin()) {
$sites = Site::orderBy('name')->get();
} else {
$projects = $user->projects;
$sites = $user->sites;
}
if (!count($projects)) {
return redirect()->route('project.create')
->with('warning', 'Vous devez tout d\'abord créer un projet pour y ajouter un ticket');
if (!count($sites)) {
return redirect()->route('sites.create')
->with('warning', 'Vous devez tout d\'abord créer un site pour y ajouter un ticket');
}
return view('ticket.create', compact('projects'));
return view('ticket.create', compact('sites'));
}
public function store(StoreTicket $request)
{
$project = Project::findOrFail($request->project_id);
$site = Site::findOrFail($request->site_id);
$ticket = new Ticket($request->validated());
if ($request->file_ticket) {
$ticket->file = Ticket::saveFile($project, $ticket, $ticket->type, $request->file_ticket);
$ticket->file = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket);
}
if ($request->file_ticket2) {
$ticket->file2 = Ticket::saveFile($project, $ticket, $ticket->type, $request->file_ticket2);
$ticket->file2 = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket2);
}
if ($request->type === TicketType::evolution()) {
@ -188,29 +188,29 @@ class TicketController extends Controller
{
$user = Auth::user();
if ($user->role->value === 1) {
$projects = Project::orderBy('name')->get();
$sites = Site::orderBy('name')->get();
} else {
$projects = $user->projects;
$sites = $user->sites;
}
return view('ticket.edit', compact('ticket', 'projects'));
return view('ticket.edit', compact('ticket', 'sites'));
}
public function update(UpdateTicket $request, Ticket $ticket)
{
$project = Project::findOrFail($request->project_id);
$site = Site::findOrFail($request->site_id);
$ticket->fill($request->validated());
if ($request->file_ticket) {
if (File::exists($ticket->file)) {
File::delete($ticket->file);
}
$ticket->file = Ticket::saveFile($project, $ticket, $ticket->type, $request->file_ticket);
$ticket->file = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket);
}
if ($request->file_ticket2) {
if (File::exists($ticket->file2)) {
File::delete($ticket->file2);
}
$ticket->file2 = Ticket::saveFile($project, $ticket, $ticket->type, $request->file_ticket2);
$ticket->file2 = Ticket::saveFile($site, $ticket, $ticket->type, $request->file_ticket2);
}
$ticket->save();
@ -247,11 +247,12 @@ class TicketController extends Controller
}
if ($request->file_message) {
$projectSlug = Str::slug($ticket->project->name);
$siteSlug = Str::slug($ticket->site->name);
$fileMessage = $request->file_message;
$fileName = $projectSlug . '-ticket-' . $ticket->id . '-message-' . $message->id . '.' . $fileMessage->extension();
$fileMessage->move(public_path('fichiers/' . $projectSlug . '/' . $ticket->id . '/'), $fileName);
$filePath = 'fichiers/' . $projectSlug . '/' . $ticket->id . '/' . $fileName;
$fileName = $siteSlug . '-ticket-' . $ticket->id .
'-message-' . $message->id . '.' . $fileMessage->extension();
$fileMessage->move(public_path('fichiers/' . $siteSlug . '/' . $ticket->id . '/'), $fileName);
$filePath = 'fichiers/' . $siteSlug . '/' . $ticket->id . '/' . $fileName;
$message->file = $filePath;
}

View file

@ -4,12 +4,16 @@ namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class UserController extends CrudController
class UserController extends Controller
{
use CrudController;
/** @var User */
protected static $modelClass = User::class;
public static $modelClass = User::class;
public static $layout = 'layout.app';
public function storedModel(Model &$model)
{

View file

@ -6,6 +6,7 @@ use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
@ -15,7 +16,6 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
\Illuminate\Session\Middleware\StartSession::class,
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
@ -33,7 +33,6 @@ class Kernel extends HttpKernel
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,

View file

@ -6,6 +6,7 @@ use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*

View file

@ -5,7 +5,6 @@ namespace App\Http\Middleware;
use App\Enums\Role;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class IsAdmin
{
@ -18,7 +17,9 @@ class IsAdmin
*/
public function handle(Request $request, Closure $next)
{
if (Auth::user()->role->equals(Role::admin())) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user->isAdmin()) {
return $next($request);
}
return redirect()->route('home');

View file

@ -6,6 +6,7 @@ use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Mi
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*

View file

@ -6,6 +6,7 @@ use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*

View file

@ -7,6 +7,7 @@ use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*

View file

@ -6,6 +6,7 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*

View file

@ -15,8 +15,9 @@ class ChangeTicketStatus extends FormRequest
*/
public function authorize()
{
return auth()->check() and
auth()->user()->role->equals(Role::admin());
/** @var \App\Models\User */
$user = auth()->user();
return auth()->check() and $user->isAdmin();
}
/**

View file

@ -14,11 +14,13 @@ class CreateMessage extends FormRequest
*/
public function authorize()
{
/** @var \App\Models\User */
$user = auth()->user();
$this->merge([
'subscribe' => $this->subscribe ? true : false
]);
return auth()->check() and (auth()->user()->role->equals(Role::admin()) or
auth()->user()->projects->contains(request()->route()->ticket->project_id));
return auth()->check() and ($user->isAdmin() or
auth()->user()->sites->contains(request()->route()->ticket->site_id));
}
/**
@ -28,9 +30,11 @@ class CreateMessage extends FormRequest
*/
public function rules()
{
/** @var \App\Models\User */
$user = auth()->user();
return [
'subscribe' => 'required|boolean',
'message' => 'required|min:' . (auth()->user()->role->equals(Role::admin()) ? '5' : '20'),
'message' => 'required|min:' . ($user->isAdmin() ? '5' : '20'),
'file_message' => 'sometimes|mimes:jpeg,jpg,png,pdf|max:2048'
];
}

View file

@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CrudIndexRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'search' => 'nullable|string'
];
}
}

View file

@ -1,90 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\CrudAction;
use App\Enums\CrudType;
use App\Exceptions\CrudException;
use App\Http\Controllers\CrudController;
use Illuminate\Support\Str;
use Illuminate\Foundation\Http\FormRequest;
class CrudRequest extends FormRequest
{
/** @var array */
protected $rules = [];
/**
* Get rules for a specific action
*
* @param CrudAction $action
* @return array
*/
public function getRulesForAction(CrudAction $action): array
{
$model = $this->getCurrentModel();
$props = $this->getCurrentModel()::getEditableProperties();
$propsRules = [];
foreach ($this->getCurrentModel()::getRules() as $k => $rules) {
$propsRules[$k] = [];
$propIsAllowedOnUpdateAction = in_array($action, $props[$k]['actions']);
foreach ($rules as $rule) {
// dont copy required if prop is not allowed on update
if (!$propIsAllowedOnUpdateAction and is_string($rule) and $rule == 'required') {
continue;
}
if (!in_array($rule, $propsRules[$k])) {
$propsRules[$k][] = $rule;
}
// handle password
if (
$props[$k]['type']->equals(CrudType::password()) and
!$this->{$k} or (is_string($this->{$k}) and !\strlen($this->{$k}))
) {
$this->offsetUnset($k);
}
// handle boolean
if ($props[$k]['type']->equals(CrudType::boolean())) {
$this->merge([$k => $this->{$k} ? 1 : 0]);
}
// handle image
if ($props[$k]['type']->equals(CrudType::image())) {
$tableName = Str::singular((new $model())->getTable());
if ($this->{$tableName}) {
$i = array_search('required', $propsRules[$k]);
unset($propsRules[$k][$i]);
}
}
// handle belongsToMany
if ($props[$k]['type']->equals(CrudType::belongsToMany())) {
$propsRules["$k.*"][0] = 'exists:' .
(new $props[$k]['belongsToMany']())->getTable() . ',id';
if (!isset($this->{$k})) {
$this->merge([$k => []]);
}
}
}
}
return $propsRules;
}
/**
* Returns the first parameter that is a crud model
*
* @return string \Illuminate\Database\Eloquent\Model class name
* @throws \App\Exceptions\CrudException if the request does not have a crud model parameter
*/
protected function getCurrentModel(): string
{
if (
!is_object($this->route()->controller) or
!$this->route()->controller instanceof CrudController
) {
throw new CrudException(sprintf(
'Request `%s` needs to be bound on route that use a CrudController',
get_class($this)
));
}
return $this->route()->controller->getModelClass();
}
}

View file

@ -38,7 +38,7 @@ class SortTickets extends FormRequest
}
}
],
'pr' => 'sometimes|int|exists:projects,id'
'pr' => 'sometimes|int|exists:sites,id'
];
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\CrudAction;
class StoreCrudModel extends CrudRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$model = $this->getCurrentModel();
/** @var \App\Models\User|null $user */
$user = auth()->user();
$authorized = auth()->check() and $user->can('create', $model);
if ($authorized) {
$this->rules = $this->getRulesForAction(CrudAction::create());
}
return $authorized;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return $this->rules;
}
}

View file

@ -2,10 +2,9 @@
namespace App\Http\Requests;
use App\Enums\Role;
use Illuminate\Foundation\Http\FormRequest;
class StoreProject extends FormRequest
class StoreSite extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
@ -14,8 +13,9 @@ class StoreProject extends FormRequest
*/
public function authorize()
{
return auth()->check() and
auth()->user()->role->equals(Role::admin());
/** @var \App\Models\User */
$user = auth()->user();
return auth()->check() and $user->isAdmin();
}
/**

View file

@ -16,7 +16,9 @@ class StoreTicket extends FormRequest
*/
public function authorize()
{
if (!auth()->user()->role->equals(Role::admin())) {
/** @var \App\Models\User */
$user = auth()->user();
if (!$user->isAdmin()) {
$this->merge([
'status' => TicketStatus::new()->value,
]);
@ -32,17 +34,19 @@ class StoreTicket extends FormRequest
public function rules()
{
return [
'project_id' => [
'site_id' => [
'required',
'exists:projects,id',
'exists:sites,id',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
if (auth()->user()->role->equals(Role::admin())) {
/** @var \App\Models\User */
$user = auth()->user();
if ($user->isAdmin()) {
return;
}
if (!in_array($value, auth()->user()->projects()->pluck('id')->all())) {
if (!in_array($value, $user->sites()->pluck('id')->all())) {
$fail(__('Veuillez choisir entre :values', [
'values' => auth()->user()->projects()->pluck('name')->implode(',')
'values' => $user->sites()->pluck('name')->implode(',')
]));
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\CrudAction;
class UpdateCrudModel extends CrudRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$model = $this->getCurrentModel();
/** @var \App\Models\User|null $user */
$user = auth()->user();
$authorized = auth()->check() and $user->can('update', $model);
if ($authorized) {
$this->rules = $this->getRulesForAction(CrudAction::update());
}
return $authorized;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return $this->rules;
}
}

View file

@ -2,6 +2,6 @@
namespace App\Http\Requests;
class UpdateProject extends StoreProject
class UpdateSite extends StoreSite
{
}

View file

@ -9,12 +9,13 @@ class UpdateTicket extends StoreTicket
{
public function authorize()
{
if (!auth()->user()->role->equals(Role::admin())) {
$this->merge([
'status' => $this->project->status->value,
/** @var \App\Models\User */
$user = auth()->user();
if (!$user->isAdmin()) {
$this->merge(['status' => $this->site->status->value,
]);
}
return auth()->check() and (auth()->user()->role->equals(Role::admin()) or
return auth()->check() and ($user->isAdmin() or
request()->route()->ticket->created_by === auth()->user()->id);
}
}

View file

@ -1,22 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CrudResource extends JsonResource
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'label' => $this->crudLabel,
'value' => $this->crudValue
];
}
}

View file

@ -1,106 +0,0 @@
<?php
namespace App\Lib\PolicySystem;
use App\Models\User;
use App\Lib\PolicySystem\StaticRules as SR;
use Illuminate\Auth\Access\Response;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
class RootPolicy
{
protected $policy = null;
protected $policies = null;
public function __construct()
{
$cls = \explode('\\', \get_called_class());
$this->policy = \strtolower(\str_replace('Policy', '', \end($cls)));
}
protected function actionPolicy(string $method, string $policy, User $user, Model $model = null)
{
$this->setPolicies($user, $model);
$ctx = [$policy, $user, $model];
if (!$this->hasPolicy($policy)) {
$m = __('Unhandled policy ":policy"', ['policy' => $policy]);
Log::critical($m, $ctx);
return $this->deny($m);
}
if (!is_array($this->policies[$policy])) {
$m = __('Policy ":policy" has to be array', ['policy' => $policy]);
Log::critical($m, $ctx);
return $this->deny($m);
}
// Policy method exists
if (!array_key_exists($method, $this->policies[$policy])) {
$m = __('Unhandled policy ":policy" for action ":action"', ['policy' => $policy, 'action' => $method]);
Log::critical($m, $ctx);
return $this->deny($m);
}
// Call policy method
if (Sr::handle($this->policies[$policy][$method], $user, $model)) {
return $this->allow();
} else {
$m = __('Policy ":policy" for action ":action" denied :onmodel', [
'policy' => $policy,
'action' => $method,
'onmodel' => $model ? get_class($model) : ''
]);
if (config('app.policyDebug')) {
Log::debug(sprintf("%s user => %s model => %s", $m, $user, $model), $ctx);
}
return $this->deny($m);
}
$m = __('Unhandled policy case on resource ":method"', ['method' => $method]);
Log::critical($m, $ctx);
return $this->deny();
}
/**
** Do the magic calling Root policy from any policy
*/
public function __call($method, $args)
{
return call_user_func_array([$this, 'actionPolicy'], array_merge([$method, $this->policy], $args));
}
private function hasPolicy(string $policy)
{
return array_key_exists($policy, $this->policies);
}
/**
* Allow action for the user
*
* @param string|null $message
* @return \Illuminate\Auth\Access\Response
*/
protected function allow(?string $message = null)
{
return Response::allow($message);
}
/**
* Deny action for the user
*
* @param string|null $message
* @param mixed $code
* @return \Illuminate\Auth\Access\Response
*/
protected function deny(?string $message = null, $code = null)
{
if (!$message) {
$message = __('This action is not allowed to you for this resource.');
}
return Response::deny($message, $code);
}
}

View file

@ -1,86 +0,0 @@
<?php
namespace App\Lib\PolicySystem;
use App\Models\User;
use Exception;
use Closure;
use Illuminate\Database\Eloquent\Model;
/**
* Generic Policiy Static Rules
*! Rules has to return boolean value
*/
abstract class StaticRules
{
/**
* Make a rule closure
*
* @param string $rulename
* @param mixed ...$args
* @return Closure
*/
public static function make(string $rulename, ...$args)
{
if (!method_exists(static::class, $rulename)) {
throw new Exception(sprintf('Unknown rule %s in Policies StaticRules', $rulename));
}
return function () use ($rulename, $args) {
return call_user_func_array([static::class, $rulename], $args);
};
}
public static function and(array $ruleClosures)
{
return function (User $user, Model $model = null) use ($ruleClosures) {
$nbRules = count($ruleClosures);
$out = true;
$i = 0;
do {
if (!($out = (self::handle($ruleClosures[$i++], $user, $model) && $out))) {
break;
}
} while ($i < $nbRules);
return $out;
};
}
public static function or(array $ruleClosures)
{
return function (User $user, Model $model = null) use ($ruleClosures) {
$nbRules = count($ruleClosures);
$out = false;
$i = 0;
do {
if ($out = (self::handle($ruleClosures[$i++], $user, $model) || $out)) {
break;
}
} while ($i < $nbRules);
return $out;
};
}
public static function handle($rule, User $user, Model $model = null)
{
if (is_array($rule)) {
$nbRule = count($rule);
$out = false;
$i = 0;
while ($i < $nbRule) {
if ($out = self::handle($rule[$i++], $user, $model)) {
break;
}
}
return $out;
}
if (!is_callable($rule)) {
throw new Exception('Rule has to ba callable.');
}
return (bool)$rule($user, $model);
}
protected static function anyone()
{
return true;
}
}

View file

@ -9,6 +9,7 @@ class Utils
$text = e($text);
// url replace and email replace
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<email>[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,})|(?P<user>@\w*)|(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) {
$match = collect($matches)->first() ?? '';

View file

@ -0,0 +1,28 @@
<?php
namespace App\Listeners;
use App\Events\Loggable;
use App\Models\Log;
class HandleLoggable
{
/**
* Handle the event.
*
* @param Loggable $loggable
* @return void
*/
public function handle(Loggable $loggable)
{
$log = new Log([
'entry' => trans(':username à crée le fichier :filename', [
'username' => $loggable->user->name,
'filename' => $loggable->file->name
])
]);
$log->user()->associate($loggable->user);
$log->parent()->associate($loggable->file->parent);
$log->saveOrFail();
}
}

View file

@ -9,7 +9,8 @@ use Illuminate\Queue\SerializesModels;
class NewTicket extends Mailable
{
use Queueable, SerializesModels;
use Queueable;
use SerializesModels;
private $ticket;
@ -31,6 +32,7 @@ class NewTicket extends Mailable
public function build()
{
$ticket = $this->ticket;
return $this->subject("[SupportWeb] Nouveau ticket n°" . $ticket->id)->view('emails.new-ticket', compact('ticket'));
return $this->subject("[SupportWeb] Nouveau ticket n°" . $ticket->id)
->view('emails.new-ticket', compact('ticket'));
}
}

View file

@ -40,6 +40,7 @@ class TicketUpdate extends Mailable
{
$ticket = $this->ticket;
$corpsMaj = Utils::plainToHtml($this->corpsMaj);
return $this->subject("[SupportWeb] Le ticket n°" . $ticket->id . " à été mis à jour")->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
return $this->subject("[SupportWeb] Le ticket n°" . $ticket->id . " à été mis à jour")
->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
}
}

135
app/Models/File.php Normal file
View file

@ -0,0 +1,135 @@
<?php
namespace App\Models;
use App\Enums\FileCategory;
use App\Enums\FileType;
use App\Events\FileCreated;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
use Kwaadpepper\Enum\Traits\CastsEnums;
class File extends Model
{
use CastsEnums;
use HasFactory;
use IsCrudModel;
protected $enumCasts = [
'category' => FileCategory::class,
'type' => FileType::class
];
protected $crudLabelColumn = 'name';
protected $crudValueColumn = 'id';
protected $fillable = ['type', 'uri', 'file', 'name', 'category'];
public function __construct($attributes = [])
{
self::$editableProperties = [
'name' => [
'label' => trans('Nom'),
'type' => CrudType::string(),
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::update(),
CrudAction::create()
]
],
'type' => [
'label' => trans('Type'),
'type' => CrudType::enum(),
'enum' => FileType::class,
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create()
]
],
'category' => [
'label' => trans('Categorie'),
'type' => CrudType::enum(),
'enum' => FileCategory::class,
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::update(),
CrudAction::create()
]
],
'uri' => [
'label' => trans('Lien uri'),
'type' => CrudType::uri(),
'required' => false,
'nullable' => true,
'validate' => [
'required_without:file',
'nullable'
],
'actions' => [
CrudAction::view(),
CrudAction::create()
]
],
'file' => [
'label' => trans('Fichier'),
'type' => CrudType::file(),
'required' => false,
'nullable' => true,
'validate' => [
'required_without:uri',
'nullable',
'mimes:pdf',
'max:5120'
],
'accept' => 'application/pdf',
'actions' => [
CrudAction::view(),
CrudAction::create()
]
],
'created_at' => [
'label' => trans('Créé le'),
'type' => CrudType::string(),
'actions' => [
CrudAction::viewAny(),
CrudAction::view()
]
]
];
parent::__construct($attributes);
}
protected static function booted()
{
static::created(function (File $file) {
event(new FileCreated($file, \auth()->user()));
});
}
public function parent()
{
return $this->morphTo();
}
public function parentSite()
{
return $this->morphTo('parent', 'site');
}
public function parentProject()
{
return $this->morphTo('parent', 'project');
}
}

83
app/Models/Log.php Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
class Log extends Model
{
use HasFactory;
use IsCrudModel;
protected $crudLabelColumn = 'id';
protected $crudValueColumn = 'id';
protected $fillable = ['entry'];
public function __construct($attributes = [])
{
self::$editableProperties = [
'id' => [
'label' => trans('ID'),
'type' => CrudType::unsignedint(),
'actions' => [
CrudAction::viewAny(),
CrudAction::view()
]
],
'entry' => [
'label' => trans('Entrée'),
'type' => CrudType::text(),
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update(),
]
],
'created_at' => [
'label' => trans('Créé le'),
'type' => CrudType::string(),
'actions' => [
CrudAction::viewAny(),
CrudAction::view()
]
]
];
parent::__construct($attributes);
}
protected static function booted()
{
static::creating(function (Log $log) {
$log->user()->associate(auth()->user());
});
}
public function user()
{
return $this->belongsTo(User::class);
}
public function parent()
{
return $this->morphTo();
}
public function parentSite()
{
return $this->morphTo('parent', 'site');
}
public function parentProject()
{
return $this->morphTo('parent', 'project');
}
}

View file

@ -2,14 +2,10 @@
namespace App\Models;
use App\Enums\Role;
use App\Events\NotifySubscriptions;
use App\Events\NotifyTicketCreator;
use App\Jobs\SendTicketMentionMail;
use App\Mail\TicketMention;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\UploadedFile;
class Message extends Model
@ -35,31 +31,7 @@ class Message extends Model
])
));
}
// find @user + send mail si un seul user est trouvé
$usersNotified = collect([]);
$matches = [];
$ticket->load('project');
\preg_match_all('/@\w*/', $message->message, $matches);
$matches = collect($matches[0] ?? [])->unique();
foreach ($matches as $pseudo_usertag) {
try {
$user = User::where('name', 'LIKE', '%' . ltrim($pseudo_usertag, '@') . '%')
->where('email', '!=', $ticket->user->email)
->where(function ($q) use ($ticket) {
$q->whereHas('projects', function ($q) use ($ticket) {
$q->where('id', $ticket->project->id);
})->orWhere('role', Role::admin()->value);
})->firstOrFail();
$usersNotified->add($user);
SendTicketMentionMail::dispatchAfterResponse(
$user,
new TicketMention($ticket, $message->message)
);
} catch (ModelNotFoundException $e) {
continue;
}
}
$usersNotified = $ticket->sendBeenMentionnedToUser($message);
event(new NotifySubscriptions(
$message->ticket,
trans('Nouvelle réponse de :name :<br> :message', [
@ -89,18 +61,18 @@ class Message extends Model
return $this->belongsTo(User::class, 'user_id');
}
public function saveFile(Project $project, Ticket $ticket, UploadedFile $file)
public function saveFile(Site $site, Ticket $ticket, UploadedFile $file)
{
$projectAssetFullPath = Project::ticketAssetsFullPath($project->slug_name);
$fileAssetPath = Project::ticketAssetsPath($project->slug_name);
$siteAssetFullPath = Site::ticketAssetsFullPath($site->slug_name);
$fileAssetPath = Site::ticketAssetsPath($site->slug_name);
$fileName = sprintf(
'%s-%s-%d-%d.%s',
$project->slug_name,
$site->slug_name,
$ticket->id,
rand(),
$file->extension()
);
$file->move(sprintf('%s/messages/%d', $projectAssetFullPath, $ticket->id), $fileName);
$file->move(sprintf('%s/messages/%d', $siteAssetFullPath, $ticket->id), $fileName);
return sprintf('%s/%d/%s', $fileAssetPath, $ticket->id, $fileName);
}
}

View file

@ -2,27 +2,26 @@
namespace App\Models;
use App\Enums\CrudAction;
use App\Enums\CrudType;
use App\Enums\Role;
use App\Traits\IsCrudModel;
use App\Events\ProjectUpdated;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
class Project extends Model
{
use HasFactory;
use IsCrudModel;
use SoftDeletes;
protected $crudLabelColumn = 'name';
protected $crudValueColumn = 'id';
protected $fillable = ['name'];
protected $fillable = [
'name', 'status',
'description', 'todo', 'sites'
];
public function __construct($attributes = [])
{
@ -43,21 +42,66 @@ class Project extends Model
CrudAction::update(),
]
],
'users' => [
'label' => trans('Utilisateurs'),
'type' => CrudType::belongsToMany(),
'belongsToMany' => User::class,
'belongsToManyLabel' => 'name',
'nullable' => true,
'readonly' => function () {
return auth()->user()->role->equals(Role::admin()) ? false : true;
},
'status' => [
'label' => trans('État'),
'type' => CrudType::string(),
'validate' => [
'string',
'max:255'
],
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update(),
]
],
'description' => [
'label' => trans('Description'),
'type' => CrudType::text(),
'validate' => [
'string'
],
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::view(),
CrudAction::create(),
CrudAction::update(),
]
],
'todo' => [
'label' => trans('À Faire'),
'type' => CrudType::text(),
'validate' => [
'string'
],
'required' => false,
'nullable' => true,
'actions' => [
CrudAction::view(),
CrudAction::create(),
CrudAction::update(),
]
],
'sites' => [
'label' => trans('Sites'),
'type' => CrudType::belongsToMany(),
'belongsToMany' => Site::class,
'belongsToManyLabel' => 'name',
'nullable' => true,
'readonly' => function () {
/** @var \App\Models\User */
$user = auth()->user();
return !$user->isAdmin();
},
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
]
]
];
parent::__construct($attributes);
@ -65,52 +109,23 @@ class Project extends Model
protected static function booted()
{
static::created(function (Project $project) {
$path = Project::ticketAssetsFullPath($project->slug_name);
if (!File::exists($path)) {
File::makeDirectory($path, 0755, true);
}
});
static::updating(function (Project $project) {
$oldName = Project::where('id', $project->id)->first()->name;
$oldPath = Project::ticketAssetsFullPath($oldName);
$newPath = Project::ticketAssetsFullPath($project->slug_name);
if (File::exists($oldPath)) {
File::move($oldPath, $newPath);
} else {
File::makeDirectory($newPath, 0755, true);
}
});
static::deleted(function (Project $project) {
$path = Project::ticketAssetsFullPath($project->slug_name);
if (File::exists($path)) {
File::deleteDirectory($path);
}
static::updated(function (Project $project) {
event(new ProjectUpdated($project, \auth()->user()));
});
}
public function users()
public function sites()
{
return $this->belongsToMany(User::class);
return $this->belongsToMany(Site::class, 'project_site');
}
public function tickets()
public function logs()
{
return $this->hasMany(Ticket::class, 'project_id');
return $this->morphMany(Log::class, 'parent');
}
public function getSlugNameAttribute()
public function files()
{
return Str::slug($this->name);
}
public static function ticketAssetsFullPath(string $str)
{
return public_path(self::ticketAssetsPath($str));
}
public static function ticketAssetsPath(string $str)
{
return sprintf('fichiers/%s', $str);
return $this->morphMany(File::class, 'parent');
}
}

10
app/Models/ProjetSite.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ProjetSite extends Pivot
{
//
}

154
app/Models/Site.php Normal file
View file

@ -0,0 +1,154 @@
<?php
namespace App\Models;
use App\Events\SiteUpdated;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
class Site extends Model
{
use HasFactory;
use IsCrudModel;
use SoftDeletes;
protected $crudLabelColumn = 'name';
protected $crudValueColumn = 'id';
protected $fillable = ['name'];
public function __construct($attributes = [])
{
self::$editableProperties = [
'name' => [
'label' => trans('Nom'),
'type' => CrudType::string(),
'validate' => [
'string',
'max:255'
],
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update(),
]
],
'users' => [
'label' => trans('Utilisateurs'),
'type' => CrudType::belongsToMany(),
'belongsToMany' => User::class,
'belongsToManyLabel' => 'name',
'nullable' => true,
'readonly' => function () {
/** @var \App\Models\User */
$user = auth()->user();
return !$user->isAdmin();
},
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update(),
]
],
'projects' => [
'label' => trans('Projets'),
'type' => CrudType::belongsToMany(),
'belongsToMany' => Project::class,
'belongsToManyLabel' => 'name',
'nullable' => true,
'readonly' => function () {
/** @var \App\Models\User */
$user = auth()->user();
return !$user->isAdmin();
},
'actions' => [
CrudAction::viewAny(),
CrudAction::view(),
CrudAction::create(),
CrudAction::update()
]
]
];
parent::__construct($attributes);
}
protected static function booted()
{
static::created(function (Site $site) {
$path = Site::ticketAssetsFullPath($site->slug_name);
if (!File::exists($path)) {
File::makeDirectory($path, 0755, true);
}
});
static::updating(function (Site $site) {
$oldName = Site::where('id', $site->id)->first()->name;
$oldPath = Site::ticketAssetsFullPath($oldName);
$newPath = Site::ticketAssetsFullPath($site->slug_name);
if (File::exists($oldPath)) {
File::move($oldPath, $newPath);
} else {
File::makeDirectory($newPath, 0755, true);
}
});
static::updated(function (Site $site) {
event(new SiteUpdated($site, \auth()->user()));
});
static::deleted(function (Site $site) {
$path = Site::ticketAssetsFullPath($site->slug_name);
if (File::exists($path)) {
File::deleteDirectory($path);
}
});
}
public function users()
{
return $this->belongsToMany(User::class);
}
public function tickets()
{
return $this->hasMany(Ticket::class, 'site_id');
}
public function projects()
{
return $this->belongsToMany(Project::class);
}
public function logs()
{
return $this->morphMany(Log::class, 'parent');
}
public function files()
{
return $this->morphMany(File::class, 'parent');
}
public function getSlugNameAttribute()
{
return Str::slug($this->name);
}
public static function ticketAssetsFullPath(string $str)
{
return public_path(self::ticketAssetsPath($str));
}
public static function ticketAssetsPath(string $str)
{
return sprintf('fichiers/%s', $str);
}
}

10
app/Models/SiteFile.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class SiteFile extends Pivot
{
//
}

View file

@ -4,7 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ProjectUser extends Pivot
class SiteUser extends Pivot
{
public $timestamps = false;

View file

@ -2,13 +2,18 @@
namespace App\Models;
use App\Enums\Role;
use App\Enums\TicketStatus;
use App\Enums\TicketType;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Jobs\SendTicketMentionMail;
use App\Mail\TicketMention;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Kwaadpepper\Enum\Traits\CastsEnums;
class Ticket extends Model
@ -22,7 +27,7 @@ class Ticket extends Model
];
protected $fillable = [
'project_id',
'site_id',
'type',
'status',
'text'
@ -48,9 +53,9 @@ class Ticket extends Model
});
}
public function project()
public function site()
{
return $this->belongsTo(Project::class);
return $this->belongsTo(Site::class);
}
public function user()
@ -68,19 +73,57 @@ class Ticket extends Model
return $this->belongsToMany(User::class, 'subscriptions');
}
public static function saveFile(Project $project, Ticket $ticket, TicketType $type, UploadedFile $file)
public static function saveFile(Site $site, Ticket $ticket, TicketType $type, UploadedFile $file)
{
$projectAssetFullPath = Project::ticketAssetsFullPath($project->slug_name);
$fileAssetPath = Project::ticketAssetsPath($project->slug_name);
$siteAssetFullPath = Site::ticketAssetsFullPath($site->slug_name);
$fileAssetPath = Site::ticketAssetsPath($site->slug_name);
$fileName = sprintf(
'%s-%s-%d-%d.%s',
$project->slug_name,
$site->slug_name,
$type->getDefinition(),
$ticket->id,
rand(),
$file->extension()
);
$file->move(sprintf('%s/%d', $projectAssetFullPath, $ticket->id), $fileName);
$file->move(sprintf('%s/%d', $siteAssetFullPath, $ticket->id), $fileName);
return sprintf('%s/%d/%s', $fileAssetPath, $ticket->id, $fileName);
}
/**
* Parse message @user tags and send emails to concerned users
* returns and array with to users notified
*
* @param Message $message
* @return array[User]
*/
public function sendBeenMentionnedToUser(Message $message): Collection
{
$ticket = $this;
// find @user + send mail si un seul user est trouvé
$usersNotified = collect([]);
$matches = [];
$ticket->load('site');
\preg_match_all('/@\w*/', $message->message, $matches);
$matches = collect($matches[0] ?? [])->unique();
foreach ($matches as $pseudo_usertag) {
try {
$user = User::where('name', 'LIKE', '%' . ltrim($pseudo_usertag, '@') . '%')
->where('email', '!=', $ticket->user->email)
->where(function ($q) use ($ticket) {
$q->whereHas('sites', function ($q) use ($ticket) {
$q->where('id', $ticket->site->id);
})->orWhere('role', Role::admin()->value)
->orWhere('role', Role::developper()->value);
})->firstOrFail();
$usersNotified->add($user);
SendTicketMentionMail::dispatchAfterResponse(
$user,
new TicketMention($ticket, $message->message)
);
} catch (ModelNotFoundException $e) {
continue;
}
}
return $usersNotified;
}
}

View file

@ -2,25 +2,25 @@
namespace App\Models;
use App\Enums\CrudAction;
use App\Enums\CrudType;
use App\Enums\Role;
use App\Jobs\SendUserCreatedEmail;
use App\Traits\IsCrudModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
use Kwaadpepper\Enum\Traits\CastsEnums;
class User extends Authenticatable
{
use HasFactory;
use Notifiable;
use CastsEnums;
use HasFactory;
use IsCrudModel;
use Notifiable;
use SoftDeletes;
protected $enumCasts = [
@ -123,14 +123,16 @@ class User extends Authenticatable
CrudAction::update(),
]
],
'projects' => [
'sites' => [
'label' => trans('Projets'),
'type' => CrudType::belongsToMany(),
'belongsToMany' => Project::class,
'belongsToMany' => Site::class,
'belongsToManyLabel' => 'name',
'nullable' => true,
'readonly' => function () {
return auth()->user()->role->equals(Role::admin()) ? false : true;
/** @var \App\Models\User */
$user = auth()->user();
return !$user->isAdmin();
},
'actions' => [
CrudAction::viewAny(),
@ -176,13 +178,24 @@ class User extends Authenticatable
return $this->belongsToMany(Ticket::class, 'subscriptions');
}
public function projects()
public function sites()
{
return $this->belongsToMany(Project::class);
return $this->belongsToMany(Site::class);
}
public function sendMailConfirm()
{
dispatch(new SendUserCreatedEmail($this));
}
/**
* Is the user an admin or a developper ?
*
* @return boolean
*/
public function isAdmin(): bool
{
return ($this->role->equals(Role::developper()) or
$this->role->equals(Role::admin()));
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
class FilePolicy extends RootPolicy
{
use HandlesAuthorization;
/**
* Init policies on call
*
* @param User $user
* @param Model $model
* @return void
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function setPolicies(User $user, Model $model = null)
{
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'file' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('userIsAdmin', $user),
'update' => URR::make('denyEveryOne'),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsDev', $user)
]
];
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Policies\Rules\LogStaticRules;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
class LogPolicy extends RootPolicy
{
use HandlesAuthorization;
/**
* Init policies on call
*
* @param User $user
* @param Model $model
* @return void
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function setPolicies(User $user, Model $model = null)
{
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'log' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('denyEveryOne'),
'update' => StaticRules::or([
URR::make('userIsDev', $user),
LogStaticRules::make('logIsOwnedByUser', $user, $model)
]),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('denyEveryOne')
]
];
}
}

View file

@ -2,11 +2,37 @@
namespace App\Policies;
use App\Models\User;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
class ProjectPolicy extends AppPolicies
class ProjectPolicy extends RootPolicy
{
use HandlesAuthorization;
// Handle delete has tickets
/**
* Init policies on call
*
* @param User $user
* @param Model $model
* @return void
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function setPolicies(User $user, Model $model = null)
{
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'project' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('userIsAdmin', $user),
'update' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsAdmin', $user)
]
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Policies\Rules;
use App\Models\Log;
use App\Models\User;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
/**
* Userlevel Policiy Static Rules
*! Rules has to return boolean value
*/
abstract class LogStaticRules extends StaticRules
{
protected static function logIsOwnedByUser(User $user, Log $log)
{
return $log->user->id === $user->id;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Policies\Rules;
use App\Models\Ticket;
use App\Models\User;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
/**
* Userlevel Policiy Static Rules
*! Rules has to return boolean value
*/
abstract class SiteStaticRules extends StaticRules
{
protected static function userSitesHasTicket(User $user, Ticket $ticket)
{
return $user->sites->contains($ticket->site_id);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Policies\Rules;
use App\Enums\TicketStatus;
use App\Models\Ticket;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
/**
* Userlevel Policiy Static Rules
*! Rules has to return boolean value
*/
abstract class TicketStaticRules extends StaticRules
{
protected static function ticketIsNotArchived(Ticket $ticket)
{
return !$ticket->status->equals(TicketStatus::archived());
}
}

View file

@ -4,7 +4,7 @@ namespace App\Policies\Rules;
use App\Enums\Role;
use App\Models\User;
use App\Lib\PolicySystem\StaticRules;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
/**
* Userlevel Policiy Static Rules
@ -15,6 +15,21 @@ abstract class UserRoleRules extends StaticRules
protected static function userIsAdmin(User $user)
{
return $user->role->equals(Role::admin());
return $user->isAdmin();
}
protected static function userIsDev(User $user)
{
return $user->role->equals(Role::developper());
}
protected static function allowEveryOne()
{
return true;
}
protected static function denyEveryOne()
{
return false;
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Policies\Rules;
use App\Models\User;
use App\Lib\PolicySystem\StaticRules;
/**
* Userlevel Policiy Static Rules
*! Rules has to return boolean value
*/
abstract class UserStaticRules extends StaticRules
{
protected static function usersAreTheSame(User $user, User $userToCheck)
{
return ($user->id == $userToCheck->id) != 0;
}
protected static function usersAreNotTheSame(User $user, User $userToCheck)
{
return !self::usersAreTheSame($user, $userToCheck);
}
protected static function everyOne()
{
return true;
}
protected static function noOne()
{
return false;
}
}

View file

@ -3,14 +3,15 @@
namespace App\Policies;
use App\Models\User;
use App\Lib\PolicySystem\RootPolicy;
use App\Lib\PolicySystem\StaticRules as Sr;
use App\Policies\Rules\UserStaticRules as USR;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
class AppPolicies extends RootPolicy
class SitePolicy extends RootPolicy
{
use HandlesAuthorization;
/**
* Init policies on call
*
@ -18,26 +19,20 @@ class AppPolicies extends RootPolicy
* @param Model $model
* @return void
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function setPolicies(User $user, Model $model = null)
{
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'project' => [
'site' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('userIsAdmin', $user),
'update' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsAdmin', $user)
],
'user' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('userIsAdmin', $user),
'update' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsAdmin', $user)
],
]
];
}
}

View file

@ -2,132 +2,60 @@
namespace App\Policies;
use App\Enums\Role;
use App\Enums\TicketStatus;
use App\Models\Ticket;
use App\Models\User;
use App\Policies\Rules\SiteStaticRules as SSR;
use App\Policies\Rules\TicketStaticRules;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
use Kwaadpepper\CrudPolicies\Policies\Rules\StaticRules;
class TicketPolicy
class TicketPolicy extends RootPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
* Init policies on call
*
* @param \App\Models\User $user
* @return mixed
* @param User $user
* @param Model $model
* @return void
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
public function viewAny(User $user)
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function setPolicies(User $user, Model $model = null)
{
return true;
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $user
* @param \App\Models\Ticket $ticket
* @return mixed
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function view(User $user, Ticket $ticket)
{
return true;
}
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $user
* @return mixed
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
public function create(User $user)
{
return true;
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @param \App\Models\Ticket $ticket
* @return mixed
*/
public function update(User $user, Ticket $ticket)
{
return Auth::user()->role->equals(Role::admin()) or $user->projects->contains($ticket->project_id);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Ticket $ticket
* @return mixed
*/
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function delete(User $user, Ticket $ticket)
{
return false;
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $user
* @param \App\Models\Ticket $ticket
* @return mixed
*/
public function restore(User $user, Ticket $ticket)
{
return Auth::user()->role->equals(Role::admin()) or $user->projects->contains($ticket->project_id);
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Ticket $ticket
* @return mixed
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function forceDelete(User $user, Ticket $ticket)
{
return false;
}
public function changeStatus(User $user)
{
return $user->role->equals(Role::admin());
}
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
public function suscribeToTicket(User $user, Ticket $ticket)
{
return !$ticket->status->equals(TicketStatus::archived());
}
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
public function createMessage(User $user, Ticket $ticket)
{
//can create if
//role == 1
// OR project_id IN user->projects
if (
!(
$user->role->equals(Role::admin())
or $user->projects->contains($ticket->project_id) )
)
{
return Response::deny(trans('Vous ne pouvez modifier un ticket qui ne vous appartient pas.'));
}
return $ticket->status->equals(TicketStatus::archived()) ?
Response::deny(trans('Vous ne pouvez ajouter un message à un ticket qui est archivé')) : Response::allow();
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'ticket' => [
'viewAny' => URR::make('allowEveryOne'),
'view' => URR::make('allowEveryOne'),
'update' => StaticRules::or([
URR::make('userIsAdmin', $user),
SSR::make('userSitesHasTicket', $user, $model)
]),
'changeStatus' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('denyEveryOne'),
'forceDelete' => URR::make('denyEveryOne'),
'restore' => StaticRules::or([
URR::make('userIsAdmin', $user),
SSR::make('userSitesHasTicket', $user, $model)
]),
'suscribeToTicket' => TicketStaticRules::make('ticketIsNotArchived', $model),
'createMessage' => StaticRules::or([
URR::make('userIsAdmin', $user),
StaticRules::and([
TicketStaticRules::make('ticketIsNotArchived', $model),
SSR::make('userSitesHasTicket', $user, $model)
])
])
]
];
}
}

View file

@ -2,9 +2,37 @@
namespace App\Policies;
use App\Models\User;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
class UserPolicy extends AppPolicies
class UserPolicy extends RootPolicy
{
use HandlesAuthorization;
/**
* Init policies on call
*
* @param User $user
* @param Model $model
* @return void
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function setPolicies(User $user, Model $model = null)
{
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'user' => [
'viewAny' => URR::make('userIsAdmin', $user),
'view' => URR::make('userIsAdmin', $user),
'update' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsAdmin', $user)
]
];
}
}

View file

@ -3,7 +3,7 @@
namespace App\Providers;
use App\Models\Ticket;
use App\Policies\ProjectPolicy;
use App\Policies\SitePolicy;
use App\Policies\TicketPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

View file

@ -2,10 +2,14 @@
namespace App\Providers;
use App\Events\FileCreated;
use App\Events\NotifySubscriptions;
use App\Events\NotifyTicketCreator;
use App\Events\ProjectUpdated;
use App\Events\SiteUpdated;
use App\Events\TicketStatusChanged;
use App\Listeners\AddStatusMessages;
use App\Listeners\HandleLoggable;
use App\Listeners\SendNotificationMail;
use App\Listeners\SendNotificationMailToTicketCreator;
use App\Listeners\SendSubscriptionsNotificationsMail;
@ -22,18 +26,13 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
TicketStatusChanged::class => [
AddStatusMessages::class
],
NotifySubscriptions::class => [
SendSubscriptionsNotificationsMail::class
],
NotifyTicketCreator::class => [
SendNotificationMailToTicketCreator::class
],
Registered::class => [SendEmailVerificationNotification::class],
TicketStatusChanged::class => [AddStatusMessages::class],
NotifySubscriptions::class => [SendSubscriptionsNotificationsMail::class],
NotifyTicketCreator::class => [SendNotificationMailToTicketCreator::class],
FileCreated::class => [HandleLoggable::class],
ProjectUpdated::class => [HandleLoggable::class],
SiteUpdated::class => [HandleLoggable::class]
];
/**

View file

@ -2,7 +2,10 @@
namespace App\Providers;
use App\Models\File;
use App\Models\Log;
use App\Models\Project;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
@ -22,15 +25,6 @@ class RouteServiceProvider extends ServiceProvider
*/
public const HOME = '/home';
/**
* The controller namespace for the application.
*
* When present, controller route declarations will automatically be prefixed with this namespace.
*
* @var string|null
*/
// protected $namespace = 'App\\Http\\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
@ -50,10 +44,13 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
Route::model('file', File::class);
Route::model('log', Log::class);
Route::model('user', User::class);
Route::model('site', Site::class);
Route::model('project', Project::class);
Route::bind('ticket', function ($value) {
return Ticket::where('id', $value)->has('project')->firstOrFail();
return Ticket::where('id', $value)->has('site')->firstOrFail();
});
}

View file

@ -1,532 +0,0 @@
<?php
namespace App\Traits;
use App\Enums\CrudAction;
use App\Enums\CrudType;
use App\Exceptions\CrudException;
use App\Http\Requests\CrudRequest;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Kwaadpepper\Enum\BaseEnum;
trait IsCrudModel
{
protected static $editableProperties = [];
private static $schemaCrud = [
'label' => null,
'placeholder' => '',
'type' => null,
'enum' => null,
'nullable' => false,
'readonly' => false,
'disabled' => false,
'required' => false,
'validate' => [],
'rules' => [],
'actions' => [],
'belongsTo' => null,
'belongsToMany' => null,
'getAttribute' => null,
'setAttribute' => null
];
// OVERLOAD MODEL FUNCTION
public function __get($key)
{
if (
isset(static::$editableProperties[$key]['getAttribute']) and
is_callable(static::$editableProperties[$key]['getAttribute'])
) {
return static::$editableProperties[$key]['getAttribute']($this);
}
return parent::__get($key);
}
// OVERLOAD MODEL FUNCTION
public function __set($key, $value)
{
if (
isset($this::$editableProperties[$key]['setAttribute']) and
is_callable($this::$editableProperties[$key]['setAttribute'])
) {
return $this::$editableProperties[$key]['setAttribute']($this, $value);
}
return parent::__set($key, $value);
}
// OVERLOAD MODEL FUNCTION
public function fill(array $attributes)
{
parent::fill($attributes);
foreach ($attributes as $k => $attribute) {
if (
isset(static::$editableProperties[$k]['setAttribute']) and
is_callable(static::$editableProperties[$k]['setAttribute'])
) {
$this->{$k} = $attribute;
}
if (\is_object($this->{$k}) and \get_class($this->{$k}) === UploadedFile::class) {
$this->moveUploadedFile($k, $this->{$k});
}
}
}
public function saveRelations(CrudRequest $request)
{
$validated = $request->validated();
$props = self::getEditableProperties();
foreach ($props as $k => $prop) {
if ($prop['belongsToMany'] and \is_array($validated[$k])) {
$this->{$k}()->sync($validated[$k]);
}
}
}
/**
* Has this model Has many relations with delete restrict ?
*
* @return bool|string
*/
public function gotHasManyRelationWithRestrictOnDelete()
{
$sm = DB::getDoctrineSchemaManager();
foreach (self::getRelationships() as $relation => $type) {
if ($type[0] === 'HasMany') {
$table = (new $type[1]())->getTable();
/** @var \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey */
foreach ($sm->listTableForeignKeys($table) as $foreignKey) {
// /vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php onEvent()
if ($foreignKey->onDelete() === 'CASCADE') {
// allow only CASCADE action, otherwise it would break the app
continue;
}
return class_basename($type[1]);
}
}
}
return false;
}
/**
* Get the crud label
*/
public function getCrudLabelAttribute()
{
return $this->{$this->crudLabelColumn};
}
/**
* Get the crud value
*/
public function getCrudValueAttribute()
{
if (is_null($this->crudValueColumn)) {
throw new CrudException(sprintf(
'[\'belongsTo\'] model `%s` must have `crudValueColumn` property',
get_class($this),
CrudAction::class
));
}
return $this->{$this->crudValueColumn};
}
/**
* Get the crud label column
*/
public function getCrudLabelColumnAttribute()
{
if (is_null($this->crudLabelColumn)) {
throw new CrudException(sprintf(
'[\'belongsTo\'] model `%s` must have `crudLabelColumn` property',
get_class($this),
CrudAction::class
));
}
return $this->crudLabelColumn;
}
/**
* Get the crud value column
*/
public function getCrudValueColumnAttribute()
{
return $this->crudValueColumn;
}
/**
* Get the model validation rules
*
* @return array
*/
public static function getRules(): array
{
return collect(static::$editableProperties)->mapWithKeys(function ($item, $key) {
$item = \array_merge(self::$schemaCrud, $item);
$defaultRules = [];
if (!$item['nullable'] === true) {
$defaultRules[] = 'required';
} elseif ($item['required'] === true) {
$defaultRules[] = 'required';
} elseif ($item['readonly'] === true) {
$defaultRules[] = 'prohibited';
}
switch ($item['type']) {
case CrudType::boolean():
$defaultRules[] = 'boolean';
break;
case CrudType::image():
$defaultRules[] = 'file';
break;
case CrudType::email():
$defaultRules[] = 'email:rfc,dns';
break;
case CrudType::belongsTo():
$defaultRules[] = 'integer';
$defaultRules[] = 'min:0';
break;
case CrudType::int():
$defaultRules[] = 'integer';
break;
case CrudType::belongsToMany():
$defaultRules[] = 'array';
break;
case CrudType::password():
// phpcs:ignore Generic.Files.LineLength.TooLong
$defaultRules[] = "regex:/^(?=.*[A-Z])(?=.*[²&~\"#'\{\(\[\-|`_\\^@\)\°=+\}¨^\$£¤%?,;.:\/!§*<>])(?=.*[0-9].*)(?=.*[a-z]).{8,}$/";
break;
case CrudType::text():
case CrudType::string():
default:
$defaultRules[] = 'string';
break;
}
return [$key => array_merge($defaultRules, $item['validate'] ?? self::$schemaCrud['validate'])];
})->all();
}
/**
* Get the model Name
*
* @return string
*/
public function getModelName(): string
{
return class_basename(get_class($this));
}
private function moveUploadedFile(string $propName, UploadedFile &$file): void
{
$this->{$propName} = $file->storePubliclyAs(
$this->getTable(),
$file->hashName(),
['disk' => 'public']
);
}
/**
* Get the Editable Properties
*
* @return array
*/
public static function getEditableProperties(): array
{
static::assertEditablePropertiesIsCorrect();
return static::$editableProperties;
}
/**
* Assert self::$editableProperties is in a correct format
*
* 'string' => CrudType::value()
*
* @return void
* @throws Exception
*/
private static function assertEditablePropertiesIsCorrect(): void
{
if (empty(static::$editableProperties)) {
throw new CrudException('$editableProperties must be defined in ' . __CLASS__);
}
foreach (static::$editableProperties as $name => &$prop) {
if (!is_string($name)) {
throw new CrudException(sprintf(
'editableProperties array value must be an array given: %s',
print_r($prop, true) // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
));
}
if (!is_array($prop)) {
throw new CrudException(sprintf(
'editableProperties array value must be an array given: %s',
print_r($prop, true) // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
));
}
$prop = array_merge_recursive_distinct(self::$schemaCrud, $prop);
// Auto label
$prop['label'] = $prop['label'] ?? ucfirst($name);
self::assertLabelIscorrect($prop);
self::assertPlaceholderIscorrect($prop);
self::assertTypeIscorrect($prop);
switch ($prop['type']) {
// Email auto rule
case CrudType::email():
$prop['validate'] = is_array($prop['validate']) ? $prop['validate'] : [];
if (!array_search('email:rfc,dns', $prop['validate'])) {
$prop['validate'][] = 'email:rfc,dns';
}
break;
case CrudType::json():
$prop['validate'] = is_array($prop['validate']) ? $prop['validate'] : [];
if (!array_search('json', $prop['validate'])) {
$prop['validate'][] = 'json';
}
break;
}
self::assertNullableIscorrect($prop);
self::assertReadonlyIscorrect($prop);
self::assertDisabledIscorrect($prop);
self::assertValidateIscorrect($prop);
self::assertRulesArecorrect($prop);
self::assertActionsArecorrect($prop);
self::assertBelongsToIsCorrect($prop);
self::assertEnumIscorrect($prop);
self::assertGetAttributeIscorrect($prop);
self::assertSetAttributeIscorrect($prop);
self::assertBelongsToManyIsCorrect($prop);
}
}
private static function assertLabelIscorrect(array $prop): void
{
if (!isset($prop['label']) or !is_string($prop['label'])) {
throw new CrudException('$editableProperties[\'label\'] array value must be a string');
}
}
private static function assertPlaceholderIscorrect(array $prop): void
{
if (!isset($prop['placeholder']) or !is_string($prop['placeholder'])) {
throw new CrudException('$editableProperties[\'placeholder\'] array value must be a string');
}
}
private static function assertTypeIscorrect(array $prop): void
{
if (!isset($prop['type']) or !is_object($prop['type']) or get_class($prop['type']) !== CrudType::class) {
throw new CrudException('$editableProperties[\'type\'] value must be a CrudType enum');
}
}
private static function assertNullableIscorrect(array $prop): void
{
if (!isset($prop['nullable']) or !is_bool($prop['nullable'])) {
throw new CrudException('$editableProperties[\'nullable\'] array value must be a boolean');
}
}
private static function assertReadonlyIscorrect(array $prop): void
{
if (
!isset($prop['readonly']) or
(!is_bool($prop['readonly']) and !\is_callable($prop['readonly']))
) {
throw new CrudException('$editableProperties[\'readonly\'] array value must be a boolean or a function returnning a boolean');
}
}
private static function assertDisabledIscorrect(array $prop): void
{
if (!isset($prop['disabled']) or !is_bool($prop['disabled'])) {
throw new CrudException('$editableProperties[\'disabled\'] array value must be a boolean');
}
}
private static function assertRequiredIscorrect(array $prop): void
{
if (!isset($prop['required']) or !is_bool($prop['required'])) {
throw new CrudException('$editableProperties[\'required\'] array value must be a boolean');
}
}
private static function assertValidateIscorrect(array $prop): void
{
if (!isset($prop['validate']) or !\is_array($prop['validate'])) {
throw new CrudException('$editableProperties[\'validate\'] array value must be an array');
}
}
private static function assertGetAttributeIscorrect(array $prop): void
{
if (!is_null($prop['getAttribute']) and !is_callable($prop['getAttribute'])) {
throw new CrudException('$editableProperties[\'getAttribute\'] array value must be callable');
}
}
private static function assertSetAttributeIscorrect(array $prop): void
{
if (!is_null($prop['setAttribute']) and !is_callable($prop['setAttribute'])) {
throw new CrudException('$editableProperties[\'setAttribute\'] array value must be callable');
}
}
private static function assertRulesArecorrect(array $prop): void
{
$allowedRules = [
'min' => function ($v) {
return \is_numeric($v);
},
'max' => function ($v) {
return \is_numeric($v);
},
'step' => function ($v) {
return \is_numeric($v);
},
];
if (!isset($prop['rules']) or !\is_array($prop['rules'])) {
throw new CrudException('$editableProperties[\'rules\'] array value must be an array');
}
if (count(array_diff_key($prop['rules'], $allowedRules))) {
throw new CrudException(sprintf(
'$editableProperties[\'rules\'] array contains unallowed rule, allowed are : %s',
implode(',', array_keys($allowedRules))
));
}
foreach ($allowedRules as $k => $f) {
if (array_key_exists($k, $prop['rules']) and !$f($prop['rules'][$k])) {
throw new CrudException(sprintf(
'$editableProperties[\'rules\'] rule `%s` does not validate with value `%s`',
$k,
$prop['rules'][$k]
));
}
}
}
private static function assertActionsArecorrect(array $prop): void
{
if (!isset($prop['actions']) or !\is_array($prop['actions'])) {
throw new CrudException('$editableProperties[\'actions\'] array value must be an array');
}
foreach ($prop['actions'] as $action) {
if (!is_object($action) or get_class($action) !== CrudAction::class) {
throw new CrudException(sprintf(
'$editableProperties[\'actions\'] array value must contain `%s` objects',
CrudAction::class
));
}
}
}
private static function assertEnumIscorrect(array $prop): void
{
if (
$prop['type']->equals(CrudType::enum()) and (is_null($prop['enum']) or
!\is_subclass_of($prop['enum'], BaseEnum::class))
) {
throw new CrudException(sprintf(
'$editableProperties[\'enum\'] array value must be an enum `%s`',
BaseEnum::class
));
}
}
private static function assertBelongsToIsCorrect(array $prop): void
{
if (is_null($prop['belongsTo'])) {
return;
}
if (
$prop['type']->equals(CrudType::belongsTo()) and
(!\is_string($prop['belongsTo']) or
!\class_exists($prop['belongsTo']) or
is_a($prop['belongsTo'], Model::class, true) or
!in_array(IsCrudModel::class, class_uses_recursive($prop['belongsTo'])))
) {
throw new CrudException(sprintf(
'$editableProperties[\'belongsTo\'] array value must be a CrudModel given: `%s`',
$prop['belongsTo']
));
}
}
private static function assertBelongsToManyIsCorrect(array $prop): void
{
if (is_null($prop['belongsToMany'])) {
return;
}
if (
$prop['type']->equals(CrudType::belongsToMany()) and
(!\is_string($prop['belongsToMany']) or
!\class_exists($prop['belongsToMany']) or
is_a($prop['belongsToMany'], Model::class, true) or
!in_array(IsCrudModel::class, class_uses_recursive($prop['belongsToMany'])))
) {
throw new CrudException(sprintf(
'$editableProperties[\'belongsToMany\'] array value must be a CrudModel given: `%s`',
$prop['belongsToMany']
));
}
if (
$prop['type']->equals(CrudType::belongsToMany()) and
(!\is_string($prop['belongsToManyLabel']))
) {
throw new CrudException(sprintf(
'$editableProperties[\'belongsToManyLabel\'] must be string using belongsToMany, given: `%s`',
$prop['belongsToMany']
));
}
}
/**
* Get eloquent relationships
*
* @url https://laracasts.com/discuss/channels/eloquent/get-all-model-relationships
*
* @return array
*/
private static function getRelationships()
{
$instance = new static();
// Get public methods declared without parameters and non inherited
$class = get_class($instance);
$allMethods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC);
$methods = array_filter(
$allMethods,
function ($method) use ($class) {
return $method->class === $class
&& !$method->getParameters() // relationships have no parameters
&& $method->getName() !== 'getRelationships' // prevent infinite recursion
&& $method->getName() !== 'gotHasManyRelationWithRestrictOnDelete'; // prevent infinite recursion
}
);
DB::beginTransaction();
$relations = [];
foreach ($methods as $method) {
try {
$methodName = $method->getName();
$methodReturn = $instance->$methodName();
if (!$methodReturn instanceof Relation) {
continue;
}
} catch (\Throwable $th) {
continue;
}
$type = (new \ReflectionClass($methodReturn))->getShortName();
$model = get_class($methodReturn->getRelated());
$relations[$methodName] = [$type, $model];
}
DB::rollBack();
return $relations;
}
}

View file

@ -13,6 +13,7 @@
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"kwaadpepper/crud-policies": "^0.1.9",
"kwaadpepper/enum": "^0.1.4",
"laravel/framework": "^8.12",
"laravel/tinker": "^2.5",

7942
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -39,7 +39,7 @@ return [
|
*/
'debug' => (bool) env('APP_DEBUG', false),
'debug' => (bool)env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
@ -171,7 +171,6 @@ return [
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TranslationServiceProvider::class
@ -233,6 +232,7 @@ return [
'CrudAction' => App\Enums\CrudAction::class,
'CrudType' => App\Enums\CrudType::class,
'Role' => App\Enums\Role::class,
'FileType' => App\Enums\FileType::class,
],
];

View file

@ -69,12 +69,7 @@ return [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
]
],
/*

View file

@ -60,9 +60,7 @@ return [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'options' => [],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
@ -99,6 +97,6 @@ return [
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'),
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'),
];

View file

@ -123,7 +123,7 @@ return [
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
],
'default' => [

View file

@ -51,7 +51,7 @@ return [
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
],

View file

@ -128,7 +128,7 @@ return [
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
Str::slug(env('APP_NAME', 'laravel'), '_') . '_session'
),
/*

View file

@ -15,20 +15,17 @@ return [
* of your proxy (e.g. if using ELB or similar).
*
*/
'proxies' => '*', // [<ip addresses>,], '*', '<ip addresses>,'
'proxies' => '*',
/*
* To trust one or more specific proxies that connect
* directly to your server, use an array or a string separated by comma of IP addresses:
*/
// 'proxies' => ['192.168.1.1'],
// 'proxies' => '192.168.1.1, 192.168.1.2',
/*
* Or, to trust all proxies that connect
* directly to your server, use a "*"
*/
// 'proxies' => '*',
/*
* Which headers to use to detect proxy related data (For, Host, Proto, Port)

View file

@ -8,6 +8,7 @@ use Illuminate\Support\Str;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*

View file

@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjects extends Migration
class CreateSites extends Migration
{
/**
* Run the migrations.

View file

@ -17,7 +17,7 @@ class UpdateTicketsStatusWithEnumStatus extends Migration
{
foreach (Ticket::where('status', 4)->get() as $ticket) {
$ticket->status = TicketStatus::archived();
$ticket->save();
$ticket->saveQuietly();
}
}

View file

@ -17,15 +17,15 @@ class UpdateTicketsStatusWithEnumStatus2 extends Migration
{
foreach (Ticket::where('status', 5)->get() as $ticket) {
$ticket->status = TicketStatus::production();
$ticket->save();
$ticket->saveQuietly();
}
foreach (Ticket::where('status', 4)->get() as $ticket) {
$ticket->status = TicketStatus::recette();
$ticket->save();
$ticket->saveQuietly();
}
foreach (Ticket::where('status', 3)->get() as $ticket) {
$ticket->status = TicketStatus::processed();
$ticket->save();
$ticket->saveQuietly();
}
}

View file

@ -1,7 +1,7 @@
<?php
use App\Models\User;
use App\Models\ProjectUser;
use App\Models\SiteUser;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@ -24,7 +24,7 @@ class CreateProjectUserTable extends Migration
/** @var \App\Models\User */
foreach (User::all() as $user) {
if ($user->project_id) {
$uP = new ProjectUser();
$uP = new SiteUser();
$uP->project_id = $user->project_id;
$uP->user_id = $user->id;
$uP->saveOrFail();
@ -47,8 +47,8 @@ class CreateProjectUserTable extends Migration
$table->foreignId('project_id')->nullable()
->after('role')->constrained('projects');
});
/** @var \App\Models\ProjectUser */
foreach (ProjectUser::all() as $uP) {
/** @var \App\Models\SiteUser */
foreach (SiteUser::all() as $uP) {
try {
/** @var \App\Models\User */
$user = User::whereId($uP->user_id)->firstOrFail();

View file

@ -0,0 +1,58 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenameProjectsTableIntoSitesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::rename('projects', 'sites');
Schema::table('project_user', function (Blueprint $table) {
$table->dropForeign('project_user_project_id_foreign');
$table->dropUnique(['project_id', 'user_id']);
});
Schema::rename('project_user', 'site_user');
Schema::table('site_user', function (Blueprint $table) {
$table->renameColumn('project_id', 'site_id');
$table->foreign('site_id')->references('id')->on('sites');
$table->unique(['site_id', 'user_id']);
});
Schema::table('tickets', function (Blueprint $table) {
$table->dropForeign('tickets_project_id_foreign');
$table->renameColumn('project_id', 'site_id');
$table->foreign('site_id')->references('id')->on('sites');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::rename('sites', 'projects');
Schema::table('site_user', function (Blueprint $table) {
$table->dropForeign('site_user_site_id_foreign');
$table->dropUnique(['site_id', 'user_id']);
});
Schema::rename('site_user', 'project_user');
Schema::table('project_user', function (Blueprint $table) {
$table->renameColumn('site_id', 'project_id');
$table->foreign('project_id')->references('id')->on('projects');
$table->unique(['project_id', 'user_id']);
});
Schema::table('tickets', function (Blueprint $table) {
$table->dropForeign('tickets_site_id_foreign');
$table->renameColumn('site_id', 'project_id');
$table->foreign('project_id')->references('id')->on('projects');
});
}
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('status');
$table->text('description');
$table->text('todo')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('projects');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectSiteTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_site', function (Blueprint $table) {
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignId('site_id')->constrained('sites');
$table->unique(['project_id', 'site_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_site');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('logs', function (Blueprint $table) {
$table->id();
$table->text('entry');
$table->foreignId('user_id')->constrained('users');
$table->morphs('parent'); // can be Site or Projet
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('logs');
}
}

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFilesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('files', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('uri', 2000)->nullable(); // max uri length is 2k
$table->string('file')->nullable();
$table->smallInteger('type');
$table->smallInteger('category');
$table->morphs('parent'); // can be Site or Projet
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('files');
}
}

27
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

535
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,3 @@
/*!
* Vue.js v2.6.14
* (c) 2014-2021 Evan You
* Released under the MIT License.
*/
/**
* @license
* Lodash <https://lodash.com/>

File diff suppressed because one or more lines are too long

1
resources/js/app.js vendored
View file

@ -1,2 +1 @@
require('./bootstrap');
require('./crud');

View file

@ -33,35 +33,4 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// forceTLS: true
// });
window.Vue = require('vue').default;
import vSelect from "vue-select"
Vue.component('v-select', vSelect)
let vueOptions = {
el: "#app"
};
// register modules
const modules = require.context("./modules", true, /\.js$/i);
modules.keys().map(key => Vue.mixin(modules(key).default));
// Register components
const vues = require.context("./components", true, /\.vue$/i);
vues.keys().map(key =>
Vue.component(
key
.split("/")
.pop()
.split(".")[0]
.replace(/\.?([A-Z])/g, function (x, y) {
return "-" + y.toLowerCase();
})
.replace(/^-/, "")
.replace("-component", ""),
vues(key).default
)
);
const app = new Vue(vueOptions);
require('fslightbox');

View file

@ -1,112 +0,0 @@
<template>
<div>
<v-select
:id="name"
ref="select"
:loading="loading"
v-model="value"
:clearable="false"
label="label"
:options='data'
@search="search"
:filterable="true"
:disabled="dReadonly||dDisabled"
:searchable="true">
<li slot="list-footer" class="pagination">
<button
@click="$event.preventDefault(); page -= 1"
:disabled="!hasPrevPage"
class="btn btn-sm btn-primary mt-2 mb-2 ms-2 me-2">{{ __('Précédent') }}</button>
<button
@click="$event.preventDefault(); page += 1"
:disabled="!hasNextPage"
class="btn btn-sm btn-primary mt-2 mb-2">{{ __('Suivant') }}</button>
</li>
</v-select>
<input type="hidden" v-model="value.value" :name="name" />
</div>
</template>
<script>
export default {
mounted() {
this.dDisabled = !!this.disabled
this.dReadonly = !!this.readonly
this.value = JSON.parse(this.dbValue)
if(!this.value) {
this.value = {
label: null,
value: null
}
}
this.search(false)
},
props: [
'disabled',
'readonly',
'name',
'dbValue'
],
data() {
return {
dDisabled : false,
dReadonly : false,
value: {
label: null,
value: null
},
loading: false,
searchText: '',
data: [],
page: 1,
perPage: 15,
hasNextPage: true,
hasPrevPage: false
}
},
watch: {
page() {
if(this.page < 1) {
this.page = 1
}
if(this.page > 1) {
this.hasPrevPage = true
} else {
this.hasPrevPage = false
}
this.search()
},
data() {
if(this.data.length == this.perPage) {
this.hasNextPage = true
} else {
this.hasNextPage = false
}
}
},
methods: {
search (search = null) {
if(search !== false && search !== null) {
this.searchText = search
}
let init = search === false ? true : false
let route = this.route('model.index')
route += '?page='+this.page
route += '&s='+this.searchText
this.loading = true
axios.get(route)
.then(response => {
this.data = response.data
if (init && this.data.length && this.value.value == null) {
this.value = this.data[0]
}
})
.catch(error => {
console.log(error);
}).then(() => {
this.loading = false
})
},
}
}
</script>

View file

@ -1,108 +0,0 @@
<template>
<div>
<v-select
:id="name"
ref="select"
:loading="loading"
v-model="values"
:clearable="true"
label="label"
:options='data'
@search="search"
multiple
:disabled="dReadonly||dDisabled"
:filterable="true"
:searchable="true">
<li slot="list-footer" class="pagination">
<button
@click="$event.preventDefault(); page -= 1"
:disabled="!hasPrevPage"
class="btn btn-sm btn-primary mt-2 mb-2 ms-2 me-2">{{ __('Précédent') }}</button>
<button
@click="$event.preventDefault(); page += 1"
:disabled="!hasNextPage"
class="btn btn-sm btn-primary mt-2 mb-2">{{ __('Suivant') }}</button>
</li>
</v-select>
<input
type="hidden"
:name="name+'[]'"
v-for="item in values" :key="item.value"
:value="item.value"/>
</div>
</template>
<script>
export default {
mounted() {
this.dDisabled = !!this.disabled
this.dReadonly = !!this.readonly
let values = JSON.parse(this.dbValue)
for(let k in values) {
this.values.push(values[k])
}
this.search(false)
},
props: [
'disabled',
'readonly',
'name',
'dbValue'
],
data() {
return {
dDisabled : false,
dReadonly : false,
values: [],
loading: false,
searchText: '',
data: [],
page: 1,
perPage: 15,
hasNextPage: true,
hasPrevPage: false
}
},
watch: {
page() {
if(this.page < 1) {
this.page = 1
}
if(this.page > 1) {
this.hasPrevPage = true
} else {
this.hasPrevPage = false
}
this.search()
},
data() {
if(this.data.length == this.perPage) {
this.hasNextPage = true
} else {
this.hasNextPage = false
}
}
},
methods: {
search (search = null) {
if(search !== false && search !== null) {
this.searchText = search
}
let init = search === false ? true : false
let route = this.route('model.index')
route += '?page='+this.page
route += '&s='+this.searchText
this.loading = true
axios.get(route)
.then(response => {
this.data = response.data
})
.catch(error => {
console.log(error);
}).then(() => {
this.loading = false
})
},
}
}
</script>

19
resources/js/crud.js vendored
View file

@ -1,19 +0,0 @@
window.__CRUD = {
confirmDelete: (e) => {
let self = e.target
e.preventDefault();
Swal.fire({
title: 'Confirmez',
text: 'Voulez-vous vraiment supprimer cet élément ?',
icon: 'question',
confirmButtonText: 'Supprimer',
showCancelButton: true,
cancelButtonText: 'Annuler',
confirmButtonColor: '#dc3545'
}).then((result) => {
if (result.isConfirmed) {
self.submit()
}
})
}
}

Some files were not shown because too many files have changed in this diff Show more