Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Thank you for your interest in contributing to Spacepad! This document provides
### Coding Standards

- Follow the [PSR-12](https://www.php-fig.org/psr/psr-12/) coding style guide for PHP
- Always use import statements instead of inline fully qualified class names - See [Coding Standards](backend/docs/CODING_STANDARDS.md) for details
- Use ESLint and Prettier for JavaScript/TypeScript
- Write meaningful commit messages
- Add comments for complex logic
Expand Down
111 changes: 111 additions & 0 deletions backend/WORKSPACE_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Workspace System Documentation

## Overview

The workspace system allows multiple users to collaborate on managing displays, devices, calendars, and rooms. Each user automatically gets their own workspace, and Pro users can invite colleagues to join their workspace.

## Architecture

### Models

1. **Workspace** - Represents a team/workspace
- Has an `owner` (User)
- Has many `members` (Users with roles)
- Contains displays, devices, calendars, rooms

2. **WorkspaceMember** - Pivot table linking users to workspaces
- Roles: `owner`, `admin`, `member`
- `owner` role is implicit for the workspace owner

### Relationships

- **User** → **Workspace** (one-to-many: owned workspaces)
- **User** ↔ **Workspace** (many-to-many: member workspaces)
- **Workspace** → **Display** (one-to-many)
- **Workspace** → **Device** (one-to-many)
- **Workspace** → **Calendar** (one-to-many)
- **Workspace** → **Room** (one-to-many)

## Migration Strategy

1. **Existing Users**: Each user automatically gets a workspace created with their name
2. **Existing Data**: All displays, devices, calendars, and rooms are migrated to the user's workspace
3. **Backward Compatibility**: The `user_id` field is kept for backward compatibility

## Permissions

### Workspace Roles

- **Owner**: Full control (can delete workspace, manage all members)
- **Admin**: Can manage members and workspace settings
- **Member**: Can view and use workspace resources

### Display Access

- Users can access displays they own directly (`user_id`)
- Users can access displays in workspaces they're members of (`workspace_id`)
- Device authentication checks workspace membership

## Usage

### Adding a Colleague

1. Navigate to workspace settings (requires Pro)
2. Enter colleague's email address
3. Select role (admin or member)
4. Colleague receives access to all workspace resources

### Managing Members

- **Add Member**: Only owners/admins can add members
- **Update Role**: Change member role between admin/member
- **Remove Member**: Remove access from workspace

## API Changes

### DisplayController

- `index()` now returns displays from user's workspace(s)
- Access checks include workspace membership

### DisplayService

- `validateDisplayPermission()` checks workspace membership
- Pro features check workspace owner's Pro status

## Frontend Changes Needed

1. **Workspace Management UI**
- List workspaces
- View workspace members
- Add/remove members
- Update member roles

2. **Display Creation**
- Automatically assign to user's primary workspace
- Allow selecting workspace (if user has multiple)

3. **Device Connection**
- Connect code should work with workspace
- Devices inherit workspace from user

## Migration Commands

Run migrations in order:

```bash
php artisan migrate
```

The migration `2025_12_30_000003_create_workspaces_for_existing_users.php` will:
1. Create a workspace for each existing user
2. Migrate all user's displays, devices, calendars, and rooms to their workspace
3. Add the user as an owner member

## Notes

- Pro subscription is required to add team members
- Workspace owner cannot be removed
- All existing functionality remains backward compatible
- `user_id` fields are kept for direct ownership tracking

28 changes: 28 additions & 0 deletions backend/app/Enums/WorkspaceRole.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Enums;

enum WorkspaceRole: string
{
case OWNER = 'owner';
case ADMIN = 'admin';
case MEMBER = 'member';

public function label(): string
{
return match($this) {
self::OWNER => 'Owner',
self::ADMIN => 'Admin',
self::MEMBER => 'Member',
};
}

/**
* Check if this role can manage the workspace
*/
public function canManage(): bool
{
return in_array($this, [self::OWNER, self::ADMIN]);
}
}

10 changes: 10 additions & 0 deletions backend/app/Http/Controllers/API/Auth/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Http\Requests\API\Auth\LoginRequest;
use App\Http\Resources\API\DeviceResource;
use App\Models\Device;
use App\Models\User;
use App\Services\OutlookService;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
Expand All @@ -32,15 +33,24 @@ public function login(LoginRequest $request): JsonResponse

// Check if the code is a valid connect code
if ($connectedUserId !== null) {
$user = User::find($connectedUserId);
$workspace = $user?->primaryWorkspace();

$device = Device::firstOrCreate([
'user_id' => $connectedUserId,
'uid' => $uid,
],[
'user_id' => $connectedUserId,
'workspace_id' => $workspace?->id,
'uid' => $uid,
'name' => $name,
]);

// Update workspace_id if device already existed but didn't have one
if ($device->workspace_id === null && $workspace) {
$device->update(['workspace_id' => $workspace->id]);
}

logger()->info('Device authentication successful', [
'user_id' => $connectedUserId,
'device_id' => $device->id,
Expand Down
28 changes: 27 additions & 1 deletion backend/app/Http/Controllers/API/DeviceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Http\Resources\API\DeviceResource;
use App\Models\Device;
use App\Models\Display;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

Expand All @@ -25,8 +26,33 @@ public function changeDisplay(ChangeDisplayRequest $request): JsonResponse
$device = auth()->user();
$data = $request->validated();

if (!$device->user_id) {
return $this->error(
message: 'Device is not associated with a user',
code: Response::HTTP_BAD_REQUEST
);
}

$user = User::with('workspaces')->find($device->user_id);
if (!$user) {
return $this->error(
message: 'User not found',
code: Response::HTTP_NOT_FOUND
);
}

// Get all workspace IDs the user is a member of
$workspaceIds = $user->workspaces->pluck('id');
if ($workspaceIds->isEmpty()) {
return $this->error(
message: 'User is not a member of any workspace',
code: Response::HTTP_BAD_REQUEST
);
}

// Find display in any of the user's workspaces
$display = Display::query()
->where('user_id', $device->user_id)
->whereIn('workspace_id', $workspaceIds)
->find($data['display_id']);

if (! $display) {
Expand Down
19 changes: 18 additions & 1 deletion backend/app/Http/Controllers/API/DisplayController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Http\Resources\API\EventResource;
use App\Models\Device;
use App\Models\Display;
use App\Models\User;
use App\Services\DisplayService;
use App\Services\EventService;
use App\Services\ImageService;
Expand All @@ -30,15 +31,31 @@ public function index(): JsonResponse
/** @var Device $device */
$device = auth()->user();

if (!$device->user_id) {
return $this->success(data: []);
}

$user = User::find($device->user_id);
if (!$user) {
return $this->success(data: []);
}

// Get displays from all workspaces the user is a member of
$workspaceIds = $user->workspaces->pluck('id');
if ($workspaceIds->isEmpty()) {
return $this->success(data: []);
}

$displays = Display::query()
->where('user_id', $device->user_id)
->whereIn('workspace_id', $workspaceIds)
->whereIn('status', [DisplayStatus::READY, DisplayStatus::ACTIVE])
->with('settings')
->get();

logger()->info('Display list requested', [
'user_id' => $device->user_id,
'device_id' => $device->id,
'workspace_ids' => $workspaceIds->toArray(),
'display_count' => $displays->count(),
'ip' => request()->ip(),
]);
Expand Down
Loading
Loading