master
Константин 2023-05-29 18:05:09 +03:00
parent 889db8f45e
commit 667b13c76c
523 changed files with 30343 additions and 48 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
Dockerfile
node_modules/
.git
.gitignore
.DS_Store
.env
storage/logs/laravel.log

49
.env.example Normal file
View File

@ -0,0 +1,49 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
* text=auto
*.css linguist-vendored
*.less linguist-vendored

62
.gitignore vendored
View File

@ -1,50 +1,16 @@
# These are some examples of commonly ignored file patterns. .vagrant/
# You should customize this list as applicable to your project. /vendor
# Learn more about .gitignore: /node_modules
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore Homestead.yaml
Homestead.json
# Node artifact files .env
node_modules/ #/public/css/
dist/ /public/js/
/public/fonts/
# Compiled Java class files /packages
*.class /storage/oauth-private.key
/storage/oauth-public.key
# Compiled Python bytecode
*.py[cod]
# Log files
*.log
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
.idea/ .idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.DS_Store .DS_Store
.phpunit.result.cache
# Generated by Windows docker/mysql/
Thumbs.db
# Applications
*.app
*.exe
*.war
# Large media files
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv

20
.php_cs.dist Normal file
View File

@ -0,0 +1,20 @@
<?php
use PhpCsFixer\Finder;
$project_path = getcwd();
$finder = Finder::create()
->in([
$project_path . '/app',
$project_path . '/config',
$project_path . '/database',
$project_path . '/resources',
$project_path . '/routes',
$project_path . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return \ShiftCS\styles($finder);

BIN
.rnd Normal file

Binary file not shown.

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jose@ditecnologia.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

32
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,32 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
We accept contributions via Pull Requests on [Github](https://github.com/joselfonseca/laravel-api).
## Pull Requests
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check and fix the code standard with ``$ composer cs``.
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
- **Create feature branches** - Don't ask us to pull from your master branch.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
## Running Tests
``` bash
$ composer test
```
**Happy coding**!

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM joselfonsecadt/nginx-php7.4:latest
WORKDIR /var/www/html/
COPY . /var/www/html/
EXPOSE 80
CMD ["/usr/bin/supervisord"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Jose Luis Fonseca
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

@ -0,0 +1,39 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
];
/**
* Define the application's command schedule.
*
* @codeCoverageIgnore
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
}
/**
* Register the Closure based commands for the application.
*
* @return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Events;
use App\Models\Asset;
/**
* Class AssetWasCreated.
*/
class AssetWasCreated
{
/**
* @var \App\Models\Asset
*/
public $asset;
/**
* AssetWasCreated constructor.
*
* @param \App\Models\Asset $asset
*/
public function __construct(Asset $asset)
{
$this->asset = $asset;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ForgotPasswordRequested {
use Dispatchable, InteractsWithSockets, SerializesModels;
public string $email;
public function __construct(string $email) {
$this->email = $email;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PasswordRecovered {
use Dispatchable, InteractsWithSockets, SerializesModels;
public User $user;
public string $password;
public function __construct(User $user, string $password) {
$this->user = $user;
$this->password = $password;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered {
use Dispatchable, InteractsWithSockets, SerializesModels;
public User $user;
public string $password;
public function __construct(User $user, string $password) {
$this->user = $user;
$this->password = $password;
}
public function broadcastOn(): PrivateChannel {
return new PrivateChannel('channel-name');
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* Class BodyTooLargeException.
*/
class BodyTooLargeException extends Exception
{
}

View File

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

View File

@ -0,0 +1,88 @@
<?php
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
/**
* Class Handler.
*/
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Throwable $exception
* @return void
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @codeCoverageIgnore
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof AuthenticationException) {
return response()->json([
'message' => 'Unauthenticated.',
'status_code' => 401,
], 401);
}
if ($exception instanceof BodyTooLargeException) {
return response()->json([
'message' => 'The body is too large',
'status_code' => 413,
], 413);
}
if ($exception instanceof ValidationException) {
return response()->json([
'message' => $exception->getMessage(),
'status_code' => 422,
'errors' => $exception->errors(),
], 422);
}
if ($exception instanceof StoreResourceFailedException) {
return response()->json([
'message' => $exception->getMessage(),
'status_code' => 422,
'errors' => $exception->errors,
], 422);
}
if ($exception instanceof NotFoundHttpException) {
return response()->json([
'message' => '404 Not Found',
'status_code' => 404,
], 404);
}
return parent::render($request, $exception);
}
}

View File

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

View File

@ -0,0 +1,17 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class StoreResourceFailedException extends Exception
{
public $errors = [];
public function __construct($message = '', $errors = [], $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->errors = $errors;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\Api\Assets;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class RenderFileController extends Controller
{
protected $model;
public function __construct(Asset $model)
{
$this->model = $model;
}
public function show($uuid, Request $request)
{
try {
$model = $this->model->byUuid($uuid)->firstOrFail();
return $this->renderFile($model, $request->get('width', null), $request->get('height', null));
} catch (ModelNotFoundException $e) {
return $this->renderPlaceholder($request->get('width', null), $request->get('height', null));
}
}
public function download($uuid) {
$model = $this->model->byUuid($uuid)->firstOrFail();
return response()->download(Storage::path($model->path), "{$model->name}.{$model->extension}", ["Content-Type: {$model->mime}"]);
}
public function open($uuid) {
$model = $this->model->byUuid($uuid)->firstOrFail();
return response()->file(Storage::path($model->path), ["Content-Type: {$model->mime}"]);
}
public function renderFile($model, $width, $height)
{
$image = $this->makeFromPath($width, $height, $model->path);
return $image->response();
}
public function renderPlaceholder($width, $height)
{
$image = Image::cache(function ($image) use ($width, $height) {
$img = $image->canvas(800, 800, '#FFFFFF');
$this->resize($img, $width, $height);
return $img;
}, 10, true);
return $image->response();
}
protected function resize($img, $width, $height)
{
if (! empty($width) && ! empty($height)) {
$img->resize($width, $height);
} elseif (! empty($width)) {
$img->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
});
} elseif (! empty($height)) {
$img->resize(null, $height, function ($constraint) {
$constraint->aspectRatio();
});
}
return $img;
}
protected function makeFromPath($width, $height, $path)
{
return Image::cache(function ($image) use ($path, $width, $height) {
$img = $image->make(Storage::get($path));
$this->resize($img, $width, $height);
return $img;
}, 10, true);
}
}

View File

@ -0,0 +1,167 @@
<?php
namespace App\Http\Controllers\Api\Assets;
use App\Events\AssetWasCreated;
use App\Exceptions\BodyTooLargeException;
use App\Exceptions\StoreResourceFailedException;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Transformers\Assets\AssetTransformer;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Psr\Http\Message\ResponseInterface;
class UploadFileController extends Controller {
protected array $validMimes = [
'image/jpeg' => [
'type' => 'image',
'extension' => 'jpeg',
],
'image/jpg' => [
'type' => 'image',
'extension' => 'jpg',
],
'image/png' => [
'type' => 'image',
'extension' => 'png',
],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => [
'type' => 'document',
'extension' => 'xlsx'
],
'application/vnd.ms-excel' => [
'type' => 'document',
'extension' => 'xls'
],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => [
'type' => 'document',
'extension' => 'docx'
],
'application/msword' => [
'type' => 'document',
'extension' => 'doc'
],
'application/pdf' => [
'type' => 'document',
'extension' => 'pdf'
]
];
protected Client $client;
protected Asset $model;
public function __construct(Client $client, Asset $model) {
$this->client = $client;
$this->model = $model;
}
public function store(Request $request): JsonResponse {
if ($request->isJson()) {
$asset = $this->uploadFromUrl([
'url' => $request->get('url'),
'user' => $request->user(),
]);
} elseif ($request->hasFile('file')) {
$file = $request->file('file')->getRealPath();
$asset = $this->uploadFromDirectFile([
'mime' => $request->file('file')->getClientMimeType(),
'name' => $request->get('name') ?? $request->file('file')->getClientOriginalName(),
'filename' => $request->file('file')->getClientOriginalName(),
'extension' => $request->file('file')->getClientOriginalExtension(),
'content' => file_get_contents($file),
'Content-Length' => $request->header('Content-Length'),
'user' => $request->user(),
'latitude' => $request->get('latitude'),
'longitude' => $request->get('longitude'),
'accuracy' => $request->get('accuracy')
]);
} else {
$body = !(base64_decode($request->getContent())) ? $request->getContent() : base64_decode($request->getContent());
$asset = $this->uploadFromDirectFile([
'mime' => $request->header('Content-Type'),
'content' => $body,
'Content-Length' => $request->header('Content-Length'),
'user' => $request->user(),
'latitude' => $request->get('latitude'),
'longitude' => $request->get('longitude')
]);
}
event(new AssetWasCreated($asset));
return fractal($asset, new AssetTransformer())->respond(201);
}
protected function uploadFromDirectFile($attributes = []) {
$this->validateMime($attributes['mime']);
$this->validateBodySize($attributes['Content-Length'], $attributes['content']);
$path = $this->storeInFileSystem($attributes);
return $this->storeInDatabase($attributes, $path);
}
protected function uploadFromUrl($attributes = []) {
$response = $this->callFileUrl($attributes['url']);
$attributes['mime'] = $response->getHeader('content-type')[0];
$attributes['filename'] = pathinfo($attributes['url'], PATHINFO_BASENAME);
$attributes['extension'] = pathinfo($attributes['url'], PATHINFO_EXTENSION);
$this->validateMime($attributes['mime']);
$attributes['content'] = $response->getBody();
$path = $this->storeInFileSystem($attributes);
return $this->storeInDatabase($attributes, $path);
}
protected function storeInDatabase(array $attributes, $path) {
$file = $this->model->create([
'type' => $this->validMimes[$attributes['mime']]['type'],
'path' => $path,
'mime' => $attributes['mime'],
'name' => $attributes['name'] ?? null,
'filename' => $attributes['filename'] ?? null,
'extension' => $attributes['extension'] ?? null,
'user_id' => ! empty($attributes['user']) ? $attributes['user']->id : null,
'latitude' => $attributes['latitude'],
'longitude' => $attributes['longitude'],
'accuracy' => $attributes['accuracy']
]);
return $file;
}
protected function storeInFileSystem(array $attributes): string {
$filename = md5(Str::random(16).date('U'));
$path = "upload/{$filename[0]}/$filename.{$this->validMimes[$attributes['mime']]['extension']}";
Storage::put($path, $attributes['content']);
return $path;
}
protected function callFileUrl($url): ResponseInterface {
try {
return $this->client->get($url);
} catch (TransferException $e) {
throw new StoreResourceFailedException('Validation Error', [
'url' => 'The url seems to be unreachable: '.$e->getCode(),
]);
}
}
protected function validateMime($mime) {
if (!array_key_exists($mime, $this->validMimes)) {
throw new StoreResourceFailedException('Validation Error', [
'Content-Type' => 'The Content Type sent is not valid',
]);
}
}
protected function validateBodySize($contentLength, $content) {
if ($contentLength > config('files.maxsize', 1000000) || mb_strlen($content) > config('files.maxsize', 1000000)) {
throw new BodyTooLargeException();
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Events\ForgotPasswordRequested;
use App\Events\PasswordRecovered;
use App\Exceptions\EmailNotSentException;
use App\Exceptions\PasswordNotUpdated;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Contracts\Auth\PasswordBroker;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
class PasswordsController extends Controller {
public function store(Request $request): JsonResponse {
$this->validate($request, [
'email' => 'required|email|exists:users,email'
]);
$response = $this->broker()->sendResetLink(['email' => $request->get('email')]);
if ($response == Password::RESET_LINK_SENT) {
event(new ForgotPasswordRequested($request->get('email')));
return response()->json(null, 201);
}
throw new EmailNotSentException(__($response));
}
public function update(Request $request): JsonResponse {
$this->validate($request, [
'email' => 'required|email|exists:users,email',
'password' => 'required|min:8'
]);
$response = $this->broker()->reset($request->except('_token'), function($user, $password) {
$user->password = Hash::make($password);
$user->save();
});
if ($response == Password::PASSWORD_RESET) {
if ($user = User::where('email', $request->get('email'))->first()) event(new PasswordRecovered($user, $request->get('password')));
return response()->json(['message' => __($response)]);
}
throw new PasswordNotUpdated(__($response));
}
public function broker(): PasswordBroker {
return Password::broker();
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Transformers\Users\UserTransformer;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RegisterController extends Controller {
protected User $model;
public function __construct(User $model) {
$this->model = $model;
}
public function checkEmail(Request $request): JsonResponse {
$this->validate($request, [
'email' => 'required|email|unique:users,email'
]);
return response()->json(null, 200);
}
public function store(Request $request): JsonResponse {
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
]);
$user = $this->model->create($request->all());
$user->assignRole('User');
event(new Registered($user));
return fractal($user, new UserTransformer())->respond(201);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Api\Forms;
use App\Http\Controllers\Controller;
use App\Models\Objects\NirObject;
use App\Models\Objects\ObjectType;
use App\Services\Filters\FiltersService;
use App\Services\Forms\FormsService;
use App\Transformers\Objects\ObjectTransformer;
use App\Transformers\Objects\ObjectTypeTransformer;
use Illuminate\Http\Request;
class FormsController extends Controller {
public function get(Request $request, $target, $type = null, $id = null) {
if ($target === 'model') {
return FormsService::getService($type)->form($id, json_decode($request->get('extra_props'), true) ?? []);
} elseif ($object = NirObject::byUuid($id)->first()) {
return fractal($object, new ObjectTransformer())->respond();
} elseif ($objectType = ObjectType::byUuidOrName($type)->first()) {
return fractal($objectType, new ObjectTypeTransformer())->respond();
}
}
public function save(Request $request, $target, $type = null, $id = null) {
if ($target === 'model') {
return FormsService::getService($type)->save($request->all(), $id);
} elseif ($object = NirObject::byUuid($id)->first()) {
$object->setValues($request->all());
return fractal($object->fresh(), new ObjectTransformer());
} elseif ($objectType = ObjectType::byUuidOrName($type)->first()) {
$object = $objectType->objects()->create();
$object->setValues($request->all());
return fractal($object->fresh(), new ObjectTransformer());
}
}
public function filters(Request $request, $type) {
$filters = collect(json_decode($request->get('filters', []), true));
return ($service = FiltersService::getService($type)) ? $service->get($filters) : null;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Transformers\Users\NotificationTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Notifications\DatabaseNotification;
class NotificationsController extends Controller {
public function __construct() {
}
public function list(Request $request): JsonResponse {
$result = fractal($request->user()->notifications, new NotificationTransformer())->respond();
$request->user()->notifications()->whereNull('read_at')->update(['read_at' => now()]);
return $result;
}
public function count(Request $request): JsonResponse {
return response()->json(['count' => $request->user()->unreadNotifications()->count()]);
}
public function show(Request $request, $uuid): JsonResponse {
$notification = DatabaseNotification::where('id', $uuid)->first();
$notification->markAsRead();
return fractal($notification, new NotificationTransformer())->respond();
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Api\Objects;
use App\Http\Controllers\Controller;
use App\Models\Objects\ObjectType;
use App\Transformers\Objects\ObjectTypeTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ObjectTypesController extends Controller {
protected ObjectType $model;
public function __construct(ObjectType $model) {
$this->model = $model;
}
public function index(Request $request): JsonResponse {
$parent = ($val = $request->get('parent')) ? $this->model->byUuidOrName($val)->first() : null;
$query = $parent ? $parent->children() : $this->model->query()->where(['parent_id' => 0]);
$query->orderBy('id');
$paginator = $query->paginate(config('app.pagination_limit'));
return fractal($paginator, new ObjectTypeTransformer())->respond();
}
public function show($id): JsonResponse {
$model = $this->model->byUuidOrName($id)->firstOrFail();
return fractal($model, new ObjectTypeTransformer())->respond();
}
public function store(Request $request): JsonResponse {
$this->validate($request, [
'name' => 'required',
'title' => 'required'
]);
$model = $this->model->create($request->all());
return fractal($model, new ObjectTypeTransformer())->respond(201);
}
public function update(Request $request, $uuid): JsonResponse {
$model = $this->model->byUuidOrName($uuid)->firstOrFail();
$this->validate($request, [
'name' => 'sometimes|required',
'title' => 'sometimes|required'
]);
$model->update($request->all());
return fractal($model->fresh(), new ObjectTypeTransformer())->respond();
}
public function destroy(Request $request, $uuid): JsonResponse {
$model = $this->model->byUuidOrName($uuid)->firstOrFail();
$model->delete();
return response()->json(null, 204);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Api\Objects;
use App\Http\Controllers\Controller;
use App\Models\Objects\Field;
use App\Models\Objects\NirObject;
use App\Models\Objects\ObjectType;
use App\Transformers\Objects\ObjectTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ObjectsController extends Controller {
protected NirObject $model;
public function __construct(NirObject $model) {
$this->model = $model;
}
public function index(Request $request): JsonResponse {
$query = $this->model->query();
if (($val = $request->get('type')) && ($type = ObjectType::query()->where(['name' => $val])->first())) {
$query->whereHas('type', function ($query) use ($val) {
$query->where('name', $val)->orWhereHas('parent', function ($query) use ($val) {
$query->where('name', $val);
});
});
if (in_array('product', [$type->name, $type->parent->name ?? null]) && ($field = Field::byUuidOrName('specification-holder')->first())) {
$field->applyFilter($query, $request->user()->companies->pluck('uuid')->all());
}
}
$filters = collect($request->has('filters') ? json_decode($request->get('filters'), true) : []);
$filters->each(function($value, $prop) use($query) {
if ($prop === 'search') $this->model->applySearchFilter($query, $value);
elseif ($field = Field::byUuidOrName($prop)->first()) $field->applyFilter($query, $value);
});
$query->orderBy('created_at', 'desc');
$paginator = $query->paginate(config('app.pagination_limit'));
return fractal($paginator, new ObjectTransformer())->respond();
}
public function show($id): JsonResponse {
$model = $this->model->byUuid($id)->firstOrFail();
return fractal($model, new ObjectTransformer())->respond();
}
public function store(Request $request): JsonResponse {
$this->validate($request, [
'type' => 'required',
'properties' => 'required|array'
]);
$objectType = ObjectType::byUuidOrName($request->get('type'))->firstOrFail();
$model = $objectType->objects()->create($request->only('name') + ['owner_id' => $request->user()->id]);
$model->setValues($request->get('properties'));
if ($objectType->name === 'product') $model->setValue('specification-holder', $request->user()->selectedMember->company->uuid ?? null);
return fractal($model, new ObjectTransformer())->respond(201);
}
public function update(Request $request, $uuid): JsonResponse {
$model = $this->model->byUuid($uuid)->firstOrFail();
$this->validate($request, [
'properties' => 'required|array'
]);
$model->setValues($request->get('properties'));
$model->update($request->only('name'));
return fractal($model->fresh(), new ObjectTransformer())->respond();
}
public function destroy(Request $request, $uuid): JsonResponse {
$model = $this->model->byUuid($uuid)->firstOrFail();
$model->delete();
return response()->json(null, 204);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Support\Carbon;
class PingController extends Controller
{
public function index()
{
return response()->json([
'status' => 'ok',
'timestamp' => Carbon::now(),
'host' => request()->ip(),
]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Http\Controllers\Controller;
use App\Models\Permission;
use App\Transformers\Users\PermissionTransformer;
use Illuminate\Http\Request;
class PermissionsController extends Controller
{
protected $model;
public function __construct(Permission $model)
{
$this->model = $model;
$this->middleware('permission:List permissions')->only('index');
}
public function index(Request $request)
{
$paginator = $this->model->paginate($request->get('limit', config('app.pagination_limit')));
if ($request->has('limit')) {
$paginator->appends('limit', $request->get('limit'));
}
return fractal($paginator, new PermissionTransformer())->respond();
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Exceptions\StoreResourceFailedException;
use App\Http\Controllers\Controller;
use App\Models\Companies\CompanyMember;
use App\Transformers\Companies\CompanyMemberTransformer;
use App\Transformers\Users\UserTransformer;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ProfileController extends Controller
{
public function membership(Request $request) {
return fractal($request->user()->membership, new CompanyMemberTransformer())->respond();
}
public function currentMember(Request $request) {
return fractal($request->user()->selectedMember, new CompanyMemberTransformer());
}
public function selectMember(Request $request) {
$this->validate($request, [
'member_id' => 'required|exists:company_members,uuid'
]);
CompanyMember::byUuid($request->get('member_id'))->firstOrFail()->select();
return fractal($request->user()->membership, new CompanyMemberTransformer())->respond();
}
public function index()
{
return fractal(Auth::user(), new UserTransformer())->respond();
}
public function update(Request $request)
{
$user = Auth::user();
$rules = [
'name' => 'required',
'email' => 'required|email|unique:users,email,'.$user->id,
];
if ($request->method() == 'PATCH') {
$rules = [
'name' => 'sometimes|required',
'email' => 'sometimes|required|email|unique:users,email,'.$user->id,
];
}
$this->validate($request, $rules);
// Except password as we don't want to let the users change a password from this endpoint
$user->update($request->except('_token', 'password'));
return fractal($user->fresh(), new UserTransformer())->respond();
}
public function updatePassword(Request $request)
{
$user = Auth::user();
$this->validate($request, [
'current_password' => 'required',
'password' => 'required|min:8|confirmed',
]);
// verify the old password given is valid
if (! app(Hasher::class)->check($request->get('current_password'), $user->password)) {
throw new StoreResourceFailedException('Validation Issue', [
'old_password' => 'The current password is incorrect',
]);
}
$user->password = bcrypt($request->get('password'));
$user->save();
return fractal($user->fresh(), new UserTransformer())->respond();
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Http\Controllers\Controller;
use App\Models\Role;
use App\Transformers\Users\RoleTransformer;
use Illuminate\Http\Request;
class RolesController extends Controller
{
protected $model;
public function __construct(Role $model)
{
$this->model = $model;
$this->middleware('permission:List roles')->only('index');
$this->middleware('permission:List roles')->only('show');
$this->middleware('permission:Create roles')->only('store');
$this->middleware('permission:Update roles')->only('update');
$this->middleware('permission:Delete roles')->only('destroy');
}
public function index(Request $request)
{
$paginator = $this->model->with('permissions')->paginate($request->get('limit', config('app.pagination_limit')));
if ($request->has('limit')) {
$paginator->appends('limit', $request->get('limit'));
}
return fractal($paginator, new RoleTransformer())->respond();
}
public function show($id)
{
$role = $this->model->with('permissions')->byUuid($id)->firstOrFail();
return fractal($role, new RoleTransformer())->respond();
}
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required',
]);
$role = $this->model->create($request->all());
if ($request->has('permissions')) {
$role->syncPermissions($request['permissions']);
}
return fractal($role, new RoleTransformer())->respond(201);
}
public function update(Request $request, $uuid)
{
$role = $this->model->byUuid($uuid)->firstOrFail();
$this->validate($request, [
'name' => 'required',
]);
$role->update($request->except('_token'));
if ($request->has('permissions')) {
$role->syncPermissions($request['permissions']);
}
return fractal($role->fresh(), new RoleTransformer())->respond();
}
public function destroy(Request $request, $uuid)
{
$role = $this->model->byUuid($uuid)->firstOrFail();
$role->delete();
return response()->json(null, 204);
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\Api\Users;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Transformers\Users\UserTransformer;
use Illuminate\Http\Request;
class UsersController extends Controller
{
protected $model;
public function __construct(User $model)
{
$this->model = $model;
$this->middleware('permission:List users')->only('index');
$this->middleware('permission:List users')->only('show');
$this->middleware('permission:Create users')->only('store');
$this->middleware('permission:Update users')->only('update');
$this->middleware('permission:Delete users')->only('destroy');
}
public function index(Request $request)
{
$paginator = $this->model->with('roles.permissions')->paginate($request->get('limit', config('app.pagination_limit', 20)));
if ($request->has('limit')) {
$paginator->appends('limit', $request->get('limit'));
}
return fractal($paginator, new UserTransformer())->respond();
}
public function show($id)
{
$user = $this->model->with('roles.permissions')->byUuid($id)->firstOrFail();
return fractal($user, new UserTransformer())->respond();
}
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
]);
$user = $this->model->create($request->all());
if ($request->has('roles')) {
$user->syncRoles($request['roles']);
}
return fractal($user, new UserTransformer())->respond(201);
}
public function update(Request $request, $uuid)
{
$user = $this->model->byUuid($uuid)->firstOrFail();
$rules = [
'name' => 'required',
'email' => 'required|email|unique:users,email,'.$user->id,
];
if ($request->method() == 'PATCH') {
$rules = [
'name' => 'sometimes|required',
'email' => 'sometimes|required|email|unique:users,email,'.$user->id,
];
}
$this->validate($request, $rules);
// Except password as we don't want to let the users change a password from this endpoint
$user->update($request->except('_token', 'password'));
if ($request->has('roles')) {
$user->syncRoles($request['roles']);
}
return fractal($user->fresh(), new UserTransformer())->respond();
}
public function destroy(Request $request, $uuid)
{
$user = $this->model->byUuid($uuid)->firstOrFail();
$user->delete();
return response()->json(null, 204);
}
}

View File

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

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

@ -0,0 +1,71 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\CORS::class,
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\AdvisorySession\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\TomLerendu\LaravelConvertCaseMiddleware\ConvertRequestToSnakeCase::class,
\TomLerendu\LaravelConvertCaseMiddleware\ConvertResponseToCamelCase::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CORS
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$response->headers->set("Access-Control-Allow-Origin", "*");
$response->headers->set("Access-Control-Allow-Credentials", "true");
$response->headers->set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, PATCH"); //Make sure you remove those you do not want to support
$response->headers->set("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, X-Requested-With, Application");
return $response;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}

View File

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

View File

@ -0,0 +1,52 @@
<?php
namespace App\Imports;
use App\Models\Advisories\Advisory;
use App\Models\Advisories\AdvisoryMemberRank;
use App\Models\Companies\Company;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
class AdvisoryMembersImport extends Import implements ToCollection, WithHeadingRow {
public Advisory $advisory;
public array $mapper = [
'name' => ['prop' => 'fio', 'required' => true],
'email' => ['prop' => 'email', 'required' => true],
'inn' => ['prop' => 'inn_organizacii', 'required' => true],
'position' => ['prop' => 'dolznost', 'required' => true],
'rank' => ['prop' => 'polozenie']
];
public function __construct(Advisory $advisory) {
$this->advisory = $advisory;
}
public function collection(Collection $collection) {
$collection->each(function($row) {
if ($row = $this->mapData($row)) $this->importRow($row);
});
}
public function importRow($row) {
$company = Company::getByData(['inn' => $row['inn']]);
$user = User::getByData($row + ['password' => 'Qwerty1!']);
$companyMember = $company->addMember($user, $row['position'] ?? null);
$this->advisory->addMember($companyMember, $this->translateRank($row['rank'] ?? null));
}
public function translateRank($rank) {
$aRanks = [
'председатель' => AdvisoryMemberRank::CHAIRMAN,
'заместитель председателя' => AdvisoryMemberRank::VICE_CHAIRMAN,
'секретарь' => AdvisoryMemberRank::SECRETARY
];
return $aRanks[Str::lower(trim($rank))] ?? AdvisoryMemberRank::ORDINARY;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Imports;
use App\Models\Catalog\Category;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
class CategoriesImport extends Import implements ToCollection {
public function collection(Collection $collection) {
$collection->each(function($row) {
list($number, $name) = $row;
if (($number = trim($number, ' ')) && ($name = trim($name))) {
$parent = $this->getParent($number);
Category::query()->firstOrCreate(['number' => $number, 'parent_id' => $parent ? $parent->id : 0])->update(['name' => $name]);
}
});
}
public function getParent($number) {
$result = false;
$digits = explode('.', $number);
if (count($digits) === 3) {
$digits = array_reverse($digits);
foreach ($digits as $k => $digit) {
if ($digit !== '00') {
$digits[$k] = '00';
break;
}
}
$parentNumber = implode('.', array_reverse($digits));
if ($parentNumber !== '00.00.00') {
$result = Category::firstOrCreate(['number' => $parentNumber]);
}
}
return $result;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Imports;
use App\Models\Classification\Classifier;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
class ClassifierCodesImport extends Import implements ToCollection {
public Classifier $classifier;
public function __construct(Classifier $classifier) {
$this->classifier = $classifier;
}
public function collection(Collection $collection) {
$collection->each(function($row) {
list($name, $title) = $row;
if (($name = trim($name)) && ($title = trim($title))) {
$this->classifier->codes()->firstOrCreate(['name' => $name])->update(['title' => $title]);
}
});
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Imports;
use App\Models\Companies\Company;
use App\Models\Companies\CompanyType;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
class CompaniesImport extends Import implements ToCollection, WithHeadingRow {
public array $mapper = [
'name' => ['prop' => 'nazvanie_organizacii', 'required' => true],
'inn' => ['prop' => 'inn_organizacii'],
'email' => ['prop' => 'email'],
'phone' => ['prop' => 'telefon'],
'types' => ['prop' => 'vid_organizacii']
];
public function collection(Collection $collection) {
$collection->each(function($row) {
if ($row = $this->mapData($row)) $this->importRow($row);
});
}
public function importRow($row) {
$row['inn'] = $row['inn'] ?? $row['name'];
$row['types'] = $this->translateTypes($row['types'] ?? '');
Company::getByData($row);
}
public function translateTypes(string $types): array {
$aTypes = [
'изготовитель' => CompanyType::PRODUCER,
'проектировщик' => CompanyType::DESIGNER
];
return collect(explode(';', $types))->map(function($type) use($aTypes) {
return $aTypes[mb_strtolower(trim($type))] ?? null;
})->filter(function($type) {return $type;})->all();
}
}

20
app/Imports/Import.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App\Imports;
use PhpOffice\PhpSpreadsheet\Shared\Date;
class Import {
public array $mapper = [];
public function mapData($row) {
$result = [];
foreach ($this->mapper as $int => $data) {
//if (!empty($data['prop']) && !empty($row[$data['prop']])) $result[$int] = trim(Str::replace(' ', '', $row[$data['prop']]));
if (!empty($data['prop']) && !empty($row[$data['prop']])) $result[$int] = trim($row[$data['prop']], '  ;,');
if (!empty($data['date']) && is_numeric($result[$int] ?? null)) $result[$int] = Date::excelToDateTimeObject($result[$int]);
if (!empty($data['required']) && empty($result[$int])) return false;
}
return $result;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Listeners;
use App\Mail\PasswordRecovered;
use Illuminate\Support\Facades\Mail;
class SendPasswordRecoveredNotification {
public function __construct() {
}
public function handle(object $event) {
try {
Mail::to($event->user->email)->send(new PasswordRecovered($event->user, $event->password));
} catch (\Exception $exception) {}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Listeners;
use App\Mail\UserRegistered;
use Illuminate\Support\Facades\Mail;
class SendRegistrationNotification {
public function __construct() {
}
public function handle(object $event) {
try {
Mail::to($event->user->email)->send(new UserRegistered($event->user, $event->password));
} catch (\Exception $exception) {}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PasswordRecovered extends Mailable implements ShouldQueue {
use Queueable, SerializesModels;
public User $recipient;
public string $password;
public function __construct(User $recipient, string $password) {
$this->recipient = $recipient;
$this->password = $password;
}
public function build(): PasswordRecovered {
return $this->subject('Пароль сброшен')->view('mail.user.password-recovered');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PasswordRefreshed extends Mailable implements ShouldQueue {
use Queueable, SerializesModels;
public User $user;
public string $password;
public function __construct(User $user, string $password) {
$this->user = $user;
$this->password = $password;
}
public function build() {
return $this->subject('Пароль обновлен')->view('mail.user.password-refreshed');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PasswordResetRequested extends Mailable implements ShouldQueue {
use Queueable, SerializesModels;
public User $recipient;
public string $token;
public function __construct(User $recipient, string $token) {
$this->recipient = $recipient;
$this->token = $token;
}
public function build(): PasswordResetRequested {
return $this->subject('Запрос на сброс пароля')->view('mail.user.password-reset-requested');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserRegistered extends Mailable implements ShouldQueue {
use Queueable, SerializesModels;
public User $recipient;
public string $password;
public function __construct(User $recipient, string $password) {
$this->recipient = $recipient;
$this->password = $password;
}
public function build(): UserRegistered {
return $this->subject('Данные для входа в систему')->view('mail.user.registered');
}
}

76
app/Models/Asset.php Normal file
View File

@ -0,0 +1,76 @@
<?php
namespace App\Models;
use App\Models\Objects\Field;
use App\Models\Objects\Values\RelationValue;
use App\Support\RelationValuesTrait;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Class Asset.
*/
class Asset extends Model {
use UuidScopeTrait, HasFactory, RelationValuesTrait;
/**
* @var array
*/
protected $guarded = ['id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function links()
{
$result = [
'open' => url("/api/assets/{$this->uuid}"),
'download' => url("/api/assets/{$this->uuid}/download")
];
if ($this->type == 'image') {
$result['full'] = url("/api/assets/{$this->uuid}/render");
$result['thumb'] = url("/api/assets/{$this->uuid}/render?width=300");
}
return $result;
}
public function coordinates()
{
return [
'latitude' => $this->latitude,
'longitude' => $this->longitude,
'accuracy' => $this->accuracy
];
}
public function output()
{
return [
'id' => $this->uuid,
'type' => $this->type,
'mime' => $this->mime,
'filename' => $this->filename,
'extension' => $this->extension,
'links' => $this->links(),
'user' => $this->user->output(),
'coordinates' => $this->coordinates(),
'created_at' => $this->created_at->toIso8601String(),
'created_by' => $this->user->name,
];
}
public function saveValue($fieldName, $object)
{
$field = Field::byUuidOrName($fieldName)->first();
$relationValue = RelationValue::create();
$relationValue->set($this);
$relationValue->update([
'field_id' => $field->id,
'object_id' => $object->id,
]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Models\Dictionaries;
use App\Support\RelationValuesTrait;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Dictionary extends Model {
use UuidScopeTrait, SoftDeletes, RelationValuesTrait;
protected $dates = [
];
protected $fillable = [
'uuid',
'name',
'title'
];
protected $hidden = [
'id'
];
public function items(): HasMany {
return $this->hasMany(DictionaryItem::class);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models\Dictionaries;
use App\Support\RelationValuesTrait;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class DictionaryItem extends Model {
use UuidScopeTrait, SoftDeletes, RelationValuesTrait;
protected $dates = [
];
protected $fillable = [
'uuid',
'dictionary_id',
'name',
'title'
];
protected $hidden = [
'id'
];
public function dictionary(): BelongsTo {
return $this->belongsTo(Dictionary::class);
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace App\Models\Objects;
use App\Services\Filters\FiltersService;
use App\Support\UuidScopeTrait;
use App\Transformers\Objects\OptionTransformer;
use Faker\Generator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class Field extends Model {
use UuidScopeTrait, SoftDeletes;
protected $dates = [
];
protected $fillable = [
'uuid',
'type',
'name',
'title',
'required',
'multiple',
'filterable',
'hidden',
'params'
];
protected $hidden = [
'id'
];
protected $casts = [
'params' => 'array'
];
public function groups(): BelongsToMany {
return $this->belongsToMany(FieldsGroup::class, 'fields_group_fields', 'field_id', 'group_id');
}
public function values(): HasMany {
return $this->hasMany(FieldType::CLASSES[$this->type]);
}
public function objectValues($objectId): HasMany {
return $this->values()->where(['object_id' => $objectId]);
}
public function objectsValues(array $ids): HasMany {
return $this->values()->whereIn('object_id', $ids);
}
public function objectTypeValues($objectTypeId): HasMany {
return $this->values()->whereHas('object', function($query) use($objectTypeId) {
$query->where(['type_id' => $objectTypeId]);
});
}
public function getOptionsAttribute(): ?Collection {
if (($this->type === FieldType::RELATION) && !empty($this->params['options']['show'])) {
if ($model = $this->params['related'] ?? null) {
$query = $model::query();
collect($this->params['options']['whereHas'] ?? [])->each(function($val, $prop) use($query) {
$query->whereHas($prop, function($query) use($val) {
foreach ($val as $attr => $v) {
$query->where([$attr => $v]);
}
});
});
if ($where = $this->params['options']['where'] ?? null) $query->where($where);
return $query->get();
}
}
return null;
}
public function getTransformerAttribute() {
$transformer = $this->params['transformer'] ?? OptionTransformer::class;
return new $transformer;
}
public function getRepresented($filters = [], ?FiltersService $service = null): ?Collection {
if ($model = $this->params['related'] ?? null) {
$query = $model::whereHas('relationValues', function($query) use($filters, $service) {
$query->where(['field_id' => $this->id])->whereHas('object', function($query) use($filters, $service) {
self::applyFilters($query, collect($filters)->except($this->name), $service);
});
});
return $query->get();
}
return null;
}
public function getRange($filters = [], ?FiltersService $service = null): ?array {
if (in_array($this->type, [FieldType::INTEGER, FieldType::FLOAT, FieldType::DATE, FieldType::DATETIME])) {
$query = $this->values()->whereHas('object', function($query) use($filters, $service) {
self::applyFilters($query, collect($filters)->except($this->name), $service);
});
return ['min' => $query->min('value') ?? 0, 'max' => $query->max('value') ?? 0];
}
return null;
}
public function getValue($objectId) {
return $this->objectValues($objectId)->get()->map(function($value) {
return $value->get();
});
}
public function setValue($objectId, $value) {
if ($this->multiple) $this->objectValues($objectId)->delete();
if (!($value instanceof Collection)) $value = collect((is_array($value) && !Arr::isAssoc($value)) ? $value : [$value]);
$value->each(function($value) use($objectId) {
if ($this->multiple) $this->values()->create(['object_id' => $objectId])->set($value);
else $this->values()->firstOrCreate(['object_id' => $objectId])->set($value);
});
return $this->getValue($objectId);
}
public function addValue($objectId, $value) {
if ($this->multiple) {
if (!($value instanceof Collection)) $value = collect((is_array($value) && !Arr::isAssoc($value)) ? $value : [$value]);
$value->each(function($value) use($objectId) {
$this->values()->create(['object_id' => $objectId])->set($value);
});
}
return $this->getValue($objectId);
}
public static function applyFilters(Builder $query, Collection $filters, ?FiltersService $service = null) {
if ($types = $filters->get('types')) {
$filters->forget('types');
$query->whereHas('type', function($query) use($types) {
$query->whereIn('uuid', is_array($types) ? $types : [$types]);
});
$filters->filter(function($value) {return $value;})->each(function($value, $prop) use($query, $types, $service) {
$field = Field::query()->where(['name' => $prop])->whereHas('groups.objectType', function($query) use($types) {
$query->whereIn('uuid', is_array($types) ? $types : [$types]);
})->first();
if ($field) $field->applyFilter($query, $value);
});
if ($service && $service->objectRelationName) $query->whereHas($service->objectRelationName, function($query) use($service, $filters) {
if (method_exists($service, 'applyNativeFilters')) $service->applyNativeFilters($query, $filters);
if (method_exists($service, 'applyPermissionsFilters')) $service->applyPermissionsFilters($query);
});
}
}
public function applyFilter($query, $value) {
if ($value = $this->prepareFilterValue($value)) $query->whereHas("{$this->type}Values", function($query) use($value) {
$query->where(['field_id' => $this->id]);
$class = FieldType::CLASSES[$this->type];
$class::applyFilter($query, $value);
});
}
public function prepareFilterValue($value) {
return is_array($value) ? collect($value)->filter(function($val) {
return !is_null($val);
})->all() : $value;
}
public function fakeValue($objectId) {
$faker = new Generator();
if ($this->type === FieldType::STRING) $value = $faker->sentence(4);
elseif ($this->type === FieldType::TEXT) $value = $faker->realText(rand(50, 240));
elseif ($this->type === FieldType::INTEGER) $value = rand(0, 500);
elseif ($this->type === FieldType::FLOAT) $value = $faker->randomFloat(1, 0, 100);
elseif ($this->type === FieldType::BOOLEAN) $value = boolval(rand(0,1));
elseif ($this->type === FieldType::DATE) $value = $faker->dateTimeBetween('-5 years', '+ 3 years');
elseif ($this->type === FieldType::DATETIME) $value = $faker->dateTimeBetween('-5 years', '+ 3 years');
elseif ($this->type === FieldType::RELATION) $value = $this->options->random(($this->multiple) ? rand(1, 3) : 1);
$this->setValue($objectId, $value ?? null);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Models\Objects;
use App\Models\Objects\Values\BooleanValue;
use App\Models\Objects\Values\DatetimeValue;
use App\Models\Objects\Values\DateValue;
use App\Models\Objects\Values\DocumentValue;
use App\Models\Objects\Values\FloatValue;
use App\Models\Objects\Values\ImageValue;
use App\Models\Objects\Values\IntegerValue;
use App\Models\Objects\Values\RelationValue;
use App\Models\Objects\Values\StringValue;
use App\Models\Objects\Values\TextValue;
use App\Models\Objects\Values\TimeValue;
class FieldType {
public const STRING = 'string';
public const TEXT = 'text';
public const INTEGER = 'integer';
public const FLOAT = 'float';
public const BOOLEAN = 'boolean';
public const DATE = 'date';
public const TIME = 'time';
public const DATETIME = 'datetime';
public const RELATION = 'relation';
public const DOCUMENT = 'document';
public const IMAGE = 'image';
public const TITLES = [
self::STRING => 'Строка',
self::TEXT => 'Текст',
self::INTEGER => 'Целое число',
self::FLOAT => 'Число с точкой',
self::BOOLEAN => 'Двоичное',
self::DATE => 'Дата',
self::TIME => 'Время',
self::DATETIME => 'Дата и время',
self::RELATION => 'Отношение',
self::DOCUMENT => 'Документ',
self::IMAGE => 'Изображение'
];
public const TABLES = [
self::STRING => 'field_string_values',
self::TEXT => 'field_text_values',
self::INTEGER => 'field_integer_values',
self::FLOAT => 'field_float_values',
self::BOOLEAN => 'field_boolean_values',
self::DATE => 'field_date_values',
self::TIME => 'field_time_values',
self::DATETIME => 'field_datetime_values',
self::RELATION => 'field_relation_values',
self::DOCUMENT => 'field_document_values',
self::IMAGE => 'field_image_values'
];
public const CLASSES = [
self::STRING => StringValue::class,
self::TEXT => TextValue::class,
self::INTEGER => IntegerValue::class,
self::FLOAT => FloatValue::class,
self::BOOLEAN => BooleanValue::class,
self::DATE => DateValue::class,
self::TIME => TimeValue::class,
self::DATETIME => DatetimeValue::class,
self::RELATION => RelationValue::class,
self::DOCUMENT => DocumentValue::class,
self::IMAGE => ImageValue::class
];
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Models\Objects;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
class FieldsGroup extends Model {
use UuidScopeTrait, SoftDeletes;
protected $dates = [
];
protected $fillable = [
'uuid',
'object_type_id',
'name',
'title',
'hidden',
'params'
];
protected $hidden = [
'id'
];
protected $casts = [
'params' => 'array'
];
public function fields(): BelongsToMany {
return $this->belongsToMany(Field::class, 'fields_group_fields', 'group_id')->orderByPivot('ord');
}
public function objectType(): BelongsTo {
return $this->belongsTo(ObjectType::class);
}
public function syncFields(array $fieldsNames) {
$data = [];
collect($fieldsNames)->each(function($name, $i) use(&$data) {
if ($field = Field::query()->where('name', $name)->first()) $data[$field->id] = ['ord' => $i];
});
$this->fields()->sync($data);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace App\Models\Objects;
use App\Models\Catalog\Specification;
use App\Models\Objects\Values\BooleanValue;
use App\Models\Objects\Values\DateValue;
use App\Models\Objects\Values\DocumentValue;
use App\Models\Objects\Values\FloatValue;
use App\Models\Objects\Values\ImageValue;
use App\Models\Objects\Values\IntegerValue;
use App\Models\Objects\Values\RelationValue;
use App\Models\Objects\Values\StringValue;
use App\Models\Objects\Values\TextValue;
use App\Models\Polls\PollInvitation;
use App\Models\User;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
class NirObject extends Model {
use UuidScopeTrait, SoftDeletes;
protected $table = 'objects';
protected $dates = [
];
protected $fillable = [
'uuid',
'type_id',
'owner_id',
'name'
];
protected $hidden = [
'id'
];
public function objects(): MorphToMany {
return $this->morphToMany(NirObject::class, 'objectable');
}
public function pollInvitations(): MorphToMany {
return $this->morphedByMany(PollInvitation::class, 'objectable');
}
public function type(): BelongsTo {
return $this->belongsTo(ObjectType::class);
}
public function owner(): BelongsTo {
return $this->belongsTo(User::class);
}
public function properties(): HasMany {
return $this->type->groups()->with('fields');
}
public function groups(): HasMany {
return $this->type->groups();
}
public function stringValues(): HasMany {
return $this->hasMany(StringValue::class, 'object_id');
}
public function textValues(): HasMany {
return $this->hasMany(TextValue::class, 'object_id');
}
public function integerValues(): HasMany {
return $this->hasMany(IntegerValue::class, 'object_id');
}
public function floatValues(): HasMany {
return $this->hasMany(FloatValue::class, 'object_id');
}
public function booleanValues(): HasMany {
return $this->hasMany(BooleanValue::class, 'object_id');
}
public function dateValues(): HasMany {
return $this->hasMany(DateValue::class, 'object_id');
}
public function relationValues(): HasMany {
return $this->hasMany(RelationValue::class, 'object_id');
}
public function documentValues(): HasMany {
return $this->hasMany(DocumentValue::class, 'object_id');
}
public function imageValues(): HasMany {
return $this->hasMany(ImageValue::class, 'object_id');
}
public function scopeApplyFilters(Builder $query, array $filters): Builder {
collect($filters)->each(function($value, $prop) use($query) {
if ($field = Field::byUuidOrName($prop)->first()) $field->applyFilter($query, $value);
});
return $query;
}
public function getValuesAttribute(): array {
$result = [];
$this->properties->map(function($group) use(&$result) {
$group->fields->map(function($field) use(&$result) {
$result[$field->name] = $this->getValue($field->name);
});
});
return $result;
}
public function getValue($fieldName) {
return ($field = $this->type->getField($fieldName)) ? $field->getValue($this->id) : null;
}
public function value($fieldName) {
$result = null;
if ($field = $this->type->getField($fieldName)) {
$result = $field->getValue($this->id);
if ($result && !$field->multiple) $result = $result->first();
}
return $result;
}
public function setValues(array $values): Collection {
return collect($values)->map(function($value, $fieldName) {
return $this->setValue($fieldName, $value);
});
}
public function setValue($fieldName, $value) {
return ($field = $this->type->getField($fieldName)) ? $field->setValue($this->id, $value) : null;
}
public function addValue($fieldName, $value) {
return ($field = $this->type->getField($fieldName)) ? $field->addValue($this->id, $value) : null;
}
public function clone() {
$clone = $this->type->objects()->create(['name' => $this->name, 'owner_id' => $this->owner_id]);
$this->properties->each(function($group) use($clone) {
$group->fields->each(function($field) use($clone) {
$clone->setValue($field->name, $this->getValue($field->name));
});
});
return $clone;
}
public function applySearchFilter(Builder $query, $search) {
$query->whereHas('stringValues', function($query) use($search) {
$query->where('value', 'like', "%{$search}%");
})->orWhereHas('textValues', function($query) use($search) {
$query->where('value', 'like', "%{$search}%");
});
}
public function fakeValues() {
$this->type->fields()->get()->each(function($field) {
$field->fakeValue($this->id);
});
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Models\Objects;
use App\Support\RelationValuesTrait;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class ObjectType extends Model {
use UuidScopeTrait, SoftDeletes, RelationValuesTrait;
protected $dates = [
];
protected $fillable = [
'uuid',
'parent_id',
'name',
'title'
];
protected $hidden = [
'id'
];
public function parent(): BelongsTo {
return $this->belongsTo(ObjectType::class, 'parent_id');
}
public function children(): HasMany {
return $this->hasMany(ObjectType::class, 'parent_id');
}
public function objects(): HasMany {
return $this->hasMany(NirObject::class, 'type_id');
}
public function groups(): HasMany {
return $this->hasMany(FieldsGroup::class)->orderBy('ord');
}
public function fields(): Builder {
return Field::query()->whereHas('groups', function($query) {
$query->where(['object_type_id' => $this->id]);
});
}
public function getField($name) {
return $this->fields()->where(['name' => $name])->first();
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models\Objects\Values;
class BooleanValue extends Value {
protected $table = 'field_boolean_values';
public function get() {
return is_null($this->value) ? null : boolval($this->value);
}
public function set($value): bool {
if ($value === 'false') $value = false;
if ($value === 'null') $value = null;
return parent::set(is_null($value) ? null : boolval($value));
}
public static function applyFilter($query, $value) {
parent::applyFilter($query, ($value === 'yes'));
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models\Objects\Values;
use Carbon\Carbon;
class DateValue extends Value {
protected $table = 'field_date_values';
protected $dates = [
'value'
];
public function get() {
return $this->value ? $this->value->toIso8601String() : null;
}
public function set($value): bool {
return parent::set($value);
}
public static function applyFilter($query, $value) {
$value = collect($value)->map(function($value) {
return $value ? Carbon::create($value)->format('Y-m-d') : null;
});
parent::applyFilter($query, $value->all());
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models\Objects\Values;
use Carbon\Carbon;
class DatetimeValue extends Value {
protected $table = 'field_datetime_values';
protected $dates = [
'value'
];
public function get() {
return $this->value ? $this->value->toIso8601String() : null;
}
public function set($value): bool {
return parent::set($value);
}
public static function applyFilter($query, $value) {
$value = collect($value)->map(function($value) {
return $value ? Carbon::create($value)->format('Y-m-d H:i') : null;
});
parent::applyFilter($query, ($value->count() === 2) ? $value->all() : $value->first());
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models\Objects\Values;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DocumentValue extends Value {
protected $table = 'field_document_values';
protected $fillable = [
'object_id',
'field_id',
'asset_id'
];
public function asset(): BelongsTo {
return $this->belongsTo(Asset::class);
}
public function get() {
return $this->asset;
}
public function set($value): bool {
if (!is_object($value)) $value = Asset::query()->where(['uuid' => $value])->first();
return is_object($value) ? $this->update(['asset_id' => $value->id]) : !$this->delete();
}
public static function applyFilter($query, $value) {
$value = is_array($value) ? $value : [$value];
$query->whereHas('asset', function($query) use($value) {
$query->where(function($query) use($value) {
$query->whereIn('uuid', $value)->orWhereIn('name', $value);
});
});
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models\Objects\Values;
class FloatValue extends Value {
protected $table = 'field_float_values';
public function get() {
return $this->value ? floatval($this->value) : null;
}
public function set($value): bool {
return parent::set(floatval($value));
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models\Objects\Values;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ImageValue extends Value {
protected $table = 'field_document_values';
protected $fillable = [
'object_id',
'field_id',
'asset_id'
];
public function asset(): BelongsTo {
return $this->belongsTo(Asset::class);
}
public function get() {
return $this->asset;
}
public function set($value): bool {
if (!is_object($value)) $value = Asset::query()->where(['uuid' => $value])->first();
return is_object($value) ? $this->update(['asset_id' => $value->id]) : !$this->delete();
}
public static function applyFilter($query, $value) {
$value = is_array($value) ? $value : [$value];
$query->whereHas('asset', function($query) use($value) {
$query->where(function($query) use($value) {
$query->whereIn('uuid', $value)->orWhereIn('name', $value);
});
});
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models\Objects\Values;
class IntegerValue extends Value {
protected $table = 'field_integer_values';
public function get() {
return $this->value ? intval($this->value) : null;
}
public function set($value): bool {
return parent::set(intval($value));
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Models\Objects\Values;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Arr;
class RelationValue extends Value {
protected $table = 'field_relation_values';
protected $fillable = [
'object_id',
'field_id',
'relatable_type',
'relatable_id'
];
public function relatable(): MorphTo {
return $this->morphTo();
}
public function get() {
return $this->relatable;
}
public function set($value): bool {
if (!is_object($value) && ($class = $this->field->params['related'] ?? null) && class_exists($class)) {
if (!is_array($value)) $value = $class::query()->where(['uuid' => $value])->first();
elseif (Arr::isAssoc($value)) {
if ($id = $value['id'] ?? $value['uuid'] ?? null) {
$value = $class::query()->where(is_numeric($id) ? ['id' => $id] : ['uuid' => $id])->first();
} else $value = $class::query()->where($value)->first();
}
}
return is_object($value) ? boolval($value->relationValues()->save($this)) : !$this->delete();
}
public static function applyFilter($query, $value) {
$value = is_array($value) ? $value : [$value];
$query->whereHas('relatable', function ($query) use ($value) {
$query->where(function ($query) use ($value) {
$query->whereIn('uuid', $value)->orWhereIn('name', $value);
});
});
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models\Objects\Values;
class StringValue extends Value {
protected $table = 'field_string_values';
public function get() {
return $this->value ? trim($this->value) : null;
}
public function set($value): bool {
return parent::set(trim($value));
}
public static function applyFilter($query, $value) {
collect(explode(' ', trim($value)))->each(function($term) use($query) {
$query->where('value', 'like', "%{$term}%");
});
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models\Objects\Values;
class TextValue extends Value {
protected $table = 'field_text_values';
public function get() {
return $this->value ? trim($this->value) : null;
}
public function set($value): bool {
return parent::set(trim($value));
}
public static function applyFilter($query, $value) {
collect(explode(' ', trim($value)))->each(function($term) use($query) {
$query->where('value', 'like', "%{$term}%");
});
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models\Objects\Values;
use Carbon\Carbon;
class TimeValue extends Value {
protected $table = 'field_time_values';
protected $dates = [
'value'
];
public function get() {
return $this->value ? $this->value->toIso8601String() : null;
}
public function set($value): bool {
return parent::set($value);
}
public static function applyFilter($query, $value) {
$value = collect($value)->map(function($value) {
return $value ? Carbon::create($value)->format('H:i:s') : null;
});
parent::applyFilter($query, $value->all());
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Models\Objects\Values;
use App\Models\Objects\Field;
use App\Models\Objects\NirObject;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
abstract class Value extends Model {
protected $dates = [
];
protected $fillable = [
'object_id',
'field_id',
'value'
];
protected $hidden = [
'id'
];
public function object(): BelongsTo {
return $this->belongsTo(NirObject::class, 'object_id');
}
public function field(): BelongsTo {
return $this->belongsTo(Field::class);
}
public function get() {
return $this->value;
}
public function set($value): bool {
return $this->update(['value' => $value]);
}
public static function applyFilter($query, $value) {
if (is_array($value)) {
if (!empty($value['gt']) && !empty($value['lt'])) $query->whereBetween('value', $value);
elseif (!empty($value['gt'])) $query->where('value', '>=', $value['gt']);
elseif (!empty($value['lt'])) $query->where('value', '<=', $value['lt']);
} else $query->where('value', '=', $value);
}
}

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

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* Class Permission.
*/
class Permission extends \Spatie\Permission\Models\Permission
{
use UuidScopeTrait, HasFactory;
/**
* @var array
*/
protected $fillable = ['name', 'uuid', 'guard_name'];
}

20
app/Models/Role.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use App\Support\HasPermissionsUuid;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* Class Role.
*/
class Role extends \Spatie\Permission\Models\Role
{
use UuidScopeTrait, HasPermissionsUuid, HasFactory;
/**
* @var array
*/
protected $fillable = ['name', 'uuid', 'guard_name'];
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SocialProvider extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'provider',
'provider_id',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

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

@ -0,0 +1,112 @@
<?php
namespace App\Models;
use App\Events\UserRegistered;
use App\Mail\PasswordResetRequested;
use App\Support\HasRolesUuid;
use App\Support\HasSocialLogin;
use App\Support\RelationValuesTrait;
use App\Support\UuidScopeTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable {
use Notifiable, UuidScopeTrait, HasFactory, HasApiTokens, HasRoles, SoftDeletes, HasSocialLogin, RelationValuesTrait, HasRolesUuid {
HasRolesUuid::getStoredRole insteadof HasRoles;
}
protected $dates = [
'deleted_at'
];
protected $fillable = [
'uuid',
'asset_id',
'name',
'phone',
'email',
'password'
];
protected $hidden = [
'id',
'password',
'remember_token',
];
public function socialProviders(): HasMany {
return $this->hasMany(SocialProvider::class);
}
public function avatar(): BelongsTo {
return $this->belongsTo(Asset::class, 'asset_id');
}
public function getInitialsAttribute(): string {
return collect(explode(' ', $this->name))->slice(0, 2)->map(function($item) {
return Str::upper(Str::substr($item, 0, 1));
})->join('');
}
public function getFirstAndMidNameAttribute(): string {
return collect(explode(' ', $this->name))->slice(1, 2)->join(' ');
}
public function getIsAdminAttribute(): bool {
return $this->hasRole('Administrator');
}
public static function getByData($data, $triggerEvent = false) {
$result = false;
if ($email = trim($data['email'] ?? null)) {
$result = self::query()->where(['email' => $email])->first();
if (!$result) {
$password = $data['password'] ?? (App::environment('local') ? 'Qwerty1!' : Str::random(8));
$result = self::create(['email' => $email, 'password' => $password]);
$result->update(['name' => trim($data['name'] ?? null), 'phone' => trim($data['phone'] ?? null)]);
$result->assignRole($data['role'] ?? 'User');
if ($triggerEvent) event(new UserRegistered($result, $password));
} else {
if ($val = trim($data['phone'] ?? null)) $result->update(['phone' => $val]);
}
if ($val = $data['avatar'] ?? null) $result->setAvatar($val);
}
return $result;
}
public function setAvatar($val) {
$asset = Asset::byUuid($val)->first();
$this->update(['asset_id' => $asset->id ?? null]);
}
public static function create(array $attributes = []) {
if (array_key_exists('password', $attributes)) {
$attributes['password'] = Hash::make($attributes['password']);
}
return static::query()->create($attributes);
}
public function sendPasswordResetNotification($token) {
Mail::to($this->email)->send(new PasswordResetRequested($this, $token));
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\OAuthGrants;
use DateInterval;
use Illuminate\Http\Request;
use Laravel\Passport\Bridge\User;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
class SocialGrant extends AbstractGrant
{
public function __construct(UserRepositoryInterface $userRepository, RefreshTokenRepositoryInterface $refreshTokenRepository)
{
$this->setUserRepository($userRepository);
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new DateInterval('P1M');
}
/**
* {@inheritdoc}
*/
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL)
{
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$user = $this->validateUser($request);
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
return $responseType;
}
/**
* {@inheritdoc}
*/
public function getIdentifier()
{
return 'social_grant';
}
/**
* @param ServerRequestInterface $request
*
* @throws OAuthServerException
*
* @return UserEntityInterface
*/
protected function validateUser(ServerRequestInterface $request)
{
$laravelRequest = new Request($request->getParsedBody());
$user = $this->getUserEntityByRequest($laravelRequest);
if (false === $user instanceof UserEntityInterface) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
}
return $user;
}
/**
* Retrieve user by request.
*
* @param \Illuminate\Http\Request $request
*
* @throws \League\OAuth2\Server\Exception\OAuthServerException
*
* @return null|\Laravel\Passport\Bridge\User
*/
protected function getUserEntityByRequest(Request $request)
{
if (is_null($model = config('auth.providers.users.model'))) {
throw OAuthServerException::serverError('Unable to determine user model from configuration.');
}
if (method_exists($model, 'byOAuthToken')) {
$user = (new $model())->byOAuthToken($request);
} else {
throw OAuthServerException::serverError('Unable to find byLoggedInUser method on user model.');
}
return ($user) ? new User($user->id) : null;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Providers;
use App\Models\Advisories\Advisory;
use App\Models\Advisories\AdvisoryMember;
use App\Models\Advisories\AdvisorySession;
use App\Models\Advisories\SessionInvitation;
use App\Models\Applications\Application;
use App\Models\Asset;
use App\Models\Catalog\Category;
use App\Models\Classification\Classifier;
use App\Models\Classification\Code;
use App\Models\Companies\Address;
use App\Models\Companies\BankDetails;
use App\Models\Companies\Company;
use App\Models\Companies\CompanyMember;
use App\Models\Companies\Contact;
use App\Models\Companies\Department;
use App\Models\Correspondence\Letter;
use App\Models\Dictionaries\Dictionary;
use App\Models\Dictionaries\DictionaryItem;
use App\Models\Normatives\Development\Development;
use App\Models\Normatives\Development\DevelopmentStage;
use App\Models\Normatives\Normative;
use App\Models\Normatives\Plans\Plan;
use App\Models\Notes\Note;
use App\Models\Objects\Field;
use App\Models\Objects\FieldsGroup;
use App\Models\Objects\NirObject;
use App\Models\Objects\ObjectType;
use App\Models\Permission;
use App\Models\Polls\Poll;
use App\Models\Polls\PollInvitation;
use App\Models\Processes\Demand;
use App\Models\Processes\Participant;
use App\Models\Processes\Performer;
use App\Models\Processes\Process;
use App\Models\Processes\Scenario;
use App\Models\Processes\Stage;
use App\Models\Processes\Task;
use App\Models\Role;
use App\Models\SocialProvider;
use App\Models\User;
use App\Models\UserKey;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot() {
Carbon::setLocale(config('app.locale'));
if ($this->app->environment('local')) Mail::alwaysTo('panabonic@yandex.ru');
Relation::enforceMorphMap([
'asset' => Asset::class,
'permission' => Permission::class,
'role' => Role::class,
'social-provider' => SocialProvider::class,
'user' => User::class,
'dictionary' => Dictionary::class,
'dictionary-item' => DictionaryItem::class,
'object-field' => Field::class,
'fields-group' => FieldsGroup::class,
'object' => NirObject::class,
'object-type' => ObjectType::class
]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Providers;
use App\OAuthGrants\SocialGrant;
use Carbon\Carbon;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Bridge\RefreshTokenRepository;
use Laravel\Passport\Bridge\UserRepository;
use Laravel\Passport\Passport;
use League\OAuth2\Server\AuthorizationServer;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
$this->extendAuthorizationServer();
Passport::routes();
Passport::tokensExpireIn(Carbon::now()->addHours(24));
Passport::refreshTokensExpireIn(Carbon::now()->addDays(60));
}
protected function makeSocialRequestGrant()
{
$grant = new SocialGrant(
$this->app->make(UserRepository::class),
$this->app->make(RefreshTokenRepository::class)
);
$grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());
return $grant;
}
protected function extendAuthorizationServer()
{
$this->app->extend(AuthorizationServer::class, function ($server) {
return tap($server, function ($server) {
$server->enableGrantType(
$this->makeSocialRequestGrant(),
Passport::tokensExpireIn()
);
});
});
}
}

View File

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

View File

@ -0,0 +1,29 @@
<?php
namespace App\Providers;
use App\Events\PasswordRecovered;
use App\Events\UserRegistered;
use App\Listeners\SendPasswordRecoveredNotification;
use App\Listeners\SendRegistrationNotification;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider {
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
UserRegistered::class => [
SendRegistrationNotification::class
],
PasswordRecovered::class => [
SendPasswordRecoveredNotification::class
]
];
public function boot() {
parent::boot();
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* This is used by Laravel authentication to redirect users after login.
*
* @var string
*/
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.
*
* @return void
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(300)->by(optional($request->user())->id ?: $request->ip());
});
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Services;
use App\Models\Companies\Company;
use Exception;
use Illuminate\Support\Facades\Storage;
use MoveMoveIo\DaData\Enums\BranchType;
use MoveMoveIo\DaData\Enums\CompanyType as DaDataCompanyType;
use MoveMoveIo\DaData\Facades\DaDataCompany;
class DaDataService {
public string $inn;
public ?string $kpp;
public function __construct($inn, $kpp = null) {
$this->inn = $inn;
$this->kpp = $kpp;
}
public function saveToCompany(Company $company): Company {
if ($result = $this->getCompanyData()) {
if (!empty($result['company'])) $company->update($result['company']);
if (!empty($result['address'])) {
$company->legalAddress->update($result['address']);
$company->actualAddress->update($result['address']);
}
}
return $company;
}
public function getCompanyData() {
$result = [];
$data = $this->getSavedResponse();
if (!$data && is_numeric($this->inn)) {
try {
$data = DaDataCompany::id($this->inn, 1, $this->kpp, BranchType::MAIN, DaDataCompanyType::LEGAL);
$this->saveResponse($data);
} catch (Exception $exception) {
echo 'INN: ' . $this->inn . ' error: ' . $exception->getMessage() . "\n";
return false;
}
}
if ($data && is_array($data) && !empty($data['suggestions'])) {
$data = $data['suggestions'];
if (!empty($data[0])) $data = $data[0];
$data = $data['data'];
$result['company'] = [
'name' => $data['name']['short_with_opf'] ?? null,
'full_name' => $data['name']['full_with_opf'] ?? null,
'kpp' => $data['kpp'] ?? null,
'ogrn' => $data['ogrn'] ?? null,
'okpo' => $data['okpo'] ?? null,
'okved' => $data['okved'] ?? null
];
$result['address'] = [
'full' => $data['address']['unrestricted_value'] ?? null,
'postcode' => $data['address']['data']['postal_code'] ?? null,
'country' => $data['address']['data']['country'] ?? null,
'region' => $data['address']['data']['region'] ?? null,
'city' => $data['address']['data']['city'] ?? null,
'district' => $data['address']['data']['federal_district'] ?? null,
'street' => $data['address']['data']['street_with_type'] ?? null,
'house' => "{$data['address']['data']['house_type_full']} {$data['address']['data']['house']}",
'block' => "{$data['address']['data']['block_type_full']} {$data['address']['data']['block']}",
'office' => "{$data['address']['data']['flat_type_full']} {$data['address']['data']['flat']}"
];
}
return $result;
}
public function saveResponse($data) {
Storage::put($this->makePath(), collect($data)->toJson());
}
public function getSavedResponse() {
$path = $this->makePath();
return Storage::exists($path) ? json_decode(Storage::get("dadata/{$this->inn}.json"), true) : null;
}
public function makePath(): string {
return "dadata/{$this->inn}.json";
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Services\Documents;
use App\Models\Asset;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class DocumentGeneratorService {
public string $template;
public array $data;
public array $options = [];
public array $mimes = [
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pdf' => 'application/pdf',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
public static array $classes = [
'pdf' => GeneratePdfDocument::class,
'word' => GenerateWordDocument::class
];
public static function generate(string $type, string $template, array $data, array $options = []) {
return ($class = self::$classes[$type] ?? null) ? new $class($template, $data, $options) : null;
}
public function __construct(string $template, array $data, array $options = []) {
$this->template = $template;
$this->data = $data;
$this->options = array_merge($this->options, $options);
}
public function makeFilePath($ext): string {
$fileName = Str::lower(Str::random());
$dir = "documents/generated/{$fileName[0]}";
Storage::makeDirectory($dir);
return "{$dir}/{$fileName}.{$ext}";
}
public function makeAsset($path, $name) {
$info = pathinfo($path);
return Asset::create([
'type' => 'document',
'path' => $path,
'mime' => $this->mimes[$info['extension']] ?? null,
'name' => $name ?? $info['basename'],
'filename' => $info['basename'],
'extension' => $info['extension'],
'user_id' => ($user = Auth::user()) ? $user->id : null
]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Services\Documents;
use Barryvdh\DomPDF\Facade as PDF;
use Illuminate\Support\Facades\Storage;
class GeneratePdfDocument extends DocumentGeneratorService {
public array $options = [
'orientation' => 'portrait'
];
public string $fontName = 'Times New Roman';
public int $fontSize = 10;
public function __construct(string $template, array $data, array $options) {
$template = "documents.pdf.{$template}";
parent::__construct($template, $data, $options);
}
public function file(): string {
$pdf = PDF::loadView($this->template, $this->data);
if ($this->options['orientation'] === 'landscape') $pdf->setPaper('a4', 'landscape');
$filePath = $this->makeFilePath('pdf');
Storage::put($filePath, $pdf->download()->getOriginalContent());
return $filePath;
}
public function asset(string $name) {
return $this->makeAsset($this->file(), $name);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Services\Documents;
use Exception;
use Illuminate\Support\Facades\Storage;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Shared\Html;
class GenerateWordDocument extends DocumentGeneratorService {
public array $options = [
'marginTop' => 300,
'marginLeft' => 600,
'marginRight' => 600,
'colsNum' => 1,
'pageNumberingStart' => 1,
'orientation' => 'portrait'
];
public string $fontName = 'Times New Roman';
public int $fontSize = 10;
public function __construct(string $template, array $data, array $options = []) {
$template = "documents.word.{$template}";
parent::__construct($template, $data, $options);
}
public function file(): string {
$template = view($this->template, $this->data);
$phpWord = new PhpWord();
$phpWord->setDefaultFontName($this->fontName);
$phpWord->setDefaultFontSize($this->fontSize);
$section = $phpWord->addSection($this->options);
Html::addHtml($section, $template->render(), false, false);
try {
$filePath = $this->makeFilePath('docx');
$objWriter = IOFactory::createWriter($phpWord);
$objWriter->save(Storage::path($filePath));
return $filePath;
} catch (Exception $e) {
var_dump($e->getMessage());
}
}
public function asset(string $name = null) {
return $this->makeAsset($this->file(), $name);
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Services\Filters;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Date;
class FiltersService {
public static array $services = [
];
public function __construct() {
}
public static function getService($type) {
foreach (self::$services as $service) {
$services = $service::$services;
if (($class = $services[$type] ?? null) && class_exists($class)) return new $class();
}
return null;
}
public function applyOrder($query, Collection $order) {
$by = $order->get('by', 'dates');
$dir = $order->get('dir', 'desc');
if ($by === 'dates') $query->orderBy('created_at', $dir);
elseif ($by === 'names') $query->orderBy('name', $dir);
}
public function applySearchFilter($query, $string, $props = [], $has = []) {
$this->stringToWords($string)->each(function($word) use($query, $props, $has) {
$query->where(function($query) use($word, $props, $has) {
collect($props)->each(function($prop, $index) use($query, $word) {
if (is_array($prop)) $this->applyRelationalSearchFilter($query, $prop, $word);
else $index ? $query->orWhere($prop, 'like', $word) : $query->where($prop, 'like', $word);
});
});
});
}
public function applyRelationalSearchFilter($query, $data, $word) {
collect($data)->each(function($props, $relation) use($query, $word) {
$query->orWhereHas($relation, function($query) use($props, $word) {
collect($props)->each(function($prop, $index) use($query, $word) {
$index ? $query->orWhere($prop, 'like', $word) : $query->where($prop, 'like', $word);
});
});
});
}
public function stringToWords($string): Collection {
return collect(explode(' ', $string))->map(function($word) {
return '%' . trim($word) . '%';
});
}
public function applyDateFilter($query, $prop, $value) {
if ($v = $value['gt'] ?? null) $query->where($prop, '>=', Date::create($v));
if ($v = $value['lt'] ?? null) $query->where($prop, '<=', Date::create($v));
}
public function applyRelationFilter(Builder $query, $relation, $value) {
$query->whereHas($relation, function($query) use($value) {
$query->whereIn('uuid', is_array($value) ? $value : [$value]);
});
}
public function getRelationItems(array $items): array {
return ['data' => collect($items)->map(function($title, $id) {
return ['id' => $id, 'title' => $title];
})->values()];
}
public function getRelationValue($value, $items): array {
return collect($value)->filter(function($value) {return $value;})->map(function($value) use($items) {
return ['id' => $value, 'title' => $items[$value] ?? null];
})->values()->all();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Services\Forms;
use App\Services\Forms\Users\UserFormsServices;
class FormsService {
public array $formTitles = ['create' => '', 'update' => ''];
public static array $services = [
UserFormsServices::class
];
public function __construct() {
}
public static function getService($type) {
foreach (self::$services as $service) {
$services = $service::$services;
if (($class = $services[$type] ?? null) && class_exists($class)) return new $class();
}
return null;
}
public function formTitle($model = null): ?string {
return $this->formTitles[$model ? 'update' : 'create'] ?? null;
}
public function form(?string $id = null, array $data = []): array {
return [];
}
public function save(array $data, ?string $id = null) {
return $id ? $this->update($id, $data) : $this->store($data);
}
public function store(array $data) {
return null;
}
public function update(string $id, array $data) {
return null;
}
protected function getRelationItems(array $items): array {
return ['data' => collect($items)->map(function($title, $name) {
return ['id' => $name, 'title' => $title];
})->values()->all()];
}
protected function getRelationValue(array $items, ?string $id): array {
return ['data' => collect($items)->filter(function($title, $name) use ($id) {
return $name == $id;
})->map(function($title, $name) {
return ['id' => $name, 'title' => $title];
})->values()->all()];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Services\Forms\Users;
use App\Models\Objects\FieldType;
use App\Models\User;
use App\Services\Forms\FormsService;
use App\Transformers\Assets\AssetTransformer;
use App\Transformers\Users\UserTransformer;
use Illuminate\Http\JsonResponse;
class UserForms extends FormsService {
public array $formTitles = ['create' => 'Создание нового профиля', 'update' => 'Редактирование профиля'];
public function form(?string $id = null, array $data = []): array {
$model = User::byUuid($id)->first();
$groups = [
['name' => 'common', 'fields' => $this->commonGroupFields($model)]
];
return ['title' => $this->formTitle($model), 'data' => $groups];
}
public function commonGroupFields(?User $model): array {
$fields = [
[
'name' => 'email',
'title' => 'Электронная почта',
'type' => FieldType::STRING,
'required' => true,
'readonly' => !!$model,
'value' => $model->email ?? null
],
[
'name' => 'name',
'title' => 'ФИО',
'type' => FieldType::STRING,
'required' => true,
'value' => $model->name ?? null
],
[
'name' => 'phone',
'title' => 'Телефон',
'type' => FieldType::STRING,
'value' => $model->phone ?? null
],
[
'name' => 'avatar',
'title' => 'Фотография профиля',
'type' => FieldType::IMAGE,
'value' => ($model->avatar ?? null) ? fractal($model->avatar, new AssetTransformer()) : null
]
];
return ['data' => $fields];
}
public function store(array $data): ?JsonResponse {
$model = User::getByData($data, true);
return fractal($model, new UserTransformer())->respond();
}
public function update(string $id, array $data): ?JsonResponse {
$model = User::byUuid($id)->firstOrFail();
$model->update($data);
$model->setAvatar($data['avatar'] ?? null);
return fractal($model->fresh(), new UserTransformer())->respond();
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Services\Forms\Users;
class UserFormsServices {
public static array $services = [
'user' => UserForms::class
];
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Model;
abstract class ModelPropertyParser {
//todo parse multiple property values
public static function parse(Model $model, $prop) {
$result = null;
collect(explode('->', $prop))->each(function($prop) use($model, &$result) {
$func = $params = null;
if (strpos($prop, '(')) {
list($func, $params) = explode('(', $prop);
$params = trim($params, '()"') ?: null;
}
if ($result && is_object($result)) $result = $func ? ($params ? $result->$func($params) : $result->$func()) : $result->$prop;
else $result = $func ? ($params ? $model->$func($params) : $model->$func()) : $model->$prop;
if (!$result) return false;
});
return $result;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Services;
use App\Models\Objects\NirObject;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class PermissionsService {
private Model $model;
private ?User $user;
private array $rules = [
NirObject::class => 'nirObject'
];
public function __construct(Model $model, ?User $user = null) {
$this->model = $model;
$this->user = $user ?? Auth::user();
}
public function get(): array {
$rule = $this->rules[get_class($this->model)] ?? null;
$func = "{$rule}Permissions";
if ($this->user->isPrivileged) return ['anything' => true];
elseif (method_exists($this, $func)) return $this->$func();
else return [];
}
public function nirObjectPermissions(): array {
return ['edit' => $this->model->owner_id === $this->user->id];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Support;
use App\Models\Classification\Code;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Str;
trait ClassifiableTrait {
public function codes(): MorphToMany {
return $this->morphToMany(Code::class, 'classifiable', 'classifiable')->withTimestamps();
}
public function getCodes($classifier) {
return $this->codes()->whereHas('classifier', function($query) use($classifier) {
$query->where(['name' => $classifier]);
})->get();
}
public function setCodes($codes) {
$codes = collect($codes)->map(function($code) {
return trim(Str::replace(' ', '', $code));
})->all();
$this->codes()->sync(Code::query()->whereIn('name', $codes)->orWhereIn('uuid', $codes)->pluck('id')->all());
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Support;
trait HasDeadlineTrait {
public function getDaysTillDeadlineAttribute(): int {
return now()->diffInDays($this->deadline, false);
}
public function getDaysTillDeadlineLabelAttribute(): string {
return $this->daysTillDeadline . ' ' . trans_choice('день|дня|дней', $this->daysTillDeadline);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Support;
use App\Models\Objects\NirObject;
use App\Models\Objects\ObjectType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
trait HasObjectsTrait {
public function objects(): MorphToMany {
return $this->morphToMany(NirObject::class, 'objectable')->withTimestamps();
}
public function getObjectAttribute() {
return $this->objects()->first();
}
public function getObject($typeName): ?Model {
return ($type = ObjectType::query()->where(['name' => $typeName])->first()) ? $this->objects()->firstOrCreate(['type_id' => $type->id]) : null;
}
public function createObject($typeName): ?Model {
return ($type = ObjectType::query()->where(['name' => $typeName])->first()) ? $this->objects()->create(['type_id' => $type->id]) : null;
}
public function getValue($fieldName) {
return $this->object ? $this->object->getValue($fieldName) : null;
}
public function setValue($fieldName, $value) {
return $this->object ? $this->object->setValue($fieldName, $value) : null;
}
public function setValues(array $values) {
return $this->object ? $this->object->setValues($values) : null;
}
public function addValue($fieldName, $value) {
return $this->object ? $this->object->addValue($fieldName, $value) : null;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Support;
use App\Models\Permission as PermissionEntity;
use Spatie\Permission\Contracts\Permission;
/**
* Class HasPermissionsUuid.
*/
trait HasPermissionsUuid
{
/**
* Added support to use a UUID to find the Permission.
*
* @param string|array|Permission|\Illuminate\Support\Collection $permissions
*
* @return Permission
*/
protected function getStoredPermission($permissions): Permission
{
if (is_string($permissions)) {
return app(PermissionEntity::class)->where('name', $permissions)->orWhere('uuid', $permissions)->first();
}
if (is_array($permissions)) {
return app(PermissionEntity::class)->whereIn('name', $permissions)->orWhereIn('uuid', $permissions)->get();
}
return $permissions;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Support;
use App\Models\Role as RoleEntity;
use Spatie\Permission\Contracts\Role;
trait HasRolesUuid
{
/**
* @param $role
*
* @return Role
*/
protected function getStoredRole($role): Role
{
if (is_string($role)) {
return app(RoleEntity::class)->where('name', $role)->orWhere('uuid', $role)->first();
}
return $role;
}
}

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