first commit
This commit is contained in:
46
app/Concerns/GeneratesUniqueTeamSlugs.php
Normal file
46
app/Concerns/GeneratesUniqueTeamSlugs.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait GeneratesUniqueTeamSlugs
|
||||
{
|
||||
/**
|
||||
* Generate a unique slug for the team.
|
||||
*/
|
||||
protected static function generateUniqueTeamSlug(string $name, ?int $excludeId = null): string
|
||||
{
|
||||
$defaultSlug = Str::slug($name);
|
||||
|
||||
$query = static::withTrashed()
|
||||
->where(function ($query) use ($defaultSlug) {
|
||||
$query->where('slug', $defaultSlug)
|
||||
->orWhere('slug', 'like', $defaultSlug.'-%');
|
||||
});
|
||||
|
||||
if ($excludeId) {
|
||||
$query->where('id', '!=', $excludeId);
|
||||
}
|
||||
|
||||
$existingSlugs = $query->pluck('slug');
|
||||
|
||||
$maxSuffix = $existingSlugs
|
||||
->map(function (string $slug) use ($defaultSlug): ?int {
|
||||
if ($slug === $defaultSlug) {
|
||||
return 0;
|
||||
} elseif (preg_match('/^'.preg_quote($defaultSlug, '/').'-(\d+)$/', $slug, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
->filter(fn (?int $suffix) => $suffix !== null)
|
||||
->max() ?? 0;
|
||||
|
||||
return $existingSlugs->isEmpty()
|
||||
? $defaultSlug
|
||||
: $defaultSlug.'-'.($maxSuffix + 1);
|
||||
}
|
||||
}
|
||||
196
app/Concerns/HasTeams.php
Normal file
196
app/Concerns/HasTeams.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Enums\TeamPermission;
|
||||
use App\Enums\TeamRole;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Team;
|
||||
use App\Support\TeamPermissions;
|
||||
use App\Support\UserTeam;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
trait HasTeams
|
||||
{
|
||||
/**
|
||||
* Get all of the teams the user belongs to.
|
||||
*
|
||||
* @return BelongsToMany<Team, $this>
|
||||
*/
|
||||
public function teams(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Team::class, 'team_members', 'user_id', 'team_id')
|
||||
->withPivot(['role'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the teams the user owns.
|
||||
*
|
||||
* @return HasManyThrough<Team, Membership, $this>
|
||||
*/
|
||||
public function ownedTeams(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Team::class,
|
||||
Membership::class,
|
||||
'user_id',
|
||||
'id',
|
||||
'id',
|
||||
'team_id',
|
||||
)->where('team_members.role', TeamRole::Owner->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the memberships for the user.
|
||||
*
|
||||
* @return HasMany<Membership, $this>
|
||||
*/
|
||||
public function teamMemberships(): HasMany
|
||||
{
|
||||
return $this->hasMany(Membership::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's current team.
|
||||
*
|
||||
* @return BelongsTo<Team, $this>
|
||||
*/
|
||||
public function currentTeam(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Team::class, 'current_team_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's personal team.
|
||||
*/
|
||||
public function personalTeam(): ?Team
|
||||
{
|
||||
return $this->teams()
|
||||
->where('is_personal', true)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to the given team.
|
||||
*/
|
||||
public function switchTeam(Team $team): bool
|
||||
{
|
||||
if (! $this->belongsToTeam($team)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->update(['current_team_id' => $team->id]);
|
||||
$this->setRelation('currentTeam', $team);
|
||||
|
||||
URL::defaults(['current_team' => $team->slug]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user belongs to the given team.
|
||||
*/
|
||||
public function belongsToTeam(Team $team): bool
|
||||
{
|
||||
return $this->teams()->where('teams.id', $team->id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given team is the user's current team.
|
||||
*/
|
||||
public function isCurrentTeam(Team $team): bool
|
||||
{
|
||||
return $this->current_team_id === $team->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is the owner of the given team.
|
||||
*/
|
||||
public function ownsTeam(Team $team): bool
|
||||
{
|
||||
return $this->teamRole($team) === TeamRole::Owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's role on the given team.
|
||||
*/
|
||||
public function teamRole(Team $team): ?TeamRole
|
||||
{
|
||||
return $this->teamMemberships()
|
||||
->where('team_id', $team->id)
|
||||
->first()
|
||||
?->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's teams as a collection of UserTeam objects.
|
||||
*
|
||||
* @return Collection<int, UserTeam>
|
||||
*/
|
||||
public function toUserTeams(bool $includeCurrent = false): Collection
|
||||
{
|
||||
return $this->teams()
|
||||
->get()
|
||||
->map(fn (Team $team) => ! $includeCurrent && $this->isCurrentTeam($team) ? null : $this->toUserTeam($team))
|
||||
->filter()
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's team as a UserTeam object.
|
||||
*/
|
||||
public function toUserTeam(Team $team): UserTeam
|
||||
{
|
||||
$role = $this->teamRole($team);
|
||||
|
||||
return new UserTeam(
|
||||
id: $team->id,
|
||||
name: $team->name,
|
||||
slug: $team->slug,
|
||||
isPersonal: $team->is_personal,
|
||||
role: $role?->value,
|
||||
roleLabel: $role?->label(),
|
||||
isCurrent: $this->isCurrentTeam($team),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the standard permissions for a team as a TeamPermissions object.
|
||||
*/
|
||||
public function toTeamPermissions(Team $team): TeamPermissions
|
||||
{
|
||||
$role = $this->teamRole($team);
|
||||
|
||||
return new TeamPermissions(
|
||||
canUpdateTeam: $role?->hasPermission(TeamPermission::UpdateTeam) ?? false,
|
||||
canDeleteTeam: $role?->hasPermission(TeamPermission::DeleteTeam) ?? false,
|
||||
canAddMember: $role?->hasPermission(TeamPermission::AddMember) ?? false,
|
||||
canUpdateMember: $role?->hasPermission(TeamPermission::UpdateMember) ?? false,
|
||||
canRemoveMember: $role?->hasPermission(TeamPermission::RemoveMember) ?? false,
|
||||
canCreateInvitation: $role?->hasPermission(TeamPermission::CreateInvitation) ?? false,
|
||||
canCancelInvitation: $role?->hasPermission(TeamPermission::CancelInvitation) ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
public function fallbackTeam(?Team $excluding = null): ?Team
|
||||
{
|
||||
return $this->teams()
|
||||
->when($excluding, fn ($query) => $query->where('teams.id', '!=', $excluding->id))
|
||||
->orderByRaw('LOWER(teams.name)')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user has the given permission on the team.
|
||||
*/
|
||||
public function hasTeamPermission(Team $team, TeamPermission $permission): bool
|
||||
{
|
||||
return $this->teamRole($team)?->hasPermission($permission) ?? false;
|
||||
}
|
||||
}
|
||||
29
app/Concerns/PasswordValidationRules.php
Normal file
29
app/Concerns/PasswordValidationRules.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate the current password.
|
||||
*
|
||||
* @return array<int, Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function currentPasswordRules(): array
|
||||
{
|
||||
return ['required', 'string', 'current_password'];
|
||||
}
|
||||
}
|
||||
50
app/Concerns/ProfileValidationRules.php
Normal file
50
app/Concerns/ProfileValidationRules.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
trait ProfileValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate user profiles.
|
||||
*
|
||||
* @return array<string, array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>>
|
||||
*/
|
||||
protected function profileRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->nameRules(),
|
||||
'email' => $this->emailRules($userId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user names.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function nameRules(): array
|
||||
{
|
||||
return ['required', 'string', 'max:255'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user emails.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function emailRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
$userId === null
|
||||
? Rule::unique(User::class)
|
||||
: Rule::unique(User::class)->ignore($userId),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user