Simplifies the namespace hierarchy by removing the intermediate Mod segment. Updates all 118 files including models, services, controllers, middleware, tests, and composer.json autoload configuration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
629 lines
19 KiB
PHP
629 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Tenant\Services;
|
|
|
|
use Core\Tenant\Models\User;
|
|
use Core\Tenant\Models\Workspace;
|
|
use Core\Tenant\Models\WorkspaceMember;
|
|
use Core\Tenant\Models\WorkspaceTeam;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Workspace Team Service - manages workspace teams and member permissions.
|
|
*/
|
|
class WorkspaceTeamService
|
|
{
|
|
protected ?Workspace $workspace = null;
|
|
|
|
public function __construct(?Workspace $workspace = null)
|
|
{
|
|
$this->workspace = $workspace;
|
|
}
|
|
|
|
/**
|
|
* Set the workspace context.
|
|
*/
|
|
public function forWorkspace(Workspace $workspace): self
|
|
{
|
|
$this->workspace = $workspace;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the current workspace, resolving from context if needed.
|
|
*/
|
|
protected function getWorkspace(): ?Workspace
|
|
{
|
|
if ($this->workspace) {
|
|
return $this->workspace;
|
|
}
|
|
|
|
// Try authenticated user's default workspace first
|
|
$this->workspace = auth()->user()?->defaultHostWorkspace();
|
|
|
|
// Fall back to session workspace if set
|
|
if (! $this->workspace) {
|
|
$sessionWorkspaceId = session('workspace_id');
|
|
if ($sessionWorkspaceId) {
|
|
$this->workspace = Workspace::find($sessionWorkspaceId);
|
|
}
|
|
}
|
|
|
|
return $this->workspace;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Team Management
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get all teams for the workspace.
|
|
*/
|
|
public function getTeams(): Collection
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return new Collection;
|
|
}
|
|
|
|
return WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->ordered()
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Get a specific team by ID.
|
|
*/
|
|
public function getTeam(int $teamId): ?WorkspaceTeam
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return null;
|
|
}
|
|
|
|
return WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('id', $teamId)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Get a specific team by slug.
|
|
*/
|
|
public function getTeamBySlug(string $slug): ?WorkspaceTeam
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return null;
|
|
}
|
|
|
|
return WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('slug', $slug)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Get the default team for new members.
|
|
*/
|
|
public function getDefaultTeam(): ?WorkspaceTeam
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return null;
|
|
}
|
|
|
|
return WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('is_default', true)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Create a new team.
|
|
*/
|
|
public function createTeam(array $data): WorkspaceTeam
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
throw new \RuntimeException('No workspace context available.');
|
|
}
|
|
|
|
$team = WorkspaceTeam::create([
|
|
'workspace_id' => $workspace->id,
|
|
'name' => $data['name'],
|
|
'slug' => $data['slug'] ?? null,
|
|
'description' => $data['description'] ?? null,
|
|
'permissions' => $data['permissions'] ?? [],
|
|
'is_default' => $data['is_default'] ?? false,
|
|
'is_system' => $data['is_system'] ?? false,
|
|
'colour' => $data['colour'] ?? 'zinc',
|
|
'sort_order' => $data['sort_order'] ?? 0,
|
|
]);
|
|
|
|
// If this is the new default, unset other defaults
|
|
if ($team->is_default) {
|
|
WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('id', '!=', $team->id)
|
|
->where('is_default', true)
|
|
->update(['is_default' => false]);
|
|
}
|
|
|
|
Log::info('Workspace team created', [
|
|
'team_id' => $team->id,
|
|
'team_name' => $team->name,
|
|
'workspace_id' => $workspace->id,
|
|
]);
|
|
|
|
return $team;
|
|
}
|
|
|
|
/**
|
|
* Update an existing team.
|
|
*/
|
|
public function updateTeam(WorkspaceTeam $team, array $data): WorkspaceTeam
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
|
|
// Don't allow updating system teams' slug
|
|
if ($team->is_system && isset($data['slug'])) {
|
|
unset($data['slug']);
|
|
}
|
|
|
|
$team->update($data);
|
|
|
|
// If this is the new default, unset other defaults
|
|
if (($data['is_default'] ?? false) && $workspace) {
|
|
WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('id', '!=', $team->id)
|
|
->where('is_default', true)
|
|
->update(['is_default' => false]);
|
|
}
|
|
|
|
Log::info('Workspace team updated', [
|
|
'team_id' => $team->id,
|
|
'team_name' => $team->name,
|
|
'workspace_id' => $team->workspace_id,
|
|
]);
|
|
|
|
return $team;
|
|
}
|
|
|
|
/**
|
|
* Delete a team (only non-system teams).
|
|
*/
|
|
public function deleteTeam(WorkspaceTeam $team): bool
|
|
{
|
|
if ($team->is_system) {
|
|
throw new \RuntimeException('Cannot delete system teams.');
|
|
}
|
|
|
|
// Check if team has any members assigned
|
|
$memberCount = WorkspaceMember::where('team_id', $team->id)->count();
|
|
if ($memberCount > 0) {
|
|
throw new \RuntimeException(
|
|
"Cannot delete team with {$memberCount} assigned members. Remove members first."
|
|
);
|
|
}
|
|
|
|
$teamId = $team->id;
|
|
$teamName = $team->name;
|
|
$workspaceId = $team->workspace_id;
|
|
|
|
$team->delete();
|
|
|
|
Log::info('Workspace team deleted', [
|
|
'team_id' => $teamId,
|
|
'team_name' => $teamName,
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Member Management
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get a member record for a user in the workspace.
|
|
*/
|
|
public function getMember(User|int $user): ?WorkspaceMember
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return null;
|
|
}
|
|
|
|
$userId = $user instanceof User ? $user->id : $user;
|
|
|
|
return WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->where('user_id', $userId)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Get all members in the workspace.
|
|
*/
|
|
public function getMembers(): Collection
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return new Collection;
|
|
}
|
|
|
|
return WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->with(['user', 'team', 'inviter'])
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Get all members in a specific team.
|
|
*/
|
|
public function getTeamMembers(WorkspaceTeam|int $team): Collection
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return new Collection;
|
|
}
|
|
|
|
$teamId = $team instanceof WorkspaceTeam ? $team->id : $team;
|
|
|
|
return WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->where('team_id', $teamId)
|
|
->with(['user', 'team', 'inviter'])
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Add a member to a team.
|
|
*/
|
|
public function addMemberToTeam(User|int $user, WorkspaceTeam|int $team): WorkspaceMember
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
throw new \RuntimeException('No workspace context available.');
|
|
}
|
|
|
|
$userId = $user instanceof User ? $user->id : $user;
|
|
$teamId = $team instanceof WorkspaceTeam ? $team->id : $team;
|
|
|
|
// Verify team belongs to workspace
|
|
$teamModel = WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('id', $teamId)
|
|
->first();
|
|
|
|
if (! $teamModel) {
|
|
throw new \RuntimeException('Team does not belong to the current workspace.');
|
|
}
|
|
|
|
$member = WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->where('user_id', $userId)
|
|
->first();
|
|
|
|
if (! $member) {
|
|
throw new \RuntimeException('User is not a member of this workspace.');
|
|
}
|
|
|
|
$member->update(['team_id' => $teamId]);
|
|
|
|
Log::info('Member added to team', [
|
|
'user_id' => $userId,
|
|
'team_id' => $teamId,
|
|
'team_name' => $teamModel->name,
|
|
'workspace_id' => $workspace->id,
|
|
]);
|
|
|
|
return $member->fresh();
|
|
}
|
|
|
|
/**
|
|
* Remove a member from their team.
|
|
*/
|
|
public function removeMemberFromTeam(User|int $user): WorkspaceMember
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
throw new \RuntimeException('No workspace context available.');
|
|
}
|
|
|
|
$userId = $user instanceof User ? $user->id : $user;
|
|
|
|
$member = WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->where('user_id', $userId)
|
|
->first();
|
|
|
|
if (! $member) {
|
|
throw new \RuntimeException('User is not a member of this workspace.');
|
|
}
|
|
|
|
$oldTeamId = $member->team_id;
|
|
$member->update(['team_id' => null]);
|
|
|
|
Log::info('Member removed from team', [
|
|
'user_id' => $userId,
|
|
'old_team_id' => $oldTeamId,
|
|
'workspace_id' => $workspace->id,
|
|
]);
|
|
|
|
return $member->fresh();
|
|
}
|
|
|
|
/**
|
|
* Set custom permissions for a member.
|
|
*/
|
|
public function setMemberCustomPermissions(User|int $user, array $customPermissions): WorkspaceMember
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
throw new \RuntimeException('No workspace context available.');
|
|
}
|
|
|
|
$userId = $user instanceof User ? $user->id : $user;
|
|
|
|
$member = WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->where('user_id', $userId)
|
|
->first();
|
|
|
|
if (! $member) {
|
|
throw new \RuntimeException('User is not a member of this workspace.');
|
|
}
|
|
|
|
$member->update(['custom_permissions' => $customPermissions]);
|
|
|
|
Log::info('Member custom permissions updated', [
|
|
'user_id' => $userId,
|
|
'workspace_id' => $workspace->id,
|
|
'custom_permissions' => $customPermissions,
|
|
]);
|
|
|
|
return $member->fresh();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Permission Checks
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get all effective permissions for a user in the workspace.
|
|
*/
|
|
public function getMemberPermissions(User|int $user): array
|
|
{
|
|
$member = $this->getMember($user);
|
|
|
|
if (! $member) {
|
|
return [];
|
|
}
|
|
|
|
return $member->getEffectivePermissions();
|
|
}
|
|
|
|
/**
|
|
* Check if a user has a specific permission in the workspace.
|
|
*/
|
|
public function hasPermission(User|int $user, string $permission): bool
|
|
{
|
|
$member = $this->getMember($user);
|
|
|
|
if (! $member) {
|
|
return false;
|
|
}
|
|
|
|
return $member->hasPermission($permission);
|
|
}
|
|
|
|
/**
|
|
* Check if a user has any of the given permissions.
|
|
*/
|
|
public function hasAnyPermission(User|int $user, array $permissions): bool
|
|
{
|
|
$member = $this->getMember($user);
|
|
|
|
if (! $member) {
|
|
return false;
|
|
}
|
|
|
|
return $member->hasAnyPermission($permissions);
|
|
}
|
|
|
|
/**
|
|
* Check if a user has all of the given permissions.
|
|
*/
|
|
public function hasAllPermissions(User|int $user, array $permissions): bool
|
|
{
|
|
$member = $this->getMember($user);
|
|
|
|
if (! $member) {
|
|
return false;
|
|
}
|
|
|
|
return $member->hasAllPermissions($permissions);
|
|
}
|
|
|
|
/**
|
|
* Check if a user is the workspace owner.
|
|
*/
|
|
public function isOwner(User|int $user): bool
|
|
{
|
|
$member = $this->getMember($user);
|
|
|
|
return $member?->isOwner() ?? false;
|
|
}
|
|
|
|
/**
|
|
* Check if a user is a workspace admin.
|
|
*/
|
|
public function isAdmin(User|int $user): bool
|
|
{
|
|
$member = $this->getMember($user);
|
|
|
|
return $member?->isAdmin() ?? false;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Member Queries
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get members with a specific permission.
|
|
*/
|
|
public function getMembersWithPermission(string $permission): Collection
|
|
{
|
|
$members = $this->getMembers();
|
|
|
|
return $members->filter(fn ($member) => $member->hasPermission($permission));
|
|
}
|
|
|
|
/**
|
|
* Count members in the workspace.
|
|
*/
|
|
public function countMembers(): int
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return 0;
|
|
}
|
|
|
|
return WorkspaceMember::where('workspace_id', $workspace->id)->count();
|
|
}
|
|
|
|
/**
|
|
* Count members in a specific team.
|
|
*/
|
|
public function countTeamMembers(WorkspaceTeam|int $team): int
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return 0;
|
|
}
|
|
|
|
$teamId = $team instanceof WorkspaceTeam ? $team->id : $team;
|
|
|
|
return WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->where('team_id', $teamId)
|
|
->count();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Seeding
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Seed default teams for a workspace.
|
|
*/
|
|
public function seedDefaultTeams(?Workspace $workspace = null): Collection
|
|
{
|
|
$workspace = $workspace ?? $this->getWorkspace();
|
|
if (! $workspace) {
|
|
throw new \RuntimeException('No workspace context available for seeding.');
|
|
}
|
|
|
|
$teams = new Collection;
|
|
|
|
foreach (WorkspaceTeam::getDefaultTeamDefinitions() as $definition) {
|
|
// Check if team already exists
|
|
$existing = WorkspaceTeam::where('workspace_id', $workspace->id)
|
|
->where('slug', $definition['slug'])
|
|
->first();
|
|
|
|
if ($existing) {
|
|
$teams->push($existing);
|
|
|
|
continue;
|
|
}
|
|
|
|
$team = WorkspaceTeam::create([
|
|
'workspace_id' => $workspace->id,
|
|
'name' => $definition['name'],
|
|
'slug' => $definition['slug'],
|
|
'description' => $definition['description'],
|
|
'permissions' => $definition['permissions'],
|
|
'is_default' => $definition['is_default'] ?? false,
|
|
'is_system' => $definition['is_system'] ?? false,
|
|
'colour' => $definition['colour'] ?? 'zinc',
|
|
'sort_order' => $definition['sort_order'] ?? 0,
|
|
]);
|
|
|
|
$teams->push($team);
|
|
}
|
|
|
|
Log::info('Default workspace teams seeded', [
|
|
'workspace_id' => $workspace->id,
|
|
'teams_count' => $teams->count(),
|
|
]);
|
|
|
|
return $teams;
|
|
}
|
|
|
|
/**
|
|
* Ensure default teams exist for the workspace, creating them if needed.
|
|
*/
|
|
public function ensureDefaultTeams(): Collection
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return new Collection;
|
|
}
|
|
|
|
// Check if any teams exist
|
|
$existingCount = WorkspaceTeam::where('workspace_id', $workspace->id)->count();
|
|
|
|
if ($existingCount === 0) {
|
|
return $this->seedDefaultTeams($workspace);
|
|
}
|
|
|
|
return $this->getTeams();
|
|
}
|
|
|
|
/**
|
|
* Migrate existing members to appropriate teams based on their role.
|
|
*/
|
|
public function migrateExistingMembers(): int
|
|
{
|
|
$workspace = $this->getWorkspace();
|
|
if (! $workspace) {
|
|
return 0;
|
|
}
|
|
|
|
// Ensure teams exist
|
|
$this->ensureDefaultTeams();
|
|
|
|
$ownerTeam = $this->getTeamBySlug(WorkspaceTeam::TEAM_OWNER);
|
|
$adminTeam = $this->getTeamBySlug(WorkspaceTeam::TEAM_ADMIN);
|
|
$memberTeam = $this->getTeamBySlug(WorkspaceTeam::TEAM_MEMBER);
|
|
|
|
$migrated = 0;
|
|
|
|
DB::transaction(function () use ($workspace, $ownerTeam, $adminTeam, $memberTeam, &$migrated) {
|
|
// Get members without team assignments
|
|
$members = WorkspaceMember::where('workspace_id', $workspace->id)
|
|
->whereNull('team_id')
|
|
->get();
|
|
|
|
foreach ($members as $member) {
|
|
$teamId = match ($member->role) {
|
|
WorkspaceMember::ROLE_OWNER => $ownerTeam?->id,
|
|
WorkspaceMember::ROLE_ADMIN => $adminTeam?->id,
|
|
default => $memberTeam?->id,
|
|
};
|
|
|
|
if ($teamId) {
|
|
$member->update([
|
|
'team_id' => $teamId,
|
|
'joined_at' => $member->joined_at ?? $member->created_at,
|
|
]);
|
|
$migrated++;
|
|
}
|
|
}
|
|
});
|
|
|
|
Log::info('Workspace members migrated to teams', [
|
|
'workspace_id' => $workspace->id,
|
|
'migrated_count' => $migrated,
|
|
]);
|
|
|
|
return $migrated;
|
|
}
|
|
}
|