first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
brusnitsyn
2026-04-06 00:06:00 +09:00
commit fb2e6c58e3
409 changed files with 42953 additions and 0 deletions

View 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
View 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;
}
}

View 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'];
}
}

View 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),
];
}
}