Версия 0.1.0 #1

Merged
kor-elf merged 20 commits from develop into main 2024-04-24 21:56:47 +05:00
121 changed files with 3298 additions and 78 deletions
Showing only changes of commit e41f63f94f - Show all commits

View File

@ -111,6 +111,7 @@ ENTRYPOINT ["composer"]
FROM BUILD AS NPM
RUN mkdir "/.npm" && chmod -R 0777 "/.npm"
WORKDIR /var/www/html
STOPSIGNAL SIGTERM
RUN apk --no-cache add nodejs npm

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Contracts\Models;
use App\Enums\StorageType;
use App\Models\Storage as StorageModel;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Collection;
interface Storage
{
public function storage(): MorphMany;
public function getStorageOne(StorageType $type): ?StorageModel;
public function getStorageMany(StorageType $type): Collection;
}

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface StorageType
{
public function getTitle(): string;
public function getAcceptMimes(): array;
public function getFolderName(): string;
public static function cases(): array;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts\StorageType;
use App\Contracts\StorageType as StorageTypeContract;
interface Audio extends StorageTypeContract
{
public function isAudio(): bool;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts\StorageType;
use App\Contracts\StorageType as StorageTypeContract;
interface Image extends StorageTypeContract
{
public function isImage(): bool;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts\StorageType;
use App\Contracts\StorageType as StorageTypeContract;
interface Video extends StorageTypeContract
{
public function isVideo(): bool;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class Project
{
public function __construct(
) { }
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Language;
use App\Dto\Service\Dto;
final readonly class NewLanguage extends Dto
{
public function __construct(
private string $name,
private int $index,
) { }
public function getName(): string
{
return $this->name;
}
public function getIndex(): int
{
return $this->index;
}
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
use App\Dto\Builder\Project;
use App\Dto\Service\Pages;
final readonly class Index extends Pages
{
public function __construct(
private Project $projectBuilderDto,
int $page
) {
parent::__construct($page);
}
public function getProjectBuilderDto(): Project
{
return $this->projectBuilderDto;
}
}

View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
use App\Dto\Service\Dto;
final readonly class Language extends Dto
{
public function __construct(
private string $title,
private string $code,
private int $sort,
private bool $isDefault,
private ?int $id,
) { }
public function getTitle(): string
{
return $this->title;
}
public function getCode(): string
{
return $this->code;
}
public function getSort(): int
{
return $this->sort;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function getId(): ?int
{
return $this->id;
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
final class Languages
{
private $languages = [];
public function addLanguage(Language $language): void
{
$this->languages[] = $language;
}
public function getLanguages(): array
{
return $this->languages;
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project;
use App\Dto\Service\Dto;
use App\Dto\Service\Storage\Storages;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $code,
private bool $isPublic,
private Languages $languages,
private Storages $storages,
private ?string $httpHost,
) { }
public function getName(): string
{
return $this->name;
}
public function getCode(): string
{
return $this->code;
}
public function isPublic(): bool
{
return $this->isPublic;
}
public function getLanguages(): Languages
{
return $this->languages;
}
public function getHttpHost(): ?string
{
return $this->httpHost;
}
public function getStorages(): Storages
{
return $this->storages;
}
}

View File

@ -3,22 +3,19 @@
namespace App\Dto\Service\Admin\Role;
use App\Dto\Builder\Role;
use App\Dto\Service\Dto;
use App\Dto\Service\Pages;
final readonly class Index extends Dto
final readonly class Index extends Pages
{
public function __construct(
private Role $roleBuilderDto,
private int $page
) { }
int $page
) {
parent::__construct($page);
}
public function getRoleBuilderDto(): Role
{
return $this->roleBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -3,22 +3,19 @@
namespace App\Dto\Service\Admin\User;
use App\Dto\Builder\User;
use App\Dto\Service\Dto;
use App\Dto\Service\Pages;
final readonly class Index extends Dto
final readonly class Index extends Pages
{
public function __construct(
private User $userBuilderDto,
private int $page
) { }
int $page
) {
parent::__construct($page);
}
public function getUserBuilderDto(): User
{
return $this->userBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
abstract readonly class Pages extends Dto
{
public function __construct(
private int $page
) { }
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Contracts\StorageType;
use App\Dto\Service\Dto;
use App\Exceptions\Dto\Storage\FileException;
final readonly class File extends Dto
{
public function __construct(
private int $id,
private StorageType $storageType,
) {
if ($this->id < 1) {
throw new FileException('ID cannot be equal to or less than zero: ' . $this->id);
}
}
public function getId(): int
{
return $this->id;
}
public function getStorageType(): StorageType
{
return $this->storageType;
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Dto\Service\Dto;
use App\Enums\StorageType;
use app\Exceptions\Dto\Storage\StorageException;
final readonly class Storage extends Dto
{
private ?File $file;
private bool $isDelete;
public function __construct(
array $data,
private StorageType $storageType,
private bool $isMany,
) {
$file = null;
if (isset($data['file'])) {
if ((int) $data['file'] <= 0) {
throw new StorageException('ID cannot be equal to or less than zero: ' . $data['file']);
}
$file = new File(
id: (int) $data['file'],
storageType: $this->storageType,
);
}
$this->file = $file;
$this->isDelete = !empty($data['delete']);
}
public function getFile(): ?File
{
return $this->file;
}
public function isDelete(): bool
{
return $this->isDelete;
}
public function isFile(): bool
{
return \is_null($this->file) === false;
}
public function getStorageType(): StorageType
{
return $this->storageType;
}
public function isMany(): bool
{
return $this->isMany;
}
}

View File

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Enums\StorageType;
use app\Exceptions\Dto\Storage\StoragesException;
final class Storages
{
private array $storages = [];
public function add(array $data, StorageType $type): void
{
if (isset($this->storages[$type->value])) {
throw new StoragesException('You cannot attach two files of the same type!');
}
$this->storages[$type->value] = new Storage(data: $data, storageType: $type, isMany: false);
}
public function addMany(array $data, StorageType $type): void
{
if (!isset($this->storages[$type->value])) {
$this->storages[$type->value] = [];
}
foreach ($data as $storageData) {
$this->storages[$type->value][] = new Storage(data: $storageData, storageType: $type, isMany: true);
}
}
public function toArray(): array
{
return $this->storages;
}
public function getAllStorageIds(): array
{
$ids = [];
foreach ($this->storages as $storage) {
/** @var Storage $storage */
if (!is_array($storage)) {
if ($storage->isFile() && !$storage->isDelete()) {
$ids[] = $storage->getFile()->getId();
}
continue;
}
foreach ($storage as $storageOne) {
/** @var Storage $storageOne */
if ($storageOne->isFile() && !$storageOne->isDelete()) {
$ids[] = $storageOne->getFile()->getId();
}
}
}
return $ids;
}
public function getAllStorages(): array
{
$storages = [];
foreach ($this->storages as $storage) {
if (!is_array($storage)) {
$storages[] = $storage;
continue;
}
foreach ($storage as $storageOne) {
$storages[] = $storageOne;
}
}
return $storages;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Storage;
use App\Dto\Service\Dto;
use App\Enums\Morph;
use App\Enums\StorageType;
use Illuminate\Http\UploadedFile;
final readonly class Upload extends Dto
{
public function __construct(
private UploadedFile $file,
private StorageType $storageType,
private Morph $morph,
) { }
public function getFile(): UploadedFile
{
return $this->file;
}
public function getStorageType(): StorageType
{
return $this->storageType;
}
public function getMorph(): Morph
{
return $this->morph;
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Enums;
use App\Models\Project;
enum Morph: int
{
case Project = 1;
public function getPathModel(): string
{
return match ($this) {
self::Project => Project::class,
};
}
public function getFolderName(): string
{
return $this->name;
}
public static function map(): array
{
$map = [];
foreach (self::cases() as $item) {
$map[$item->value] = $item->getPathModel();
}
return $map;
}
}

View File

@ -7,6 +7,7 @@ enum Permission: string
case AdminPanel = 'allow-admin-panel';
case Role = 'role';
case User = 'user';
case Project = 'project';
public function getPermissions(): array
{

View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace App\Enums;
use App\Contracts\StorageType\Audio;
use App\Contracts\StorageType\Image;
use App\Contracts\StorageType\Video;
enum StorageType: int implements Image, Video, Audio
{
case Logo = 1;
public function getTitle(): string
{
return match ($this) {
self::Logo => __('validation.attributes.logo'),
};
}
public function getAcceptMimes(): array
{
return match ($this) {
self::Logo => ['jpeg', 'jpg', 'png'],
};
}
public function isImage(): bool
{
return match ($this) {
self::Logo => true,
default => false
};
}
public function isVideo(): bool
{
return match ($this->name) {
default => false
};
}
public function isAudio(): bool
{
return match ($this->name) {
default => false
};
}
public function getFolderName(): string
{
return $this->name;
}
}

View File

@ -5,4 +5,5 @@ namespace App\Enums;
enum SystemRole: string
{
case Admin = 'admin';
case AdminProject = 'admin-project';
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Dto\Storage;
final class FileException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace app\Exceptions\Dto\Storage;
final class StorageException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace app\Exceptions\Dto\Storage;
final class StoragesException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace app\Exceptions\Services\Storage;
final class StorageCommandHandlerException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Services\Storage;
final class StorageSaveFileException extends \Exception
{
}

View File

@ -5,6 +5,7 @@ namespace App\Helpers;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
final readonly class Helpers
{
@ -22,4 +23,18 @@ final readonly class Helpers
public static function getUserTimeZone() {
return auth()->user()?->timezone ?? config('app.user_timezone');
}
/**
* $name = 'field[key]' return 'field.key'
*/
public static function formatAttributeNameToRequestName(string $name): string
{
return Str::of($name)
->replace(
['.', '[', ']'],
['_', '.', ''],
)
->rtrim('.')
->value();
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\Languages\NewLanguageRequest;
use App\Http\Resources\Admin\Languages\NewLanguage;
use App\Services\Admin\LanguageService;
use Illuminate\Http\JsonResponse;
final class LanguagesController extends Controller
{
public function __construct(
private readonly LanguageService $languageService,
) { }
public function newLanguage(NewLanguageRequest $request): JsonResponse
{
$result = $this->languageService->newLanguage($request->getDto());
if ($result->isError()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new NewLanguage($result));
}
}

View File

@ -0,0 +1,93 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Admin\Projects\IndexRequest;
use App\Http\Requests\Admin\Projects\StoreUpdateRequest;
use App\Services\Admin\ProjectService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ProjectsController extends Controller
{
public function __construct(
private readonly ProjectService $projectService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->projectService->index($data->getProjectBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/projects/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->projectService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/projects/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->projectService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/projects/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->projectService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.projects.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->projectService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.projects.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->projectService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.projects.index')->withSuccess($result->getMessage());
}
}

View File

@ -22,7 +22,7 @@ final class RolesController extends Controller
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
queryWith: ['morph']
);
$result = $this->roleService->index($data->getRoleBuilderDto(), $querySettingsDto, $user);

View File

@ -11,7 +11,7 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UsersController extends Controller
final class UsersController extends Controller
{
public function __construct(
private readonly UserService $userService

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Storage;
use App\Http\Controllers\Controller as BaseController;
abstract class Controller extends BaseController
{
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace App\Http\Controllers\Storage;
use App\Http\Requests\Storage\ImageRequest;
use App\Http\Resources\Storage\Upload;
use App\Services\Storage\ImageService;
final class ImagesController extends Controller
{
public function __construct(
private readonly ImageService $imageService,
) { }
public function uploadAndResize(ImageRequest $request)
{
$data = $request->getDto();
$user = $request->user();
$result = $this->imageService->uploadAndResize($data, $user);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new Upload($result->getStorage()));
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Languages;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Language\NewLanguage;
use Illuminate\Foundation\Http\FormRequest;
class NewLanguageRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'index' => ['required', 'numeric'],
'name' => ['required', 'string'],
];
}
public function getDto(): NewLanguage
{
return new NewLanguage(
name: $this->input('name'),
index: $this->input('index'),
);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Projects;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\Project;
use App\Dto\Service\Admin\Project\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('admin.projects.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
projectBuilderDto: new Project(),
page: (int) $this->input('page', 1)
);
}
}

View File

@ -0,0 +1,104 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Projects;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\Language;
use App\Dto\Service\Admin\Project\Languages;
use App\Dto\Service\Admin\Project\StoreUpdate;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use App\Rules\HttpHost;
use Illuminate\Foundation\Http\FormRequest;
class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'code' => ['required', 'string', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/i'],
'http_host' => ['nullable', 'string', 'max:255', new HttpHost()],
'is_public' => ['required', 'boolean'],
'logo.file' => ['nullable', 'numeric', 'min:1'],
'logo.delete' => ['nullable', 'boolean'],
'languages.items.*.id' => ['nullable', 'numeric'],
'languages.items.*.title' => ['required', 'string', 'max:255'],
'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-zA-Z_]+$/i'],
'languages.items.*.sort' => ['required', 'numeric', 'min:-1000', 'max:1000'],
'languages.default' => ['required', 'numeric', function (string $attribute, mixed $value, \Closure $fail) {
$languages = $this->input('languages.items', []);
if (!isset($languages[$value])) {
$this->validator->getMessageBag()->add('languages.default', __('validation.required', ['attribute' => __('validation.attributes.language-default')]));
}
}],
'languages.items' => ['required', 'array', function (string $attribute, mixed $value, \Closure $fail) {
$values = [];
foreach ($value as $index => $item) {
if (!isset($item['code'])) {
continue;
}
if (in_array($item['code'], $values)) {
$this->validator->getMessageBag()->add('languages.items.' . $index . '.code', __('validation.unique', ['attribute' => __('validation.attributes.language-code')]));
}
$values[] = $item['code'];
}
}],
];
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
code: $this->input('code'),
isPublic: (bool) $this->input('is_public', false),
languages: $this->languages(),
storages: $this->storages(),
httpHost: $this->input('http_host', null),
);
}
private function languages(): Languages
{
$languages = new Languages();
$default = $this->input('languages.default', null);
if ($default !== null) {
$default = (int) $default;
}
foreach ($this->input('languages.items', []) as $index => $lang) {
$languageId = $lang['id'] ?? null;
if ($languageId !== null) {
$languageId = (int) $languageId;
}
$language = new Language(
title: $lang['title'],
code: $lang['code'],
sort: (int) $lang['sort'],
isDefault: ($default === $index),
id: $languageId,
);
$languages->addLanguage($language);
}
return $languages;
}
private function storages(): Storages
{
$storages = new Storages();
$logo = $this->get('logo', []);
$storages->add($logo, StorageType::Logo);
return $storages;
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Storage;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Storage\Upload;
use App\Enums\Morph;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
final class ImageRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$mimes = '';
if ($this->has('storage_type')) {
$storageType = StorageType::tryFrom((int) $this->input('storage_type')) ?? [];
$mimes = implode(',', $storageType->getAcceptMimes());
}
return [
'file' => ['required', 'file', 'mimes:' . $mimes],
'storage_type' => ['required', 'numeric', new Enum(StorageType::class)],
'morph' => ['required', 'numeric', new Enum(Morph::class)],
];
}
public function getDto(): Upload
{
return new Upload(
file: $this->file('file'),
storageType: StorageType::from((int) $this->input('storage_type')),
morph: Morph::from((int) $this->input('morph')),
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Resources\Admin\Languages;
use App\ServiceResults\Admin\LanguageService\NewLanguageResult;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class NewLanguage extends JsonResource
{
/**
* @var NewLanguageResult
*/
public $resource;
public function toArray(Request $request): array
{
return [
'html' => view('components.volt.forms.languages.language', [
'index' => $this->resource->getIndex(),
'name' => $this->resource->getName(),
'lang' => $this->resource->getLanguage()->toArray(),
])->render(),
];
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Http\Resources\Storage;
use App\Models\Storage;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class Upload extends JsonResource
{
/**
* @var Storage
*/
public $resource;
public function toArray(Request $request): array
{
return [
'id' => $this->resource->id,
'url' => $this->resource->url,
];
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Contracts\Models\Storage as StorageContract;
final class Project extends Model implements StorageContract
{
use HasFactory, SoftDeletes, StorageTrait;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'code',
'http_host',
'is_public'
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_public' => 'boolean',
];
}
public function roles(): MorphMany
{
return $this->morphMany(Role::class, 'morph');
}
public function languages(): HasMany
{
return $this->hasMany(ProjectLanguage::class);
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Models\Scopes\SortScope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([SortScope::class])]
final class ProjectLanguage extends Model
{
use HasFactory, SoftDeletes;
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'is_default' => false,
'sort' => 100,
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
'code',
'is_default',
'sort',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_default' => 'boolean',
'sort' => 'integer',
];
}
}

View File

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\hasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
final class Role extends Model
@ -21,7 +22,7 @@ final class Role extends Model
*/
protected $fillable = [
'name',
'code'
'code',
];
public function scopeLatest(Builder $query): Builder
@ -34,6 +35,25 @@ final class Role extends Model
return $query->orderBy('name', 'asc');
}
protected function nameWithMorph(): Attribute
{
return Attribute::make(
get: function () {
$name = $this->name;
if ($this->morph_type) {
$name .= ' (' . __('admin-sections.Projects') . ': ' . $this->morph?->name . ')';
}
return $name;
},
)->shouldCache();
}
public function morph(): MorphTo
{
return $this->morphTo('morph');
}
public function permissions(): hasMany
{
return $this->hasMany(RolePermission::class, 'role_id', 'id');
@ -49,7 +69,7 @@ final class Role extends Model
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => ( $this->code === SystemRoleEnum::Admin->value ),
get: fn () => ( \is_null($this->morphable_type) && \is_null($this->morphable_id) && $this->code === SystemRoleEnum::Admin->value ),
)->shouldCache();
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
final class SortScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
$builder->orderBy('sort', 'asc');
}
}

View File

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\StorageType;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage as StorageSupport;
class Storage extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'storage';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'data',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'data' => 'array',
'type' => StorageType::class,
];
}
protected function dataImages(): Attribute
{
return Attribute::make(
get: fn () => $this->data['images'] ?? [],
);
}
protected function url(): Attribute
{
return Attribute::make(
get: function () {
if (\is_null($this->file)) {
return '';
}
return StorageSupport::disk(config('storage.disk'))->url($this->file) . '?time=' . $this->updated_at->getTimestamp();
},
);
}
protected function path(): Attribute
{
return Attribute::make(
get: function () {
if (\is_null($this->file)) {
return '';
}
return StorageSupport::disk(config('storage.disk'))->path($this->file);
},
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Models\Traits;
use App\Enums\StorageType;
use App\Models\Storage as StorageModel;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait StorageTrait
{
public function storage(): MorphMany
{
return $this->morphMany(StorageModel::class, 'morph');
}
public function getStorageOne(StorageType $type): ?StorageModel
{
return $this->storage->firstWhere('type', $type);
}
public function getStorageMany(StorageType $type): Collection
{
return $this->storage->where('type', $type);
}
}

View File

@ -12,7 +12,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Authenticatable
final class User extends Authenticatable
{
use HasFactory, Notifiable, SoftDeletes;
@ -59,11 +59,6 @@ class User extends Authenticatable
return $this->belongsToMany(Role::class);
}
public function hasRole(string $role): bool
{
return $this->roles->where('code', $role)->isNotEmpty();
}
public function hasPermission(string $permission): bool
{
return $this->permissions->search($permission) !== false;
@ -72,7 +67,11 @@ class User extends Authenticatable
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => $this->hasRole(SystemRole::Admin->value),
get: fn () => $this->roles
->where('code', SystemRole::Admin->value)
->whereNull('morphable_type')
->whereNull('morphable_id')
->isNotEmpty(),
)->shouldCache();
}

View File

@ -2,12 +2,15 @@
namespace App\Policies;
use App\Enums\SystemRole;
use App\Models\User;
readonly class AdminPanel extends Policy
{
public function view(User $user): bool
{
return $user->hasPermission('allow-admin-panel.view');
return
$user->hasPermission('allow-admin-panel.view')
|| $user->roles->where('code', SystemRole::AdminProject->value)->isNotEmpty();
}
}

View File

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
final readonly class ProjectPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('project.view');
}
public function view(User $user, Project $project): bool
{
return $user->hasPermission('project.view');
}
public function create(User $user): bool
{
return $user->hasPermission('project.create');
}
public function update(User $user, Project $project): bool
{
return $user->hasPermission('project.update');
}
public function delete(User $user, Project $project): bool
{
return $user->hasPermission('project.delete');
}
public function upload(User $user): bool
{
if ($user->hasPermission('project.create') || $user->hasPermission('project.update')) {
return true;
}
return false;
}
}

View File

@ -2,9 +2,15 @@
namespace App\Providers;
use App\Enums\Morph;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use App\Services\Storage\Image\ResizeCommandHandler;
use App\Services\Storage\ImageService;
use App\Services\Storage\StorageCommandHandler;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
@ -22,6 +28,19 @@ class AppServiceProvider extends ServiceProvider
$this->app->bind(CreateSearchInstanceCommand::class, function () {
return new CreateSearchInstanceCommand(Search::class);
});
$this->app->bind(StorageCommandHandler::class, function () {
return new StorageCommandHandler(disc: (string) config('storage.disk'));
});
$this->app->bind(ImageService::class, function (Application $app) {
return new ImageService(
storageCommandHandler: $app->make(StorageCommandHandler::class),
resizeCommandHandler: $app->make(ResizeCommandHandler::class),
maxImageWidth: (int) config('storage.max_image_width', 4000),
maxImageHeight: (int) config('storage.max_image_height', 4000),
);
});
}
/**
@ -33,19 +52,9 @@ class AppServiceProvider extends ServiceProvider
URL::forceScheme('https');
}
Password::defaults(function () {
$rule = Password::min(8);
$this->passwordDefaults();
if ($this->app->isProduction()) {
$rule->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised();
}
return $rule;
});
Relation::enforceMorphMap(Morph::map());
$this->configureRateLimiting();
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);
@ -63,4 +72,21 @@ class AppServiceProvider extends ServiceProvider
];
});
}
private function passwordDefaults(): void
{
Password::defaults(function () {
$rule = Password::min(8);
if ($this->app->isProduction()) {
$rule->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised();
}
return $rule;
});
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Dto\Builder\Project as ProjectBuilderDto;
use App\Models\Project;
use App\Services\Project\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder;
final readonly class ProjectRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getProjects(ProjectBuilderDto $projectBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: Project::query()->with($with),
projectBuilderDto: $projectBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function getProjectById(int $id): ?Project
{
return Project::query()->where('id', $id)->first();
}
public function getProjectByCode(string $code): ?Project
{
return Project::query()->where('code', $code)->first();
}
public function isExistsCode(string $code, ?int $exceptId = null): bool
{
return Project::query()
->where('code', $code)
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
}

View File

@ -38,18 +38,20 @@ final readonly class RoleRepository
public function getRolesForSelect(array $withExcepts = []): array
{
return Role::query()
$roles = Role::query()
->with(['morph'])
->when($withExcepts, function (Builder $query, array $withExcepts) {
$query->withTrashed()->whereNull('deleted_at')->orWhereIn('id', $withExcepts);
})
->pluck('name', 'id')
->toArray();
})->get();
return $roles->pluck('name_with_morph', 'id')->toArray();
}
public function isExistsCode(string $code, ?int $exceptId = null): bool
{
return Role::query()
->where('code', $code)
->whereNull('morphable_type')
->whereNull('morphable_id')
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Models\Storage;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Contracts\Search;
final readonly class StorageRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
) { }
public function getStorageById(int $id): ?Storage
{
return Storage::query()->where('id', $id)->first();
}
public function getStoregsByIds(array $ids): Search
{
$query = Storage::query()->whereIn('id', $ids);
return $this->createSearchInstanceCommand->execute($query);
}
}

View File

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
final readonly class HttpHost implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
if (is_string($value)) {
$this->validateHttpHost($value, $fail);
}
if (is_array($value)) {
foreach ($value as $item) {
$this->validateHttpHost($item, $fail);
}
}
}
private function validateHttpHost(string $value, Closure $fail): void
{
$scheme = explode('://', $value, 2);
if (count($scheme) != 2 || in_array($scheme[0], ['http', 'https'], true) !== true) {
$fail('validation.http_host')->translate();
return;
}
$host = explode(':', $scheme[1], 2);
if (!filter_var($host[0], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
$fail('validation.http_host')->translate();
return;
}
if (isset($host[1]) && (!is_numeric($host[1]) || $host[1] <= 0)) {
$fail('validation.http_host')->translate();
return;
}
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace App\ServiceResults\Admin\LanguageService;
use App\Models\ProjectLanguage;
use App\ServiceResults\ServiceResult;
final class NewLanguageResult extends ServiceResult
{
public function __construct(
private readonly ProjectLanguage $language,
private readonly string $name,
private readonly int $index,
) { }
public function getLanguage(): ProjectLanguage
{
return $this->language;
}
public function getName(): string
{
return $this->name;
}
public function getIndex(): int
{
return $this->index;
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace App\ServiceResults\Storage;
use App\Dto\Service\Storage\Storage as StorageDto;
use App\Models\Storage as StorageModel;
use App\ServiceResults\ServiceResult;
final class SaveDeleteStorageResult extends ServiceResult
{
private array $storagesForDelete = [];
private array $storagesForSave = [];
public function addDelete(StorageDto $storageDto): void
{
$type = $storageDto->getStorageType();
if (!isset($this->storagesForDelete[$type->value])) {
$this->storagesForDelete[$type->value] = [
'ids' => [],
'type' => $type,
'isDeleteType' => ($storageDto->isMany() !== true),
];
}
if ($storageDto->isFile()) {
$id = $storageDto->getFile()->getId();
$this->storagesForDelete[$type->value]['ids'][] = $id;
}
}
public function addSave(StorageModel $storage, StorageDto $storageDto): void
{
$this->storagesForSave[] = [
'storage' => $storage,
'isMany' => $storageDto->isMany(),
];
}
public function getStoragesForDelete(): array
{
return $this->storagesForDelete;
}
public function getStoragesForSave(): array
{
return $this->storagesForSave;
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
namespace App\ServiceResults\Storage;
use App\Models\Storage;
use App\ServiceResults\ServiceResult;
final class UploadResult extends ServiceResult
{
public function __construct(
private readonly Storage $storage,
) { }
public function getStorage(): Storage
{
return $this->storage;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Services\Admin;
use App\Dto\Service\Admin\Language\NewLanguage;
use App\Models\ProjectLanguage;
use App\ServiceResults\Admin\LanguageService\NewLanguageResult;
use App\ServiceResults\ServiceResultError;
use App\Services\Service;
final class LanguageService extends Service
{
public function newLanguage(NewLanguage $data): ServiceResultError | NewLanguageResult
{
$language = new ProjectLanguage();
$language->title = "";
$language->code = "";
return new NewLanguageResult(
language: $language,
name: $data->getName(),
index: $data->getIndex(),
);
}
}

View File

@ -0,0 +1,199 @@
<?php declare(strict_types=1);
namespace App\Services\Admin;
use App\Dto\Builder\Project as ProjectBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Project\StoreUpdate;
use App\Enums\Morph;
use App\Models\Project;
use App\Models\ProjectLanguage;
use App\Models\User;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\Project\ProjectCommandHandler;
use App\Services\ProjectLanguage\ModelSyncCommand;
use App\Services\Role\CreateAdminRoleForProjectCommand;
use App\Services\Service;
use App\Services\Storage\StorageService;
use Illuminate\Support\Facades\DB;
final class ProjectService extends Service
{
public function __construct(
private readonly ProjectRepository $projectRepository,
private readonly ProjectCommandHandler $projectCommandHandler,
private readonly CreateAdminRoleForProjectCommand $createAdminRoleForProjectCommand,
private readonly ModelSyncCommand $languageModelSyncCommand,
private readonly StorageService $storageService,
) { }
public function index(ProjectBuilderDto $projectBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', Project::class)) {
return $this->errFobidden(__('Access is denied'));
}
$projects = $this->projectRepository->getProjects(
$projectBuilderDto,
$querySettingsDto->getQueryWith()
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'projects' => $projects,
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', Project::class)) {
return $this->errFobidden(__('Access is denied'));
}
$language = new ProjectLanguage();
$language->is_default = true;
$language->title = $user->lang?->getTitle();
$language->code = $user->lang?->getLocale();
return $this->result([
'project' => new Project(),
'languages' => collect([$language])->toArray(),
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$project = $this->projectRepository->getProjectById($id);
if (is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $project)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'project' => $project,
'languages' => $project->languages->toArray(),
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', Project::class)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->projectRepository->isExistsCode($data->getCode())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.code')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.code')])]
);
}
$storages = $this->storageService->getStoragesAndValidate($data->getStorages(), Morph::Project);
if (!$storages->isSuccess()) {
return $storages;
}
try {
$project = DB::transaction(function () use ($data, $user, $storages) {
$dataProject = $this->getDataProject($data);
$project = $this->projectCommandHandler->handleStore($dataProject);
$this->createAdminRoleForProjectCommand->execute($project, $user);
$this->languageModelSyncCommand->execute($project, $data->getLanguages());
$this->storageService->saveAndDelete($project, $storages);
return $project;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($project, __('The project was successfully created'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$project = $this->projectRepository->getProjectById($id);
if (is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $project)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->projectRepository->isExistsCode($data->getCode(), $project->id)) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.code')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.code')])]
);
}
$storages = $this->storageService->getStoragesAndValidate($data->getStorages(), Morph::Project, $project->id);
if (!$storages->isSuccess()) {
return $storages;
}
try {
$project = DB::transaction(function () use ($data, $project, $storages) {
$dataProject = $this->getDataProject($data);
$project = $this->projectCommandHandler->handleUpdate($project, $dataProject);
$this->languageModelSyncCommand->execute($project, $data->getLanguages());
$this->storageService->saveAndDelete($project, $storages);
return $project;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($project, __('The project was successfully updated'));
}
public function destroy(int $id, User $user): ServiceResultError|ServiceResultSuccess
{
$project = $this->projectRepository->getProjectById($id);
if (is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $project)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($project) {
$this->projectCommandHandler->handleDestroy($project);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The project has been deleted'));
}
private function getDataProject(StoreUpdate $data): array
{
return [
'name' => $data->getName(),
'code' => $data->getCode(),
'http_host' => $data->getHttpHost(),
'is_public' => $data->isPublic(),
];
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Services\Project;
use App\Dto\Builder\Project as ProjectBuilderDto;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class BuilderCommand
{
public function execute(Relation | Builder $query, ProjectBuilderDto $projectBuilderDto): Relation | Builder
{
return $query;
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Services\Project;
use App\Models\Project;
final readonly class ProjectCommandHandler
{
public function handleStore(array $data): Project
{
return Project::create($data);
}
public function handleUpdate(Project $project, array $data): Project
{
$project->update($data);
$project->touch();
return $project;
}
public function handleDestroy(Project $project): void
{
$project->delete();
}
}

View File

@ -0,0 +1,74 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectLanguage;
use App\Dto\Service\Admin\Project\Language;
use App\Dto\Service\Admin\Project\Languages;
use App\Models\Project;
use Illuminate\Database\Eloquent\Collection;
final readonly class ModelSyncCommand
{
public function execute(Project $project, Languages $languages): void
{
$projectLanguages = $project->languages()->get();
$this->deleteLanguages($languages, $projectLanguages);
$languagesInsert = [];
$languagesUpdate = [];
foreach ($languages->getLanguages() as $language) {
/** @var Language $language */
$data = [
'code' => $language->getCode(),
'title' => $language->getTitle(),
'sort' => $language->getSort(),
'is_default' => $language->isDefault(),
];
if ($language->getId() !== null) {
$projectLanguage = $projectLanguages->firstWhere('id', $language->getId());
if ($projectLanguage !== null) {
$projectLanguageFix = $projectLanguages->firstWhere('code', $language->getCode());
if ($projectLanguageFix !== null && !in_array($projectLanguageFix->id, $languagesUpdate)) {
// Fixing a bug when language codes are swapped.
$projectLanguageFix->update([
'code' => $projectLanguageFix->code . '-' . $projectLanguageFix->id,
]);
}
$projectLanguage->update($data);
$languagesUpdate[] = $projectLanguage->id;
continue;
}
}
$languagesInsert[] = $data;
}
if (!empty($languagesInsert)) {
$project->languages()->createMany($languagesInsert);
}
}
private function deleteLanguages(Languages $languages, Collection $projectLanguages): void
{
$languagesIdUpdate = [];
foreach ($languages->getLanguages() as $language) {
/** @var Language $language */
if ($language->getId() === null) {
continue;
}
$languagesIdUpdate[] = $language->getId();
}
foreach ($projectLanguages as $projectLanguage) {
if (in_array($projectLanguage->id, $languagesIdUpdate)) {
continue;
}
$projectLanguage->update([
'code' => $projectLanguage->code . '-' . $projectLanguage->id,
]);
$projectLanguage->delete();
}
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Services\Role;
use App\Enums\SystemRole;
use App\Models\Project;
use App\Models\Role;
use App\Models\User;
use App\Services\User\UserCommandHandler;
final readonly class CreateAdminRoleForProjectCommand
{
public function __construct(
private UserCommandHandler $userCommandHandler,
){ }
public function execute(Project $project, User $user): Role
{
$data = [
'name' => 'Administrator',
'code' => SystemRole::AdminProject->value,
];
$role = $project->roles()->create($data);
$this->userCommandHandler->attachRole($user, $role);
return $role;
}
}

View File

@ -0,0 +1,51 @@
<?php declare(strict_types=1);
namespace App\Services\Storage\Image;
use App\Models\Storage;
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
final readonly class ResizeCommandHandler
{
public function resize(Storage $storage, int $width, int $height): Storage
{
$image = InterventionImage::read($storage->path);
if ($image->width() < $width && $image->height() < $height) {
return $storage;
}
$image->scale($width, $height)
->save($storage->path);
$storage->touch();
return $storage;
}
public function width(Storage $storage, int $width): Storage
{
$image = InterventionImage::read($storage->path);
if ($image->width() < $width) {
return $storage;
}
$image->scale(width: $width)
->save($storage->path);
$storage->touch();
return $storage;
}
public function height(Storage $storage, int $height): Storage
{
$image = InterventionImage::read($storage->path);
if ($image->height() < $height) {
return $storage;
}
$image->scale(height: $height)
->save($storage->path);
$storage->touch();
return $storage;
}
}

View File

@ -0,0 +1,65 @@
<?php declare(strict_types=1);
namespace App\Services\Storage;
use app\Dto\Service\Storage\Upload;
use App\Models\Storage;
use App\Models\User;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\Storage\UploadResult;
use App\Services\Service;
use App\Services\Storage\Image\ResizeCommandHandler;
use Illuminate\Support\Facades\DB;
final class ImageService extends Service
{
public function __construct(
private readonly StorageCommandHandler $storageCommandHandler,
private readonly ResizeCommandHandler $resizeCommandHandler,
private readonly int $maxImageWidth,
private readonly int $maxImageHeight,
) { }
public function uploadAndResize(Upload $upload, User $user): ServiceResultError | UploadResult
{
if ($user->cannot('upload', $upload->getMorph()->getPathModel())) {
return $this->errFobidden(__('Access is denied'));
}
if ($upload->getStorageType()->isImage() !== true) {
return $this->errValidate(__('storage.Trying to upload a wrong image'));
}
try {
$storage = DB::transaction(function () use ($upload, $user) {
$storage = $this->storageCommandHandler->handleStore(
upload: $upload,
user: $user,
);
return $this->imageResize($storage, $this->maxImageWidth, $this->maxImageHeight);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return new UploadResult($storage);
}
private function imageResize(Storage $storage, ?int $width, ?int $height): Storage
{
if (empty($width) && empty($height)) {
return $storage;
}
if (!empty($width) && !empty($height)) {
return $this->resizeCommandHandler->resize($storage, $width, $height);
}
if (!empty($height)) {
return $this->resizeCommandHandler->height($storage, $height);
}
return $this->resizeCommandHandler->width($storage, $width);
}
}

View File

@ -0,0 +1,91 @@
<?php declare(strict_types=1);
namespace App\Services\Storage;
use App\Dto\Service\Storage\Upload;
use App\Enums\StorageType;
use app\Exceptions\Services\Storage\StorageCommandHandlerException;
use App\Exceptions\Services\Storage\StorageSaveFileException;
use App\Models\Storage;
use App\Models\User;
use App\Contracts\Models\Storage as ModelStorageContract;
use Illuminate\Support\Str;
final readonly class StorageCommandHandler
{
public function __construct(
private string $disc,
) { }
public function handleStore(Upload $upload, User $user, array $data = []): Storage
{
$folder = '/' . $upload->getMorph()->getFolderName();
$folder .= date("/Y/m/d");
$folder .= '/' . $upload->getStorageType()->getFolderName();
$folder = Str::lower($folder);
$path = $upload->getFile()->store($folder, $this->disc);
if ($path === false) {
throw new StorageSaveFileException('Could not save the file: ' . $upload->getFile()->getFilename() . '.');
}
$storage = new Storage();
$storage->created_user_id = $user->id;
$storage->file = '/' . $path;
$storage->type = $upload->getStorageType();
$storage->morph_type = $upload->getMorph()->value;
if (!empty($data)) {
$storage->fill($data);
}
$storage->save();
return $storage;
}
public function handleStorageAttachModel(Storage $storage, ModelStorageContract $model): Storage
{
if ($storage->morph_id === $model->id) {
return $storage;
}
if (!\is_null($storage->morph_id)) {
throw new StorageCommandHandlerException('The model is already attached!');
}
$storage->morph_id = $model->id;
$storage->save();
return $storage;
}
public function handleUpdate(Storage $storage, array $data = []): Storage
{
$storage->update($data);
return $storage;
}
public function handleDestroy(Storage $storage): void
{
$storage->delete();
}
public function handleDestroyByIds(array $ids): void
{
if (!empty($ids)) {
Storage::whereIn('id', $ids)->delete();
}
}
public function handleDestroyByModel(ModelStorageContract $model, StorageType $type): void
{
$model->storage()->where('type', $type)->delete();
}
public function handleDestroyByModelStorageIds(ModelStorageContract $model, array $ids, StorageType $type): void
{
if (!empty($ids)) {
$model->storage()->whereIn('id', $ids)->where('type', $type)->delete();
}
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Services\Storage;
use App\Dto\Service\Storage\Storage;
use App\Dto\Service\Storage\Storages;
use App\Enums\Morph;
use App\Contracts\Models\Storage as ModelStorageContract;
use App\Repositories\StorageRepository;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\Storage\SaveDeleteStorageResult;
use App\Services\Service;
class StorageService extends Service
{
public function __construct(
private readonly StorageRepository $storageRepository,
private readonly ValidationService $storageValidation,
private readonly StorageCommandHandler $storageCommandHandler,
) { }
public function getStoragesAndValidate(Storages $storages, Morph $morph, ?int $modelId = null): ServiceResultError | SaveDeleteStorageResult
{
$storageModels = $this->storageRepository->getStoregsByIds($storages->getAllStorageIds())->all();
$result = new SaveDeleteStorageResult();
foreach ($storages->getAllStorages() as $storage) {
/** @var Storage $storage */
if ($storage->isDelete()) {
$result->addDelete($storage);
continue;
}
if (!$storage->isFile()) {
continue;
}
$storageModel = $storageModels->firstWhere('id', $storage->getFile()->getId());
if (\is_null($storageModel)) {
return $this->errValidate(__('storage.File_not_found'));
}
$validate = $this->storageValidation->execute($storageModel, $morph, $storage->getStorageType(), $modelId);
if (!$validate->isSuccess()) {
return $this->errValidate($validate->getMessage());
}
$result->addSave($storageModel, $storage);
}
return $result;
}
public function saveAndDelete(ModelStorageContract $model, SaveDeleteStorageResult $storageResult): void
{
foreach ($storageResult->getStoragesForDelete() as $dataStorage) {
$type = $dataStorage['type'];
if ($dataStorage['isDeleteType']) {
$this->storageCommandHandler->handleDestroyByModel($model, $type);
}
if ($dataStorage['isDeleteType'] || empty($dataStorage['ids'])) {
continue;
}
$this->storageCommandHandler->handleDestroyByModelStorageIds($model, $dataStorage['ids'], $type);
}
foreach ($storageResult->getStoragesForSave() as $dataStorage) {
$storage = $dataStorage['storage'];
if ($dataStorage['isMany'] !== true && $storage->morph_id !== $model->id) {
$this->storageCommandHandler->handleDestroyByModel($model, $storage->type);
}
$this->storageCommandHandler->handleStorageAttachModel($storage, $model);
}
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace app\Services\Storage;
use App\Enums\Morph;
use App\Enums\StorageType;
use App\Models\Storage as StorageModel;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\Service;
final class ValidationService extends Service
{
public function execute(StorageModel $storageModel, Morph $morph, StorageType $storageType, ?int $modelId = null): ServiceResultError | ServiceResultSuccess
{
if (! $this->isMorphMatched($storageModel, $morph, $modelId)) {
return $this->errValidate(__('storage.Attempt to replace a file'));
}
if (! $this->isTypeMatched($storageModel, $storageType)) {
return $this->errValidate(__('storage.Wrong file type'));
}
return $this->ok(__('OK'));
}
private function isMorphMatched(StorageModel $storageModel, Morph $morph, ?int $modelId = null): bool
{
if ($storageModel->morph_type !== $morph->value) {
return false;
}
if (!\is_null($storageModel->morph_id) && $storageModel->morph_id !== $modelId) {
return false;
}
return true;
}
private function isTypeMatched(StorageModel $storageModel, StorageType $type): bool
{
if ($storageModel->type !== $type) {
return false;
}
return true;
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\User;
use App\Dto\User\ManyRoleDto;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
@ -50,6 +51,16 @@ final readonly class UserCommandHandler
$user->roles()->sync($roles->toArray());
}
public function attachRole(User $user, Role $role): void
{
$user->roles()->attach($role);
}
public function detachRole(User $user, Role $role): void
{
$user->roles()->detach($user);
}
private function hashPassword(int|string $password): string
{
return Hash::make($password);

View File

@ -2,6 +2,7 @@
namespace App\View\Components;
use Illuminate\Pagination\Paginator;
use Illuminate\View\Component;
use Illuminate\View\View;
@ -11,6 +12,8 @@ abstract class PrivateLayout extends Component
public function render(): View
{
Paginator::useBootstrapFive();
return view('layout.private', [
'navigation' => $this->getNavigation(),
]);

View File

@ -30,12 +30,12 @@ final class Checkbox extends Form
return (string) old($this->getRequestName(), $this->checkboxValue);
}
public function getUserValue(): string
private function getUserValue(): string
{
return (string) $this->userValue;
}
public function getNotCheckedValue(): ?string
private function getNotCheckedValue(): ?string
{
return $this->notCheckedValue;
}

View File

@ -2,7 +2,7 @@
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use App\Helpers\Helpers;
use Illuminate\View\Component;
use Illuminate\View\View;
@ -19,13 +19,7 @@ abstract class Form extends Component
return $this->requestName;
}
$this->requestName = Str::of($this->getName())
->replace(
['.', '[', ']'],
['_', '.', ''],
)
->rtrim('.')
->value();
$this->requestName = Helpers::formatAttributeNameToRequestName($this->getName());
return $this->requestName;
}

View File

@ -11,7 +11,8 @@ final class Input extends Form
private readonly string $title,
private readonly string $name,
private readonly string $type = 'text',
private readonly ?string $value = ''
private readonly ?string $value = '',
private readonly ?string $example = null,
) { }
protected function getName(): string
@ -34,6 +35,11 @@ final class Input extends Form
return (string) old($this->getRequestName(), $this->value);
}
private function getExample(): ?string
{
return $this->example;
}
/**
* @inheritDoc
*/
@ -44,7 +50,8 @@ final class Input extends Form
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'type' => $this->getType(),
'value' => $this->getValue()
'value' => $this->getValue(),
'example' => $this->getExample(),
]);
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\View\View;
final class InputTypeHidden extends Form
{
public function __construct(
private readonly string $name,
private readonly ?string $value = null,
) { }
protected function getName(): string
{
return $this->name;
}
private function getValue(): ?string
{
return (string) old($this->getRequestName(), $this->value);
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.input-type-hidden', [
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'value' => $this->getValue(),
]);
}
}

View File

@ -0,0 +1,54 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\View\View;
final class Languages extends Form
{
public function __construct(
private readonly string $name,
private readonly array $value,
) { }
protected function getName(): string
{
return $this->name;
}
private function getValue(): array
{
$value = old($this->getRequestName(), null);
if (\is_null($value)) {
return $this->value;
}
$langs = [];
$default = $value['default'] ?? null;
if ($default !== null) {
$default = (int) $default;
}
foreach ($value['items'] as $index => $lang) {
$langs[$index] = [
'title' => $lang['title'] ?? '',
'code' => $lang['code'] ?? '',
'is_default' => ($index === $default),
'sort' => $lang['sort'] ?? '',
'id' => $lang['id'] ?? null,
];
}
return $langs;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.languages', [
'name' => $this->getName(),
'value' => $this->getValue(),
]);
}
}

View File

@ -22,26 +22,17 @@ final class PermissionsForRole extends Form
return $this->name;
}
/**
* @return string
*/
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
/**
* @return array
*/
private function getValue(): Collection
{
$value = old($this->getRequestName(), $this->value);
return collect($value);
}
/**
* @return Role
*/
private function getRole(): Role
{
return $this->role;

View File

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms\Upload;
use App\Enums\Morph;
use App\Models\Storage;
use App\Repositories\StorageRepository;
use App\View\Components\Volt\Forms\Form;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\View\View;
use App\Contracts\StorageType\Image as ImageContract;
final class Image extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly Morph $morph,
private readonly ImageContract $storageType,
private readonly StorageRepository $storageRepository,
private readonly ?Storage $storage = null,
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getMorph(): Morph
{
return $this->morph;
}
private function getStorageType(): ImageContract
{
return $this->storageType;
}
private function getAcceptedFiles(): Collection
{
return collect($this->getStorageType()->getAcceptMimes());
}
private function getStorage(): ?Storage
{
$name = $this->getRequestName();
$storageId = old($name . '.file');
if (!empty($storageId)) {
return $this->storageRepository->getStorageById((int) $storageId);
}
return $this->storage;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.upload.image', [
'storage' => $this->getStorage(),
'title' => $this->getTitle(),
'name' => $this->getName(),
'morph' => $this->getMorph(),
'storageType' => $this->getStorageType(),
'acceptedFiles' => $this->getAcceptedFiles(),
]);
}
}

View File

@ -1,5 +1,6 @@
<?php
return [
Intervention\Image\Laravel\ServiceProvider::class,
App\Providers\AppServiceProvider::class,
];

View File

@ -6,6 +6,7 @@
"license": "MIT",
"require": {
"php": "^8.3",
"intervention/image-laravel": "^1.2",
"kor-elf/captcha-rule-for-laravel": "^1.0",
"laravel/framework": "^11.0",
"laravel/tinker": "^2.9"

215
app/src/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9e88c9e1f78fbf9527a1fc1d837b132d",
"content-hash": "69cad3ad961845edc7e2ac7c0f9fb5e6",
"packages": [
{
"name": "brick/math",
@ -1045,6 +1045,219 @@
],
"time": "2023-12-03T19:50:20+00:00"
},
{
"name": "intervention/gif",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/gif.git",
"reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/gif/zipball/3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3",
"reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^1",
"phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Gif\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Native PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif",
"keywords": [
"animation",
"gd",
"gif",
"image"
],
"support": {
"issues": "https://github.com/Intervention/gif/issues",
"source": "https://github.com/Intervention/gif/tree/4.1.0"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2024-03-26T17:23:47+00:00"
},
{
"name": "intervention/image",
"version": "3.5.1",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "67be90e5700370c88833190d4edc07e4bb7d157b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/67be90e5700370c88833190d4edc07e4bb7d157b",
"reference": "67be90e5700370c88833190d4edc07e4bb7d157b",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"intervention/gif": "^4.0.1",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^1",
"phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8"
},
"suggest": {
"ext-exif": "Recommended to be able to read EXIF data properly."
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "PHP image manipulation",
"homepage": "https://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"resize",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/3.5.1"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2024-03-22T07:12:19+00:00"
},
{
"name": "intervention/image-laravel",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image-laravel.git",
"reference": "d30b62fea3c49896dbf26ea7799e7d718e779310"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image-laravel/zipball/d30b62fea3c49896dbf26ea7799e7d718e779310",
"reference": "d30b62fea3c49896dbf26ea7799e7d718e779310",
"shasum": ""
},
"require": {
"illuminate/support": "^8|^9|^10|^11",
"intervention/image": "^3",
"php": "^8.1"
},
"require-dev": {
"orchestra/testbench": "^8.18",
"phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Intervention\\Image\\Laravel\\ServiceProvider"
],
"aliases": {
"Image": "Intervention\\Image\\Laravel\\Facades\\Image"
}
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\Laravel\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Laravel Integration of Intervention Image",
"homepage": "https://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"resize",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image-laravel/issues",
"source": "https://github.com/Intervention/image-laravel/tree/1.2.0"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2024-03-03T09:14:35+00:00"
},
{
"name": "kor-elf/captcha-rule-for-laravel",
"version": "1.0.1",

View File

@ -44,6 +44,14 @@ return [
'throw' => false,
],
'storage' => [
'driver' => 'local',
'root' => storage_path('app/public/storage'),
'url' => env('APP_URL') . '/storage/storage',
'visibility' => 'public',
'throw' => true,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),

21
app/src/config/image.php Normal file
View File

@ -0,0 +1,21 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports “GD Library” and “Imagick” to process images
| internally. Depending on your PHP setup, you can choose one of them.
|
| Included options:
| - \Intervention\Image\Drivers\Gd\Driver::class
| - \Intervention\Image\Drivers\Imagick\Driver::class
|
*/
'driver' => \Intervention\Image\Drivers\Gd\Driver::class
];

View File

@ -0,0 +1,9 @@
<?php
return [
// to resize
'max_image_width' => 1000,
// to resize
'max_image_height' => 1000,
// filesystems
'disk' => 'storage',
];

View File

@ -0,0 +1,106 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->string('name')->index();
$table->string('code')->unique();
$table->string('http_host')->nullable()->unique()->comment('Scheme and Domain and Port (example: http://localhost:8080).');
$table->boolean('is_public')->default(0)->index();
$table->timestamps();
$table->softDeletes()->index();
$table->index('created_at');
$table->index(['is_public', 'deleted_at']);
});
Schema::table('roles', function (Blueprint $table) {
$table->unsignedInteger('morph_type')->nullable();
$table->unsignedBigInteger('morph_id')->nullable();
$table->index(['morph_type', 'morph_id']);
$table->dropUnique(['code']);
$table->unique(['code', 'morph_type', 'morph_id']);
});
Schema::create('storage', function (Blueprint $table) {
$table->id();
$table->string('file');
$table->unsignedInteger('morph_type');
$table->unsignedBigInteger('morph_id')->nullable()->index();
$table->unsignedInteger('type')->comment('File type (main photo, video, gallery, etc.).');
$table->json('data')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unsignedBigInteger('created_user_id')->nullable();
$table->foreign('created_user_id')->references('id')->on('users');
$table->index(['morph_type', 'morph_id']);
});
Schema::create('project_languages', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('project_id')->index();
$table->foreign('project_id')->references('id')->on('projects');
$table->string('code');
$table->string('title');
$table->boolean('is_default');
$table->integer('sort')->index();
$table->timestamps();
$table->softDeletes();
$table->index(['project_id', 'deleted_at']);
$table->unique(['project_id', 'code']);
});
Schema::create('project_content', function (Blueprint $table) {
$table->id();
$table->string('title')->index();
$table->unsignedBigInteger('project_id')->index();
$table->foreign('project_id')->references('id')->on('projects');
$table->unsignedBigInteger('language_id')->index();
$table->foreign('language_id')->references('id')->on('project_languages');
$table->longText('description');
$table->timestamps();
$table->softDeletes();
$table->index(['project_id', 'language_id', 'deleted_at']);
});
Schema::create('project_links', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('link');
$table->unsignedBigInteger('project_id')->index();
$table->foreign('project_id')->references('id')->on('projects');
$table->unsignedBigInteger('language_id')->nullable()->index();
$table->foreign('language_id')->references('id')->on('project_languages');
$table->index(['project_id', 'language_id', 'deleted_at']);
$table->integer('sort')->index();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_links');
Schema::dropIfExists('project_content');
Schema::dropIfExists('project_languages');
Schema::dropIfExists('storage');
Schema::table('roles', function (Blueprint $table) {
$table->dropUnique(['code', 'morph_type', 'morph_id']);
$table->dropColumn('morph_type');
$table->dropColumn('morph_id');
$table->unique('code');
});
Schema::dropIfExists('projects');
}
};

View File

@ -238,5 +238,15 @@
"The group has been deleted": "The group has been deleted",
"The user was successfully created": "The user was successfully created",
"The user was successfully updated": "The user was successfully updated",
"The user has been deleted": "The user has been deleted"
"The user has been deleted": "The user has been deleted",
"example:": "example:",
"or": "or",
"The project was successfully created": "The project was successfully created",
"The project was successfully updated": "The project was successfully updated",
"The project has been deleted": "The project has been deleted",
"Languages": "Languages",
"Add language": "Add language",
"There was an error adding a language": "There was an error adding a language",
"Select images": "Select images",
"loading": "loading"
}

View File

@ -4,4 +4,5 @@ return [
'User group' => 'User group',
'Users' => 'Users',
'AdminPanel' => 'Admin Panel',
'Projects' => 'Projects',
];

View File

@ -9,4 +9,5 @@ return [
'Allowed to delete' => 'Allowed to delete',
'Administrative panel allowed' => 'Administrative panel allowed',
'AdminPanel' => 'Administrative panel allowed',
'Project' => 'Projects',
];

View File

@ -0,0 +1,7 @@
<?php
return [
'Trying to upload a wrong image' => 'Trying to upload a wrong image!',
'File_not_found' => 'File not found.',
'Attempt to replace a file' => 'Attempt to replace a file.',
'Wrong file type' => 'Wrong file type.',
];

View File

@ -150,6 +150,7 @@ return [
'uuid' => 'The :attribute must be a valid UUID.',
'no_type' => 'The :attribute can only use: :type.',
'captcha' => 'Failed to pass human verification.',
'http_host' => 'The :attribute must be a valid domain.',
'attributes' => [
'address' => 'address',
'affiliate_url' => 'affiliate URL',
@ -277,5 +278,19 @@ return [
'permissions' => 'permissions',
'is_active' => 'is active',
'roles' => 'user group',
'is_public' => 'public',
'http_host' => 'hostname',
'sort' => 'sorting',
'is_default' => 'default',
'language-code' => 'language code',
'languages.items.*.code' => 'language code',
'languages.items.*.title' => 'language title',
'languages.items.*.sort' => 'language sorting',
'languages.items.*.id' => 'language ID',
'languages.default' => 'default language',
'language-default' => 'default language',
'logo' => 'logo',
'logo.file' => 'logo file',
'logo.delete' => 'remove logo',
],
];

View File

@ -238,5 +238,15 @@
"The group has been deleted": "Группа была удалена",
"The user was successfully created": "Пользователь был успешно создан",
"The user was successfully updated": "Пользователь был успешно обновлен",
"The user has been deleted": "Пользователь был удален"
"The user has been deleted": "Пользователь был удален",
"example:": "пример:",
"or": "или",
"The project was successfully created": "Проект был успешно создан",
"The project was successfully updated": "Проект был успешно обновлен",
"The project has been deleted": "Проект был удален",
"Languages": "Языки",
"Add language": "Добавить язык",
"There was an error adding a language": "Произошла ошибка при добавлении языка",
"Select images": "Выберите изображения",
"loading": "загрузка"
}

View File

@ -4,4 +4,5 @@ return [
'User group' => 'Группа пользователей',
'Users' => 'Пользователи',
'AdminPanel' => 'Админка',
'Projects' => 'Проекты',
];

View File

@ -10,4 +10,5 @@ return [
'Allowed to delete' => 'Разрешено удалять',
'Administrative panel allowed' => 'Административная панель разрешена',
'AdminPanel' => 'Административная панель разрешена',
'Project' => 'Проекты',
];

View File

@ -0,0 +1,7 @@
<?php
return [
'Trying to upload a wrong image' => 'Попытка загрузить не картинку!',
'File_not_found' => 'Файл не найден.',
'Attempt to replace a file' => 'Попытка подменить файл.',
'Wrong file type' => 'Неправильный тип файла.',
];

View File

@ -150,6 +150,7 @@ return [
'uuid' => 'Значение поля :attribute должно быть корректным UUID.',
'no_type' => 'Значение поля :attribute может использовать только: :type.',
'captcha' => 'Не удалось пройти проверку человеком.',
'http_host' => 'Значение поля :attribute не является доменом или имеет некорректный формат.',
'attributes' => [
'address' => 'адрес',
'affiliate_url' => 'Партнёрская ссылка',
@ -213,7 +214,7 @@ return [
'minute' => 'минута',
'mobile' => 'моб. номер',
'month' => 'месяц',
'name' => 'имя',
'name' => 'название',
'national_code' => 'национальный код',
'number' => 'номер',
'password' => 'пароль',
@ -264,7 +265,7 @@ return [
'test_name' => 'тестовое имя',
'text' => 'текст',
'time' => 'время',
'title' => 'наименование',
'title' => 'заголовок',
'type' => 'тип',
'updated_at' => 'обновлено в',
'user' => 'пользователь',
@ -277,5 +278,19 @@ return [
'permissions' => 'разрешения',
'is_active' => 'активен',
'roles' => 'группа пользователей',
'is_public' => 'публичный',
'http_host' => 'имя хоста',
'sort' => 'сортировка',
'is_default' => 'по умолчанию',
'language-code' => 'код языка',
'languages.items.*.code' => 'код языка',
'languages.items.*.title' => 'заголовок языка',
'languages.items.*.sort' => 'языковая сортировка',
'languages.items.*.id' => 'ID языка',
'languages.default' => 'язык по умолчанию',
'language-default' => 'язык по умолчанию',
'logo' => 'логотип',
'logo.file' => 'файл логотипа',
'logo.delete' => 'удалить логотип',
],
];

View File

@ -9,6 +9,7 @@
"bootstrap": "5.0.2",
"chartist": "^0.11.4",
"chartist-plugin-tooltips": "^0.0.17",
"dropzone": "^6.0.0-beta.2",
"notyf": "^3.10.0",
"nouislider": "^15.2.0",
"onscreen": "^1.4.0",
@ -667,6 +668,11 @@
"win32"
]
},
"node_modules/@swc/helpers": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
"integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA=="
},
"node_modules/@types/eslint": {
"version": "8.56.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.6.tgz",
@ -1153,6 +1159,15 @@
"node": ">=0.4.0"
}
},
"node_modules/dropzone": {
"version": "6.0.0-beta.2",
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
"integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
"dependencies": {
"@swc/helpers": "^0.2.13",
"just-extend": "^5.0.0"
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.722",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz",
@ -1465,6 +1480,11 @@
"dev": true,
"peer": true
},
"node_modules/just-extend": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
"integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ=="
},
"node_modules/laravel-vite-plugin": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.2.tgz",

View File

@ -10,6 +10,7 @@
"bootstrap": "5.0.2",
"chartist": "^0.11.4",
"chartist-plugin-tooltips": "^0.0.17",
"dropzone": "^6.0.0-beta.2",
"notyf": "^3.10.0",
"nouislider": "^15.2.0",
"onscreen": "^1.4.0",

View File

@ -9,6 +9,23 @@
<span class="sidebar-text">{{ __('admin-sections.Dashboard') }}</span>
</a>
</li>
@can('viewAny', \App\Models\Project::class)
<li @class([
'nav-item',
'active' => request()->route()->named('admin.projects.*'),
])>
<a href="{{ route('admin.projects.index') }}" class="nav-link">
<span class="sidebar-icon">
<svg class="icon icon-xs me-2" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-2.25Z"></path>
</svg>
</span>
<span class="sidebar-text">{{ __('admin-sections.Projects') }}</span>
</a>
</li>
@endcan
@can('viewAny', \App\Models\User::class)
<li @class([
'nav-item',

View File

@ -0,0 +1,11 @@
@csrf
<x-volt.forms.checkbox :title="__('validation.attributes.is_public')" name="is_public" checkboxValue="1" notCheckedValue="0" :userValue="(string) $project->is_public" />
<x-volt.forms.upload.image :title="__('validation.attributes.logo')" :storage="$project->getStorageOne(\App\Enums\StorageType::Logo)" name="logo" :morph="\App\Enums\Morph::Project" :storageType="\App\Enums\StorageType::Logo" />
<x-volt.forms.input :title="__('validation.attributes.name')" name="name" type="text" :value="$project->name" required autofocus />
<x-volt.forms.input :title="__('validation.attributes.code')" name="code" type="text" :value="$project->code" required />
<x-volt.forms.input :title="__('validation.attributes.http_host')" :example="'<strong>https://localhost.com</strong> ' . __('or') . ' <strong>http://localhost.com:8080</strong>'" name="http_host" type="text" :value="$project->http_host" />
<x-volt.forms.languages name="languages" :value="$languages" />
@canany(['create', 'update'], $project)
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
@endcanany

View File

@ -0,0 +1,8 @@
@can('create', \App\Models\Project::class)
<div class="mb-4">
<a href="{{ route('admin.projects.create') }}" class="btn btn-secondary d-inline-flex align-items-center me-2">
<svg class="icon icon-xs me-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
{{ __('Create') }}
</a>
</div>
@endcan

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