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
|
||||
{
|
||||
//
|
||||
use \Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UserFactory> */
|
||||
use HasFactory, Notifiable, SoftDeletes;
|
||||
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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