Deploy Ladill QR Plus / deploy (push) Successful in 29s
Exposes /api/v1/qr-codes with analytics, team account header support, and qr:read/qr:write token abilities. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.1 KiB
PHP
154 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\QrCode;
|
|
use App\Services\Qr\QrAnalyticsService;
|
|
use App\Services\Qr\QrCodeManagerService;
|
|
use App\Support\Qr\QrTypeCatalog;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use RuntimeException;
|
|
|
|
class QrCodeController extends Controller
|
|
{
|
|
public function __construct(
|
|
private QrCodeManagerService $manager,
|
|
private QrAnalyticsService $analytics,
|
|
) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$codes = $this->accountQuery()
|
|
->latest()
|
|
->get()
|
|
->map(fn (QrCode $code) => $this->present($code));
|
|
|
|
return response()->json(['data' => $codes]);
|
|
}
|
|
|
|
public function show(QrCode $qrCode): JsonResponse
|
|
{
|
|
$this->ensurePlusCode($qrCode);
|
|
$this->authorize('view', $qrCode);
|
|
|
|
return response()->json(['data' => $this->present($qrCode, detailed: true)]);
|
|
}
|
|
|
|
public function analytics(QrCode $qrCode): JsonResponse
|
|
{
|
|
$this->ensurePlusCode($qrCode);
|
|
$this->authorize('view', $qrCode);
|
|
|
|
return response()->json([
|
|
'data' => [
|
|
'summary' => $this->analytics->summaryFor($qrCode),
|
|
'daily_scans' => $this->analytics->dailyScans($qrCode, 30)->values(),
|
|
'devices' => $this->analytics->breakdown($qrCode, 'device_type'),
|
|
'browsers' => $this->analytics->breakdown($qrCode, 'browser'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
abort_unless($request->user()->tokenCan('qr:write'), 403, 'Token requires qr:write ability.');
|
|
|
|
$validated = $request->validate([
|
|
'label' => ['required', 'string', 'max:120'],
|
|
'type' => ['required', 'in:'.implode(',', QrTypeCatalog::keys())],
|
|
'destination_url' => ['nullable', 'url', 'max:2048'],
|
|
'custom_short_code' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$/', 'unique:qr_codes,short_code'],
|
|
'is_active' => ['sometimes', 'boolean'],
|
|
]);
|
|
|
|
if ($validated['type'] === QrCode::TYPE_DOCUMENT) {
|
|
return response()->json([
|
|
'message' => 'PDF QR codes must be created in the QR Plus app (file upload required).',
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$qrCode = $this->manager->create(ladill_account(), array_merge(
|
|
$request->all(),
|
|
$validated,
|
|
));
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['data' => $this->present($qrCode->fresh(), detailed: true)], 201);
|
|
}
|
|
|
|
public function update(Request $request, QrCode $qrCode): JsonResponse
|
|
{
|
|
abort_unless($request->user()->tokenCan('qr:write'), 403, 'Token requires qr:write ability.');
|
|
|
|
$this->ensurePlusCode($qrCode);
|
|
$this->authorize('update', $qrCode);
|
|
|
|
$request->validate([
|
|
'label' => ['sometimes', 'string', 'max:120'],
|
|
'destination_url' => ['nullable', 'url', 'max:2048'],
|
|
'is_active' => ['sometimes', 'boolean'],
|
|
]);
|
|
|
|
if ($qrCode->isDocumentType() && $request->hasFile('document')) {
|
|
return response()->json([
|
|
'message' => 'Replace PDF files in the QR Plus app.',
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$updated = $this->manager->update($qrCode, $request->all());
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['data' => $this->present($updated->fresh(), detailed: true)]);
|
|
}
|
|
|
|
/** @return \Illuminate\Database\Eloquent\Builder<QrCode> */
|
|
private function accountQuery()
|
|
{
|
|
return QrCode::query()
|
|
->where('user_id', ladill_account()->id)
|
|
->whereIn('type', QrTypeCatalog::plusTypes());
|
|
}
|
|
|
|
private function ensurePlusCode(QrCode $qrCode): void
|
|
{
|
|
abort_unless(
|
|
$qrCode->user_id === ladill_account()->id && QrTypeCatalog::isValid($qrCode->type),
|
|
404,
|
|
);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function present(QrCode $qrCode, bool $detailed = false): array
|
|
{
|
|
$data = [
|
|
'id' => $qrCode->id,
|
|
'label' => $qrCode->label,
|
|
'type' => $qrCode->type,
|
|
'type_label' => $qrCode->typeLabel(),
|
|
'short_code' => $qrCode->short_code,
|
|
'public_url' => $qrCode->publicUrl(),
|
|
'is_active' => $qrCode->is_active,
|
|
'scans_total' => $qrCode->scans_total,
|
|
'unique_scans_total' => $qrCode->unique_scans_total,
|
|
'last_scanned_at' => $qrCode->last_scanned_at?->toIso8601String(),
|
|
'created_at' => $qrCode->created_at?->toIso8601String(),
|
|
'updated_at' => $qrCode->updated_at?->toIso8601String(),
|
|
];
|
|
|
|
if ($detailed) {
|
|
$data['destination_url'] = $qrCode->destination_url;
|
|
$data['content'] = $qrCode->content();
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|