Skip to content

Commit abc32dd

Browse files
committed
Implement user management, improve some style, and enhance middleware
1 parent 886491c commit abc32dd

19 files changed

Lines changed: 542 additions & 40 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class EnsureUserIsAdministrator
10+
{
11+
/**
12+
* Handle an incoming request.
13+
*
14+
* @param Closure(Request): (Response) $next
15+
*/
16+
public function handle(Request $request, Closure $next): Response
17+
{
18+
if ($request->user()->role != 'admin') {
19+
return abort(404);
20+
}
21+
22+
return $next($request);
23+
}
24+
}

app/Livewire/Passport/CreateClient.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
namespace App\Livewire\Passport;
44

5-
use App\Models\User;
65
use Illuminate\Support\Facades\Auth;
7-
use Illuminate\Support\Facades\Gate;
86
use Illuminate\Support\Facades\Session;
97
use Laravel\Passport\ClientRepository;
108
use Livewire\Component;
@@ -33,8 +31,6 @@ public function boot()
3331

3432
public function mount()
3533
{
36-
Gate::allowIf(fn(User $user) => $user->role != 'user');
37-
3834
$this->user = Auth::user();
3935
}
4036

app/Livewire/Passport/Home.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use App\Models\User;
66
use Exception;
77
use Illuminate\Support\Facades\Auth;
8-
use Illuminate\Support\Facades\Gate;
98
use Laravel\Passport\Passport;
109
use Laravel\Passport\Token;
1110
use Livewire\Component;
@@ -20,8 +19,6 @@ class Home extends Component
2019

2120
public function mount(): void
2221
{
23-
Gate::allowIf(fn(User $user) => $user->role != 'user');
24-
2522
$this->user = Auth::user();
2623
}
2724

app/Livewire/Profile.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public function updateProfileInformation(): void
116116
]);
117117
}
118118

119-
public function deleteUser(Logout $logout): void
119+
public function deleteAccount(Logout $logout): void
120120
{
121121
try {
122122
$this->validate([

app/Livewire/Security/TwoFactor.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Exception;
66
use Illuminate\Support\Facades\Auth;
7+
use Illuminate\Support\Facades\RateLimiter;
78
use Illuminate\Support\Facades\Session;
89
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
910
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
@@ -89,7 +90,7 @@ public function render()
8990
}
9091

9192
return view('livewire.security.two-factor')->layout('layouts::app', [
92-
'title' => 'Manage Two-Factor',
93+
'title' => 'Two-Factor Authentication',
9394
'user' => $this->user
9495
]);
9596
}
@@ -136,9 +137,22 @@ public function disableTwoFactor(DisableTwoFactorAuthentication $disableTwoFacto
136137

137138
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generateNewRecoveryCodes): void
138139
{
139-
$generateNewRecoveryCodes($this->user);
140+
$newRecoveryCodes = RateLimiter::attempt(
141+
'regenerate-code.' . $this->user->id,
142+
1,
143+
function () use ($generateNewRecoveryCodes) {
144+
$generateNewRecoveryCodes($this->user);
140145

141-
$this->loadRecoveryCodes();
146+
$this->loadRecoveryCodes();
147+
},
148+
3600
149+
);
150+
151+
if (!$newRecoveryCodes) {
152+
abort(429);
153+
154+
return;
155+
}
142156
}
143157

144158
private function loadRecoveryCodes(): void

app/Livewire/Users/Home.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Livewire\Users;
4+
5+
use App\Models\User;
6+
use Illuminate\Support\Facades\Auth;
7+
use Illuminate\Support\Facades\Session;
8+
use Livewire\Component;
9+
use Livewire\WithoutUrlPagination;
10+
use Livewire\WithPagination;
11+
12+
class Home extends Component
13+
{
14+
use WithPagination, WithoutUrlPagination;
15+
16+
public mixed $user;
17+
18+
public string $search = '';
19+
20+
public function mount(): void
21+
{
22+
$this->user = Auth::user();
23+
}
24+
25+
public function render()
26+
{
27+
if (Session::has('status') || Session::has('error')) {
28+
$this->dispatch('toastify', [
29+
'type' => Session::has('error') ? 'error' : 'success',
30+
'message' => Session::get('error') ?? Session::get('status')
31+
]);
32+
}
33+
34+
return view('livewire.users.home', [
35+
'users' => User::getUsersWithSocialAccounts($this->user)
36+
->when($this->search, function ($query, $search) {
37+
$query->where('name', 'like', "{$search}%");
38+
})
39+
->paginate(10)
40+
->onEachSide(1)
41+
])->layout('layouts::app', [
42+
'title' => 'Manage Users',
43+
'user' => $this->user
44+
]);
45+
}
46+
47+
public function searchUser(): void
48+
{
49+
$this->resetPage();
50+
}
51+
}

app/Livewire/Users/Profile.php

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
namespace App\Livewire\Users;
4+
5+
use App\Concerns\PasswordValidationRules;
6+
use App\Concerns\ProfileValidationRules;
7+
use App\Models\Social;
8+
use App\Models\User;
9+
use Illuminate\Contracts\Auth\MustVerifyEmail;
10+
use Illuminate\Support\Facades\Auth;
11+
use Illuminate\Support\Facades\Session;
12+
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
13+
use Laravel\Fortify\Features;
14+
use Livewire\Attributes\Computed;
15+
use Livewire\Attributes\Locked;
16+
use Livewire\Component;
17+
18+
class Profile extends Component
19+
{
20+
use ProfileValidationRules, PasswordValidationRules;
21+
22+
public $user;
23+
24+
public string $name = '';
25+
26+
public string $username = '';
27+
28+
public string $email = '';
29+
30+
public string $password = '';
31+
32+
public mixed $socials_google;
33+
34+
public mixed $socials_github;
35+
36+
#[Locked]
37+
public bool $canManageTwoFactor;
38+
39+
#[Locked]
40+
public bool $twoFactorEnabled;
41+
42+
public function boot()
43+
{
44+
$this->withValidator(function ($validator) {
45+
$validator->after(function ($validator) {
46+
if ($validator->errors()->count() > 0) {
47+
$this->dispatch('toastify', [
48+
'type' => 'error',
49+
'message' => $validator->errors()->all()[0]
50+
]);
51+
}
52+
});
53+
});
54+
}
55+
56+
public function mount(string $id)
57+
{
58+
$this->user = User::getUserWithSocialAccount($id)->first();
59+
60+
if ($this->user) {
61+
if ($this->user->id == Auth::user()->id) {
62+
return $this->redirectRoute('profile', navigate: true);
63+
}
64+
65+
$this->name = $this->user->name;
66+
67+
$this->username = $this->user->username;
68+
69+
$this->email = $this->user->email;
70+
71+
$this->socials_google = $this->user->socialAccounts()->where('provider', 'google')->first();
72+
73+
$this->socials_github = $this->user->socialAccounts()->where('provider', 'github')->first();
74+
}
75+
76+
$this->canManageTwoFactor = Features::canManageTwoFactorAuthentication();
77+
78+
if ($this->canManageTwoFactor) {
79+
$this->twoFactorEnabled = $this->user->hasEnabledTwoFactorAuthentication();
80+
}
81+
}
82+
83+
public function render()
84+
{
85+
return view('livewire.users.profile')->layout('layouts::app', [
86+
'title' => $this->name . ' Profile',
87+
'user' => Auth::user()
88+
]);
89+
}
90+
91+
#[Computed]
92+
public function hasUnverifiedEmail(): bool
93+
{
94+
return $this->user instanceof MustVerifyEmail && !$this->user->hasVerifiedEmail();
95+
}
96+
97+
public function updateProfileInformation(): void
98+
{
99+
$validated = $this->validate($this->profileRules($this->user->id));
100+
101+
$this->user->fill($validated);
102+
103+
if ($this->user->isDirty('email')) {
104+
$this->user->email_verified_at = null;
105+
}
106+
107+
$this->user->save();
108+
109+
$this->dispatch('toastify', [
110+
'type' => 'success',
111+
'message' => 'User Profile Updated Successfully!'
112+
]);
113+
}
114+
115+
public function unlinkSocialAccount(string $provider): void
116+
{
117+
$socialAccontsById = Social::getUserBySocialAccoutsEmail($provider, $this->user->email);
118+
119+
if ($socialAccontsById->first()) {
120+
$socialAccontsById->delete();
121+
122+
if ($provider == 'google') {
123+
$this->socials_google = null;
124+
}
125+
126+
if ($provider == 'github') {
127+
$this->socials_github = null;
128+
}
129+
130+
$this->dispatch('toastify', [
131+
'type' => 'success',
132+
'message' => 'Social Account Unlinked Successfully!'
133+
]);
134+
}
135+
}
136+
137+
public function disableTwoFactor(DisableTwoFactorAuthentication $disableTwoFactorAuthentication)
138+
{
139+
$disableTwoFactorAuthentication($this->user);
140+
141+
$this->twoFactorEnabled = false;
142+
143+
$this->dispatch('toastify', [
144+
'type' => 'success',
145+
'message' => 'Two-Factor Disable Successfully!'
146+
]);
147+
}
148+
149+
public function deleteAccount(): void
150+
{
151+
$this->user->oauthApps()->delete();
152+
153+
$this->user->socialAccounts()->delete();
154+
155+
$this->user->delete();
156+
157+
Session::flash('status', 'User Deleted Successfully');
158+
159+
$this->redirectRoute('users.home', navigate: true);
160+
}
161+
}

app/Models/User.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Illuminate\Contracts\Auth\MustVerifyEmail;
77
use Illuminate\Database\Eloquent\Attributes\Fillable;
88
use Illuminate\Database\Eloquent\Attributes\Hidden;
9+
use Illuminate\Database\Eloquent\Attributes\Scope;
10+
use Illuminate\Database\Eloquent\Builder;
911
use Illuminate\Database\Eloquent\Concerns\HasUuids;
1012
use Illuminate\Database\Eloquent\Factories\HasFactory;
1113
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -44,4 +46,16 @@ public function socialAccounts(): HasMany
4446
{
4547
return $this->hasMany(Social::class, 'user_id', 'id');
4648
}
49+
50+
#[Scope]
51+
protected function getUserWithSocialAccount(Builder $query, string $id)
52+
{
53+
return $query->where('id', '=', $id)->with('socialAccounts');
54+
}
55+
56+
#[Scope]
57+
protected function getUsersWithSocialAccounts(Builder $query, User $user)
58+
{
59+
return $query->where('id', '<>', $user->id)->with('socialAccounts')->orderBy('created_at', 'desc');
60+
}
4761
}

bootstrap/app.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Middleware\EnsureUserIsAdministrator;
34
use App\Http\Middleware\PassportMiddleware;
45
use Illuminate\Foundation\Application;
56
use Illuminate\Foundation\Configuration\Exceptions;
@@ -21,10 +22,12 @@
2122
->group(base_path('routes/web.php'));
2223
}
2324
)
24-
->withMiddleware(function (Middleware $middleware): void {
25-
$middleware->append([
26-
PassportMiddleware::class
25+
->withMiddleware(function (Middleware $middleware): void {
26+
$middleware->alias([
27+
'user.admin' => EnsureUserIsAdministrator::class
2728
]);
29+
30+
$middleware->append([PassportMiddleware::class]);
2831
})
2932
->withExceptions(function (Exceptions $exceptions): void {
3033
//
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="relative inline-flex gap-2 text-[#3d3530]">
2+
<input
3+
type="text"
4+
class="rounded-xs min-w-52 inset-ring inset-ring-[#5e6e754d] focus:inset-ring-[#c8b96ea6] w-full bg-[#ffffff73] px-2.5 py-2 text-sm/5 font-normal focus:bg-[#ffffffb3] focus:outline-none"
5+
autocomplete="off"
6+
name="search"
7+
{{ $attributes }}
8+
/>
9+
10+
<button
11+
type="submit"
12+
class="rounded-xs px-2.5 cursor-pointer bg-[#3d3530e6] text-sm/4 text-[#f0ede8] transition-colors duration-300 hover:bg-[#3d3530]"
13+
>
14+
<span class="icon-[tabler--search] size-4">
15+
</span>
16+
</button>
17+
</div>

0 commit comments

Comments
 (0)