feat(api): mobile API Milestones 5+6 — media upload, sync_logs idempotency, OpenAPI
Milestone 5 (media): - POST /api/v1/media — multipart upload, attaches to feature/issue/project/ phase/layer, idempotent by uuid, authz member + 'upload media'. Added uuid+client_updated_at to media. - Bundle now includes a 'media' array (URLs) for the project's project/feature/ issue attachments (delta-aware). Milestone 6 (hardening + docs): - sync_logs table/model: every applied op is logged; /sync short-circuits on a repeated op uuid -> 'duplicate' (true idempotency for updates too, not just creates). - Rate limiting on login (10/min), sync (60/min), media (120/min). - docs/openapi.yaml: OpenAPI 3 contract for the mobile team. Tests: 18 passing (added media upload idempotency + sync_logs idempotency). The mobile API (Milestones 1-6) is now feature-complete on the webapp side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Models\Issue;
|
||||
use App\Models\Phase;
|
||||
use App\Models\ProgressUpdate;
|
||||
use App\Models\Project;
|
||||
use App\Models\SyncLog;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -49,8 +50,15 @@ class SyncController extends Controller
|
||||
{
|
||||
$uuid = $op['uuid'];
|
||||
|
||||
// Op-level idempotency: if this operation was already applied, replay its result.
|
||||
$prior = SyncLog::where('op_uuid', $uuid)
|
||||
->where('entity', $op['entity'])->where('op', $op['op'])->first();
|
||||
if ($prior) {
|
||||
return ['uuid' => $uuid, 'status' => 'duplicate', 'server_id' => $prior->server_id];
|
||||
}
|
||||
|
||||
try {
|
||||
return match ($op['entity'] . '.' . $op['op']) {
|
||||
$result = match ($op['entity'] . '.' . $op['op']) {
|
||||
'progress_update.create' => $this->progressUpdateCreate($user, $uuid, $op),
|
||||
'inspection.create' => $this->inspectionCreate($user, $uuid, $op),
|
||||
'issue.create' => $this->issueCreate($user, $uuid, $op),
|
||||
@@ -59,8 +67,22 @@ class SyncController extends Controller
|
||||
default => $this->error($uuid, 'unsupported entity/op: ' . $op['entity'] . '.' . $op['op']),
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($uuid, $e->getMessage());
|
||||
$result = $this->error($uuid, $e->getMessage());
|
||||
}
|
||||
|
||||
// Record only terminal successes so conflicts/errors can be safely retried.
|
||||
if ($result['status'] === 'applied') {
|
||||
SyncLog::create([
|
||||
'user_id' => $user->id,
|
||||
'op_uuid' => $uuid,
|
||||
'entity' => $op['entity'],
|
||||
'op' => $op['op'],
|
||||
'status' => 'applied',
|
||||
'server_id' => $result['server_id'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// ── progress_update.create ───────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user