feat: complete Epic 1 — team management & permission system
- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration - Story 1.2: Team page with member list, invitation system with queued email - Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging - Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal) - Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping - Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching - 83 new/modified files, 182 tests passing with zero regressions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Concerns\HasWorkspaceScope;
|
||||
use App\Enums\DeclarationPriority;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Enums\DeclarationType;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Http\Requests\StoreDeclarationRequest;
|
||||
use App\Http\Requests\UpdateDeclarationRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -18,6 +19,8 @@ use Inertia\Response;
|
||||
|
||||
class DeclarationController extends Controller
|
||||
{
|
||||
use HasWorkspaceScope;
|
||||
|
||||
protected function declarationTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
@@ -46,11 +49,9 @@ class DeclarationController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
protected function isWorker(): bool
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
return auth()->user()->currentWorkspaceUser()->role->is(WorkspaceUserRole::Worker);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,11 +59,15 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$workspace = $this->currentWorkspace();
|
||||
$user = auth()->user();
|
||||
$workspaceUser = $user->currentWorkspaceUser();
|
||||
$isWorker = $workspaceUser->role->is(WorkspaceUserRole::Worker);
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 10), 10), 100);
|
||||
|
||||
$declarations = $workspace->declarations()
|
||||
->forUser($user, $workspaceUser)
|
||||
->with(['client', 'assignee'])
|
||||
->latest()
|
||||
->paginate($perPage)
|
||||
@@ -82,6 +87,9 @@ class DeclarationController extends Controller
|
||||
'declarations' => $declarations,
|
||||
'createUrl' => route('declarations.create'),
|
||||
'workspaceName' => $workspace->name,
|
||||
'canCreate' => ! $isWorker,
|
||||
'canEdit' => ! $isWorker,
|
||||
'canDelete' => ! $isWorker,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -90,7 +98,11 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
$initialClientId = $request->integer('client_id', 0) ?: null;
|
||||
|
||||
return Inertia::render('declarations/Create', [
|
||||
@@ -121,7 +133,11 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function store(StoreDeclarationRequest $request): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
$data = $request->validated();
|
||||
$data['workspace_id'] = $workspace->id;
|
||||
$data['created_by'] = $request->user()?->id;
|
||||
@@ -143,8 +159,16 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function show(Request $request, Declaration $declaration): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
$this->authorizeWorkspaceAccess($declaration);
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
$user = auth()->user();
|
||||
$workspaceUser = $user->currentWorkspaceUser();
|
||||
$isWorker = $workspaceUser->role->is(WorkspaceUserRole::Worker);
|
||||
|
||||
if ($isWorker && $declaration->assigned_to !== $user->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$declaration->load(['client', 'creator', 'assignee', 'messages' => fn ($q) => $q->with(['senderUser', 'senderClient'])->latest()]);
|
||||
|
||||
@@ -197,6 +221,8 @@ class DeclarationController extends Controller
|
||||
'is_downloaded' => in_array($m->id, $downloadedMediaIds),
|
||||
])->values()->all();
|
||||
|
||||
$canMention = ! $isWorker;
|
||||
|
||||
return Inertia::render('declarations/Show', [
|
||||
'declaration' => [
|
||||
'id' => $declaration->id,
|
||||
@@ -236,10 +262,9 @@ class DeclarationController extends Controller
|
||||
->get()->map(fn ($u) => ['id' => $u->id, 'name' => $u->name])
|
||||
->values()->all(),
|
||||
'mentionStoreUrl' => route('declarations.mentions.store', $declaration),
|
||||
'canMention' => in_array(
|
||||
$workspace->users()->where('users.id', $request->user()->id)->first()?->pivot?->role?->value,
|
||||
['owner', 'manager']
|
||||
),
|
||||
'canMention' => $canMention,
|
||||
'canEdit' => ! $isWorker,
|
||||
'canDelete' => ! $isWorker,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -248,8 +273,13 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function edit(Request $request, Declaration $declaration): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorizeWorkspaceAccess($declaration);
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
|
||||
return Inertia::render('declarations/Edit', [
|
||||
'declaration' => [
|
||||
@@ -294,8 +324,11 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function update(UpdateDeclarationRequest $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorizeWorkspaceAccess($declaration);
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
@@ -315,18 +348,14 @@ class DeclarationController extends Controller
|
||||
*/
|
||||
public function destroy(Request $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorizeWorkspaceAccess($declaration);
|
||||
|
||||
$declaration->delete();
|
||||
|
||||
return to_route('declarations.index');
|
||||
}
|
||||
|
||||
protected function authorizeDeclaration(Workspace $workspace, Declaration $declaration): void
|
||||
{
|
||||
if ($declaration->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user