feat(api): REST API v1 — links CRUD with Sanctum auth
Adds /api/v1/workspaces/{ulid}/links endpoints (index, store, destroy)
protected by Sanctum token auth; adds HasApiTokens to User model and
AuthorizesRequests to base Controller; wires routes/api.php into bootstrap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
parent
8f1398380f
commit
1375519cdb
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Api\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class LinkResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray($request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Domains\Api\Resources\LinkResource;
|
||||||
|
use App\Domains\Link\Actions\CreateLink;
|
||||||
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
|
||||||
|
class LinkController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request, string $workspaceUlid): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
$workspace = Workspace::where('ulid', $workspaceUlid)->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class WorkspaceController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request): \Illuminate\Http\JsonResponse
|
||||||
|
{
|
||||||
|
$workspaces = $request->user()
|
||||||
|
->workspaces()
|
||||||
|
->get(['id', 'ulid', 'name', 'slug']);
|
||||||
|
|
||||||
|
return response()->json(['data' => $workspaces]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,5 +4,5 @@ namespace App\Http\Controllers;
|
||||||
|
|
||||||
abstract class Controller
|
abstract class Controller
|
||||||
{
|
{
|
||||||
//
|
use \Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable, SoftDeletes;
|
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = ['id'];
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
channels: __DIR__.'/../routes/channels.php',
|
channels: __DIR__.'/../routes/channels.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V1\LinkController;
|
||||||
|
use App\Http\Controllers\Api\V1\WorkspaceController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::prefix('v1')->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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
it('lists links for authenticated workspace', function () {
|
||||||
|
$user = User::factory()->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');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue