first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace Common\Workspaces\Actions;
use Auth;
use Common\Workspaces\Workspace;
class CrupdateWorkspace
{
public function __construct(protected Workspace $workspace)
{
}
public function execute(
array $data,
Workspace|null $initialWorkspace = null
): Workspace {
if ($initialWorkspace) {
$workspace = $initialWorkspace;
} else {
$workspace = $this->workspace->newInstance([
'owner_id' => Auth::id(),
]);
}
$attributes = [
'name' => $data['name'],
];
$workspace->fill($attributes)->save();
if (!$initialWorkspace) {
$workspace
->members()
->create(['user_id' => Auth::id(), 'is_owner' => true]);
}
return $workspace;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Common\Workspaces\Actions;
use App\Models\User;
use Common\Workspaces\Notifications\WorkspaceInvitation;
use Common\Workspaces\WorkspaceInvite;
use Illuminate\Notifications\DatabaseNotification;
class DeleteInviteNotification
{
public function execute(WorkspaceInvite $invite, User $user): void
{
$notifications = $user
->notifications()
->where('type', WorkspaceInvitation::class)
->limit(20)
->get();
$notification = $notifications->first(function (
DatabaseNotification $notification,
) use ($invite) {
return $notification->data['inviteId'] === $invite->id;
});
if ($notification) {
$notification->delete();
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Common\Workspaces\Actions;
use Common\Workspaces\Events\WorkspaceDeleted;
use Common\Workspaces\Workspace;
use Common\Workspaces\WorkspaceMember;
class DeleteWorkspaces
{
/**
* @var Workspace
*/
private $workspace;
public function __construct(Workspace $workspace)
{
$this->workspace = $workspace;
}
public function execute($ids)
{
$workspaces = $this->workspace->whereIn('id', $ids)->get();
$workspaces->each(function(Workspace $workspace) {
$workspace->invites()->delete();
$workspace->members->each(function (WorkspaceMember $member) use($workspace) {
app(RemoveMemberFromWorkspace::class)->execute($workspace, $member->id);
});
event(new WorkspaceDeleted($workspace->id, $workspace->owner_id));
});
$this->workspace->whereIn('id', $ids)->delete();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Common\Workspaces\Actions;
use App\Models\User;
use Common\Workspaces\WorkspaceInvite;
use Session;
class JoinWorkspace
{
public function execute(WorkspaceInvite $invite, User $user)
{
if ($invite->email === $user->email) {
$invite->workspace
->members()
->firstOrCreate(
['user_id' => $user->id],
['role_id' => $invite->role_id],
);
app(DeleteInviteNotification::class)->execute($invite, $user);
$invite->delete();
}
Session::remove('activeWorkspace');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Common\Workspaces\Actions;
use Common\Workspaces\Workspace;
use Common\Workspaces\WorkspaceMember;
use const App\Providers\WORKSPACED_RESOURCES;
class RemoveMemberFromWorkspace
{
public function execute(Workspace $workspace, int $userToBeRemoved)
{
// transfer workspace resources to owner
if ($workspace->owner_id !== $userToBeRemoved) {
foreach (WORKSPACED_RESOURCES as $model) {
$baseName = class_basename($model);
$namespace = "App\Workspaces\Transfer{$baseName}";
if (class_exists($namespace)) {
app($namespace)->execute(
$workspace->id,
$workspace->owner_id,
$userToBeRemoved,
);
} else {
app($model)
->where('workspace_id', $workspace->id)
->where('user_id', $userToBeRemoved)
->update(['user_id' => $workspace->owner_id]);
}
}
}
app(WorkspaceMember::class)
->where('workspace_id', $workspace->id)
->where('user_id', $userToBeRemoved)
->delete();
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Common\Workspaces;
use App\Models\User;
use Auth;
class ActiveWorkspace
{
protected Workspace|null $cachedWorkspace = null;
public array $memberCache = [];
public int $id = 0;
/**
* Whether selected workspace was explicitly specified via request query params or defaulted to personal.
*/
public bool $explicitlySelected = false;
public function __construct()
{
$this->explicitlySelected = request()->has('workspaceId');
$this->id = (int) request()->get('workspaceId', 0);
}
public function workspace(): ?Workspace
{
if (
is_null($this->cachedWorkspace) ||
$this->cachedWorkspace->id !== $this->id
) {
$this->cachedWorkspace = $this->isPersonal()
? null
: Workspace::find($this->id);
}
return $this->cachedWorkspace ?: null;
}
public function isPersonal(): bool
{
return !$this->id;
}
public function owner(): User
{
return $this->workspace()->owner_id === Auth::id()
? Auth::user()
: $this->workspace()->owner;
}
public function currentUserIsOwner(): bool
{
if ($this->isPersonal()) {
return true;
}
return $this->workspace() &&
$this->workspace()->owner_id === Auth::id();
}
public function member(int $userId): ?WorkspaceMember
{
if (!$this->workspace()) {
return null;
}
if (!isset($this->memberCache[$userId])) {
$this->memberCache[$userId] = app(WorkspaceMember::class)
->where([
'user_id' => $userId,
'workspace_id' => $this->workspace()->id,
])
->first();
}
return $this->memberCache[$userId];
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Common\Workspaces\Controllers;
use Auth;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Common\Workspaces\Actions\CrupdateWorkspace;
use Common\Workspaces\Actions\DeleteWorkspaces;
use Common\Workspaces\Requests\CrupdateWorkspaceRequest;
use Common\Workspaces\Workspace;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class WorkspaceController extends BaseController
{
public function __construct(
protected Workspace $workspace,
protected Request $request
) {
}
public function index()
{
$userId = $this->request->get('userId');
$this->authorize('index', [Workspace::class, $userId]);
$builder = $this->workspace
->newQuery()
->withCount(['members'])
->with([
'members' => function (HasMany $builder) {
$builder->with('permissions')->currentUserAndOwnerOnly();
},
]);
if ($userId) {
$builder->forUser($userId);
}
$dataSource = new Datasource($builder, $this->request->all());
$pagination = $dataSource->paginate();
$pagination->transform(function (Workspace $workspace) {
return $workspace->setCurrentUserAndOwner();
});
return $this->success(['pagination' => $pagination]);
}
public function show(Workspace $workspace)
{
$this->authorize('show', $workspace);
$workspace->load(['invites', 'members']);
if (
$workspace->currentUser = $workspace->members
->where('id', Auth::id())
->first()
) {
$workspace->currentUser->load('permissions');
}
return $this->success(['workspace' => $workspace]);
}
public function store(CrupdateWorkspaceRequest $request)
{
$this->authorize('store', Workspace::class);
$workspace = app(CrupdateWorkspace::class)->execute($request->all());
$workspace->loadCount('members');
$workspace
->load([
'members' => function (HasMany $builder) {
$builder->currentUserAndOwnerOnly();
},
])
->setCurrentUserAndOwner();
return $this->success(['workspace' => $workspace]);
}
public function update(
Workspace $workspace,
CrupdateWorkspaceRequest $request
) {
$this->authorize('update', $workspace);
$workspace = app(CrupdateWorkspace::class)->execute(
$request->all(),
$workspace,
);
return $this->success(['workspace' => $workspace]);
}
public function destroy(string $ids)
{
$workspaceIds = explode(',', $ids);
$this->authorize('destroy', [Workspace::class, $workspaceIds]);
app(DeleteWorkspaces::class)->execute($workspaceIds);
return $this->success();
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace Common\Workspaces\Controllers;
use App\Models\User;
use Arr;
use Auth;
use Common\Core\BaseController;
use Common\Settings\Settings;
use Common\Validation\Validators\EmailsAreValid;
use Common\Workspaces\Actions\DeleteInviteNotification;
use Common\Workspaces\Notifications\WorkspaceInvitation;
use Common\Workspaces\Workspace;
use Common\Workspaces\WorkspaceInvite;
use Common\Workspaces\WorkspaceMember;
use Illuminate\Http\Request;
use Notification;
use Str;
class WorkspaceInvitesController extends BaseController
{
/**
* @var Request
*/
private $request;
/**
* @var WorkspaceInvite
*/
private $workspaceInvite;
/**
* @var User
*/
private $user;
/**
* @var Settings
*/
private $settings;
public function __construct(
Request $request,
WorkspaceInvite $workspaceInvite,
User $user,
Settings $settings,
) {
$this->request = $request;
$this->workspaceInvite = $workspaceInvite;
$this->user = $user;
$this->settings = $settings;
}
public function resend(
Workspace $workspace,
WorkspaceInvite $workspaceInvite,
) {
$this->authorize('store', [WorkspaceMember::class, $workspace, false]);
$notification = new WorkspaceInvitation(
$workspace,
Auth::user()->display_name,
$workspaceInvite['id'],
);
if ($workspaceInvite->user) {
Notification::send($workspaceInvite->user, $notification);
} else {
Notification::route('mail', $workspaceInvite['email'])->notify(
$notification,
);
}
$workspaceInvite->touch();
return $this->success(['invite' => $workspaceInvite]);
}
public function store(Workspace $workspace)
{
$this->authorize('store', [WorkspaceMember::class, $workspace]);
$emailsRules = ['required', 'array'];
if (settings('registration.disable')) {
$emailsRules[] = new EmailsAreValid();
}
$validatedData = $this->request->validate([
'emails' => $emailsRules,
'emails.*' => 'required|email',
'roleId' => 'required|int',
]);
$invites = app(WorkspaceInvite::class)
->where('workspace_id', $workspace->id)
->whereIn('email', $validatedData['emails'])
->pluck('email');
$alreadyInvitedEmails = app(WorkspaceMember::class)
->where('workspace_id', $workspace->id)
->join('users', 'users.id', 'workspace_user.user_id')
->where('users.email', $validatedData['emails'])
->pluck('email')
->merge($invites)
->toArray();
$validatedData['emails'] = array_diff(
$validatedData['emails'],
$alreadyInvitedEmails,
);
if (!empty($validatedData['emails'])) {
$existingUsers = $this->user
->whereIn('email', $validatedData['emails'])
->get()
->keyBy('email');
$workspaceInvites = collect($validatedData['emails'])
->map(function ($email) use (
$existingUsers,
$validatedData,
$workspace,
) {
// if registration is disabled, only allow inviting already registered users
if (
settings('registration.disable') &&
!isset($existingUsers[$email])
) {
return null;
}
return [
'id' => Str::orderedUuid(),
'email' => $email,
'user_id' => $existingUsers[$email]['id'] ?? null,
'workspace_id' => $workspace->id,
'avatar' => isset($existingUsers[$email])
? $existingUsers[$email]->getRawOriginal(
'avatar',
) ?? null
: null,
'role_id' => $validatedData['roleId'],
'created_at' => now(),
'updated_at' => now(),
];
})
->filter();
$this->workspaceInvite->insert($workspaceInvites->toArray());
$workspaceInvites->each(function ($invite) use (
$workspace,
$existingUsers,
) {
$notification = new WorkspaceInvitation(
$workspace,
Auth::user()->display_name,
$invite['id'],
);
if ($user = Arr::get($existingUsers, $invite['email'])) {
Notification::send($user, $notification);
} else {
Notification::route('mail', $invite['email'])->notify(
$notification,
);
}
});
$invites = $workspace
->invites()
->whereIn(
'workspace_invites.id',
$workspaceInvites->pluck('id'),
)
->get();
}
return $this->success([
'invites' => $invites ?? [],
]);
}
public function destroy(WorkspaceInvite $workspaceInvite)
{
$workspace = Workspace::findOrFail($workspaceInvite->workspace_id);
$this->authorize('destroy', [
WorkspaceMember::class,
$workspace,
$workspaceInvite->user_id,
]);
if ($workspaceInvite->user) {
app(DeleteInviteNotification::class)->execute(
$workspaceInvite,
$workspaceInvite->user,
);
}
$workspaceInvite->delete();
return $this->success();
}
public function changeRole(Workspace $workspace, string $inviteId)
{
$this->authorize('update', [WorkspaceMember::class, $workspace]);
$validatedData = $this->request->validate([
'roleId' => 'required|integer',
]);
app(WorkspaceInvite::class)
->where('id', $inviteId)
->update(['role_id' => $validatedData['roleId']]);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Common\Workspaces\Controllers;
use App\Models\User;
use Auth;
use Common\Core\BaseController;
use Common\Workspaces\Actions\JoinWorkspace;
use Common\Workspaces\Actions\RemoveMemberFromWorkspace;
use Common\Workspaces\Workspace;
use Common\Workspaces\WorkspaceInvite;
use Common\Workspaces\WorkspaceMember;
use Illuminate\Http\Request;
use Session;
use const App\Providers\WORKSPACE_HOME_ROUTE;
class WorkspaceMembersController extends BaseController
{
public function __construct(
protected Request $request,
protected User $user
) {
}
public function join(WorkspaceInvite $workspaceInvite)
{
if ($user = Auth::user()) {
app(JoinWorkspace::class)->execute($workspaceInvite, $user);
if ($this->request->expectsJson()) {
return $this->success([
'workspace' => $workspaceInvite->workspace->loadCount(
'members',
),
]);
} else {
return redirect(WORKSPACE_HOME_ROUTE);
}
} else {
Session::put('workspaceInvite', $workspaceInvite->id);
if (User::where('email', $workspaceInvite->email)->exists()) {
return redirect(
"workspace/join/login?email={$workspaceInvite->email}",
);
} else {
return redirect(
"workspace/join/register?email={$workspaceInvite->email}",
);
}
}
}
public function destroy(Workspace $workspace, int $userId)
{
$this->authorize('destroy', [
WorkspaceMember::class,
$workspace,
$userId,
]);
app(RemoveMemberFromWorkspace::class)->execute($workspace, $userId);
return $this->success();
}
public function changeRole(Workspace $workspace, int $memberId)
{
$this->authorize('update', [WorkspaceMember::class, $workspace]);
$validatedData = $this->request->validate([
'roleId' => 'required|integer',
]);
app(WorkspaceMember::class)
->where('id', $memberId)
->update(['role_id' => $validatedData['roleId']]);
return $this->success();
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Common\Workspaces\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class WorkspaceDeleted
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* @var int
*/
public $workspaceId;
/**
* @var int
*/
public $ownerId;
public function __construct(int $workspaceId, int $ownerId)
{
$this->workspaceId = $workspaceId;
$this->ownerId = $ownerId;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Common\Workspaces\Listeners;
use Common\Workspaces\Actions\JoinWorkspace;
use Common\Workspaces\WorkspaceInvite;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered;
class AttachWorkspaceToUser
{
/**
* @param Login|Registered $event
* @return void
*/
public function handle($event)
{
$inviteId = session()->get('workspaceInvite');
if ( ! $inviteId) return;
$invite = app(WorkspaceInvite::class)->find($inviteId);
if ($invite) {
app(JoinWorkspace::class)->execute($invite, $event->user);
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Common\Workspaces\Notifications;
use Common\Workspaces\Workspace;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class WorkspaceInvitation extends Notification implements ShouldQueue
{
use Queueable;
const NOTIF_ID = 'W01';
/**
* @var string
*/
private $workspace;
/**
* @var string
*/
private $inviterName;
/**
* @var string
*/
private $joinCode;
public function __construct(
Workspace $workspace,
string $inviterName,
string $joinCode
) {
$this->workspace = $workspace;
$this->inviterName = $inviterName;
$this->joinCode = $joinCode;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail', 'database'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable)
{
$data = [
'inviter' => ucfirst($this->inviterName),
'workspace' => ucfirst($this->workspace->name),
'siteName' => config('app.name'),
];
return (new MailMessage())
->subject(__(':inviter invited you to :siteName :workspace', $data))
->line(__('Join your :workspace teammates on :siteName', $data))
->action(
__('Join your team'),
url("workspace/join/{$this->joinCode}"),
)
->line(__('This invitation link will expire in 3 days.'))
->line(
__(
'If you do not wish to join this workspace, no further action is required.',
),
);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
$translateData = [
'inviter' => ucfirst($this->inviterName),
'workspace' => ucfirst($this->workspace->name),
];
return [
'inviteId' => $this->joinCode,
'lines' => [
[
'content' => __(
':inviter invited you to join `:workspace.`',
$translateData,
),
],
[
'content' => __(
'Accepting the invitation will give you access to links, domains, overlays and other resources in this workspace.',
),
],
],
'buttonActions' => [
['label' => 'Join', 'action' => 'join', 'color' => 'primary'],
[
'label' => 'Decline',
'action' => 'decline',
'color' => 'error',
],
],
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Common\Workspaces\Policies;
use App\Models\User;
use Common\Core\Policies\BasePolicy;
use Common\Workspaces\Workspace;
class WorkspaceMemberPolicy extends BasePolicy
{
public function store(
User $currentUser,
Workspace $workspace,
$checkMemberCount = true
) {
$member = $workspace->findMember($currentUser);
if (!$member || !$member->hasPermission('workspace_members.invite')) {
return false;
}
$owner =
$currentUser->id === $workspace->owner_id
? $currentUser
: $workspace->owner;
$maxMemberCount = $owner->getRestrictionValue(
'workspaces.create',
'member_count',
);
if (!$checkMemberCount || !$maxMemberCount) {
return true;
}
$currentMemberCount =
$workspace->members()->count() + $workspace->invites->count();
if ($currentMemberCount >= $maxMemberCount) {
$message = __('policies.workspace_member_quota_exceeded');
return $this->denyWithAction(
$message,
$owner->id === $currentUser->id ? $this->upgradeAction() : null,
);
}
return true;
}
public function update(User $currentUser, Workspace $workspace)
{
if ($workspace->isOwner($currentUser)) {
return true;
} else {
return $workspace
->findMember($currentUser)
->hasPermission('workspace_members.update');
}
}
public function destroy(
User $currentUser,
Workspace $workspace,
int $userId = null
) {
if ($workspace->isOwner($currentUser)) {
return true;
} elseif ($currentUser->id === $userId) {
// user is trying to delete their own membership, aka leaving workspace
return true;
} else {
return $workspace
->findMember($currentUser)
->hasPermission('workspace_members.delete');
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Common\Workspaces\Policies;
use Common\Auth\BaseUser;
use Common\Core\Policies\BasePolicy;
use Common\Workspaces\Workspace;
class WorkspacePolicy extends BasePolicy
{
public function index(BaseUser $user, int $userId = null)
{
return $user->hasPermission('workspaces.view') || $user->id === $userId;
}
public function show(BaseUser $user, Workspace $workspace)
{
return $user->hasPermission('workspaces.view') || $workspace->owner_id === $user->id || $workspace->isMember($user);
}
public function store(BaseUser $user)
{
return $this->storeWithCountRestriction($user, Workspace::class);
}
public function update(BaseUser $user, Workspace $workspace)
{
return $user->hasPermission('workspaces.update') || $workspace->owner_id === $user->id;
}
public function destroy(BaseUser $user, $workspaceIds)
{
if ($user->hasPermission('workspaces.delete')) {
return true;
} else {
$dbCount = app(Workspace::class)
->whereIn('id', $workspaceIds)
->where('owner_id', $user->id)
->count();
return $dbCount === count($workspaceIds);
}
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Common\Workspaces\Policies;
use App\Models\User;
use Common\Core\Policies\BasePolicy;
use Common\Workspaces\ActiveWorkspace;
use Illuminate\Auth\Access\Response;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
abstract class WorkspacedResourcePolicy extends BasePolicy
{
protected string $resource;
const NO_PERMISSION = 1;
const NO_WORKSPACE_PERMISSION = 2;
public function index(User $currentUser, int $userId = null)
{
$userId = $userId ?? (int) $this->request->get('userId');
[, $permission] = $this->parseNamespace($this->resource, 'view');
// filtering resources by user id
if ($userId) {
return $currentUser->id === $userId;
// if we're requesting resources for a particular workspace,let user view the resources
// as long as they are a member, even without explicit "resource.view" permission
} elseif ($this->userIsWorkspaceMember($currentUser)) {
return true;
} else {
return $this->userHasPermission($currentUser, $permission);
}
}
public function show(User $currentUser, Model $resource)
{
[, $permission] = $this->parseNamespace($this->resource, 'view');
if ($resource->user_id === $currentUser->id) {
return true;
// if we're requesting resources for a particular workspace,let user view the resources
// as long as they are a member, event without explicit "resource.view" permission
} elseif ($this->userIsWorkspaceMember($currentUser)) {
return true;
} else {
return $this->userHasPermission($currentUser, $permission);
}
}
public function store(User $currentUser)
{
return $this->storeWithCountRestriction($currentUser, $this->resource);
}
public function update(User $currentUser, Model $resource)
{
[, $permission] = $this->parseNamespace($this->resource, 'update');
if ($resource->user_id === $currentUser->id) {
return true;
} else {
return $this->userHasPermission($currentUser, $permission);
}
}
public function destroy(User $currentUser, $resourceIds = null)
{
[, $permission] = $this->parseNamespace($this->resource, 'delete');
$response = $this->userHasPermission($currentUser, $permission);
if ($response->allowed()) {
return $response;
} elseif ($resourceIds) {
$dbCount = app($this->resource)
->whereIn('id', $resourceIds)
->where('user_id', $currentUser->id)
->count();
return $dbCount === count($resourceIds);
} else {
return $response;
}
}
protected function userHasPermission(
User $user,
string $permission,
): Response {
$permission = Str::snake($permission);
$activeWorkspace = app(ActiveWorkspace::class);
$userOwnsWorkspace =
$activeWorkspace->isPersonal() ||
!$activeWorkspace->workspace() ||
$user->id === $activeWorkspace->workspace()->owner_id;
// check if user has permission when they own workspace or no workspace at all
if ($userOwnsWorkspace && !parent::hasPermission($user, $permission)) {
return Response::deny('No permission', self::NO_PERMISSION);
}
// check if user has this permission for the workspace as well if they are not the owner
elseif (!$userOwnsWorkspace) {
$workspaceUser = $activeWorkspace->member($user->id);
if (!$workspaceUser?->hasPermission($permission)) {
return Response::deny(
'No permission',
self::NO_WORKSPACE_PERMISSION,
);
}
}
return Response::allow();
}
protected function userIsWorkspaceMember(User $user): bool
{
return !is_null(app(ActiveWorkspace::class)->member($user->id));
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Common\Workspaces\Requests;
use Auth;
use Common\Core\BaseFormRequest;
use Illuminate\Validation\Rule;
class CrupdateWorkspaceRequest extends BaseFormRequest
{
public function rules(): array
{
$required = $this->getMethod() === 'POST' ? 'required' : '';
$ignore =
$this->getMethod() === 'PUT' ? $this->route('workspace')->id : '';
$userId = $this->route('workspace')
? $this->route('workspace')->user_id
: Auth::id();
return [
'name' => [
$required,
'string',
'min:3',
Rule::unique('workspaces')
->where('owner_id', $userId)
->ignore($ignore),
],
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Common\Workspaces\Rules;
use Auth;
use Common\Workspaces\ActiveWorkspace;
use Illuminate\Validation\Rules\Unique;
class UniqueWorkspacedResource extends Unique
{
public function __construct($table, $column = 'NULL', $userId = null)
{
parent::__construct($table, $column);
if (!app(ActiveWorkspace::class)->isPersonal()) {
$this->where('workspace_id', app(ActiveWorkspace::class)->id);
} else {
$this->where('user_id', $userId ?? Auth::id());
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Common\Workspaces\Traits;
use Common\Workspaces\ActiveWorkspace;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
trait BelongsToWorkspace
{
protected string $ownerColumn = 'user_id';
protected static function booted(): void
{
static::creating(function (Model $builder) {
$builder->workspace_id = app(ActiveWorkspace::class)->id;
});
}
public function scopeForActiveWorkspaceOrOwner(Builder $builder, int|string $userId): Builder
{
if ($workspaceId = app(ActiveWorkspace::class)->id) {
$builder->where('workspace_id', $workspaceId);
} else {
$builder->where($this->ownerColumn, $userId);
}
return $builder;
}
protected function workspaceId(): Attribute
{
return Attribute::make(
get: fn($value) => $value ?? 0,
set: fn($value) => $value ?? 0,
);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Common\Workspaces;
use Auth;
use Common\Core\BaseController;
class UserWorkspacesController extends BaseController
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
$workspaces = Workspace::forUser(Auth::id())
->with(['members'])
->withCount(['members'])
->limit(20)
->get()
->map(function (Workspace $workspace) {
$workspace->setCurrentUserAndOwner();
return $workspace;
});
return $this->success([
'workspaces' => $workspaces,
]);
}
}

154
common/Workspaces/Workspace.php Executable file
View File

@@ -0,0 +1,154 @@
<?php
namespace Common\Workspaces;
use App\Models\User;
use App\Workspaces\WorkspaceRelationships;
use Auth;
use Common\Core\BaseModel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Workspace extends BaseModel
{
use WorkspaceRelationships, HasFactory;
const MODEL_TYPE = 'workspace';
protected $guarded = ['id'];
protected $casts = [
'id' => 'integer',
'owner_id' => 'integer',
];
public function invites(): HasMany
{
return $this->hasMany(WorkspaceInvite::class)
->join('roles', 'roles.id', '=', 'workspace_invites.role_id')
->select([
'workspace_invites.id',
'workspace_invites.workspace_id',
'roles.name as role_name',
'workspace_invites.email',
'workspace_invites.role_id',
'email',
'avatar',
]);
}
public function owner()
{
return $this->belongsTo(User::class, 'owner_id')->select([
'id',
'email',
'first_name',
'last_name',
'avatar',
]);
}
public function members()
{
return $this->hasMany(WorkspaceMember::class)
->join('roles', 'roles.id', '=', 'workspace_user.role_id', 'left')
->join('users', 'users.id', '=', 'workspace_user.user_id')
->select([
'roles.name as role_name',
'users.email',
'workspace_user.workspace_id',
'workspace_user.created_at as joined_at',
'workspace_user.role_id',
'workspace_user.is_owner',
'workspace_user.id as member_id',
'users.id',
'users.first_name',
'users.last_name',
'users.avatar',
]);
}
public function isMember(User $user): bool
{
return $this->members()
->where('user_id', $user->id)
->exists();
}
public function isOwner(User $user): bool
{
return $this->owner_id === $user->id;
}
public function findMember(User $user): WorkspaceMember
{
return $this->members()
->where('user_id', $user->id)
->first();
}
public function scopeForUser(Builder $builder, int $userId): Builder
{
return $builder
->where('owner_id', $userId)
->orWhereHas('members', function (Builder $builder) use ($userId) {
return $builder->where('workspace_user.user_id', $userId);
});
}
public function setCurrentUserAndOwner(): self
{
$this->setRelation(
'owner',
$this->members->where('is_owner', true)->first(),
);
$this->currentUser = $this->members->where('id', Auth::id())->first();
$this->unsetRelation('members');
// load workspace permissions for current user in case front-end needs it
if (
app(ActiveWorkspace::class)->id === $this->id &&
$this->currentUser &&
!$this->currentUser->relationLoaded('permissions')
) {
$this->currentUser->load('permissions');
}
return $this;
}
public function toNormalizedArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'model_type' => self::MODEL_TYPE,
];
}
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at->timestamp ?? '_null',
'updated_at' => $this->updated_at->timestamp ?? '_null',
];
}
public static function filterableFields(): array
{
return ['id', 'created_at', 'updated_at'];
}
protected static function newFactory(): WorkspaceFactory
{
return WorkspaceFactory::new();
}
public static function getModelTypeAttribute(): string
{
return self::MODEL_TYPE;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Common\Workspaces;
use Illuminate\Database\Eloquent\Factories\Factory;
class WorkspaceFactory extends Factory
{
protected $model = Workspace::class;
public function definition(): array
{
return [
'name' => $this->faker->words(2, true),
'owner_id' => $this->faker->numberBetween(1, 100),
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Common\Workspaces;
use App\Models\User;
use Common\Auth\Traits\HasAvatarAttribute;
use Common\Auth\Traits\HasDisplayNameAttribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WorkspaceInvite extends Model
{
use HasDisplayNameAttribute, HasAvatarAttribute;
protected $guarded = ['id'];
protected $appends = ['display_name', 'model_type'];
protected $keyType = 'orderedUuid';
public $incrementing = false;
protected $casts = [
'user_id' => 'integer',
];
public function workspace(): BelongsTo
{
return $this->belongsTo(Workspace::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public static function getModelTypeAttribute(): string
{
return 'invite';
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Common\Workspaces;
use Auth;
use Common\Auth\Permissions\Permission;
use Common\Auth\Roles\Role;
use Common\Auth\Traits\HasAvatarAttribute;
use Common\Auth\Traits\HasDisplayNameAttribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class WorkspaceMember extends Model
{
use HasAvatarAttribute, HasDisplayNameAttribute;
protected $table = 'workspace_user';
protected $guarded = ['id'];
protected $appends = ['display_name', 'model_type'];
protected $casts = ['is_owner' => 'boolean'];
public function permissions()
{
return $this->belongsToMany(
Permission::class,
'permissionables',
'permissionable_id',
'permission_id',
'role_id',
)
->where('permissionable_type', Role::MODEL_TYPE)
->select([
'permissions.id',
'permissions.name',
'permissions.restrictions',
]);
}
public function workspace()
{
return $this->belongsTo(Workspace::class);
}
public function scopeCurrentUserAndOwnerOnly(Builder $builder): self
{
$builder->where(function (Builder $builder) {
$builder
->where('workspace_user.user_id', Auth::id())
->orWhere('workspace_user.is_owner', true);
});
return $this;
}
public static function getModelTypeAttribute(): string
{
return 'member';
}
public function hasPermission(string $name): bool
{
return $this->is_owner || !is_null($this->getPermission($name));
}
public function getPermission(string $name): ?Permission
{
return $this->permissions->first(function (Permission $permission) use (
$name
) {
return $permission->name === $name;
});
}
public function getRoleNameAttribute()
{
return $this->is_owner
? 'Workspace Owner'
: $this->attributes['role_name'];
}
}