Commit 9b946284 by Shaganaz

Implemented notification,api rate limiting and background job processing

parent e226894f
......@@ -8,6 +8,11 @@
use App\Models\User;
use Illuminate\Http\Request;
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
{
......@@ -48,41 +53,69 @@ public function assignUsers(Request $request, Task $task)
'created_at' => now(),
'updated_at' => now()
]);
foreach ($validated['assigned_users'] as $userId) {
$user = User::find($userId);
Mail::to($user->email)->send(new TaskAssignedMail($task, $user));
}
return redirect()->route('admin.tasks')->with('success', 'Users assigned to task successfully.');
if (!$user) {
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()
{
$tasks = Task::with(['project', 'creator', 'assignedUsers', 'files.uploader'])->get();
return view('admin.task-list', compact('tasks'));
}
public function update(Request $request, $id)
{
$task = Task::findOrFail($id);
{
$task = Task::findOrFail($id);
$request->validate([
'project_id' => 'required|exists:projects,id',
'title' => 'required|string|max:255|unique:tasks,title,' . $id,
'description' => 'required|string',
'status' => 'required|string|max:255',
'priority' => 'required|string|max:255',
'due_date' => 'required|date',
'assigned_users' => 'nullable|array',
'assigned_users.*' => 'exists:users,id',
]);
$task->update($request->only(['project_id', 'title', 'description', 'status', 'priority', 'due_date']));
if ($request->has('assigned_users')) {
$task->assignedUsers()->syncWithPivotValues($request->assigned_users, [
'assigned_by' => auth()->id(),
'created_at' => now(),
'updated_at' => now()
]);
}
return redirect()->route('admin.tasks')->with('success', 'Task updated successfully.');
$request->validate([
'project_id' => 'required|exists:projects,id',
'title' => 'required|string|max:255|unique:tasks,title,' . $id,
'description' => 'required|string',
'status' => 'required|string|max:255',
'priority' => 'required|string|max:255',
'due_date' => 'required|date',
'assigned_users' => 'nullable|array',
'assigned_users.*' => 'exists:users,id',
]);
$task->update($request->only(['project_id', 'title', 'description', 'status', 'priority', 'due_date']));
if ($request->has('assigned_users')) {
$task->assignedUsers()->syncWithPivotValues($request->assigned_users, [
'assigned_by' => auth()->id(),
'created_at' => now(),
'updated_at' => now()
]);
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)
......
<?php
namespace App\Http\Controllers;
use App\Jobs\UploadedFile;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use App\Models\File;
......@@ -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',
]);
$uploadedFile = $request->file('task_file');
$path = $uploadedFile->store('task_uploads', 'public');
$file = $request->file('task_file');
$tempPath = $file->storeAs('temp', $file->getClientOriginalName());
File::create([
'task_id' => $task->id,
'file_path' => $path,
'original_name' => $uploadedFile->getClientOriginalName(),
'uploaded_by' => auth()->id(),
]);
dispatch(new UploadedFile(
storage_path("app/{$tempPath}"),
$file->getClientOriginalName(),
$task->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
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 () {
Route::middleware('api')
->prefix('api')
......
......@@ -12,15 +12,12 @@
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('task_id')->nullable()->constrained()->onDelete('set null');
$table->string('message');
$table->boolean('is_read')->default(false);
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$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
'name' => 'admin',
'email' => 'admin@gmail.com',
'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 @@
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; }
.save-btn { background-color: #28a745; color: white; }
......
......@@ -10,6 +10,7 @@ import '../css/tasklist.css';
import '../css/userdash.css';
import '../css/assigned-task.css';
import '../css/task-list.css';
import '../css/notification.css';
window.Alpine = Alpine;
Alpine.start();
......@@ -12,7 +12,7 @@
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- 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>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
......
<x-app-layout>
<x-slot name="header">My Assigned Tasks</x-slot>
<div class="tasks-container">
<h1>Tasks Assigned to Me</h1>
<table class="task-table">
......
......@@ -14,6 +14,7 @@
<div class="dashboard-actions">
<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>
</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 @@
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
Route::middleware('auth')->prefix('user')->group(function () {
Route::get('/dashboard', [UserController::class, 'dashboard'])->name('user.dashboard');
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')->prefix('admin')->group(function () {
Route::middleware(['auth','throttle:admin-web'])->prefix('admin')->group(function () {
Route::get('/dashboard', function() {
if(Auth::user()->role->name !== 'admin') {
return redirect()->route('user.dashboard');
......@@ -56,16 +56,19 @@
Route::post('/users', [AdminUserController::class, 'store'])->name('admin.users.store');
Route::put('/users/{user}', [AdminUserController::class, 'update'])->name('admin.users.update');
Route::delete('/users/{user}', [AdminUserController::class, 'delete'])->name('admin.users.delete');
Route::get('projects', [ProjectController::class, 'index'])->name('admin.projects');
Route::post('projects', [ProjectController::class, 'store'])->name('admin.projects.store');
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::get('tasks', [TaskController::class, 'index'])->name('admin.tasks');
Route::post('tasks', [TaskController::class, 'store'])->name('admin.tasks.store');
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::delete('/admin/tasks/{id}', [TaskController::class, 'delete'])->name('admin.tasks.delete');
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