Commit 9b946284 by Shaganaz

Implemented notification,api rate limiting and background job processing

parent e226894f
...@@ -8,6 +8,11 @@ ...@@ -8,6 +8,11 @@
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use App\Notifications\TaskAssignedNotification;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use App\Jobs\SendTaskAssignedMail;
use App\Jobs\SendTaskNotification;
class TaskController extends Controller class TaskController extends Controller
{ {
...@@ -48,41 +53,69 @@ public function assignUsers(Request $request, Task $task) ...@@ -48,41 +53,69 @@ public function assignUsers(Request $request, Task $task)
'created_at' => now(), 'created_at' => now(),
'updated_at' => now() 'updated_at' => now()
]); ]);
foreach ($validated['assigned_users'] as $userId) { foreach ($validated['assigned_users'] as $userId) {
$user = User::find($userId); $user = User::find($userId);
Mail::to($user->email)->send(new TaskAssignedMail($task, $user));
} if (!$user) {
return redirect()->route('admin.tasks')->with('success', 'Users assigned to task successfully.'); Log::error("User with ID {$userId} not found");
continue;
}
dispatch(new SendTaskAssignedMail($task, $user));
dispatch(new SendTaskNotification($task, $user, auth()->user()->name));
} }
}
public function taskList() public function taskList()
{ {
$tasks = Task::with(['project', 'creator', 'assignedUsers', 'files.uploader'])->get(); $tasks = Task::with(['project', 'creator', 'assignedUsers', 'files.uploader'])->get();
return view('admin.task-list', compact('tasks')); return view('admin.task-list', compact('tasks'));
} }
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$task = Task::findOrFail($id); $task = Task::findOrFail($id);
$request->validate([ $request->validate([
'project_id' => 'required|exists:projects,id', 'project_id' => 'required|exists:projects,id',
'title' => 'required|string|max:255|unique:tasks,title,' . $id, 'title' => 'required|string|max:255|unique:tasks,title,' . $id,
'description' => 'required|string', 'description' => 'required|string',
'status' => 'required|string|max:255', 'status' => 'required|string|max:255',
'priority' => 'required|string|max:255', 'priority' => 'required|string|max:255',
'due_date' => 'required|date', 'due_date' => 'required|date',
'assigned_users' => 'nullable|array', 'assigned_users' => 'nullable|array',
'assigned_users.*' => 'exists:users,id', 'assigned_users.*' => 'exists:users,id',
]); ]);
$task->update($request->only(['project_id', 'title', 'description', 'status', 'priority', 'due_date'])); $task->update($request->only(['project_id', 'title', 'description', 'status', 'priority', 'due_date']));
if ($request->has('assigned_users')) {
$task->assignedUsers()->syncWithPivotValues($request->assigned_users, [ if ($request->has('assigned_users')) {
'assigned_by' => auth()->id(), $task->assignedUsers()->syncWithPivotValues($request->assigned_users, [
'created_at' => now(), 'assigned_by' => auth()->id(),
'updated_at' => now() 'created_at' => now(),
]); 'updated_at' => now()
} ]);
return redirect()->route('admin.tasks')->with('success', 'Task updated successfully.');
foreach ($request->assigned_users as $userId) {
$user = User::find($userId);
if (!$user) {
Log::error("User with ID {$userId} not found");
continue;
}
dispatch(new SendTaskAssignedMail($task, $user));
dispatch(new SendTaskNotification($task, $user, auth()->user()->name));
}
}
return redirect()->route('admin.tasks')->with('success', 'Task updated and notifications queued.');
} }
public function delete($id) public function delete($id)
......
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Jobs\UploadedFile;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Models\File; use App\Models\File;
...@@ -30,18 +32,26 @@ public function uploadFile(Request $request, Task $task) ...@@ -30,18 +32,26 @@ public function uploadFile(Request $request, Task $task)
'task_file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xlsx,xls,ppt,pptx,txt|max:10240', 'task_file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xlsx,xls,ppt,pptx,txt|max:10240',
]); ]);
$uploadedFile = $request->file('task_file'); $file = $request->file('task_file');
$path = $uploadedFile->store('task_uploads', 'public'); $tempPath = $file->storeAs('temp', $file->getClientOriginalName());
File::create([ dispatch(new UploadedFile(
'task_id' => $task->id, storage_path("app/{$tempPath}"),
'file_path' => $path, $file->getClientOriginalName(),
'original_name' => $uploadedFile->getClientOriginalName(), $task->id,
'uploaded_by' => auth()->id(), auth()->id()
]); ));
return back()->with('success', 'File is being uploaded in background.');
}
public function notifications()
{
$user = auth()->user();
$user->unreadNotifications->markAsRead();
$notifications = $user->notifications;
return view('user.notifications', compact('notifications'));
}
return back()->with('success', 'File uploaded successfully.');
}
} }
<?php
namespace App\Jobs;
use App\Mail\TaskAssignedMail;
use App\Models\Task;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendTaskAssignedMail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $task;
protected $user;
public function __construct(Task $task, User $user)
{
$this->task=$task;
$this->user=$user;
}
public function handle()
{
Mail::to($this->user->email)->send(new TaskAssignedMail($this->task, $this->user));
}
}
<?php
namespace App\Jobs;
use App\Models\Task;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Notifications\TaskAssignedNotification;
class SendTaskNotification implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $task;
protected $user;
protected $assignedBy;
public function __construct(Task $task, User $user, $assignedBy)
{
$this->task=$task;
$this->user=$user;
$this->assignedBy=$assignedBy;
}
public function handle()
{
$this->user->notify(new TaskAssignedNotification($this->task, $this->assignedBy));
}
}
<?php
namespace App\Jobs;
use App\Models\File;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class UploadedFile implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tempPath, $originalName, $taskId, $uploadedBy;
public function __construct($tempPath, $originalName, $taskId, $uploadedBy) {
$this->tempPath = $tempPath;
$this->originalName = $originalName;
$this->taskId = $taskId;
$this->uploadedBy = $uploadedBy;
}
public function handle() {
$fileContent = file_get_contents($this->tempPath);
$finalPath = 'task_uploads/' . uniqid() . '_' . $this->originalName;
Storage::disk('public')->put($finalPath, $fileContent);
\App\Models\File::create([
'task_id' => $this->taskId,
'file_path' => $finalPath,
'original_name' => $this->originalName,
'uploaded_by' => $this->uploadedBy,
]);
unlink($this->tempPath);
}
}
\ No newline at end of file
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
use HasFactory;
}
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class TaskAssignedNotification extends Notification
{
use Queueable;
public $task;
public $assignedBy;
public function __construct($task, $assignedBy)
{
$this->task = $task;
$this->assignedBy = $assignedBy;
}
public function via($notifiable)
{
return ['database'];
}
public function toDatabase($notifiable)
{
return [
'task_id' => $this->task->id,
'task_title' => $this->task->title,
'assigned_by' => $this->assignedBy,
'message' => "You have been assigned to task: {$this->task->title}"
];
}
}
\ No newline at end of file
...@@ -30,6 +30,26 @@ public function boot(): void ...@@ -30,6 +30,26 @@ public function boot(): void
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
}); });
RateLimiter::for('admin-web',function(Request $request){
$user=$request->user();
if($user && $user->role->name === 'admin'){
return Limit::perMinute(20)->by($user->id)->response(function(){
return response()->json([
'message' => 'Limit exceeded. Please wait before trying again.'
], 429);
});
}
return Limit::perMinute(10)->by(optional($user)->id ? : $request->ip());
});
RateLimiter::for('upload-limit',function(Request $request){
return Limit::perHour(3)->by(optional($request->user())->id ?: $request->ip())->response(function(){
return response()->json([
'message'=>'Upload limit exceeded. Please wait before trying again.'
],429);
});
});
$this->routes(function () { $this->routes(function () {
Route::middleware('api') Route::middleware('api')
->prefix('api') ->prefix('api')
......
...@@ -12,15 +12,12 @@ ...@@ -12,15 +12,12 @@
public function up(): void public function up(): void
{ {
Schema::create('notifications', function (Blueprint $table) { Schema::create('notifications', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('type');
$table->foreignId('task_id')->nullable()->constrained()->onDelete('set null'); $table->morphs('notifiable');
$table->string('message'); $table->text('data');
$table->boolean('is_read')->default(false); $table->timestamp('read_at')->nullable();
$table->timestamps(); $table->timestamps();
$table->index('user_id');
$table->index('task_id');
}); });
} }
......
<?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('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
}
};
...@@ -17,7 +17,8 @@ public function run(): void ...@@ -17,7 +17,8 @@ public function run(): void
'name' => 'admin', 'name' => 'admin',
'email' => 'admin@gmail.com', 'email' => 'admin@gmail.com',
'password' => Hash::make('admin@123'), 'password' => Hash::make('admin@123'),
'role_id' => 1 'role_id' => 1 ,
'designation' => 'Administrator',
]); ]);
} }
} }
.notifications-container {
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
}
.notifications-container h2 {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.notification-card {
background-color: #f9f9f9;
border-left: 4px solid #3490dc;
padding: 15px 20px;
margin-bottom: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.notification-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 6px;
color: #2d3748;
}
.notification-meta {
font-size: 14px;
color: #718096;
display: flex;
justify-content: space-between;
}
.time {
font-style: italic;
}
.no-notifications {
text-align: center;
font-size: 16px;
color: #a0aec0;
}
...@@ -75,6 +75,22 @@ ...@@ -75,6 +75,22 @@
pointer-events: none; pointer-events: none;
} }
.notification-button {
display: inline-block;
background-color: #f59e0b;
color: white;
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 5px;
text-decoration: none;
transition: background-color 0.3s ease;
margin-left: 1rem;
}
.notification-button:hover {
background-color: #d97706;
}
.edit-btn { background-color: #007bff; color: white; } .edit-btn { background-color: #007bff; color: white; }
.save-btn { background-color: #28a745; color: white; } .save-btn { background-color: #28a745; color: white; }
......
...@@ -10,6 +10,7 @@ import '../css/tasklist.css'; ...@@ -10,6 +10,7 @@ import '../css/tasklist.css';
import '../css/userdash.css'; import '../css/userdash.css';
import '../css/assigned-task.css'; import '../css/assigned-task.css';
import '../css/task-list.css'; import '../css/task-list.css';
import '../css/notification.css';
window.Alpine = Alpine; window.Alpine = Alpine;
Alpine.start(); Alpine.start();
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts --> <!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js','resources/css/admindash.css','resources/css/listuser.css','resources/css/projectlist.css','resources/css/tasklist.css','resources/css/userdash.css','resources/css/assigned-task.css','resources/css/task-list.css']) @vite(['resources/css/app.css', 'resources/js/app.js','resources/css/admindash.css','resources/css/listuser.css','resources/css/projectlist.css','resources/css/tasklist.css','resources/css/userdash.css','resources/css/assigned-task.css','resources/css/task-list.css','resources/css/notification.css'])
</head> </head>
<body class="font-sans antialiased"> <body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100 dark:bg-gray-900"> <div class="min-h-screen bg-gray-100 dark:bg-gray-900">
......
<x-app-layout> <x-app-layout>
<x-slot name="header">My Assigned Tasks</x-slot> <x-slot name="header">My Assigned Tasks</x-slot>
<div class="tasks-container"> <div class="tasks-container">
<h1>Tasks Assigned to Me</h1> <h1>Tasks Assigned to Me</h1>
<table class="task-table"> <table class="task-table">
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<div class="dashboard-actions"> <div class="dashboard-actions">
<a href="{{ route('user.tasks') }}" class="dashboard-button">My Assigned Tasks</a> <a href="{{ route('user.tasks') }}" class="dashboard-button">My Assigned Tasks</a>
<a href="{{ route('user.notifications') }}" class="notification-button">View My Notifications</a>
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>
<x-app-layout>
<x-slot name="header">Your Notifications</x-slot>
<div class="notifications-container">
<h2>Recent Notifications</h2>
@forelse ($notifications as $notification)
<div class="notification-card">
<div class="notification-title">{{ $notification->data['task_title'] }}</div>
<div class="notification-meta">
Assigned by <strong>{{ $notification->data['assigned_by'] }}</strong>
<span class="time">{{ \Carbon\Carbon::parse($notification->created_at)->diffForHumans() }}</span>
</div>
</div>
@empty
<p class="no-notifications">You have no notifications.</p>
@endforelse
</div>
</x-app-layout>
...@@ -33,18 +33,18 @@ ...@@ -33,18 +33,18 @@
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
Route::middleware('auth')->prefix('user')->group(function () { Route::middleware('auth')->prefix('user')->group(function () {
Route::get('/dashboard', [UserController::class, 'dashboard'])->name('user.dashboard'); Route::get('/dashboard', [UserController::class, 'dashboard'])->name('user.dashboard');
Route::get('/tasks', [UserController::class, 'assignedTasks'])->name('user.tasks'); Route::get('/tasks', [UserController::class, 'assignedTasks'])->name('user.tasks');
Route::post('/tasks/{task}/upload', [UserController::class, 'uploadFile'])->name('user.upload'); Route::post('/tasks/{task}/upload', [UserController::class, 'uploadFile'])->middleware((['auth','throttle:upload-limit']))->name('user.upload');
Route::get('/notifications', [UserController::class, 'notifications'])->name('user.notifications');
}); });
}); Route::middleware(['auth','throttle:admin-web'])->prefix('admin')->group(function () {
Route::middleware('auth')->prefix('admin')->group(function () {
Route::get('/dashboard', function() { Route::get('/dashboard', function() {
if(Auth::user()->role->name !== 'admin') { if(Auth::user()->role->name !== 'admin') {
return redirect()->route('user.dashboard'); return redirect()->route('user.dashboard');
...@@ -56,16 +56,19 @@ ...@@ -56,16 +56,19 @@
Route::post('/users', [AdminUserController::class, 'store'])->name('admin.users.store'); Route::post('/users', [AdminUserController::class, 'store'])->name('admin.users.store');
Route::put('/users/{user}', [AdminUserController::class, 'update'])->name('admin.users.update'); Route::put('/users/{user}', [AdminUserController::class, 'update'])->name('admin.users.update');
Route::delete('/users/{user}', [AdminUserController::class, 'delete'])->name('admin.users.delete'); Route::delete('/users/{user}', [AdminUserController::class, 'delete'])->name('admin.users.delete');
Route::get('projects', [ProjectController::class, 'index'])->name('admin.projects'); Route::get('projects', [ProjectController::class, 'index'])->name('admin.projects');
Route::post('projects', [ProjectController::class, 'store'])->name('admin.projects.store'); Route::post('projects', [ProjectController::class, 'store'])->name('admin.projects.store');
Route::put('/admin/projects/{id}', [ProjectController::class, 'update'])->name('admin.projects.update'); Route::put('/admin/projects/{id}', [ProjectController::class, 'update'])->name('admin.projects.update');
Route::delete('/admin/projects/{id}', [ProjectController::class, 'delete'])->name('admin.projects.delete'); Route::delete('/admin/projects/{id}', [ProjectController::class, 'delete'])->name('admin.projects.delete');
Route::get('tasks', [TaskController::class, 'index'])->name('admin.tasks'); Route::get('tasks', [TaskController::class, 'index'])->name('admin.tasks');
Route::post('tasks', [TaskController::class, 'store'])->name('admin.tasks.store'); Route::post('tasks', [TaskController::class, 'store'])->name('admin.tasks.store');
Route::post('tasks/{task}/assign', [TaskController::class, 'assignUsers'])->name('admin.tasks.assign'); Route::post('tasks/{task}/assign', [TaskController::class, 'assignUsers'])->name('admin.tasks.assign');
Route::put('/admin/tasks/{id}', [TaskController::class, 'update'])->name('admin.tasks.update'); Route::put('/admin/tasks/{id}', [TaskController::class, 'update'])->name('admin.tasks.update');
Route::delete('/admin/tasks/{id}', [TaskController::class, 'delete'])->name('admin.tasks.delete'); Route::delete('/admin/tasks/{id}', [TaskController::class, 'delete'])->name('admin.tasks.delete');
Route::get('/tasks/list', [TaskController::class, 'taskList'])->name('admin.tasks.list'); Route::get('/tasks/list', [TaskController::class, 'taskList'])->name('admin.tasks.list');
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment