Compare commits
2 Commits
8c46816ff8
...
2f767c29c2
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f767c29c2 | |||
| c769b7aafe |
34
src/app/Domain/Role/Actions/DestroyAction.php
Normal file
34
src/app/Domain/Role/Actions/DestroyAction.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Actions;
|
||||
|
||||
use App\Domain\Role\Data\DestroyRequest;
|
||||
use App\Domain\Role\Repositories\RoleRepository;
|
||||
use App\Domain\Shared\Exceptions\AppException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DestroyAction
|
||||
{
|
||||
public function __construct(
|
||||
private RoleRepository $roleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(DestroyRequest $request): void
|
||||
{
|
||||
$role = $this->roleRepository->whereUuid($request->role_uuid)->firstOrFail();
|
||||
|
||||
// Нельзя удалить роль, если к ней привязаны пользователи
|
||||
if ($role->users()->count() > 0) {
|
||||
AppException::new(
|
||||
'role_in_use',
|
||||
'Нельзя удалить роль, к которой привязаны пользователи',
|
||||
Response::HTTP_CONFLICT
|
||||
);
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
}
|
||||
}
|
||||
31
src/app/Domain/Role/Actions/IndexAction.php
Normal file
31
src/app/Domain/Role/Actions/IndexAction.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Actions;
|
||||
|
||||
use App\Domain\Role\Data\IndexRequest;
|
||||
use App\Domain\Role\Data\IndexResponseData;
|
||||
use App\Domain\Role\Repositories\RoleRepository;
|
||||
|
||||
class IndexAction
|
||||
{
|
||||
public function __construct(
|
||||
private RoleRepository $roleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(IndexRequest $request): IndexResponseData
|
||||
{
|
||||
$query = $this->roleRepository
|
||||
->withCount('users')
|
||||
->orderBy($request->sort_by ?? 'name', $request->sort_dir ?? 'asc');
|
||||
|
||||
$paginator = $query->paginate(
|
||||
perPage: $request->per_page ?? 15,
|
||||
page: $request->page ?? 1,
|
||||
);
|
||||
|
||||
return IndexResponseData::fromPaginator($paginator);
|
||||
}
|
||||
}
|
||||
23
src/app/Domain/Role/Actions/ShowAction.php
Normal file
23
src/app/Domain/Role/Actions/ShowAction.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Actions;
|
||||
|
||||
use App\Domain\Role\Data\ShowRequest;
|
||||
use App\Domain\Role\Data\ShowResponseData;
|
||||
use App\Domain\Role\Repositories\RoleRepository;
|
||||
|
||||
class ShowAction
|
||||
{
|
||||
public function __construct(
|
||||
private RoleRepository $roleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(ShowRequest $request): ShowResponseData
|
||||
{
|
||||
$result = $this->roleRepository->whereUuid($request->role_uuid)->firstOrFail();
|
||||
return ShowResponseData::fromModel($result);
|
||||
}
|
||||
}
|
||||
26
src/app/Domain/Role/Actions/StoreAction.php
Normal file
26
src/app/Domain/Role/Actions/StoreAction.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Actions;
|
||||
|
||||
use App\Domain\Role\Data\StoreRequest;
|
||||
use App\Domain\Role\Repositories\RoleRepository;
|
||||
use App\Models\Role;
|
||||
|
||||
class StoreAction
|
||||
{
|
||||
public function __construct(
|
||||
private RoleRepository $roleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(StoreRequest $request): Role
|
||||
{
|
||||
return $this->roleRepository->create([
|
||||
'name' => $request->name,
|
||||
'code' => $request->code,
|
||||
'description' => $request->description,
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
src/app/Domain/Role/Actions/UpdateAction.php
Normal file
22
src/app/Domain/Role/Actions/UpdateAction.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Actions;
|
||||
|
||||
use App\Domain\Role\Data\UpdateRequest;
|
||||
use App\Domain\Role\Repositories\RoleRepository;
|
||||
|
||||
class UpdateAction
|
||||
{
|
||||
public function __construct(
|
||||
private RoleRepository $roleRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(UpdateRequest $request): void
|
||||
{
|
||||
$role = $this->roleRepository->whereUuid($request->role_uuid)->firstOrFail();
|
||||
$role->update($request->getFilledFields());
|
||||
}
|
||||
}
|
||||
23
src/app/Domain/Role/Data/DestroyRequest.php
Normal file
23
src/app/Domain/Role/Data/DestroyRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\FromRouteParameter;
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class DestroyRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[StringType, FromRouteParameter('role_uuid'), Exists('roles', 'uuid'), Required]
|
||||
public readonly string $role_uuid,
|
||||
) {
|
||||
}
|
||||
}
|
||||
29
src/app/Domain/Role/Data/IndexRequest.php
Normal file
29
src/app/Domain/Role/Data/IndexRequest.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\IntegerType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Min;
|
||||
use Spatie\LaravelData\Attributes\Validation\Nullable;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class IndexRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[IntegerType, Min(1), Nullable]
|
||||
public readonly ?int $page = 1,
|
||||
#[IntegerType, Min(1), Nullable]
|
||||
public readonly ?int $per_page = 15,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $sort_by = 'name',
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $sort_dir = 'asc',
|
||||
) {
|
||||
}
|
||||
}
|
||||
38
src/app/Domain/Role/Data/IndexResponseData.php
Normal file
38
src/app/Domain/Role/Data/IndexResponseData.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
|
||||
class IndexResponseData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
/** @var DataCollection<array-key, IndexRoleItemData> */
|
||||
public readonly DataCollection $roles,
|
||||
public readonly int $current_page,
|
||||
public readonly int $last_page,
|
||||
public readonly int $per_page,
|
||||
public readonly int $total,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromPaginator(LengthAwarePaginator $paginator): self
|
||||
{
|
||||
$items = $paginator->getCollection()->map(
|
||||
fn (Role $role) => IndexRoleItemData::fromModel($role)
|
||||
);
|
||||
|
||||
return new self(
|
||||
roles: new DataCollection(IndexRoleItemData::class, $items),
|
||||
current_page: $paginator->currentPage(),
|
||||
last_page: $paginator->lastPage(),
|
||||
per_page: $paginator->perPage(),
|
||||
total: $paginator->total(),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
src/app/Domain/Role/Data/IndexRoleItemData.php
Normal file
33
src/app/Domain/Role/Data/IndexRoleItemData.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use App\Models\Role;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class IndexRoleItemData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $uuid,
|
||||
public readonly string $name,
|
||||
public readonly string $code,
|
||||
public readonly string $description,
|
||||
public readonly int $users_count,
|
||||
public readonly string $created_at,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromModel(Role $model): self
|
||||
{
|
||||
return new self(
|
||||
uuid: $model->uuid->toString(),
|
||||
name: $model->name,
|
||||
code: $model->code,
|
||||
description: $model->description,
|
||||
users_count: $model->users_count ?? $model->users()->count(),
|
||||
created_at: $model->created_at->toDateTimeString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/app/Domain/Role/Data/ShowRequest.php
Normal file
23
src/app/Domain/Role/Data/ShowRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\FromRouteParameter;
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class ShowRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[StringType, FromRouteParameter('role_uuid'), Exists('roles', 'uuid'), Required]
|
||||
public readonly string $role_uuid,
|
||||
) {
|
||||
}
|
||||
}
|
||||
35
src/app/Domain/Role/Data/ShowResponseData.php
Normal file
35
src/app/Domain/Role/Data/ShowResponseData.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use App\Models\Role;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class ShowResponseData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $uuid,
|
||||
public readonly string $name,
|
||||
public readonly string $code,
|
||||
public readonly string $description,
|
||||
public readonly int $users_count,
|
||||
public readonly string $created_at,
|
||||
public readonly string $updated_at,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromModel(Role $model): self
|
||||
{
|
||||
return new self(
|
||||
uuid: $model->uuid->toString(),
|
||||
name: $model->name,
|
||||
code: $model->code,
|
||||
description: $model->description,
|
||||
users_count: $model->users()->count(),
|
||||
created_at: $model->created_at->toDateTimeString(),
|
||||
updated_at: $model->updated_at->toDateTimeString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
26
src/app/Domain/Role/Data/StoreRequest.php
Normal file
26
src/app/Domain/Role/Data/StoreRequest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Unique;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class StoreRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[StringType, Required]
|
||||
public readonly string $name,
|
||||
#[StringType, Required, Unique('roles', 'code')]
|
||||
public readonly string $code,
|
||||
#[StringType, Required]
|
||||
public readonly string $description,
|
||||
) {
|
||||
}
|
||||
}
|
||||
40
src/app/Domain/Role/Data/UpdateRequest.php
Normal file
40
src/app/Domain/Role/Data/UpdateRequest.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\FromRouteParameter;
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||
use Spatie\LaravelData\Attributes\Validation\Nullable;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Unique;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class UpdateRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[StringType, Required, FromRouteParameter('role_uuid'), Exists('roles', 'uuid')]
|
||||
public readonly string $role_uuid,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $name,
|
||||
#[StringType, Nullable, Unique('roles', 'code')]
|
||||
public readonly ?string $code,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $description,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFilledFields(): array
|
||||
{
|
||||
return array_filter([
|
||||
'name' => $this->name,
|
||||
'code' => $this->code,
|
||||
'description' => $this->description,
|
||||
], fn ($value) => $value !== null);
|
||||
}
|
||||
}
|
||||
13
src/app/Domain/Role/Repositories/RoleRepository.php
Normal file
13
src/app/Domain/Role/Repositories/RoleRepository.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Role\Repositories;
|
||||
|
||||
use App\Domain\Shared\Repositories\BaseRepository;
|
||||
use App\Models\Role;
|
||||
|
||||
class RoleRepository extends BaseRepository
|
||||
{
|
||||
public string $model = Role::class;
|
||||
}
|
||||
38
src/app/Domain/User/Actions/DestroyAction.php
Normal file
38
src/app/Domain/User/Actions/DestroyAction.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Actions;
|
||||
|
||||
use App\Domain\User\Data\DestroyRequest;
|
||||
use App\Domain\User\Repositories\UserRepository;
|
||||
use App\Domain\Shared\Exceptions\AppException;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DestroyAction
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(DestroyRequest $request): void
|
||||
{
|
||||
/** @var User|null $currentUser */
|
||||
$currentUser = Auth::user();
|
||||
|
||||
if (!$currentUser) {
|
||||
AppException::new('UNAUTHORIZED', 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Нельзя удалить самого себя
|
||||
if ($currentUser->uuid->toString() === $request->user_uuid) {
|
||||
AppException::new('self_delete', 'Нельзя удалить самого себя', Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->whereUuid($request->user_uuid)->firstOrFail();
|
||||
$user->delete();
|
||||
}
|
||||
}
|
||||
43
src/app/Domain/User/Actions/IndexAction.php
Normal file
43
src/app/Domain/User/Actions/IndexAction.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Actions;
|
||||
|
||||
use App\Domain\User\Data\IndexRequest;
|
||||
use App\Domain\User\Data\IndexResponseData;
|
||||
use App\Domain\User\Repositories\UserRepository;
|
||||
|
||||
class IndexAction
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(IndexRequest $request): IndexResponseData
|
||||
{
|
||||
$query = $this->userRepository
|
||||
->with('role')
|
||||
->orderBy($request->sort_by ?? 'created_at', $request->sort_dir ?? 'desc');
|
||||
|
||||
if ($request->search) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search): void {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->role) {
|
||||
$query->where('role_uuid', $request->role);
|
||||
}
|
||||
|
||||
$paginator = $query->paginate(
|
||||
perPage: $request->per_page ?? 15,
|
||||
page: $request->page ?? 1,
|
||||
);
|
||||
|
||||
return IndexResponseData::fromPaginator($paginator);
|
||||
}
|
||||
}
|
||||
28
src/app/Domain/User/Actions/StoreAction.php
Normal file
28
src/app/Domain/User/Actions/StoreAction.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Actions;
|
||||
|
||||
use App\Domain\User\Data\StoreRequest;
|
||||
use App\Domain\User\Repositories\UserRepository;
|
||||
use App\Models\User;
|
||||
|
||||
class StoreAction
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(StoreRequest $request): User
|
||||
{
|
||||
return $this->userRepository->create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => bcrypt($request->password),
|
||||
'role_uuid' => $request->role_uuid,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,10 @@ namespace App\Domain\User\Actions;
|
||||
|
||||
use App\Domain\User\Data\UpdateRequest;
|
||||
use App\Domain\User\Repositories\UserRepository;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Domain\Shared\Exceptions\AppException;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class UpdateAction
|
||||
{
|
||||
@ -18,6 +21,26 @@ class UpdateAction
|
||||
|
||||
public function execute(UpdateRequest $request)
|
||||
{
|
||||
// Получаем текущего аутентифицированного пользователя
|
||||
/** @var User|null $currentUser */
|
||||
$currentUser = Auth::user();
|
||||
|
||||
// Проверяем, что пользователь аутентифицирован
|
||||
if (!$currentUser) {
|
||||
AppException::new('UNAUTHORIZED', 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Загружаем роль текущего пользователя
|
||||
$currentUser->load('role');
|
||||
|
||||
// Проверяем, является ли пользователь администратором
|
||||
$isAdmin = $currentUser->role && $currentUser->role->code === 'admin';
|
||||
|
||||
// Если пользователь не администратор и пытается редактировать не себя, запрещаем доступ
|
||||
if (!$isAdmin && $currentUser->uuid->toString() !== $request->user_uuid) {
|
||||
AppException::new('forbidden', 'Недостаточно прав для редактирования этого пользователя', Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->whereUuid($request->user_uuid)->firstOrFail();
|
||||
$user->update($request->getFilledFields());
|
||||
}
|
||||
|
||||
26
src/app/Domain/User/Data/DestroyRequest.php
Normal file
26
src/app/Domain/User/Data/DestroyRequest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Data;
|
||||
|
||||
use App\Domain\Shared\Casts\UuidCast;
|
||||
use App\Domain\Shared\ValueObjects\Uuid;
|
||||
use Spatie\LaravelData\Attributes\FromRouteParameter;
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\Uuid as UuidValidation;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class DestroyRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[UuidValidation, WithCast(UuidCast::class) , FromRouteParameter('user_uuid'), Exists('users', 'uuid'), Required]
|
||||
public readonly Uuid $user_uuid,
|
||||
) {
|
||||
}
|
||||
}
|
||||
33
src/app/Domain/User/Data/IndexRequest.php
Normal file
33
src/app/Domain/User/Data/IndexRequest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\IntegerType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Min;
|
||||
use Spatie\LaravelData\Attributes\Validation\Nullable;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class IndexRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[IntegerType, Min(1), Nullable]
|
||||
public readonly ?int $page = 1,
|
||||
#[IntegerType, Min(1), Nullable]
|
||||
public readonly ?int $per_page = 15,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $search = null,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $role = null,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $sort_by = 'created_at',
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $sort_dir = 'desc',
|
||||
) {
|
||||
}
|
||||
}
|
||||
38
src/app/Domain/User/Data/IndexResponseData.php
Normal file
38
src/app/Domain/User/Data/IndexResponseData.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Data;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
|
||||
class IndexResponseData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
/** @var DataCollection<array-key, IndexUserItemData> */
|
||||
public readonly DataCollection $users,
|
||||
public readonly int $current_page,
|
||||
public readonly int $last_page,
|
||||
public readonly int $per_page,
|
||||
public readonly int $total,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromPaginator(LengthAwarePaginator $paginator): self
|
||||
{
|
||||
$items = $paginator->getCollection()->map(
|
||||
fn (User $user) => IndexUserItemData::fromModel($user)
|
||||
);
|
||||
|
||||
return new self(
|
||||
users: new DataCollection(IndexUserItemData::class, $items),
|
||||
current_page: $paginator->currentPage(),
|
||||
last_page: $paginator->lastPage(),
|
||||
per_page: $paginator->perPage(),
|
||||
total: $paginator->total(),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/app/Domain/User/Data/IndexUserItemData.php
Normal file
37
src/app/Domain/User/Data/IndexUserItemData.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Data;
|
||||
|
||||
use App\Domain\User\Data\ValueObjects\RoleData;
|
||||
use App\Models\User;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class IndexUserItemData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $uuid,
|
||||
public readonly string $name,
|
||||
public readonly string $email,
|
||||
public readonly bool $email_verified,
|
||||
public readonly RoleData $role,
|
||||
public readonly string $created_at,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromModel(User $model): self
|
||||
{
|
||||
return new self(
|
||||
uuid: $model->uuid->toString(),
|
||||
name: $model->name,
|
||||
email: $model->email,
|
||||
email_verified: $model->email_verified_at !== null,
|
||||
role: new RoleData(
|
||||
uuid: $model->role->uuid->toString(),
|
||||
name: $model->role->name,
|
||||
),
|
||||
created_at: $model->created_at->toDateTimeString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/app/Domain/User/Data/StoreRequest.php
Normal file
35
src/app/Domain/User/Data/StoreRequest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Attributes\Validation\Email;
|
||||
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Unique;
|
||||
use Spatie\LaravelData\Attributes\Validation\Uuid as UuidValidation;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class StoreRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[StringType, Required]
|
||||
public readonly string $name,
|
||||
#[StringType, Email, Required, Unique('users', 'email')]
|
||||
public readonly string $email,
|
||||
#[StringType, Required]
|
||||
public readonly string $password,
|
||||
#[
|
||||
UuidValidation,
|
||||
Required,
|
||||
Exists('roles', 'uuid')
|
||||
]
|
||||
public readonly string $role_uuid,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,8 @@ use Spatie\LaravelData\Attributes\Validation\Email;
|
||||
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||
use Spatie\LaravelData\Attributes\Validation\Nullable;
|
||||
use Spatie\LaravelData\Attributes\Validation\Required;
|
||||
use Spatie\LaravelData\Attributes\Validation\Uuid as UuidValidation;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
@ -17,7 +19,12 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
class UpdateRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
#[StringType, FromRouteParameter('user_uuid'), Exists('users', 'uuid')]
|
||||
#[
|
||||
UuidValidation,
|
||||
FromRouteParameter('user_uuid'),
|
||||
Exists('users', 'uuid'),
|
||||
Required
|
||||
]
|
||||
public readonly string $user_uuid,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $name,
|
||||
@ -25,6 +32,12 @@ class UpdateRequest extends Data
|
||||
public readonly ?string $email,
|
||||
#[StringType, Nullable]
|
||||
public readonly ?string $password,
|
||||
#[
|
||||
UuidValidation,
|
||||
Nullable,
|
||||
Exists('roles', 'uuid')
|
||||
]
|
||||
public readonly ?string $role_uuid,
|
||||
) {
|
||||
|
||||
}
|
||||
@ -34,6 +47,7 @@ class UpdateRequest extends Data
|
||||
$fields = [
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'role_uuid' => $this->role_uuid,
|
||||
'password' => $this->password ? bcrypt($this->password) : null,
|
||||
];
|
||||
|
||||
|
||||
@ -12,13 +12,33 @@ class MenuHelper
|
||||
[
|
||||
'icon' => 'dashboard',
|
||||
'name' => 'Dashboard',
|
||||
'path' => route('dashboard')
|
||||
'path' => route('dashboard'),
|
||||
],
|
||||
[
|
||||
'icon' => 'user-profile',
|
||||
'name' => 'User Profile',
|
||||
'path' => auth()->check() ? route('users.show', ['user_uuid' => auth()->user()->uuid]) : '#'
|
||||
]
|
||||
'path' => auth()->check() ? route('users.show', ['user_uuid' => auth()->user()->uuid]) : '#',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function getAdminNavItems(): array
|
||||
{
|
||||
if (!auth()->check() || auth()->user()->role->code !== 'admin') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'icon' => 'users',
|
||||
'name' => 'Users',
|
||||
'path' => route('users.index'),
|
||||
],
|
||||
[
|
||||
'icon' => 'roles',
|
||||
'name' => 'Roles',
|
||||
'path' => route('roles.index'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -34,23 +54,29 @@ class MenuHelper
|
||||
'title' => 'Menu',
|
||||
'items' => self::getMainNavItems(),
|
||||
],
|
||||
[
|
||||
'title' => 'Administration',
|
||||
'items' => self::getAdminNavItems(),
|
||||
],
|
||||
[
|
||||
'title' => 'Others',
|
||||
'items' => self::getOterItems()
|
||||
]
|
||||
'items' => self::getOterItems(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function isActive($path)
|
||||
public static function isActive($path): bool
|
||||
{
|
||||
return request()->is($path, '/');
|
||||
return request()->is(ltrim($path, '/'), '/');
|
||||
}
|
||||
|
||||
public static function getIconSvg($iconName)
|
||||
public static function getIconSvg($iconName): string
|
||||
{
|
||||
$icons = [
|
||||
'dashboard' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>Dashboard SVG Icon</title><path fill="currentColor" d="M24 21h2v5h-2zm-4-5h2v10h-2zm-9 10a5.006 5.006 0 0 1-5-5h2a3 3 0 1 0 3-3v-2a5 5 0 0 1 0 10"/><path fill="currentColor" d="M28 2H4a2.002 2.002 0 0 0-2 2v24a2.002 2.002 0 0 0 2 2h24a2.003 2.003 0 0 0 2-2V4a2.002 2.002 0 0 0-2-2m0 9H14V4h14ZM12 4v7H4V4ZM4 28V13h24l.002 15Z"/></svg>',
|
||||
'user-profile' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>User-avatar SVG Icon</title><path fill="currentColor" d="M16 8a5 5 0 1 0 5 5a5 5 0 0 0-5-5m0 8a3 3 0 1 1 3-3a3.003 3.003 0 0 1-3 3"/><path fill="currentColor" d="M16 2a14 14 0 1 0 14 14A14.016 14.016 0 0 0 16 2m-6 24.377V25a3.003 3.003 0 0 1 3-3h6a3.003 3.003 0 0 1 3 3v1.377a11.899 11.899 0 0 1-12 0m13.993-1.451A5.002 5.002 0 0 0 19 20h-6a5.002 5.002 0 0 0-4.992 4.926a12 12 0 1 1 15.985 0"/></svg>'
|
||||
'user-profile' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>User-avatar SVG Icon</title><path fill="currentColor" d="M16 8a5 5 0 1 0 5 5a5 5 0 0 0-5-5m0 8a3 3 0 1 1 3-3a3.003 3.003 0 0 1-3 3"/><path fill="currentColor" d="M16 2a14 14 0 1 0 14 14A14.016 14.016 0 0 0 16 2m-6 24.377V25a3.003 3.003 0 0 1 3-3h6a3.003 3.003 0 0 1 3 3v1.377a11.899 11.899 0 0 1-12 0m13.993-1.451A5.002 5.002 0 0 0 19 20h-6a5.002 5.002 0 0 0-4.992 4.926a12 12 0 1 1 15.985 0"/></svg>',
|
||||
'users' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>Users SVG Icon</title><path fill="currentColor" d="M16 17a5 5 0 1 0-5-5a5 5 0 0 0 5 5m0-8a3 3 0 1 1-3 3a3 3 0 0 1 3-3"/><path fill="currentColor" d="M16 19c-4.4 0-8 2.3-8 5v2h16v-2c0-2.7-3.6-5-8-5m-5.6 5c.9-1.1 3.1-3 5.6-3s4.7 1.9 5.6 3Z"/><path fill="currentColor" d="M30 17a5 5 0 1 0-5-5a5 5 0 0 0 5 5m-2 2c-1.2 0-2.4.3-3.4.8a8.5 8.5 0 0 1 2.6 2.2H30v-2c0-.6-.5-1-2-1"/><path fill="currentColor" d="M2 17a5 5 0 1 0 5-5a5 5 0 0 0-5 5m2 2c-1.5 0-2 .4-2 1v2h2.8a8.5 8.5 0 0 1 2.6-2.2A6.5 6.5 0 0 0 4 19"/></svg>',
|
||||
'roles' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>Roles SVG Icon</title><path fill="currentColor" d="M16 2a14 14 0 1 0 14 14A14.016 14.016 0 0 0 16 2m0 4a5 5 0 1 1-5 5a5 5 0 0 1 5-5m0 20a10 10 0 0 1-8.4-4.4c.1-2.8 5.6-4.3 8.4-4.3s8.3 1.5 8.4 4.3A10 10 0 0 1 16 26"/></svg>',
|
||||
];
|
||||
return $icons[$iconName] ?? '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>';
|
||||
}
|
||||
|
||||
@ -4,7 +4,51 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Domain\Role\Actions\DestroyAction;
|
||||
use App\Domain\Role\Actions\IndexAction;
|
||||
use App\Domain\Role\Actions\ShowAction;
|
||||
use App\Domain\Role\Actions\StoreAction;
|
||||
use App\Domain\Role\Actions\UpdateAction;
|
||||
use App\Domain\Role\Data\DestroyRequest;
|
||||
use App\Domain\Role\Data\IndexRequest;
|
||||
use App\Domain\Role\Data\ShowRequest;
|
||||
use App\Domain\Role\Data\StoreRequest;
|
||||
use App\Domain\Role\Data\UpdateRequest;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
//
|
||||
public function index(IndexRequest $request, IndexAction $action): View
|
||||
{
|
||||
$data = $action->execute($request);
|
||||
return view('pages.role.index', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function show(ShowRequest $request, ShowAction $action): View
|
||||
{
|
||||
$data = $action->execute($request);
|
||||
return view('pages.role.show', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function store(StoreRequest $request, StoreAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($request);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Role created successfully']);
|
||||
}
|
||||
|
||||
public function update(UpdateRequest $request, UpdateAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($request);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Role updated successfully']);
|
||||
}
|
||||
|
||||
public function destroy(DestroyRequest $request, DestroyAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($request);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Role deleted successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,17 +4,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Domain\User\Actions\DestroyAction;
|
||||
use App\Domain\User\Actions\IndexAction;
|
||||
use App\Domain\User\Actions\ShowAction;
|
||||
use App\Domain\User\Actions\StoreAction;
|
||||
use App\Domain\User\Actions\UpdateAction;
|
||||
use App\Domain\User\Data\DestroyRequest;
|
||||
use App\Domain\User\Data\IndexRequest;
|
||||
use App\Domain\User\Data\ShowRequest;
|
||||
use App\Domain\User\Data\StoreRequest;
|
||||
use App\Domain\User\Data\UpdateRequest;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
public function index(IndexRequest $request, IndexAction $action): View
|
||||
{
|
||||
return view('pages.user.index', []);
|
||||
$data = $action->execute($request);
|
||||
return view('pages.user.index', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function show(ShowRequest $request, ShowAction $action): View
|
||||
@ -22,11 +30,25 @@ class UserController extends Controller
|
||||
$data = $action->execute($request);
|
||||
return view('pages.user.show', ['data' => $data]);
|
||||
}
|
||||
|
||||
public function update(UpdateRequest $request, UpdateAction $action)
|
||||
|
||||
public function store(StoreRequest $request, StoreAction $action): JsonResponse
|
||||
{
|
||||
$data = $action->execute($request);
|
||||
|
||||
$action->execute($request);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'User created successfully']);
|
||||
}
|
||||
|
||||
public function update(UpdateRequest $request, UpdateAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($request);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Profile updated successfully']);
|
||||
}
|
||||
|
||||
public function destroy(DestroyRequest $request, DestroyAction $action): JsonResponse
|
||||
{
|
||||
$action->execute($request);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'User deleted successfully']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,13 +33,10 @@ class CheckRoleMiddleware
|
||||
AppException::new('UNAUTHORIZED', 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$userRole = $user->load('role')->role();
|
||||
$user->load('role');
|
||||
|
||||
$hasRole = $userRole->whereIn('code', $roles)
|
||||
->count();
|
||||
|
||||
if (!$hasRole) {
|
||||
AppException::new('forbidden', 'Недостаточно прав');
|
||||
if (!$user->role || !in_array($user->role->code, $roles, true)) {
|
||||
AppException::new('forbidden', 'Недостаточно прав', Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Domain\Role\Repositories\RoleRepository;
|
||||
use App\Domain\User\Repositories\UserRepository;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@ -13,7 +15,13 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->singleton(UserRepository::class, function () {
|
||||
return new UserRepository();
|
||||
});
|
||||
|
||||
$this->app->singleton(RoleRepository::class, function () {
|
||||
return new RoleRepository();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -50,7 +50,7 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
|
||||
// Custom routes defined specifically
|
||||
Fortify::registerView(function () {
|
||||
return view('auth.register');
|
||||
return view('pages.auth.register');
|
||||
});
|
||||
|
||||
Fortify::loginView(function () {
|
||||
@ -58,23 +58,23 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
});
|
||||
|
||||
Fortify::verifyEmailView(function () {
|
||||
return view('auth.verify-mail');
|
||||
return view('pages.auth.verify-mail');
|
||||
});
|
||||
|
||||
Fortify::requestPasswordResetLinkView(function () {
|
||||
return view('auth.forgot-password');
|
||||
return view('pages.auth.forgot-password');
|
||||
});
|
||||
|
||||
Fortify::resetPasswordView(function (Request $request) {
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
return view('pages.auth.reset-password', ['request' => $request]);
|
||||
});
|
||||
|
||||
Fortify::confirmPasswordView(function () {
|
||||
return view('auth.confirm-password');
|
||||
return view('pages.auth.confirm-password');
|
||||
});
|
||||
|
||||
Fortify::twoFactorChallengeView(function () {
|
||||
return view('auth.two-factor-challange');
|
||||
return view('pages.auth.two-factor-challange');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,4 +43,12 @@ class UserFactory extends Factory
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function hasRole(Role|Factory|null $role = null): self
|
||||
{
|
||||
return $this->for(
|
||||
$role ?? Role::factory(),
|
||||
'role'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
name: '{{ $user->name }}',
|
||||
email: '{{ $user->email }}',
|
||||
password: '{{ $user->password ?? null }}',
|
||||
role_uuid: '{{ $user->role->uuid ?? null }}',
|
||||
errors: {},
|
||||
saveProfile() {
|
||||
// Очищаем предыдущие ошибки
|
||||
@ -22,6 +23,10 @@
|
||||
data.email = this.email;
|
||||
}
|
||||
|
||||
if (this.role_uuid) {
|
||||
data.role_uuid = this.role_uuid;
|
||||
}
|
||||
|
||||
if (this.password) {
|
||||
data.password = this.password;
|
||||
}
|
||||
@ -187,10 +192,13 @@
|
||||
@if(auth()->user()->role->code === 'admin')
|
||||
<div class="col-span-2">
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||
Bio
|
||||
Role
|
||||
</label>
|
||||
<input type="text" value="{{ $user->role->name }}"
|
||||
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
|
||||
<select x-model="roleUuid" class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800">
|
||||
@foreach(App\Models\Role::all() as $role)
|
||||
<option value="{{ $role->uuid }}" @if($user->role->uuid == $role->uuid) selected @endif>{{ $role->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,69 @@
|
||||
@extends ('layouts.app')
|
||||
|
||||
@section ('content')
|
||||
<h1>dashboard</h1>
|
||||
<x-common.page-breadcrumb pageTitle="{{ __('Dashboard') }}" />
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Total Users') }}</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-800 dark:text-white/90">{{ \App\Models\User::count() }}</p>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-500/10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-blue-600 dark:text-blue-400">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="{{ route('users.index') }}" class="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ __('Manage Users') }} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Total Roles') }}</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-800 dark:text-white/90">{{ \App\Models\Role::count() }}</p>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-purple-100 dark:bg-purple-500/10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-purple-600 dark:text-purple-400">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="{{ route('roles.index') }}" class="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ __('Manage Roles') }} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Verified Users') }}</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-800 dark:text-white/90">{{ \App\Models\User::whereNotNull('email_verified_at')->count() }}</p>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-500/10">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-green-600 dark:text-green-400">
|
||||
<polyline points="9 11 12 14 22 4"/>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ round((\App\Models\User::whereNotNull('email_verified_at')->count() / max(\App\Models\User::count(), 1)) * 100) }}% {{ __('verified') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
91
src/resources/views/pages/role/index.blade.php
Normal file
91
src/resources/views/pages/role/index.blade.php
Normal file
@ -0,0 +1,91 @@
|
||||
@extends ('layouts.app')
|
||||
|
||||
@section ('content')
|
||||
<x-common.page-breadcrumb pageTitle="{{ __('Roles') }}" />
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">{{ __('Roles List') }}</h3>
|
||||
@if(auth()->user()?->role?->code === 'admin')
|
||||
<button type="button" class="btn btn-primary" onclick="alert('Create role modal')">
|
||||
+ {{ __('Create Role') }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Name') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Code') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Description') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Users') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Created') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($data->roles as $role)
|
||||
<tr class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="py-3 px-4 text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{{ $role->name }}
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-500/10 dark:text-blue-400">
|
||||
{{ $role->code }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{{ $role->description }}</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{{ $role->users_count }}</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{{ $role->created_at }}</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
@if(auth()->user()?->role?->code === 'admin' && $role->users_count === 0)
|
||||
<form method="POST" action="{{ route('roles.destroy', ['role_uuid' => $role->uuid]) }}"
|
||||
onsubmit="return confirm('{{ __('Are you sure?') }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:underline dark:text-red-400">
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ __('No roles found') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($data->last_page > 1)
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ __('Page') }} {{ $data->current_page }} {{ __('of') }} {{ $data->last_page }}
|
||||
({{ $data->total }} {{ __('total') }})
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($data->current_page > 1)
|
||||
<a href="{{ route('roles.index', ['page' => $data->current_page - 1]) }}"
|
||||
class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
← {{ __('Previous') }}
|
||||
</a>
|
||||
@endif
|
||||
@if($data->current_page < $data->last_page)
|
||||
<a href="{{ route('roles.index', ['page' => $data->current_page + 1]) }}"
|
||||
class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
{{ __('Next') }} →
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
39
src/resources/views/pages/role/show.blade.php
Normal file
39
src/resources/views/pages/role/show.blade.php
Normal file
@ -0,0 +1,39 @@
|
||||
@extends ('layouts.app')
|
||||
|
||||
@section ('content')
|
||||
<x-common.page-breadcrumb pageTitle="{{ __('Role Details') }}" />
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<div class="mb-5">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">{{ $data->name }}</h3>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-500/10 dark:text-blue-400 mt-1">
|
||||
{{ $data->code }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Description') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-800 dark:text-white/90">{{ $data->description }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Users Count') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-800 dark:text-white/90">{{ $data->users_count }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Created') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-800 dark:text-white/90">{{ $data->created_at }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Updated') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-800 dark:text-white/90">{{ $data->updated_at }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="{{ route('roles.index') }}" class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
← {{ __('Back to roles') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -1,5 +1,103 @@
|
||||
@extends ('layouts.app')
|
||||
|
||||
@section ('content')
|
||||
<h1>Users index</h1>
|
||||
<x-common.page-breadcrumb pageTitle="{{ __('Users') }}" />
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">{{ __('Users List') }}</h3>
|
||||
@if(auth()->user()?->role?->code === 'admin')
|
||||
<button type="button" class="btn btn-primary" onclick="alert('Create user modal')">
|
||||
+ {{ __('Create User') }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700">
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Name') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Email') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Role') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Verified') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Created') }}</th>
|
||||
<th class="py-3 px-4 text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($data->users as $user)
|
||||
<tr class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="py-3 px-4 text-sm text-gray-800 dark:text-white/90">
|
||||
<a href="{{ route('users.show', ['user_uuid' => $user->uuid]) }}" class="text-blue-600 hover:underline">
|
||||
{{ $user->name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{{ $user->email }}</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-500/10 dark:text-blue-400">
|
||||
{{ $user->role->name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
@if($user->email_verified)
|
||||
<span class="text-green-600 dark:text-green-400">✓</span>
|
||||
@else
|
||||
<span class="text-red-600 dark:text-red-400">✗</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{{ $user->created_at }}</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('users.show', ['user_uuid' => $user->uuid]) }}"
|
||||
class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ __('View') }}
|
||||
</a>
|
||||
@if(auth()->user()?->role?->code === 'admin' && auth()->user()?->uuid !== $user->uuid)
|
||||
<form method="POST" action="{{ route('users.destroy', ['user_uuid' => $user->uuid]) }}"
|
||||
onsubmit="return confirm('{{ __('Are you sure?') }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:underline dark:text-red-400">
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ __('No users found') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($data->last_page > 1)
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ __('Page') }} {{ $data->current_page }} {{ __('of') }} {{ $data->last_page }}
|
||||
({{ $data->total }} {{ __('total') }})
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($data->current_page > 1)
|
||||
<a href="{{ route('users.index', ['page' => $data->current_page - 1]) }}"
|
||||
class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
← {{ __('Previous') }}
|
||||
</a>
|
||||
@endif
|
||||
@if($data->current_page < $data->last_page)
|
||||
<a href="{{ route('users.index', ['page' => $data->current_page + 1]) }}"
|
||||
class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
{{ __('Next') }} →
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@ -13,6 +13,7 @@ Route::get('/', function () {
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
||||
->name('dashboard');
|
||||
|
||||
Route::controller(UserController::class)
|
||||
->prefix('/users')
|
||||
->as('users.')
|
||||
@ -20,12 +21,38 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/', 'index')
|
||||
->name('index')
|
||||
->middleware('role:admin');
|
||||
Route::post('/', 'store')
|
||||
->name('store')
|
||||
->middleware('role:admin');
|
||||
Route::get('/{user_uuid}', 'show')
|
||||
->name('show')
|
||||
->whereUuid('user_uuid');
|
||||
Route::patch('/{user_uuid}/update', 'update')
|
||||
->name('update')
|
||||
->whereUuid('user_uuid');
|
||||
Route::delete('/{user_uuid}', 'destroy')
|
||||
->name('destroy')
|
||||
->middleware('role:admin')
|
||||
->whereUuid('user_uuid');
|
||||
});
|
||||
|
||||
Route::controller(\App\Http\Controllers\RoleController::class)
|
||||
->prefix('/roles')
|
||||
->as('roles.')
|
||||
->middleware('role:admin')
|
||||
->group(static function (): void {
|
||||
Route::get('/', 'index')
|
||||
->name('index');
|
||||
Route::post('/', 'store')
|
||||
->name('store');
|
||||
Route::get('/{role_uuid}', 'show')
|
||||
->name('show')
|
||||
->whereUuid('role_uuid');
|
||||
Route::patch('/{role_uuid}/update', 'update')
|
||||
->name('update')
|
||||
->whereUuid('role_uuid');
|
||||
Route::delete('/{role_uuid}', 'destroy')
|
||||
->name('destroy')
|
||||
->whereUuid('role_uuid');
|
||||
});
|
||||
});
|
||||
|
||||
166
src/tests/Feature/RoleControllerTest.php
Normal file
166
src/tests/Feature/RoleControllerTest.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
// ========== Index ==========
|
||||
|
||||
it('администратор может просматривать список ролей', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
Role::factory()->count(3)->create();
|
||||
|
||||
$response = actingAs($admin)
|
||||
->get(route('roles.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('pages.role.index');
|
||||
$response->assertViewHas('data');
|
||||
|
||||
$viewData = $response->viewData('data');
|
||||
expect($viewData->total)->toBe(5); // 3 factory + 2 seeder (admin, user)
|
||||
});
|
||||
|
||||
it('обычный пользователь не может просматривать список ролей', function () {
|
||||
$user = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->get(route('roles.index'));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
// ========== Show ==========
|
||||
|
||||
it('администратор может просматривать роль', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$role = Role::factory()->createOne();
|
||||
|
||||
$response = actingAs($admin)
|
||||
->get(route('roles.show', ['role_uuid' => $role->uuid]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('pages.role.show');
|
||||
$response->assertViewHas('data');
|
||||
|
||||
$viewData = $response->viewData('data');
|
||||
expect($viewData->uuid)->toBe($role->uuid->toString());
|
||||
expect($viewData->name)->toBe($role->name);
|
||||
});
|
||||
|
||||
// ========== Store ==========
|
||||
|
||||
it('администратор может создать роль', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->postJson(route('roles.store'), [
|
||||
'name' => 'Moderator',
|
||||
'code' => 'moderator',
|
||||
'description' => 'Moderator role',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$this->assertDatabaseHas('roles', [
|
||||
'code' => 'moderator',
|
||||
'name' => 'Moderator',
|
||||
]);
|
||||
});
|
||||
|
||||
it('обычный пользователь не может создать роль', function () {
|
||||
$user = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->postJson(route('roles.store'), [
|
||||
'name' => 'Moderator',
|
||||
'code' => 'moderator',
|
||||
'description' => 'Moderator role',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
it('создание роли требует уникальный code', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
Role::factory()->createOne(['code' => 'moderator']);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->postJson(route('roles.store'), [
|
||||
'name' => 'Moderator',
|
||||
'code' => 'moderator',
|
||||
'description' => 'Moderator role',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
});
|
||||
|
||||
// ========== Update ==========
|
||||
|
||||
it('администратор может обновить роль', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne();
|
||||
$admin->role_uuid = $adminRole->uuid;
|
||||
$admin->save();
|
||||
$role = Role::factory()->createOne(['code' => 'moderator']);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->patchJson(route('roles.update', ['role_uuid' => $role->uuid]), [
|
||||
'name' => 'Updated Moderator',
|
||||
'description' => 'Updated description',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$role->refresh();
|
||||
expect($role->name)->toBe('Updated Moderator');
|
||||
expect($role->description)->toBe('Updated description');
|
||||
});
|
||||
|
||||
// ========== Destroy ==========
|
||||
|
||||
it('администратор может удалить роль без пользователей', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$role = Role::factory()->createOne();
|
||||
|
||||
$response = actingAs($admin)
|
||||
->deleteJson(route('roles.destroy', ['role_uuid' => $role->uuid]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$this->assertDatabaseMissing('roles', ['uuid' => $role->uuid]);
|
||||
});
|
||||
|
||||
it('нельзя удалить роль с привязанными пользователями', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->deleteJson(route('roles.destroy', ['role_uuid' => $adminRole->uuid]));
|
||||
|
||||
$response->assertStatus(409); // Conflict
|
||||
});
|
||||
|
||||
it('обычный пользователь не может удалить роль', function () {
|
||||
$user = User::factory()->createOne();
|
||||
$role = Role::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->deleteJson(route('roles.destroy', ['role_uuid' => $role->uuid]));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
@ -3,13 +3,14 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
|
||||
// ========== Show ==========
|
||||
|
||||
it('получение профиля пользователя', function () {
|
||||
$user = User::factory()->create();
|
||||
@ -19,5 +20,215 @@ it('получение профиля пользователя', function () {
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('pages.user.show');
|
||||
$response->assertViewHas('data', $user);
|
||||
$response->assertViewHas('data');
|
||||
|
||||
$viewData = $response->viewData('data');
|
||||
expect($viewData->uuid)->toBe($user->uuid->toString());
|
||||
expect($viewData->name)->toBe($user->name);
|
||||
expect($viewData->email)->toBe($user->email);
|
||||
});
|
||||
|
||||
// ========== Index ==========
|
||||
|
||||
it('администратор может просматривать список пользователей', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
User::factory()->count(5)->create();
|
||||
|
||||
$response = actingAs($admin)
|
||||
->get(route('users.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('pages.user.index');
|
||||
$response->assertViewHas('data');
|
||||
|
||||
$viewData = $response->viewData('data');
|
||||
expect($viewData->total)->toBe(6); // 5 + 1 admin
|
||||
expect($viewData->users)->toHaveCount(6);
|
||||
});
|
||||
|
||||
it('обычный пользователь не может просматривать список пользователей', function () {
|
||||
$user = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->get(route('users.index'));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
it('список пользователей поддерживает поиск', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid, 'name' => 'Admin User']);
|
||||
User::factory()->createOne(['name' => 'John Doe']);
|
||||
User::factory()->createOne(['name' => 'Jane Smith']);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->get(route('users.index', ['search' => 'John']));
|
||||
|
||||
$response->assertOk();
|
||||
$viewData = $response->viewData('data');
|
||||
expect($viewData->total)->toBe(1);
|
||||
});
|
||||
|
||||
// ========== Store ==========
|
||||
|
||||
it('администратор может создать пользователя', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$userRole = Role::factory()->createOne(['code' => 'user']);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->postJson(route('users.store'), [
|
||||
'name' => 'New User',
|
||||
'email' => 'newuser@example.com',
|
||||
'password' => 'password123',
|
||||
'role_uuid' => $userRole->uuid->toString(),
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'newuser@example.com',
|
||||
'name' => 'New User',
|
||||
'role_uuid' => $userRole->uuid->toString(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('обычный пользователь не может создать пользователя', function () {
|
||||
$user = User::factory()->createOne();
|
||||
$userRole = Role::factory()->createOne(['code' => 'user']);
|
||||
|
||||
$response = actingAs($user)
|
||||
->postJson(route('users.store'), [
|
||||
'name' => 'New User',
|
||||
'email' => 'newuser@example.com',
|
||||
'password' => 'password123',
|
||||
'role_uuid' => $userRole->uuid->toString(),
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
it('создание пользователя требует уникальный email', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$userRole = Role::factory()->createOne(['code' => 'user']);
|
||||
|
||||
User::factory()->createOne(['email' => 'existing@example.com']);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->postJson(route('users.store'), [
|
||||
'name' => 'New User',
|
||||
'email' => 'existing@example.com',
|
||||
'password' => 'password123',
|
||||
'role_uuid' => $userRole->uuid->toString(),
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
});
|
||||
|
||||
// ========== Destroy ==========
|
||||
|
||||
it('администратор может удалить пользователя', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$targetUser = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($admin)
|
||||
->deleteJson(route('users.destroy', ['user_uuid' => $targetUser->uuid]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$this->assertDatabaseMissing('users', ['uuid' => $targetUser->uuid]);
|
||||
});
|
||||
|
||||
it('администратор не может удалить самого себя', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->deleteJson(route('users.destroy', ['user_uuid' => $admin->uuid]));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
it('обычный пользователь не может удалить пользователя', function () {
|
||||
$user = User::factory()->createOne();
|
||||
$targetUser = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->deleteJson(route('users.destroy', ['user_uuid' => $targetUser->uuid]));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
// ========== Update ==========
|
||||
|
||||
it('обычный пользователь не может редактировать другого пользователя', function () {
|
||||
$user = User::factory()->createOne();
|
||||
$targetUser = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->patchJson(route('users.update', ['user_uuid' => $targetUser->uuid]), [
|
||||
'name' => 'Новое имя',
|
||||
'email' => 'new@example.com'
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
it('администратор может редактировать любого пользователя', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$targetUser = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($admin)
|
||||
->patchJson(route('users.update', ['user_uuid' => $targetUser->uuid]), [
|
||||
'name' => 'Новое имя',
|
||||
'email' => 'new@example.com'
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$targetUser->refresh();
|
||||
expect($targetUser->name)->toBe('Новое имя');
|
||||
expect($targetUser->email)->toBe('new@example.com');
|
||||
});
|
||||
|
||||
it('пользователь может редактировать свои данные', function () {
|
||||
$user = User::factory()->createOne();
|
||||
|
||||
$response = actingAs($user)
|
||||
->patchJson(route('users.update', ['user_uuid' => $user->uuid]), [
|
||||
'name' => 'Новое имя',
|
||||
'email' => 'new@example.com'
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$user->refresh();
|
||||
expect($user->name)->toBe('Новое имя');
|
||||
expect($user->email)->toBe('new@example.com');
|
||||
});
|
||||
|
||||
it('администратор может изменить роль пользователя', function () {
|
||||
$adminRole = Role::factory()->createOne(['code' => 'admin']);
|
||||
$userRole = Role::factory()->createOne(['code' => 'user']);
|
||||
$admin = User::factory()->createOne(['role_uuid' => $adminRole->uuid]);
|
||||
$targetUser = User::factory()->createOne(['role_uuid' => $userRole->uuid]);
|
||||
|
||||
$response = actingAs($admin)
|
||||
->patchJson(route('users.update', ['user_uuid' => $targetUser->uuid]), [
|
||||
'role_uuid' => $adminRole->uuid->toString()
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
$targetUser->refresh();
|
||||
expect($targetUser->role_uuid)->toBe($adminRole->uuid->toString());
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user