feat(workspace): create action, middleware, policies
- CreateWorkspace action: creates workspace + owner member + sets default_workspace_id - ResolveWorkspace middleware: resolves workspace by ULID, 404s for non-members - WorkspacePolicy: owner/admin/editor/viewer role-based gates - Register WorkspacePolicy in AppServiceProvider - Add ulid string cast to Workspace model to ensure scalar after create() - Feature test: CreateWorkspaceTest (4 assertions, passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
ee0824302a
commit
49f2e3b00f
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Workspace\Actions;
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Domains\Workspace\Models\WorkspaceMember;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateWorkspace
|
||||||
|
{
|
||||||
|
public function handle(User $owner, array $data): Workspace
|
||||||
|
{
|
||||||
|
$workspace = Workspace::create([
|
||||||
|
'name' => $data['name'],
|
||||||
|
'slug' => Str::slug($data['name']),
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
WorkspaceMember::create([
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'role' => 'owner',
|
||||||
|
'joined_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $owner->default_workspace_id) {
|
||||||
|
$owner->update(['default_workspace_id' => $workspace->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $workspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ class Workspace extends Model
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'ulid' => 'string',
|
||||||
'branding' => 'array',
|
'branding' => 'array',
|
||||||
'stripe_customer_metadata' => 'array',
|
'stripe_customer_metadata' => 'array',
|
||||||
'trial_ends_at' => 'datetime',
|
'trial_ends_at' => 'datetime',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Workspace\Policies;
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class WorkspacePolicy
|
||||||
|
{
|
||||||
|
public function view(User $user, Workspace $workspace): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole($user, $workspace, ['owner', 'admin', 'editor', 'viewer']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, Workspace $workspace): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole($user, $workspace, ['owner', 'admin']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, Workspace $workspace): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole($user, $workspace, ['owner']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manageBilling(User $user, Workspace $workspace): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole($user, $workspace, ['owner']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasRole(User $user, Workspace $workspace, array $roles): bool
|
||||||
|
{
|
||||||
|
return $workspace->members()
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->whereIn('role', $roles)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ResolveWorkspace
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
$ulid = $request->route('workspace');
|
||||||
|
|
||||||
|
if (! $ulid) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$workspace = Workspace::where('ulid', $ulid)->firstOrFail();
|
||||||
|
|
||||||
|
// 404 instead of 403 — don't leak existence across workspace boundaries
|
||||||
|
if (! $request->user()?->workspaceMemberships()->where('workspace_id', $workspace->id)->exists()) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->merge(['current_workspace' => $workspace]);
|
||||||
|
app()->instance('current_workspace', $workspace);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Domains\Workspace\Policies\WorkspacePolicy;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
|
@ -19,6 +22,6 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Gate::policy(Workspace::class, WorkspacePolicy::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Actions\CreateWorkspace;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
it('creates a workspace and assigns owner as member', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$workspace = (new CreateWorkspace)->handle($user, ['name' => 'Acme Corp']);
|
||||||
|
|
||||||
|
expect($workspace->name)->toBe('Acme Corp')
|
||||||
|
->and($workspace->owner_id)->toBe($user->id)
|
||||||
|
->and($workspace->ulid)->toHaveLength(26);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('workspace_members', [
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue