user profiles
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the specified user's profile.
|
||||
*/
|
||||
public function show(User $user)
|
||||
{
|
||||
return view('profile.show', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the authenticated user's profile.
|
||||
*/
|
||||
public function edit()
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('profile.edit', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the authenticated user's profile.
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'avatar' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif', 'max:2048'],
|
||||
'bio' => ['nullable', 'string', 'max:1000'],
|
||||
'website' => ['nullable', 'url', 'max:255'],
|
||||
'twitter' => ['nullable', 'string', 'max:255'],
|
||||
'github' => ['nullable', 'string', 'max:255'],
|
||||
'linkedin' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
// Handle avatar upload
|
||||
if ($request->hasFile('avatar')) {
|
||||
// Delete old avatar if exists
|
||||
if ($user->avatar) {
|
||||
Storage::disk('public')->delete('avatars/' . $user->avatar);
|
||||
}
|
||||
|
||||
// Store new avatar
|
||||
$avatarName = time() . '_' . $user->id . '.' . $request->avatar->extension();
|
||||
$request->avatar->storeAs('avatars', $avatarName, 'public');
|
||||
$validated['avatar'] = $avatarName;
|
||||
}
|
||||
|
||||
// Update user profile
|
||||
$user->update($validated);
|
||||
|
||||
return redirect()->route('profile.show', $user)
|
||||
->with('success', 'Profile updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's avatar.
|
||||
*/
|
||||
public function deleteAvatar()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->avatar) {
|
||||
Storage::disk('public')->delete('avatars/' . $user->avatar);
|
||||
$user->update(['avatar' => null]);
|
||||
}
|
||||
|
||||
return redirect()->route('profile.edit')
|
||||
->with('success', 'Avatar deleted successfully!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsAdmin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!auth()->check() || !auth()->user()->isAdmin()) {
|
||||
abort(403, 'Unauthorized action. Admin access required.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsModerator
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!auth()->check() || !auth()->user()->hasAnyRole(['moderator', 'admin'])) {
|
||||
abort(403, 'Unauthorized action. Moderator access required.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the users that belong to the role.
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
@@ -21,6 +22,12 @@ class User extends Authenticatable
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'avatar',
|
||||
'bio',
|
||||
'website',
|
||||
'twitter',
|
||||
'github',
|
||||
'linkedin',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -45,4 +52,63 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles that belong to the user.
|
||||
*/
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a specific role.
|
||||
*/
|
||||
public function hasRole(string $role): bool
|
||||
{
|
||||
return $this->roles()->where('slug', $role)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has any of the given roles.
|
||||
*/
|
||||
public function hasAnyRole(array $roles): bool
|
||||
{
|
||||
return $this->roles()->whereIn('slug', $roles)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is an admin.
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->hasRole('admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is a moderator.
|
||||
*/
|
||||
public function isModerator(): bool
|
||||
{
|
||||
return $this->hasRole('moderator');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is a regular user.
|
||||
*/
|
||||
public function isUser(): bool
|
||||
{
|
||||
return $this->hasRole('user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's avatar URL.
|
||||
*/
|
||||
public function getAvatarUrlAttribute(): ?string
|
||||
{
|
||||
if ($this->avatar) {
|
||||
return asset('storage/avatars/' . $this->avatar);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -19,6 +20,50 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
// Product-related Gates
|
||||
Gate::define('create-product', function ($user) {
|
||||
return $user->hasAnyRole(['user', 'moderator', 'admin']);
|
||||
});
|
||||
|
||||
Gate::define('edit-product', function ($user, $product) {
|
||||
return $user->id === $product->user_id || $user->hasAnyRole(['moderator', 'admin']);
|
||||
});
|
||||
|
||||
Gate::define('delete-product', function ($user, $product) {
|
||||
return $user->id === $product->user_id || $user->hasAnyRole(['moderator', 'admin']);
|
||||
});
|
||||
|
||||
Gate::define('approve-product', function ($user) {
|
||||
return $user->hasAnyRole(['moderator', 'admin']);
|
||||
});
|
||||
|
||||
Gate::define('reject-product', function ($user) {
|
||||
return $user->hasAnyRole(['moderator', 'admin']);
|
||||
});
|
||||
|
||||
// Comment-related Gates
|
||||
Gate::define('delete-comment', function ($user, $comment) {
|
||||
return $user->id === $comment->user_id || $user->hasAnyRole(['moderator', 'admin']);
|
||||
});
|
||||
|
||||
// User management Gates
|
||||
Gate::define('manage-users', function ($user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
|
||||
Gate::define('manage-roles', function ($user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
|
||||
Gate::define('manage-settings', function ($user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
|
||||
// Admin check
|
||||
Gate::before(function ($user, $ability) {
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -11,7 +11,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->alias([
|
||||
'admin' => \App\Http\Middleware\IsAdmin::class,
|
||||
'moderator' => \App\Http\Middleware\IsModerator::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
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::table('users', function (Blueprint $table) {
|
||||
$table->string('avatar')->nullable()->after('email');
|
||||
$table->text('bio')->nullable()->after('avatar');
|
||||
$table->string('website')->nullable()->after('bio');
|
||||
$table->string('twitter')->nullable()->after('website');
|
||||
$table->string('github')->nullable()->after('twitter');
|
||||
$table->string('linkedin')->nullable()->after('github');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['avatar', 'bio', 'website', 'twitter', 'github', 'linkedin']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
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('roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('roles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
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('role_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('role_id')->constrained()->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'role_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('role_user');
|
||||
}
|
||||
};
|
||||
@@ -15,11 +15,11 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
$this->call([
|
||||
RoleSeeder::class,
|
||||
]);
|
||||
|
||||
// Uncomment to create additional users with factory
|
||||
// User::factory(10)->create();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class RoleSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Create roles
|
||||
$adminRole = Role::create([
|
||||
'name' => 'Admin',
|
||||
'slug' => 'admin',
|
||||
'description' => 'Full access to all features and settings',
|
||||
]);
|
||||
|
||||
$moderatorRole = Role::create([
|
||||
'name' => 'Moderator',
|
||||
'slug' => 'moderator',
|
||||
'description' => 'Can approve/reject products and delete comments',
|
||||
]);
|
||||
|
||||
$userRole = Role::create([
|
||||
'name' => 'User',
|
||||
'slug' => 'user',
|
||||
'description' => 'Can submit products, upvote, and comment',
|
||||
]);
|
||||
|
||||
// Create test users
|
||||
$admin = User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
'bio' => 'Administrator of the platform',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$admin->roles()->attach($adminRole);
|
||||
|
||||
$moderator = User::create([
|
||||
'name' => 'Moderator User',
|
||||
'email' => 'moderator@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
'bio' => 'Content moderator',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$moderator->roles()->attach($moderatorRole);
|
||||
|
||||
$regularUser = User::create([
|
||||
'name' => 'Regular User',
|
||||
'email' => 'user@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
'bio' => 'Regular platform user',
|
||||
'website' => 'https://example.com',
|
||||
'twitter' => 'example',
|
||||
'github' => 'example',
|
||||
'linkedin' => 'example',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$regularUser->roles()->attach($userRole);
|
||||
|
||||
$this->command->info('Roles and test users created successfully!');
|
||||
$this->command->info('Admin: admin@example.com / password');
|
||||
$this->command->info('Moderator: moderator@example.com / password');
|
||||
$this->command->info('User: user@example.com / password');
|
||||
}
|
||||
}
|
||||
Generated
+2856
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Edit Profile</div>
|
||||
|
||||
<div class="card-body">
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Avatar Section -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Profile Picture</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
@if ($user->avatar)
|
||||
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="rounded-circle" style="width: 100px; height: 100px; object-fit: cover;">
|
||||
@else
|
||||
<div class="bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 100px; height: 100px; font-size: 36px;">
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<input type="file" class="form-control @error('avatar') is-invalid @enderror" id="avatar" name="avatar" accept="image/*">
|
||||
@error('avatar')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
<small class="form-text text-muted">JPG, PNG, or GIF. Max size 2MB.</small>
|
||||
@if ($user->avatar)
|
||||
<div class="mt-2">
|
||||
<form method="POST" action="{{ route('profile.avatar.delete') }}" class="d-inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to delete your avatar?')">
|
||||
Delete Avatar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control @error('name') is-invalid @enderror" id="name" name="name" value="{{ old('name', $user->name) }}" required>
|
||||
@error('name')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $user->email) }}" required>
|
||||
@error('email')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Bio -->
|
||||
<div class="mb-3">
|
||||
<label for="bio" class="form-label">Bio</label>
|
||||
<textarea class="form-control @error('bio') is-invalid @enderror" id="bio" name="bio" rows="4" maxlength="1000">{{ old('bio', $user->bio) }}</textarea>
|
||||
@error('bio')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
<small class="form-text text-muted">Max 1000 characters</small>
|
||||
</div>
|
||||
|
||||
<!-- Website -->
|
||||
<div class="mb-3">
|
||||
<label for="website" class="form-label">Website</label>
|
||||
<input type="url" class="form-control @error('website') is-invalid @enderror" id="website" name="website" value="{{ old('website', $user->website) }}" placeholder="https://example.com">
|
||||
@error('website')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="mb-3">
|
||||
<label for="twitter" class="form-label">Twitter Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">@</span>
|
||||
<input type="text" class="form-control @error('twitter') is-invalid @enderror" id="twitter" name="twitter" value="{{ old('twitter', $user->twitter) }}" placeholder="username">
|
||||
@error('twitter')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="github" class="form-label">GitHub Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">github.com/</span>
|
||||
<input type="text" class="form-control @error('github') is-invalid @enderror" id="github" name="github" value="{{ old('github', $user->github) }}" placeholder="username">
|
||||
@error('github')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="linkedin" class="form-label">LinkedIn Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">linkedin.com/in/</span>
|
||||
<input type="text" class="form-control @error('linkedin') is-invalid @enderror" id="linkedin" name="linkedin" value="{{ old('linkedin', $user->linkedin) }}" placeholder="username">
|
||||
@error('linkedin')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ route('profile.show', $user) }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Update Profile</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,93 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>{{ $user->name }}'s Profile</span>
|
||||
@auth
|
||||
@if (Auth::id() === $user->id)
|
||||
<a href="{{ route('profile.edit') }}" class="btn btn-sm btn-primary">Edit Profile</a>
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 text-center">
|
||||
@if ($user->avatar)
|
||||
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" class="img-fluid rounded-circle mb-3" style="max-width: 150px;">
|
||||
@else
|
||||
<div class="bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center mb-3" style="width: 150px; height: 150px; font-size: 48px; margin: 0 auto;">
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<h3>{{ $user->name }}</h3>
|
||||
<p class="text-muted">{{ $user->email }}</p>
|
||||
|
||||
@if ($user->bio)
|
||||
<div class="mt-3">
|
||||
<h5>Bio</h5>
|
||||
<p>{{ $user->bio }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($user->website || $user->twitter || $user->github || $user->linkedin)
|
||||
<div class="mt-4">
|
||||
<h5>Links</h5>
|
||||
<ul class="list-unstyled">
|
||||
@if ($user->website)
|
||||
<li class="mb-2">
|
||||
<strong>Website:</strong>
|
||||
<a href="{{ $user->website }}" target="_blank" rel="noopener">{{ $user->website }}</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($user->twitter)
|
||||
<li class="mb-2">
|
||||
<strong>Twitter:</strong>
|
||||
<a href="https://twitter.com/{{ $user->twitter }}" target="_blank" rel="noopener">@{{ $user->twitter }}</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($user->github)
|
||||
<li class="mb-2">
|
||||
<strong>GitHub:</strong>
|
||||
<a href="https://github.com/{{ $user->github }}" target="_blank" rel="noopener">{{ $user->github }}</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($user->linkedin)
|
||||
<li class="mb-2">
|
||||
<strong>LinkedIn:</strong>
|
||||
<a href="https://linkedin.com/in/{{ $user->linkedin }}" target="_blank" rel="noopener">{{ $user->linkedin }}</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($user->roles->isNotEmpty())
|
||||
<div class="mt-4">
|
||||
<h5>Roles</h5>
|
||||
<div>
|
||||
@foreach ($user->roles as $role)
|
||||
<span class="badge bg-primary me-1">{{ $role->name }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 text-muted">
|
||||
<small>Member since {{ $user->created_at->format('F Y') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
@@ -9,3 +10,38 @@ Route::get('/', function () {
|
||||
Auth::routes();
|
||||
|
||||
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
|
||||
|
||||
// Profile Routes
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/profile/edit', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::put('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/profile/avatar', [ProfileController::class, 'deleteAvatar'])->name('profile.avatar.delete');
|
||||
});
|
||||
|
||||
Route::get('/profile/{user}', [ProfileController::class, 'show'])->name('profile.show');
|
||||
|
||||
// Admin Routes (example)
|
||||
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/dashboard', function () {
|
||||
return 'Admin Dashboard';
|
||||
})->name('dashboard');
|
||||
|
||||
Route::get('/users', function () {
|
||||
return 'User Management (Admin Only)';
|
||||
})->name('users.index');
|
||||
|
||||
Route::get('/settings', function () {
|
||||
return 'Settings (Admin Only)';
|
||||
})->name('settings');
|
||||
});
|
||||
|
||||
// Moderator Routes (example)
|
||||
Route::middleware(['auth', 'moderator'])->prefix('moderator')->name('moderator.')->group(function () {
|
||||
Route::get('/dashboard', function () {
|
||||
return 'Moderator Dashboard';
|
||||
})->name('dashboard');
|
||||
|
||||
Route::get('/products/pending', function () {
|
||||
return 'Pending Products (Moderator Only)';
|
||||
})->name('products.pending');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake('public');
|
||||
});
|
||||
|
||||
test('users can view a profile', function () {
|
||||
$user = User::factory()->create([
|
||||
'name' => 'John Doe',
|
||||
'bio' => 'Test bio',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('profile.show', $user));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('John Doe');
|
||||
$response->assertSee('Test bio');
|
||||
});
|
||||
|
||||
test('authenticated users can view their edit profile page', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('profile.edit'));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Edit Profile');
|
||||
});
|
||||
|
||||
test('guests cannot view edit profile page', function () {
|
||||
$response = $this->get(route('profile.edit'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('users can update their profile', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->put(route('profile.update'), [
|
||||
'name' => 'Updated Name',
|
||||
'email' => $user->email,
|
||||
'bio' => 'Updated bio',
|
||||
'website' => 'https://example.com',
|
||||
'twitter' => 'twitter_handle',
|
||||
'github' => 'github_handle',
|
||||
'linkedin' => 'linkedin_handle',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('profile.show', $user));
|
||||
$response->assertSessionHas('success');
|
||||
|
||||
$user->refresh();
|
||||
expect($user->name)->toBe('Updated Name');
|
||||
expect($user->bio)->toBe('Updated bio');
|
||||
expect($user->website)->toBe('https://example.com');
|
||||
expect($user->twitter)->toBe('twitter_handle');
|
||||
});
|
||||
|
||||
test('users can upload an avatar', function () {
|
||||
$user = User::factory()->create();
|
||||
$file = UploadedFile::fake()->image('avatar.jpg');
|
||||
|
||||
$response = $this->actingAs($user)->put(route('profile.update'), [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar' => $file,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('profile.show', $user));
|
||||
|
||||
$user->refresh();
|
||||
expect($user->avatar)->not->toBeNull();
|
||||
Storage::disk('public')->assertExists('avatars/' . $user->avatar);
|
||||
});
|
||||
|
||||
test('avatar must be an image', function () {
|
||||
$user = User::factory()->create();
|
||||
$file = UploadedFile::fake()->create('document.pdf');
|
||||
|
||||
$response = $this->actingAs($user)->put(route('profile.update'), [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar' => $file,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('avatar');
|
||||
});
|
||||
|
||||
test('avatar cannot exceed 2MB', function () {
|
||||
$user = User::factory()->create();
|
||||
$file = UploadedFile::fake()->image('avatar.jpg')->size(3000);
|
||||
|
||||
$response = $this->actingAs($user)->put(route('profile.update'), [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar' => $file,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('avatar');
|
||||
});
|
||||
|
||||
test('users can delete their avatar', function () {
|
||||
Storage::fake('public');
|
||||
$user = User::factory()->create(['avatar' => 'test_avatar.jpg']);
|
||||
Storage::disk('public')->put('avatars/test_avatar.jpg', 'fake content');
|
||||
|
||||
$response = $this->actingAs($user)->delete(route('profile.avatar.delete'));
|
||||
|
||||
$response->assertRedirect(route('profile.edit'));
|
||||
$response->assertSessionHas('success');
|
||||
|
||||
$user->refresh();
|
||||
expect($user->avatar)->toBeNull();
|
||||
Storage::disk('public')->assertMissing('avatars/test_avatar.jpg');
|
||||
});
|
||||
|
||||
test('email must be unique when updating profile', function () {
|
||||
$existingUser = User::factory()->create(['email' => 'existing@example.com']);
|
||||
$user = User::factory()->create(['email' => 'user@example.com']);
|
||||
|
||||
$response = $this->actingAs($user)->put(route('profile.update'), [
|
||||
'name' => $user->name,
|
||||
'email' => 'existing@example.com',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('website must be a valid url', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->put(route('profile.update'), [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'website' => 'not-a-valid-url',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('website');
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Create roles
|
||||
$this->adminRole = Role::create([
|
||||
'name' => 'Admin',
|
||||
'slug' => 'admin',
|
||||
'description' => 'Full access',
|
||||
]);
|
||||
|
||||
$this->moderatorRole = Role::create([
|
||||
'name' => 'Moderator',
|
||||
'slug' => 'moderator',
|
||||
'description' => 'Moderate content',
|
||||
]);
|
||||
|
||||
$this->userRole = Role::create([
|
||||
'name' => 'User',
|
||||
'slug' => 'user',
|
||||
'description' => 'Regular user',
|
||||
]);
|
||||
});
|
||||
|
||||
test('users can be assigned roles', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect($user->roles)->toHaveCount(1);
|
||||
expect($user->roles->first()->slug)->toBe('user');
|
||||
});
|
||||
|
||||
test('user can have multiple roles', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach([$this->userRole->id, $this->moderatorRole->id]);
|
||||
|
||||
expect($user->roles)->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('hasRole method works correctly', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect($user->hasRole('user'))->toBeTrue();
|
||||
expect($user->hasRole('admin'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('hasAnyRole method works correctly', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->moderatorRole);
|
||||
|
||||
expect($user->hasAnyRole(['moderator', 'admin']))->toBeTrue();
|
||||
expect($user->hasAnyRole(['user', 'admin']))->toBeFalse();
|
||||
});
|
||||
|
||||
test('isAdmin method works correctly', function () {
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect($admin->isAdmin())->toBeTrue();
|
||||
expect($user->isAdmin())->toBeFalse();
|
||||
});
|
||||
|
||||
test('isModerator method works correctly', function () {
|
||||
$moderator = User::factory()->create();
|
||||
$moderator->roles()->attach($this->moderatorRole);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect($moderator->isModerator())->toBeTrue();
|
||||
expect($user->isModerator())->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin middleware blocks non-admin users', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
$response = $this->actingAs($user)->get('/admin/dashboard');
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
test('admin middleware allows admin users', function () {
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
$response = $this->actingAs($admin)->get('/admin/dashboard');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('moderator middleware blocks regular users', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
$response = $this->actingAs($user)->get('/moderator/dashboard');
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
test('moderator middleware allows moderators', function () {
|
||||
$moderator = User::factory()->create();
|
||||
$moderator->roles()->attach($this->moderatorRole);
|
||||
|
||||
$response = $this->actingAs($moderator)->get('/moderator/dashboard');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('moderator middleware allows admins', function () {
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
$response = $this->actingAs($admin)->get('/moderator/dashboard');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('guests cannot access protected routes', function () {
|
||||
$response = $this->get('/admin/dashboard');
|
||||
$response->assertStatus(302);
|
||||
|
||||
$response = $this->get('/moderator/dashboard');
|
||||
$response->assertStatus(302);
|
||||
});
|
||||
|
||||
test('create product gate allows users moderators and admins', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect(Gate::forUser($user)->allows('create-product'))->toBeTrue();
|
||||
|
||||
$moderator = User::factory()->create();
|
||||
$moderator->roles()->attach($this->moderatorRole);
|
||||
|
||||
expect(Gate::forUser($moderator)->allows('create-product'))->toBeTrue();
|
||||
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
expect(Gate::forUser($admin)->allows('create-product'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('approve product gate allows only moderators and admins', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect(Gate::forUser($user)->allows('approve-product'))->toBeFalse();
|
||||
|
||||
$moderator = User::factory()->create();
|
||||
$moderator->roles()->attach($this->moderatorRole);
|
||||
|
||||
expect(Gate::forUser($moderator)->allows('approve-product'))->toBeTrue();
|
||||
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
expect(Gate::forUser($admin)->allows('approve-product'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('manage users gate allows only admins', function () {
|
||||
$user = User::factory()->create();
|
||||
$user->roles()->attach($this->userRole);
|
||||
|
||||
expect(Gate::forUser($user)->allows('manage-users'))->toBeFalse();
|
||||
|
||||
$moderator = User::factory()->create();
|
||||
$moderator->roles()->attach($this->moderatorRole);
|
||||
|
||||
expect(Gate::forUser($moderator)->allows('manage-users'))->toBeFalse();
|
||||
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
expect(Gate::forUser($admin)->allows('manage-users'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin has access to all gates via gate before', function () {
|
||||
$admin = User::factory()->create();
|
||||
$admin->roles()->attach($this->adminRole);
|
||||
|
||||
expect(Gate::forUser($admin)->allows('create-product'))->toBeTrue();
|
||||
expect(Gate::forUser($admin)->allows('approve-product'))->toBeTrue();
|
||||
expect(Gate::forUser($admin)->allows('manage-users'))->toBeTrue();
|
||||
expect(Gate::forUser($admin)->allows('manage-roles'))->toBeTrue();
|
||||
expect(Gate::forUser($admin)->allows('manage-settings'))->toBeTrue();
|
||||
});
|
||||
+6
-1
@@ -6,5 +6,10 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withoutVite();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user