feat: invitation resend, WorkspaceMemberRole enum, configurable expiry #70
4 changed files with 85 additions and 17 deletions
41
Enums/WorkspaceMemberRole.php
Normal file
41
Enums/WorkspaceMemberRole.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Enums;
|
||||
|
||||
/**
|
||||
* Workspace member roles.
|
||||
*
|
||||
* Backed enum replacing the legacy string constants on WorkspaceMember.
|
||||
*/
|
||||
enum WorkspaceMemberRole: string
|
||||
{
|
||||
case OWNER = 'owner';
|
||||
case ADMIN = 'admin';
|
||||
case MEMBER = 'member';
|
||||
|
||||
/**
|
||||
* Get a human-readable label for the role.
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::OWNER => 'Owner',
|
||||
self::ADMIN => 'Admin',
|
||||
self::MEMBER => 'Member',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the colour for this role's badge.
|
||||
*/
|
||||
public function colour(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::OWNER => 'violet',
|
||||
self::ADMIN => 'blue',
|
||||
self::MEMBER => 'zinc',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -689,10 +689,12 @@ class Workspace extends Model
|
|||
* @param string $email The email address to invite
|
||||
* @param string $role The role to assign (owner, admin, member)
|
||||
* @param User|null $invitedBy The user sending the invitation
|
||||
* @param int $expiresInDays Number of days until invitation expires
|
||||
* @param int|null $expiresInDays Number of days until invitation expires (defaults to config)
|
||||
*/
|
||||
public function invite(string $email, string $role = 'member', ?User $invitedBy = null, int $expiresInDays = 7): WorkspaceInvitation
|
||||
public function invite(string $email, string $role = 'member', ?User $invitedBy = null, ?int $expiresInDays = null): WorkspaceInvitation
|
||||
{
|
||||
$expiresInDays ??= (int) config('tenant.invitation_expiry_days', 7);
|
||||
|
||||
// Check if there's already a pending invitation for this email
|
||||
$existing = $this->invitations()
|
||||
->where('email', $email)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Core\Tenant\Models;
|
||||
|
||||
use Core\Tenant\Database\Factories\WorkspaceInvitationFactory;
|
||||
use Core\Tenant\Notifications\WorkspaceInvitationNotification;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
|
@ -211,6 +212,29 @@ class WorkspaceInvitation extends Model
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the invitation by regenerating the token and resetting expiry.
|
||||
*
|
||||
* Generates a new plaintext token, hashes it, resets the expiry
|
||||
* to the configured number of days, and re-sends the notification.
|
||||
*
|
||||
* @return string The new plaintext token (for testing/logging; not stored in plaintext)
|
||||
*/
|
||||
public function resend(): string
|
||||
{
|
||||
$plaintextToken = static::generateToken();
|
||||
|
||||
$this->token = Hash::make($plaintextToken);
|
||||
$this->expires_at = now()->addDays(
|
||||
(int) config('tenant.invitation_expiry_days', 7)
|
||||
);
|
||||
$this->save();
|
||||
|
||||
$this->notify(new WorkspaceInvitationNotification($this, $plaintextToken));
|
||||
|
||||
return $plaintextToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification routing for mail.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Core\Tenant\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Tenant\Enums\WorkspaceMemberRole;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
|
@ -49,12 +50,16 @@ class WorkspaceMember extends Model
|
|||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Role Constants (legacy, for backwards compatibility)
|
||||
// Prefer WorkspaceMemberRole enum for new code.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** @deprecated Use WorkspaceMemberRole::OWNER instead */
|
||||
public const ROLE_OWNER = 'owner';
|
||||
|
||||
/** @deprecated Use WorkspaceMemberRole::ADMIN instead */
|
||||
public const ROLE_ADMIN = 'admin';
|
||||
|
||||
/** @deprecated Use WorkspaceMemberRole::MEMBER instead */
|
||||
public const ROLE_MEMBER = 'member';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -140,7 +145,7 @@ class WorkspaceMember extends Model
|
|||
*/
|
||||
public function scopeOwners($query)
|
||||
{
|
||||
return $query->where('role', self::ROLE_OWNER);
|
||||
return $query->where('role', WorkspaceMemberRole::OWNER->value);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -186,8 +191,8 @@ class WorkspaceMember extends Model
|
|||
// Legacy fallback: if no team, derive from role
|
||||
if (! $this->team_id) {
|
||||
$rolePermissions = match ($this->role) {
|
||||
self::ROLE_OWNER => WorkspaceTeam::getDefaultPermissionsFor(WorkspaceTeam::TEAM_OWNER),
|
||||
self::ROLE_ADMIN => WorkspaceTeam::getDefaultPermissionsFor(WorkspaceTeam::TEAM_ADMIN),
|
||||
WorkspaceMemberRole::OWNER->value => WorkspaceTeam::getDefaultPermissionsFor(WorkspaceTeam::TEAM_OWNER),
|
||||
WorkspaceMemberRole::ADMIN->value => WorkspaceTeam::getDefaultPermissionsFor(WorkspaceTeam::TEAM_ADMIN),
|
||||
default => WorkspaceTeam::getDefaultPermissionsFor(WorkspaceTeam::TEAM_MEMBER),
|
||||
};
|
||||
$permissions = array_unique(array_merge($permissions, $rolePermissions));
|
||||
|
|
@ -308,7 +313,7 @@ class WorkspaceMember extends Model
|
|||
*/
|
||||
public function isOwner(): bool
|
||||
{
|
||||
return $this->role === self::ROLE_OWNER
|
||||
return $this->role === WorkspaceMemberRole::OWNER->value
|
||||
|| $this->team?->slug === WorkspaceTeam::TEAM_OWNER;
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +323,7 @@ class WorkspaceMember extends Model
|
|||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->isOwner()
|
||||
|| $this->role === self::ROLE_ADMIN
|
||||
|| $this->role === WorkspaceMemberRole::ADMIN->value
|
||||
|| $this->team?->slug === WorkspaceTeam::TEAM_ADMIN;
|
||||
}
|
||||
|
||||
|
|
@ -353,11 +358,9 @@ class WorkspaceMember extends Model
|
|||
return $this->team->name;
|
||||
}
|
||||
|
||||
return match ($this->role) {
|
||||
self::ROLE_OWNER => 'Owner',
|
||||
self::ROLE_ADMIN => 'Admin',
|
||||
default => 'Member',
|
||||
};
|
||||
$roleEnum = WorkspaceMemberRole::tryFrom($this->role);
|
||||
|
||||
return $roleEnum?->label() ?? 'Member';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -369,10 +372,8 @@ class WorkspaceMember extends Model
|
|||
return $this->team->colour;
|
||||
}
|
||||
|
||||
return match ($this->role) {
|
||||
self::ROLE_OWNER => 'violet',
|
||||
self::ROLE_ADMIN => 'blue',
|
||||
default => 'zinc',
|
||||
};
|
||||
$roleEnum = WorkspaceMemberRole::tryFrom($this->role);
|
||||
|
||||
return $roleEnum?->colour() ?? 'zinc';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue