diff --git a/app/Domains/Api/Resources/LinkResource.php b/app/Domains/Api/Resources/LinkResource.php new file mode 100644 index 0000000..2abe0af --- /dev/null +++ b/app/Domains/Api/Resources/LinkResource.php @@ -0,0 +1,22 @@ + $this->ulid, + 'slug' => $this->slug, + 'target_url' => $this->target_url, + 'short_url' => 'https://' . config('app.short_link_domain', 'nimu.li') . '/' . $this->slug, + 'status' => $this->status, + 'title' => $this->title, + 'tags' => $this->tags ?? [], + 'created_at' => $this->created_at?->toISOString(), + ]; + } +} diff --git a/app/Http/Controllers/Api/V1/LinkController.php b/app/Http/Controllers/Api/V1/LinkController.php new file mode 100644 index 0000000..86b0b97 --- /dev/null +++ b/app/Http/Controllers/Api/V1/LinkController.php @@ -0,0 +1,57 @@ +firstOrFail(); + $this->authorize('view', $workspace); + + $links = $workspace->links()->latest()->paginate(50); + + return LinkResource::collection($links); + } + + public function store(Request $request, string $workspaceUlid, CreateLink $action): \Illuminate\Http\JsonResponse + { + $workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail(); + $this->authorize('update', $workspace); + + $validated = $request->validate([ + 'target_url' => 'required|url|max:2048', + 'slug' => 'nullable|string|max:64|alpha_dash', + 'title' => 'nullable|string|max:255', + 'tags' => 'nullable|array', + ]); + + $link = $action->handle($workspace, $request->user(), $validated); + + return (new LinkResource($link)) + ->response() + ->setStatusCode(201); + } + + public function destroy(string $workspaceUlid, string $linkUlid): \Illuminate\Http\Response + { + $workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail(); + $this->authorize('update', $workspace); + + $link = Link::where('ulid', $linkUlid) + ->where('workspace_id', $workspace->id) + ->firstOrFail(); + + $link->delete(); + + return response()->noContent(); + } +} diff --git a/app/Http/Controllers/Api/V1/WorkspaceController.php b/app/Http/Controllers/Api/V1/WorkspaceController.php new file mode 100644 index 0000000..9b8041c --- /dev/null +++ b/app/Http/Controllers/Api/V1/WorkspaceController.php @@ -0,0 +1,19 @@ +user() + ->workspaces() + ->get(['id', 'ulid', 'name', 'slug']); + + return response()->json(['data' => $workspaces]); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..4a02245 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,5 +4,5 @@ namespace App\Http\Controllers; abstract class Controller { - // + use \Illuminate\Foundation\Auth\Access\AuthorizesRequests; } diff --git a/app/Models/User.php b/app/Models/User.php index 6f3d3eb..37463ec 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,11 +10,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory */ - use HasFactory, Notifiable, SoftDeletes; + use HasApiTokens, HasFactory, Notifiable, SoftDeletes; protected $guarded = ['id']; diff --git a/bootstrap/app.php b/bootstrap/app.php index 76201f0..0039474 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', channels: __DIR__.'/../routes/channels.php', health: '/up', diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..31d1170 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,15 @@ +middleware(['auth:sanctum', 'throttle:60,1'])->group(function () { + Route::get('workspaces', [WorkspaceController::class, 'index']); + + Route::prefix('workspaces/{workspaceUlid}')->group(function () { + Route::get('links', [LinkController::class, 'index']); + Route::post('links', [LinkController::class, 'store']); + Route::delete('links/{linkUlid}', [LinkController::class, 'destroy']); + }); +}); diff --git a/tests/Feature/Api/V1/LinkApiTest.php b/tests/Feature/Api/V1/LinkApiTest.php new file mode 100644 index 0000000..6209d8f --- /dev/null +++ b/tests/Feature/Api/V1/LinkApiTest.php @@ -0,0 +1,33 @@ +create(); + $workspace = (new \App\Domains\Workspace\Actions\CreateWorkspace)->handle($user, ['name' => 'Test']); + Link::factory()->count(3)->create(['workspace_id' => $workspace->id]); + + $token = $user->createToken('api-test')->plainTextToken; + + $response = $this->withToken($token) + ->getJson("/api/v1/workspaces/{$workspace->ulid}/links"); + + $response->assertOk() + ->assertJsonCount(3, 'data'); +}); + +it('creates link via API', function () { + $user = User::factory()->create(); + $workspace = (new \App\Domains\Workspace\Actions\CreateWorkspace)->handle($user, ['name' => 'Test']); + $token = $user->createToken('api-test')->plainTextToken; + + $response = $this->withToken($token) + ->postJson("/api/v1/workspaces/{$workspace->ulid}/links", [ + 'target_url' => 'https://example.com', + ]); + + $response->assertCreated() + ->assertJsonPath('data.target_url', 'https://example.com'); +});