added teams
This commit is contained in:
+4
-1
@@ -3,10 +3,13 @@ root = true
|
|||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
indent_style = space
|
indent_style = space
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.php]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum TeamMemberRole: string
|
||||||
|
{
|
||||||
|
case Admin = 'admin';
|
||||||
|
case Member = 'member';
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreTeamRequest;
|
||||||
|
use App\Http\Requests\UpdateTeamRequest;
|
||||||
|
use App\Models\Team;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class TeamController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return Inertia::render('team/Index', [
|
||||||
|
'teams' => Team::visibleTo($request->user()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(StoreTeamRequest $request)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(Team $team)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(UpdateTeamRequest $request, Team $team)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Team $team)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class StoreTeamRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'slug' => ['required', 'string', 'max:255', 'unique:teams,slug'],
|
||||||
|
'require_2fa' => ['required', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'slug' => Str::slug($this->name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class UpdateTeamRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['nullable', 'string', 'max:255'],
|
||||||
|
'slug' => ['nullable', 'required_if:name', 'string', 'max:255', 'unique:teams,slug,'.$this->route('team')->id],
|
||||||
|
'require_2fa' => ['required', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'slug' => Str::slug($this->name),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin User
|
||||||
|
*/
|
||||||
|
class TeamMemberResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->name,
|
||||||
|
'email' => $this->email,
|
||||||
|
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
||||||
|
'role' => $this->pivot->role,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use App\Models\Team;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin Team
|
||||||
|
*/
|
||||||
|
class TeamResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->name,
|
||||||
|
'slug' => $this->slug,
|
||||||
|
'members' => TeamMemberResource::collection($this->members),
|
||||||
|
'multi_factor_required' => $this->require_2fa,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $slug
|
||||||
|
* @property int $created_by
|
||||||
|
* @property bool $require_2fa
|
||||||
|
* @property \Carbon\CarbonImmutable|null $created_at
|
||||||
|
* @property \Carbon\CarbonImmutable|null $updated_at
|
||||||
|
* @property-read \App\Models\TeamMember|null $pivot
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $members
|
||||||
|
* @property-read int|null $members_count
|
||||||
|
* @method static Builder<static>|Team newModelQuery()
|
||||||
|
* @method static Builder<static>|Team newQuery()
|
||||||
|
* @method static Builder<static>|Team query()
|
||||||
|
* @method static Builder<static>|Team visibleTo(\App\Models\User $user)
|
||||||
|
* @method static Builder<static>|Team whereCreatedAt($value)
|
||||||
|
* @method static Builder<static>|Team whereCreatedBy($value)
|
||||||
|
* @method static Builder<static>|Team whereId($value)
|
||||||
|
* @method static Builder<static>|Team whereName($value)
|
||||||
|
* @method static Builder<static>|Team whereRequire2fa($value)
|
||||||
|
* @method static Builder<static>|Team whereSlug($value)
|
||||||
|
* @method static Builder<static>|Team whereUpdatedAt($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
#[Fillable(['name', 'slug', 'require_2fa'])]
|
||||||
|
class Team extends Model
|
||||||
|
{
|
||||||
|
protected function casts()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'require_2fa' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function members(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'team_members')
|
||||||
|
->using(TeamMember::class)
|
||||||
|
->withPivot('role')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeVisibleTo(Builder $query, User $user): Builder
|
||||||
|
{
|
||||||
|
return $query->whereHas('members', function (Builder $query) use ($user): void {
|
||||||
|
$query->whereKey($user->getKey());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\TeamMemberRole;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $team_id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property TeamMemberRole $role
|
||||||
|
* @property \Carbon\CarbonImmutable|null $created_at
|
||||||
|
* @property \Carbon\CarbonImmutable|null $updated_at
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember whereRole($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember whereTeamId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|TeamMember whereUserId($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class TeamMember extends Pivot
|
||||||
|
{
|
||||||
|
protected $table = 'team_members';
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'role' => TeamMemberRole::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-8
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Database\Factories\UserFactory;
|
use Database\Factories\UserFactory;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Laravel\Fortify\Contracts\PasskeyUser;
|
use Laravel\Fortify\Contracts\PasskeyUser;
|
||||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
@@ -18,18 +18,41 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
|||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $email
|
* @property string $email
|
||||||
* @property Carbon|null $email_verified_at
|
* @property \Carbon\CarbonImmutable|null $email_verified_at
|
||||||
* @property string $password
|
* @property string $password
|
||||||
|
* @property string|null $remember_token
|
||||||
|
* @property \Carbon\CarbonImmutable|null $created_at
|
||||||
|
* @property \Carbon\CarbonImmutable|null $updated_at
|
||||||
* @property string|null $two_factor_secret
|
* @property string|null $two_factor_secret
|
||||||
* @property string|null $two_factor_recovery_codes
|
* @property string|null $two_factor_recovery_codes
|
||||||
* @property Carbon|null $two_factor_confirmed_at
|
* @property \Carbon\CarbonImmutable|null $two_factor_confirmed_at
|
||||||
* @property string|null $remember_token
|
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
|
||||||
* @property Carbon|null $created_at
|
* @property-read int|null $notifications_count
|
||||||
* @property Carbon|null $updated_at
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Passkeys\Passkey> $passkeys
|
||||||
|
* @property-read int|null $passkeys_count
|
||||||
|
* @property-read \App\Models\TeamMember|null $pivot
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Team> $teams
|
||||||
|
* @property-read int|null $teams_count
|
||||||
|
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTwoFactorConfirmedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTwoFactorRecoveryCodes($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTwoFactorSecret($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
#[Fillable(['name', 'email', 'password'])]
|
#[Fillable(['name', 'email', 'password'])]
|
||||||
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||||
class User extends Authenticatable implements PasskeyUser
|
class User extends Authenticatable implements PasskeyUser, MustVerifyEmail
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||||
@@ -47,4 +70,12 @@ class User extends Authenticatable implements PasskeyUser
|
|||||||
'two_factor_confirmed_at' => 'datetime',
|
'two_factor_confirmed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function teams(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Team::class, 'team_members')
|
||||||
|
->using(TeamMember::class)
|
||||||
|
->withPivot(['role'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Enums\TeamMemberRole;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class TeamPolicy
|
||||||
|
{
|
||||||
|
public function view(User $user, Team $team): bool
|
||||||
|
{
|
||||||
|
return Team::query()
|
||||||
|
->visibleTo($user)
|
||||||
|
->whereKey($team->getKey())
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasVerifiedEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manage(User $user, Team $team): bool
|
||||||
|
{
|
||||||
|
$membership = $team->members()->whereKey($user->id)->first();
|
||||||
|
if (! $membership) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $membership->pivot->role === TeamMemberRole::Admin;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"laravel/wayfinder": "^0.1.14"
|
"laravel/wayfinder": "^0.1.14"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"barryvdh/laravel-ide-helper": "^3.7",
|
||||||
"fakerphp/faker": "^1.24",
|
"fakerphp/faker": "^1.24",
|
||||||
"larastan/larastan": "^3.9",
|
"larastan/larastan": "^3.9",
|
||||||
"laravel/boost": "^2.2",
|
"laravel/boost": "^2.2",
|
||||||
|
|||||||
Generated
+294
-2
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "859863f5ca81e4a2e3cf62f862513073",
|
"content-hash": "5dc37eb696378f9735e0d1d9e44899f6",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -7629,6 +7629,153 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
{
|
||||||
|
"name": "barryvdh/laravel-ide-helper",
|
||||||
|
"version": "v3.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
|
||||||
|
"reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a",
|
||||||
|
"reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"barryvdh/reflection-docblock": "^2.4",
|
||||||
|
"composer/class-map-generator": "^1.0",
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.15 || ^12 || ^13.0",
|
||||||
|
"illuminate/database": "^11.15 || ^12 || ^13.0",
|
||||||
|
"illuminate/filesystem": "^11.15 || ^12 || ^13.0",
|
||||||
|
"illuminate/support": "^11.15 || ^12 || ^13.0",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-pdo_sqlite": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3",
|
||||||
|
"illuminate/config": "^11.15 || ^12 || ^13.0",
|
||||||
|
"illuminate/view": "^11.15 || ^12 || ^13.0",
|
||||||
|
"larastan/larastan": "^3.1",
|
||||||
|
"mockery/mockery": "^1.4",
|
||||||
|
"orchestra/testbench": "^9.2 || ^10 || ^11.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0",
|
||||||
|
"phpunit/phpunit": "^10.5 || ^11.5.3 || ^12.5.12",
|
||||||
|
"spatie/phpunit-snapshot-assertions": "^4 || ^5",
|
||||||
|
"vlucas/phpdotenv": "^5"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10|^11)."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.6-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Barryvdh\\LaravelIdeHelper\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Barry vd. Heuvel",
|
||||||
|
"email": "barryvdh@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.",
|
||||||
|
"keywords": [
|
||||||
|
"autocomplete",
|
||||||
|
"codeintel",
|
||||||
|
"dev",
|
||||||
|
"helper",
|
||||||
|
"ide",
|
||||||
|
"laravel",
|
||||||
|
"netbeans",
|
||||||
|
"phpdoc",
|
||||||
|
"phpstorm",
|
||||||
|
"sublime"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
|
||||||
|
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.7.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://fruitcake.nl",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/barryvdh",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-17T14:12:51+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "barryvdh/reflection-docblock",
|
||||||
|
"version": "v2.4.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/barryvdh/ReflectionDocBlock.git",
|
||||||
|
"reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b",
|
||||||
|
"reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^8.5.14|^9"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"dflydev/markdown": "~1.0",
|
||||||
|
"erusev/parsedown": "~1.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Barryvdh": [
|
||||||
|
"src/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mike van Riel",
|
||||||
|
"email": "mike.vanriel@naenius.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.1"
|
||||||
|
},
|
||||||
|
"time": "2026-03-05T20:09:01+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "brianium/paratest",
|
"name": "brianium/paratest",
|
||||||
"version": "v7.24.1",
|
"version": "v7.24.1",
|
||||||
@@ -7722,6 +7869,151 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-08-17T06:31:26+00:00"
|
"time": "2026-08-17T06:31:26+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "composer/class-map-generator",
|
||||||
|
"version": "1.7.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/composer/class-map-generator.git",
|
||||||
|
"reference": "86d8208fc3c649a3a999daf1a63c25201be2990f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/86d8208fc3c649a3a999daf1a63c25201be2990f",
|
||||||
|
"reference": "86d8208fc3c649a3a999daf1a63c25201be2990f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer/pcre": "^2.1 || ^3.1",
|
||||||
|
"php": "^7.2 || ^8.0",
|
||||||
|
"symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.12 || ^2",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
|
||||||
|
"phpstan/phpstan-phpunit": "^1 || ^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.1 || ^2",
|
||||||
|
"phpunit/phpunit": "^8",
|
||||||
|
"symfony/filesystem": "^5.4 || ^6 || ^7 || ^8"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\ClassMapGenerator\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "https://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Utilities to scan PHP code and generate class maps.",
|
||||||
|
"keywords": [
|
||||||
|
"classmap"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/composer/class-map-generator/issues",
|
||||||
|
"source": "https://github.com/composer/class-map-generator/tree/1.7.3"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-05T09:17:07+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "composer/pcre",
|
||||||
|
"version": "3.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/composer/pcre.git",
|
||||||
|
"reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
|
||||||
|
"reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpstan/phpstan": "<2.2.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2",
|
||||||
|
"phpunit/phpunit": "^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"phpstan": {
|
||||||
|
"includes": [
|
||||||
|
"extension.neon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Composer\\Pcre\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jordi Boggiano",
|
||||||
|
"email": "j.boggiano@seld.be",
|
||||||
|
"homepage": "http://seld.be"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||||
|
"keywords": [
|
||||||
|
"PCRE",
|
||||||
|
"preg",
|
||||||
|
"regex",
|
||||||
|
"regular expression"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/composer/pcre/issues",
|
||||||
|
"source": "https://github.com/composer/pcre/tree/3.4.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://packagist.com",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/composer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-06-07T11:47:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "composer/semver",
|
"name": "composer/semver",
|
||||||
"version": "3.4.4",
|
"version": "3.4.4",
|
||||||
@@ -11528,5 +11820,5 @@
|
|||||||
"php": "^8.3"
|
"php": "^8.3"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('teams', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->boolean('require_2fa')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('teams');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\TeamMemberRole;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('team_members', function (Blueprint $table) {
|
||||||
|
$table->foreignIdFor(Team::class)->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
|
||||||
|
$table->enum('role', TeamMemberRole::cases())->default(TeamMemberRole::Member);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['team_id', 'user_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('team_members');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import { BookOpen, FolderGit2, LayoutGrid } from '@lucide/vue';
|
import { UserGroup } from '@lucide/vue';
|
||||||
import AppLogo from '@/components/AppLogo.vue';
|
import AppLogo from '@/components/AppLogo.vue';
|
||||||
import NavFooter from '@/components/NavFooter.vue';
|
import NavFooter from '@/components/NavFooter.vue';
|
||||||
import NavMain from '@/components/NavMain.vue';
|
import NavMain from '@/components/NavMain.vue';
|
||||||
@@ -16,26 +16,17 @@ import {
|
|||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
import type { NavItem } from '@/types';
|
import type { NavItem } from '@/types';
|
||||||
|
import teams from "@/routes/teams";
|
||||||
|
|
||||||
const mainNavItems: NavItem[] = [
|
const mainNavItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Meine Teams',
|
||||||
href: dashboard(),
|
href: teams.index(),
|
||||||
icon: LayoutGrid,
|
icon: UserGroup,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const footerNavItems: NavItem[] = [
|
const footerNavItems: NavItem[] = [
|
||||||
{
|
|
||||||
title: 'Repository',
|
|
||||||
href: 'https://github.com/laravel/vue-starter-kit',
|
|
||||||
icon: FolderGit2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Documentation',
|
|
||||||
href: 'https://laravel.com/docs/starter-kits#vue',
|
|
||||||
icon: BookOpen,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import teamRoutes from "@/routes/teams";
|
||||||
|
import {Head} from "@inertiajs/vue3";
|
||||||
|
import type {Team} from "@/types/models";
|
||||||
|
import TextLink from "@/components/TextLink.vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
layout: {
|
||||||
|
breadcrumbs: [
|
||||||
|
{
|
||||||
|
title: 'Meine Teams',
|
||||||
|
href: teamRoutes.index(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
teams: Team[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Teams"/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Slug</th>
|
||||||
|
<th>Mitglieder</th>
|
||||||
|
<th>2FA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-if="teams.length > 0"
|
||||||
|
v-for="team in teams"
|
||||||
|
>
|
||||||
|
<th>{{ team.name }}</th>
|
||||||
|
<th>{{ team.slug }}</th>
|
||||||
|
<th>{{ team.members.length }}</th>
|
||||||
|
<th>{{ team.multi_factor_required }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-else class="text-center font-bold">
|
||||||
|
Du bist noch von keinem Team mitglied.
|
||||||
|
Tritt einem Team bei oder <TextLink href="{{ route('teams.create') }}">erstelle eins.</TextLink>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export interface Team {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
members: TeamMember[];
|
||||||
|
multi_factor_required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TeamMemberRole = 'admin' | 'member'
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
has_multi_factor: boolean;
|
||||||
|
role: TeamMemberRole;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,9 +10,7 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
|
|
||||||
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
});
|
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->group(function () {
|
|
||||||
Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
|
|
||||||
Route::get('settings/security', [SecurityController::class, 'edit'])
|
Route::get('settings/security', [SecurityController::class, 'edit'])
|
||||||
|
|||||||
+8
-1
@@ -1,11 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\TeamController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::inertia('/', 'Welcome')->name('home');
|
Route::inertia('/', 'Welcome')->name('home');
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->group(function () {
|
Route::middleware(['auth'])->group(function () {
|
||||||
Route::inertia('dashboard', 'Dashboard')->name('dashboard');
|
Route::inertia('dashboard', 'Dashboard')->name('dashboard');
|
||||||
|
|
||||||
|
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
||||||
|
Route::get('/', 'index')->name('index');
|
||||||
|
Route::get('/create', 'create')->name('create');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user