Skip to content

Commit

Permalink
Implement New Team-Based User Model (#127)
Browse files Browse the repository at this point in the history
* activate teams and invitations

* add missing team policy

* add link to team settings

* change forms to use team_id

* fix initial team creation and add logout to missing team page

* change naming

* wip make dedicated create team page after signup

* fix team creation

* isntall telescope

* change message for team creation page

* fix tests

* remove underscores from test

* update policies

* add tests to preview form

* fix migrations for sqlite

* change default team name
  • Loading branch information
PhilReinking authored Jan 13, 2024
1 parent 5512055 commit f1e19f6
Show file tree
Hide file tree
Showing 62 changed files with 979 additions and 159 deletions.
21 changes: 2 additions & 19 deletions app/Actions/Fortify/CreateNewUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\Actions\Fortify;

use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
Expand All @@ -28,26 +27,10 @@ public function create(array $input)
])->validate();

return DB::transaction(function () use ($input) {
return tap(User::create([
return User::create([
'email' => $input['email'],
'password' => Hash::make($input['password']),
]), function (User $user) {
$this->createTeam($user);
});
]);
});
}

/**
* Create a personal team for the user.
*
* @return void
*/
protected function createTeam(User $user)
{
$user->ownedTeams()->save(Team::forceCreate([
'user_id' => $user->id,
'name' => explode(' ', $user->name, 2)[0]."'s Team",
'personal_team' => true,
]));
}
}
2 changes: 2 additions & 0 deletions app/Http/Controllers/Api/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function index(Request $request)
]);

$forms = $request->user()
->currentTeam
->forms()
->withFilter($request->filter ?? null)
->get();
Expand All @@ -42,6 +43,7 @@ public function create(Request $request)
$form = Form::create([
'name' => 'Untitled Form',
'user_id' => $request->user()->id,
'team_id' => $request->user()->currentTeam->id,
'has_data_privacy' => false,
'brand_color' => Form::DEFAULT_BRAND_COLOR,
]);
Expand Down
10 changes: 1 addition & 9 deletions app/Http/Controllers/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ class DashboardController extends Controller
{
public function show(Request $request)
{
$request->validate([
'filter' => 'in:published,unpublished,trashed',
]);

$forms = $request->user()->forms()->withFilter($request->filter ?? null)->get();

return Inertia::render('Dashboard', [
'initialForms' => $forms,
]);
return Inertia::render('Dashboard');
}
}
13 changes: 13 additions & 0 deletions app/Http/Controllers/MissingTeamController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MissingTeamController extends Controller
{
public function __invoke(Request $request)
{
return inertia('Teams/MissingTeam');
}
}
2 changes: 1 addition & 1 deletion app/Http/Controllers/ViewFormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public function show(Request $request, $uuid)
abort(404);
}

if (! $form->is_published && $request->user()->id !== $form->user_id) {
if (! $form->is_published && $request->user()->current_team_id !== $form->team_id) {
abort(404);
}

Expand Down
3 changes: 3 additions & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Http;

use App\Http\Middleware\CheckUserSetup;
use App\Http\Middleware\EnsureHasTeam;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Routing\Router;
Expand Down Expand Up @@ -40,13 +41,15 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
EnsureHasTeam::class,
\App\Http\Middleware\HandleInertiaRequests::class,
],

'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
EnsureHasTeam::class,
],
];

Expand Down
97 changes: 97 additions & 0 deletions app/Http/Middleware/EnsureHasTeam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace App\Http\Middleware;

use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureHasTeam
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->expectsJson()) {
return $this->handleApi($request, $next);
} else {
return $this->handleWeb($request, $next);
}
}

protected function handleApi(Request $request, Closure $next): Response
{
// if user is not logged in, allow them to proceed
if (! $request->user()) {
return $next($request);
}

// check if user has a current team
// if they do, allow them to proceed
if ($request->user()->currentTeam) {
return $next($request);
}

// otherwise, get all teams for the user
$teams = $request->user()->allTeams();

// if user has no team at this point, return a 403
if ($teams->count() === 0) {
return abort(403, 'You are not a member of any team.');
}

// otherwise, switch to the first team
$request->user()->switchTeam($teams->first());

return $next($request);
}

protected function handleWeb(Request $request, Closure $next): Response
{
// if user is not logged in, allow them to proceed
if (! $request->user()) {
return $next($request);
}

// check if user has a current team
// if they do, allow them to proceed
if ($request->user()->currentTeam) {
return $next($request);
}

$whitelistedRoutes = [
'teams.missing',
'teams.create',
'teams.store',
'team-invitations.accept',
'logout',
];

// whitelist some routes
if (in_array($request->route()->getName(), $whitelistedRoutes)) {
return $next($request);
}

// otherwise, get all teams for the user
$teams = $request->user()->allTeams();

// if the user is the first user, redirect them to the create team page
if ($teams->count() === 0 && User::count() === 1) {
return redirect()->route('teams.create');
}

// if user has no team at this point, redirect them to the missing team page
if ($teams->count() === 0) {
return redirect()->route('teams.missing');
}

// otherwise, switch to the first team
$request->user()->switchTeam($teams->first());

return $next($request);
}
}
7 changes: 7 additions & 0 deletions app/Models/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ public function user(): BelongsTo
return $this->belongsTo(User::class);
}

public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}

public function scopeWithFilter(Builder $builder, ?string $filter)
{
return match ($filter) {
Expand Down Expand Up @@ -380,6 +385,8 @@ public function duplicate(string $newName): Form
{
$newForm = Form::create([
'name' => $newName,
'user_id' => $this->user_id,
'team_id' => $this->team_id,
]);

$newForm->applyTemplate($this->toTemplate());
Expand Down
5 changes: 5 additions & 0 deletions app/Models/Team.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ class Team extends JetstreamTeam
'updated' => TeamUpdated::class,
'deleted' => TeamDeleted::class,
];

public function forms()
{
return $this->hasMany(Form::class);
}
}
17 changes: 16 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\Features;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Jetstream\HasTeams;
use Laravel\Sanctum\HasApiTokens;
Expand Down Expand Up @@ -70,8 +72,21 @@ protected static function boot()
});
}

public function forms()
protected function defaultProfilePhotoUrl()
{
$name = trim(collect(explode('@', $this->email))->first());

return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=c5cfdb&background=060b16';
}

public function forms(): HasMany
{
if (Features::hasTeamFeatures()) {
if ($this->currentTeam) {
return $this->currentTeam->forms();
}
}

return $this->hasMany(Form::class);
}
}
6 changes: 3 additions & 3 deletions app/Policies/FormBlockInteractionPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ class FormBlockInteractionPolicy

public function view(User $user, FormBlockInteraction $interaction)
{
return $user->id === $interaction->formBlock->form->user_id;
return $user->id === $interaction->formBlock->form->user_id || $user->current_team_id === $interaction->formBlock->form->team_id;
}

public function update(User $user, FormBlockInteraction $interaction)
{
return $user->id === $interaction->formBlock->form->user_id;
return $user->id === $interaction->formBlock->form->user_id || $user->current_team_id === $interaction->formBlock->form->team_id;
}

public function delete(User $user, FormBlockInteraction $interaction)
{
return $user->id === $interaction->formBlock->form->user_id;
return $user->id === $interaction->formBlock->form->user_id || $user->current_team_id === $interaction->formBlock->form->team_id;
}
}
4 changes: 2 additions & 2 deletions app/Policies/FormBlockPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ class FormBlockPolicy

public function update(User $user, FormBlock $block)
{
return $user->id === $block->form->user_id;
return $user->id === $block->form->user_id || $user->current_team_id === $block->form->team_id;
}

public function delete(User $user, FormBlock $block)
{
return $user->id === $block->form->user_id;
return $user->id === $block->form->user_id || $user->current_team_id === $block->form->team_id;
}
}
6 changes: 3 additions & 3 deletions app/Policies/FormPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ class FormPolicy

public function view(User $user, Form $form)
{
return $user->id === $form->user_id;
return $user->id === $form->user_id || $user->current_team_id === $form->team_id;
}

public function update(User $user, Form $form)
{
return $user->id === $form->user_id;
return $user->id === $form->user_id || $user->current_team_id === $form->team_id;
}

public function delete(User $user, Form $form)
{
return $user->id === $form->user_id;
return $user->id === $form->user_id || $user->current_team_id === $form->team_id;
}
}
Loading

0 comments on commit f1e19f6

Please sign in to comment.