feat: harden admin auth and ticket workflows
- add rotating admin refresh tokens and password gates\n- expand ticket galleries, exports, and client resources\n- improve IP allocation auditing and dashboard data\n- enforce encrypted client access tokens on public routes
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Ticket;
|
||||
|
||||
use App\Exports\BrandedReportWriter;
|
||||
use App\Exports\TicketsExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Events\TicketReplyCreated;
|
||||
use App\Http\Resources\Api\Ticket\TicketResource;
|
||||
@@ -9,7 +11,10 @@ use App\Http\Resources\Api\Ticket\TicketReplyResource;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Ticket;
|
||||
use App\Repositories\TicketReply\TicketReplyRepositoryInterface;
|
||||
use App\Support\TicketGallery;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
@@ -17,29 +22,19 @@ class TicketController extends Controller
|
||||
protected TicketReplyRepositoryInterface $ticketReplyRepository,
|
||||
) {}
|
||||
|
||||
private const FILTER_RULES = [
|
||||
'status' => ['nullable', 'array'],
|
||||
'status.*' => ['string', 'in:open,pending,resolved,closed'],
|
||||
'room' => ['nullable', 'string', 'max:50'],
|
||||
'customer_id' => ['nullable', 'integer'],
|
||||
'date_from' => ['nullable', 'date'],
|
||||
'date_to' => ['nullable', 'date'],
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$filters = $request->validate([
|
||||
'status' => ['nullable', 'array'],
|
||||
'status.*' => ['string', 'in:open,pending,resolved,closed'],
|
||||
'room' => ['nullable', 'string', 'max:50'],
|
||||
'customer_id' => ['nullable', 'integer'],
|
||||
'date_from' => ['nullable', 'date'],
|
||||
'date_to' => ['nullable', 'date'],
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$query = Ticket::query()
|
||||
->with(['customer'])
|
||||
->latest('id')
|
||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->whereIn('status', $status))
|
||||
->when($filters['room'] ?? null, fn ($q, $room) => $q->where('room', $room))
|
||||
->when($filters['customer_id'] ?? null, fn ($q, $id) => $q->where('customer_id', $id))
|
||||
->when($filters['date_from'] ?? null, fn ($q, $from) => $q->whereDate('created_at', '>=', $from))
|
||||
->when($filters['date_to'] ?? null, fn ($q, $to) => $q->whereDate('created_at', '<=', $to))
|
||||
->when($filters['search'] ?? null, fn ($q, $search) => $q->where(
|
||||
fn ($q) => $q->where('subject', 'like', "%{$search}%")->orWhere('ticket_num', 'like', "%{$search}%")
|
||||
));
|
||||
$query = $this->filteredQuery($request->validate(self::FILTER_RULES))->with(['customer']);
|
||||
|
||||
$paginated = $query->paginate($request->input('per_page', 15));
|
||||
|
||||
@@ -58,6 +53,39 @@ class TicketController extends Controller
|
||||
/**
|
||||
* Show a ticket with its replies and customer info.
|
||||
*/
|
||||
public function export(Request $request, BrandedReportWriter $writer): BinaryFileResponse
|
||||
{
|
||||
$filters = $request->validate(self::FILTER_RULES + ['filter' => ['nullable', 'string', 'max:255']]);
|
||||
$filters['search'] ??= $filters['filter'] ?? null;
|
||||
|
||||
$query = $this->filteredQuery($filters)->with(['customer'])->withCount('reply');
|
||||
|
||||
return $writer->download(new TicketsExport($query), 'ticket-' . now()->format('Y-m-d-His') . '.xlsx');
|
||||
}
|
||||
|
||||
private function filteredQuery(array $filters): Builder
|
||||
{
|
||||
return Ticket::query()
|
||||
->latest('id')
|
||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->whereIn('status', $status))
|
||||
->when($filters['room'] ?? null, fn ($q, $room) => $q->where('room', $room))
|
||||
->when($filters['customer_id'] ?? null, fn ($q, $id) => $q->where('customer_id', $id))
|
||||
->when($filters['date_from'] ?? null, fn ($q, $from) => $q->whereDate('created_at', '>=', $from))
|
||||
->when($filters['date_to'] ?? null, fn ($q, $to) => $q->whereDate('created_at', '<=', $to))
|
||||
->when($filters['search'] ?? null, function ($q, $search) {
|
||||
$keyword = '%' . addcslashes($search, '%_\\') . '%';
|
||||
$q->where(fn ($q) => $q->where('subject', 'like', $keyword)->orWhere('ticket_num', 'like', $keyword));
|
||||
});
|
||||
}
|
||||
|
||||
public function pendingCount()
|
||||
{
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => ['count' => Ticket::whereIn('status', Ticket::UNRESOLVED_STATUSES)->count()],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(string $ticket_num)
|
||||
{
|
||||
$ticket = Ticket::query()
|
||||
@@ -74,7 +102,7 @@ class TicketController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => new TicketResource($ticket),
|
||||
'data' => [...(new TicketResource($ticket))->resolve(), 'status_history' => $ticket->statusHistory()],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -162,4 +190,9 @@ class TicketController extends Controller
|
||||
'data' => new TicketReplyResource($reply),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function image(string $ticket, int $index)
|
||||
{
|
||||
return TicketGallery::response(Ticket::where('ticket_num', $ticket)->firstOrFail(), $index);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user