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:
2026-06-18 10:23:50 +02:00
parent 9d2b63c8f4
commit 14758136b6
10 changed files with 490 additions and 3 deletions
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('media', function (Blueprint $table) {
if (! Schema::hasColumn('media', 'uuid')) {
$table->uuid('uuid')->nullable()->unique()->after('id');
}
if (! Schema::hasColumn('media', 'client_updated_at')) {
$table->timestamp('client_updated_at')->nullable();
}
});
}
public function down(): void
{
Schema::table('media', function (Blueprint $table) {
foreach (['uuid', 'client_updated_at'] as $col) {
if (Schema::hasColumn('media', $col)) {
$table->dropColumn($col);
}
}
});
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sync_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->uuid('op_uuid')->index(); // idempotency key of the operation
$table->string('entity');
$table->string('op');
$table->string('status'); // applied | duplicate | conflict | error
$table->unsignedBigInteger('server_id')->nullable();
$table->text('error')->nullable();
$table->timestamps();
// One processed result per (entity, op, op_uuid).
$table->unique(['entity', 'op', 'op_uuid']);
});
}
public function down(): void
{
Schema::dropIfExists('sync_logs');
}
};