diff --git a/Enums/WorkspaceMemberRole.php b/Enums/WorkspaceMemberRole.php new file mode 100644 index 0000000..207cbf9 --- /dev/null +++ b/Enums/WorkspaceMemberRole.php @@ -0,0 +1,41 @@ + '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', + }; + } +} diff --git a/Models/Workspace.php b/Models/Workspace.php index 490bc64..cc5b472 100644 --- a/Models/Workspace.php +++ b/Models/Workspace.php @@ -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) diff --git a/Models/WorkspaceInvitation.php b/Models/WorkspaceInvitation.php index a832d10..96f0eda 100644 --- a/Models/WorkspaceInvitation.php +++ b/Models/WorkspaceInvitation.php @@ -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. */ diff --git a/Models/WorkspaceMember.php b/Models/WorkspaceMember.php index 1d04a9c..52b923b 100644 --- a/Models/WorkspaceMember.php +++ b/Models/WorkspaceMember.php @@ -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'; } }