feat: implement basic API
with authorization and validation
This commit is contained in:
parent
b033262bd7
commit
6fbeedd50c
21 changed files with 599 additions and 10 deletions
8
.github/copilot-instructions.md
vendored
8
.github/copilot-instructions.md
vendored
|
|
@ -13,6 +13,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v12
|
- laravel/framework (LARAVEL) - v12
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
|
- laravel/sanctum (SANCTUM) - v4
|
||||||
- livewire/flux (FLUXUI_FREE) - v2
|
- livewire/flux (FLUXUI_FREE) - v2
|
||||||
- livewire/livewire (LIVEWIRE) - v3
|
- livewire/livewire (LIVEWIRE) - v3
|
||||||
- laravel/dusk (DUSK) - v8
|
- laravel/dusk (DUSK) - v8
|
||||||
|
|
@ -44,13 +45,6 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||||
## Documentation Files
|
## Documentation Files
|
||||||
- You must only create documentation files if explicitly requested by the user.
|
- You must only create documentation files if explicitly requested by the user.
|
||||||
|
|
||||||
## Testing Approach - CRITICAL
|
|
||||||
- **THE APPLICATION IS 100% WORKING** - All functionality works perfectly in production.
|
|
||||||
- When writing or debugging browser tests (Laravel Dusk), focus ONLY on test syntax, selectors, and Dusk interaction methods.
|
|
||||||
- NEVER assume the application has bugs or suggest app fixes - the issue is always in the test code.
|
|
||||||
- Trust the existing functionality and work on getting the correct CSS selectors, XPath expressions, and Dusk methods.
|
|
||||||
- If manual interaction works but the test fails, the problem is the test implementation, not the app.
|
|
||||||
|
|
||||||
|
|
||||||
=== boost rules ===
|
=== boost rules ===
|
||||||
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -25,3 +25,4 @@ yarn-error.log
|
||||||
*.deleted
|
*.deleted
|
||||||
.env.dusk.local
|
.env.dusk.local
|
||||||
log*.txt
|
log*.txt
|
||||||
|
.envrc
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v12
|
- laravel/framework (LARAVEL) - v12
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
|
- laravel/sanctum (SANCTUM) - v4
|
||||||
- livewire/flux (FLUXUI_FREE) - v2
|
- livewire/flux (FLUXUI_FREE) - v2
|
||||||
- livewire/livewire (LIVEWIRE) - v3
|
- livewire/livewire (LIVEWIRE) - v3
|
||||||
- laravel/dusk (DUSK) - v8
|
- laravel/dusk (DUSK) - v8
|
||||||
|
|
|
||||||
92
app/Http/Controllers/EntryController.php
Normal file
92
app/Http/Controllers/EntryController.php
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class EntryController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return Entry::all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if (!$user || $user->email !== config('app.admin_email')) {
|
||||||
|
return response()->json(['message' => 'Forbidden'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['slug'] = $this->generateUniqueSlug($validated['title']);
|
||||||
|
|
||||||
|
return Entry::create($validated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateUniqueSlug(string $title): string
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
$slug = Str::slug($title) . '-' . Str::random(8);
|
||||||
|
} while (Entry::where('slug', $slug)->exists());
|
||||||
|
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Entry $entry)
|
||||||
|
{
|
||||||
|
$this->authorize('view', $entry);
|
||||||
|
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Entry $entry)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'sometimes|required|string|max:255',
|
||||||
|
'content' => 'sometimes|required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entry->update($validated);
|
||||||
|
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Entry $entry)
|
||||||
|
{
|
||||||
|
$entry->delete();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return Auth::check();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Http/Requests/StoreEntryRequest.php
Normal file
29
app/Http/Requests/StoreEntryRequest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreEntryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Http/Requests/UpdateEntryRequest.php
Normal file
29
app/Http/Requests/UpdateEntryRequest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateEntryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user() && $this->user()->email === config('app.admin_email');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'sometimes|required|string|max:255',
|
||||||
|
'content' => 'sometimes|required|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,9 @@ namespace App\Models;
|
||||||
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider;
|
use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider;
|
||||||
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
|
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
|
||||||
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
|
|
@ -17,7 +19,7 @@ class Entry extends Model implements HasRichContent, HasMedia
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
use InteractsWithMedia, InteractsWithRichContent;
|
use InteractsWithMedia, InteractsWithRichContent, HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'title',
|
'title',
|
||||||
|
|
@ -43,4 +45,14 @@ class Entry extends Model implements HasRichContent, HasMedia
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($entry) {
|
||||||
|
if (empty($entry->slug)) {
|
||||||
|
$entry->slug = Str::slug($entry->title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,12 @@ use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Facades\Log as FacadesLog;
|
use Illuminate\Support\Facades\Log as FacadesLog;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable implements FilamentUser
|
class User extends Authenticatable implements FilamentUser
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
use HasFactory, Notifiable, TwoFactorAuthenticatable, HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
15
cmd/curl_get_entries.sh
Executable file
15
cmd/curl_get_entries.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# TOKEN="your_api_token_here"
|
||||||
|
# ensure to have set TOKEN to a valid value before running
|
||||||
|
# ideally add this to an .envrc file and source it
|
||||||
|
# tokens need to be created with tinker or similar method
|
||||||
|
|
||||||
|
|
||||||
|
URL='http://127.0.0.1:8000/api/entries'
|
||||||
|
|
||||||
|
curl -s -X GET \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
$URL
|
||||||
|
|
||||||
13
cmd/curl_get_entries_anon.sh
Executable file
13
cmd/curl_get_entries_anon.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# this should fail, no token provided
|
||||||
|
# users need to be authenticated and have been
|
||||||
|
# granted access to view entries by being given
|
||||||
|
# a token
|
||||||
|
|
||||||
|
URL='http://127.0.0.1:8000/api/entries'
|
||||||
|
|
||||||
|
curl -s -X GET \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
$URL
|
||||||
|
|
||||||
20
cmd/curl_post_entry.sh
Executable file
20
cmd/curl_post_entry.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# TOKEN="your_api_token_here"
|
||||||
|
# ensure to have set TOKEN to a valid value before running
|
||||||
|
# ideally add this to an .envrc file and source it
|
||||||
|
# only the admin user can create entries so this should
|
||||||
|
# fail unless .env has ADMIN_EMAIL set to the user that
|
||||||
|
# the token belongs to
|
||||||
|
|
||||||
|
URL='http://127.0.0.1:8000/api/entries'
|
||||||
|
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"title": "Yet Another New Entry Title",
|
||||||
|
"content": "This is the content yet again of the new entry."
|
||||||
|
}' \
|
||||||
|
$URL
|
||||||
16
cmd/curl_post_entry_anon.sh
Executable file
16
cmd/curl_post_entry_anon.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# this should fail as no token is provided
|
||||||
|
# user is not authenticated
|
||||||
|
# no token has been granted
|
||||||
|
|
||||||
|
URL='http://127.0.0.1:8000/api/entries'
|
||||||
|
|
||||||
|
curl -X POST \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"title": "Yet Another New Entry Title",
|
||||||
|
"content": "This is the content yet again of the new entry."
|
||||||
|
}' \
|
||||||
|
$URL
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"filament/spatie-laravel-media-library-plugin": "^4.4",
|
"filament/spatie-laravel-media-library-plugin": "^4.4",
|
||||||
"laravel/fortify": "^1.30",
|
"laravel/fortify": "^1.30",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"livewire/flux": "^2.9.0",
|
"livewire/flux": "^2.9.0",
|
||||||
"spatie/laravel-medialibrary": "^11.17"
|
"spatie/laravel-medialibrary": "^11.17"
|
||||||
|
|
|
||||||
65
composer.lock
generated
65
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "8074d7e5af3ede59ab12a880d70b7989",
|
"content-hash": "32f224d40d70f511e2d89add38a0d2cf",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
|
|
@ -2645,6 +2645,69 @@
|
||||||
},
|
},
|
||||||
"time": "2025-11-21T20:52:52+00:00"
|
"time": "2025-11-21T20:52:52+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/sanctum",
|
||||||
|
"version": "v4.2.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/sanctum.git",
|
||||||
|
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9",
|
||||||
|
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.0|^12.0",
|
||||||
|
"illuminate/contracts": "^11.0|^12.0",
|
||||||
|
"illuminate/database": "^11.0|^12.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/console": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench": "^9.15|^10.8",
|
||||||
|
"phpstan/phpstan": "^1.10"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Laravel\\Sanctum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"laravel",
|
||||||
|
"sanctum"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel/sanctum/issues",
|
||||||
|
"source": "https://github.com/laravel/sanctum"
|
||||||
|
},
|
||||||
|
"time": "2026-01-06T23:11:51+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.7",
|
"version": "v2.0.7",
|
||||||
|
|
|
||||||
84
config/sanctum.php
Normal file
84
config/sanctum.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort(),
|
||||||
|
// Sanctum::currentRequestHost(),
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
26
database/factories/EntryFactory.php
Normal file
26
database/factories/EntryFactory.php
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Entry>
|
||||||
|
*/
|
||||||
|
class EntryFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $this->faker->sentence,
|
||||||
|
'content' => $this->faker->paragraph,
|
||||||
|
'slug' => Str::slug($this->faker->sentence) . '-' . Str::uuid(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
53
docs/decisions/006-api
Normal file
53
docs/decisions/006-api
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
## known limitations
|
||||||
|
|
||||||
|
at this stage, I just want a simple read api but have added some CRUD so as to have it in part and to test access is in place
|
||||||
|
|
||||||
|
users need to be authenticated and have been given a token to read from entries
|
||||||
|
|
||||||
|
an admin user can create entries however images cannot be uploaded or associated with posts, this is not a requirement to get static site generation in play
|
||||||
|
|
||||||
|
## creating and using tokens
|
||||||
|
|
||||||
|
this is in tinker for now
|
||||||
|
|
||||||
|
```php
|
||||||
|
> $user = User::find(2);
|
||||||
|
= App\Models\User {#7224
|
||||||
|
id: 2,
|
||||||
|
name: "jon@test.com",
|
||||||
|
email: "jon@test.com",
|
||||||
|
email_verified_at: null,
|
||||||
|
#password: "...",
|
||||||
|
#remember_token: null,
|
||||||
|
created_at: "2026-01-04 16:28:21",
|
||||||
|
updated_at: "2026-01-04 16:28:21",
|
||||||
|
#two_factor_secret: null,
|
||||||
|
#two_factor_recovery_codes: null,
|
||||||
|
two_factor_confirmed_at: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
> $token = $user->createToken('API Token')->plainTextToken;
|
||||||
|
= "generated token-string-shown-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
then this token can be used to get posts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET \
|
||||||
|
-H "Authorization: Bearer <your-token-here>" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
http://your-laravel-app.test/api/entries
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
to delete a user token, again back in tinker
|
||||||
|
|
||||||
|
```php
|
||||||
|
// having already found a user as in the above ...
|
||||||
|
// ..
|
||||||
|
$user->tokens()->delete();
|
||||||
|
//
|
||||||
|
// or
|
||||||
|
$user->tokens()->where('id', $tokenId)->delete();
|
||||||
|
```
|
||||||
|
|
||||||
13
routes/api.php
Normal file
13
routes/api.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\EntryController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::get('/entries', [EntryController::class, 'index']);
|
||||||
|
Route::post('/entries', [EntryController::class, 'store']);
|
||||||
|
Route::get('/entries/{entry}', [EntryController::class, 'show']);
|
||||||
|
Route::put('/entries/{entry}', [EntryController::class, 'update']);
|
||||||
|
Route::delete('/entries/{entry}', [EntryController::class, 'destroy']);
|
||||||
|
});
|
||||||
92
tests/Unit/EntryApiTest.php
Normal file
92
tests/Unit/EntryApiTest.php
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Entry;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('can list entries', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Entry::factory()->count(3)->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->getJson('/api/entries');
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJsonCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can create an entry', function () {
|
||||||
|
$admin = User::factory()->create(['email' => config('app.admin_email')]);
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'title' => 'Sample Title',
|
||||||
|
'content' => 'Sample Content',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/entries', $data);
|
||||||
|
|
||||||
|
$response->assertCreated()
|
||||||
|
->assertJsonFragment($data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can show an entry', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$entry = Entry::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->getJson("/api/entries/{$entry->id}");
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJsonFragment(['id' => $entry->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can update an entry', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$entry = Entry::factory()->create();
|
||||||
|
$data = ['title' => 'Updated Title'];
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->putJson("/api/entries/{$entry->id}", $data);
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJsonFragment($data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can delete an entry', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$entry = Entry::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->deleteJson("/api/entries/{$entry->id}");
|
||||||
|
|
||||||
|
$response->assertNoContent();
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('entries', ['id' => $entry->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only admin can create entries', function () {
|
||||||
|
$adminEmail = Config::get('app.admin_email');
|
||||||
|
$user = User::factory()->create(['email' => $adminEmail]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson('/api/entries', ['title' => 'Test', 'content' => 'Test content'])
|
||||||
|
->assertCreated();
|
||||||
|
|
||||||
|
$nonAdmin = User::factory()->create(['email' => 'nonadmin@example.com']);
|
||||||
|
|
||||||
|
$this->actingAs($nonAdmin)
|
||||||
|
->postJson('/api/entries', ['title' => 'Test', 'content' => 'Test content'])
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('authenticated users can read entries', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->getJson('/api/entries')
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->getJson('/api/entries')
|
||||||
|
->assertOk();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue