This commit is contained in:
2026-07-23 12:50:03 +07:00
parent db969767db
commit eee0ba6dd2
15 changed files with 914 additions and 71 deletions
-8
View File
@@ -14,12 +14,4 @@ enum TaxonomyType: string
self::TAG => 'Tag',
};
}
public function value(): string
{
return match ($this) {
self::CATEGORY => 'category',
self::TAG => 'tag',
};
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\JsonResponse;
use App\Models\User;
class AuthController extends Controller
{
/**
* Login
*/
public function login(Request $request): JsonResponse
{
$validated = $request->validate([
'username' => ['required'],
'password' => ['required'],
], [
'username.required' => 'Username harus diisi.',
'password.required' => 'Password harus diisi.',
]);
if (! Auth::guard()->attempt($validated)) {
return response()->json([
'success' => false,
'message' => 'Kredensial yang digunakan salah.',
]);
}
$user = Auth::user();
$user->tokens()->delete();
$token = $user->createToken('web')->plainTextToken;
return response()->json([
'success' => true,
'message' => 'Login berhasil.',
'data' => [
'user' => $user->toResource(),
'token' => $token,
]
]);
}
/**
* Logout
*/
public function logout(Request $request): JsonResponse
{
if ($request->user()?->currentAccessToken()) {
$request->user()->currentAccessToken()->delete();
}
return response()->json([
'success' => true,
'message' => 'Logout berhasil.',
]);
}
/**
* Display the authenticated user.
*/
public function me(Request $request): JsonResponse
{
return response()->json([
'success' => true,
'data' => [
'user' => $request->user()->toResource()
]
]);
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api\Content;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Http\JsonResponse;
use App\Enums\TaxonomyType;
use App\Models\Taxonomy;
use App\Http\Resources\TaxonomyResource;
class TaxonomyController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$validated = $request->validate([
'type' => ['nullable', Rule::enum(TaxonomyType::class)],
]);
$taxonomies = Taxonomy::query()
->when(
isset($validated['type']),
fn ($query) => $query->where('type', $validated['type'])
)
->get();
return response()->json([
'success' => true,
'data' => [
'taxonomies' => TaxonomyResource::collection($taxonomies),
],
]);
}
/**
* Display the specified resource.
*/
public function show(Taxonomy $taxonomy): JsonResponse
{
return response()->json([
'success' => true,
'data' => [
'taxonomy' => new TaxonomyResource($taxonomy),
],
]);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaxonomyResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'type' => [
'value' => $this->type->value,
'label' => $this->type->label(),
],
];
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'username' => $this->username,
];
}
}
+12
View File
@@ -8,10 +8,22 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
#[Fillable(['type', 'name', 'slug', 'created_by', 'updated_by', 'deleted_by'])]
#[UseResource(\App\Http\Resources\TaxonomyResource::class)]
class Taxonomy extends Model
{
use SoftDeletes;
public function resolveRouteBinding($value, $field = null)
{
return $this->where(function ($query) use ($value) {
if (is_numeric($value)) {
$query->where('id', $value);
}
$query->orWhere('slug', $value);
})->firstOrFail();
}
protected function casts(): array
{
return [
+3 -1
View File
@@ -9,13 +9,15 @@ use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
#[Fillable(['name', 'username', 'password', 'created_by', 'updated_by', 'deleted_by'])]
#[Hidden(['password', 'remember_token'])]
#[UseResource(\App\Http\Resources\UserResource::class)]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
+46 -1
View File
@@ -3,11 +3,17 @@
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
@@ -16,6 +22,45 @@ return Application::configure(basePath: dirname(__DIR__))
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*'),
fn (Request $request) => $request->is('api/*')
);
$exceptions->render(function (ModelNotFoundException $e) {
return response()->json([
'success' => false,
'message' => 'Data tidak ditemukan.',
], 404);
});
$exceptions->render(function (
NotFoundHttpException $e,
Request $request
) {
return response()->json([
'success' => false,
'message' => 'Data tidak ditemukan.',
], 404);
});
$exceptions->render(function (AuthenticationException $e) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated.',
], 401);
});
$exceptions->render(function (ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'Validasi gagal.',
'errors' => $e->errors(),
], 422);
});
$exceptions->render(function (HttpException $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage() ?: 'Terjadi kesalahan.',
], $e->getStatusCode());
});
})->create();
+2
View File
@@ -7,7 +7,9 @@
"license": "MIT",
"require": {
"php": "^8.3",
"dedoc/scramble": "^0.13.35",
"laravel/framework": "^13.8",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^3.0"
},
"require-dev": {
Generated
+313 -61
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "36793882908b6cbdd344b5a4840231db",
"content-hash": "f18e50d8c9d6bcf4d309afe2727d096e",
"packages": [
{
"name": "brick/math",
@@ -134,6 +134,87 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "dedoc/scramble",
"version": "v0.13.35",
"source": {
"type": "git",
"url": "https://github.com/dedoc/scramble.git",
"reference": "c072f731e060972134b6916ca6899609bacc0dd2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/c072f731e060972134b6916ca6899609bacc0dd2",
"reference": "c072f731e060972134b6916ca6899609bacc0dd2",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"myclabs/deep-copy": "^1.12",
"nikic/php-parser": "^5.0",
"php": "^8.1",
"phpstan/phpdoc-parser": "^1.0|^2.0",
"spatie/laravel-package-tools": "^1.9.2"
},
"require-dev": {
"larastan/larastan": "^3.3",
"laravel/pint": "^v1.1.0",
"laravel/scout": "^10.0|^11.0",
"nunomaduro/collision": "^7.0|^8.0",
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
"pestphp/pest": "^2.34|^3.7|^4.4",
"pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5|^11.5.3|^12.5.12",
"spatie/laravel-permission": "^6.10|^7.2",
"spatie/pest-plugin-snapshots": "^2.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Dedoc\\Scramble\\ScrambleServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Dedoc\\Scramble\\": "src",
"Dedoc\\Scramble\\Database\\Factories\\": "database/factories"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Lytvynenko",
"email": "litvinenko95@gmail.com",
"role": "Developer"
}
],
"description": "Automatic generation of API documentation for Laravel applications.",
"homepage": "https://github.com/dedoc/scramble",
"keywords": [
"documentation",
"laravel",
"openapi"
],
"support": {
"issues": "https://github.com/dedoc/scramble/issues",
"source": "https://github.com/dedoc/scramble/tree/v0.13.35"
},
"funding": [
{
"url": "https://github.com/romalytvynenko",
"type": "github"
}
],
"time": "2026-07-15T13:21:15+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -1343,6 +1424,69 @@
},
"time": "2026-06-26T00:11:25+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.3.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fee27a573d1a013af3721d86153a65e0b11927e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fee27a573d1a013af3721d86153a65e0b11927e6",
"reference": "fee27a573d1a013af3721d86153a65e0b11927e6",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.15|^10.8|^11.0",
"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-06-23T18:26:55+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.13",
@@ -2135,6 +2279,66 @@
],
"time": "2026-01-02T08:56:05+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.13.1",
@@ -2617,6 +2821,53 @@
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.3.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3",
"reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3"
},
"time": "2026-07-08T07:01:06+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
@@ -3306,6 +3557,67 @@
},
"time": "2026-06-18T03:57:49+00:00"
},
{
"name": "spatie/laravel-package-tools",
"version": "1.93.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
"reference": "d5552849801f2642aea710557463234b59ef65eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb",
"reference": "d5552849801f2642aea710557463234b59ef65eb",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.5",
"orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
"pestphp/pest": "^2.1|^3.1|^4.0",
"phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
"phpunit/phpunit": "^10.5|^11.5|^12.5",
"spatie/pest-plugin-test-time": "^2.2|^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\LaravelPackageTools\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Tools for creating Laravel packages",
"homepage": "https://github.com/spatie/laravel-package-tools",
"keywords": [
"laravel-package-tools",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
"source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-05-19T14:06:37+00:00"
},
{
"name": "symfony/clock",
"version": "v8.1.0",
@@ -6563,66 +6875,6 @@
},
"time": "2024-05-16T03:13:13+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/collections": "<1.6.8",
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
"phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
"files": [
"src/DeepCopy/deep_copy.php"
],
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v8.9.5",
+87
View File
@@ -0,0 +1,87 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
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' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];
+174
View File
@@ -0,0 +1,174 @@
<?php
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
return [
/*
* Which routes to document. String or array form; use Scramble::routes() for custom selection.
*
* 'api_path' => [
* 'include' => 'api',
* 'exclude' => ['api/internal'],
* ],
*
* Without *, patterns match path segments (api matches api and api/users, not apiary).
* With *, Str::is is used (e.g. api/v*).
*
* One static include → default server is /{include} and paths are stripped (/users).
* Multiple includes or wildcards → server defaults to / and paths stay full (/api/users).
* Override with `servers`, or use Scramble::registerApi() for separate bases.
*/
'api_path' => 'api',
/*
* Your API domain. By default, app domain is used. This is also a part of the default API routes
* matcher, so when implementing your own, make sure you use this config if needed.
*/
'api_domain' => null,
/*
* The path where your OpenAPI specification will be exported.
*/
'export_path' => 'api.json',
/*
* Cache configuration for the generated OpenAPI document.
*
* Use `scramble:cache` to warm the cache and `scramble:clear` to invalidate it.
*/
'cache' => [
'key' => 'scramble.openapi',
'store' => 'file',
],
'info' => [
/*
* API version.
*/
'version' => env('API_VERSION', '0.0.1'),
/*
* Description rendered on the home page of the API documentation (`/docs/api`).
*/
'description' => '',
],
'ui' => [
'title' => null,
],
'renderer' => 'elements',
'renderers' => [
/*
* Stoplight Elements config options: https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
*/
'elements' => [
'view' => 'scramble::docs',
'theme' => 'light',
'hideTryIt' => false,
'hideSchemas' => false,
'logo' => '',
'tryItCredentialsPolicy' => 'include',
'layout' => 'responsive',
'router' => 'hash',
],
/*
* Scalar API reference config options: https://scalar.com/products/api-references/configuration
*/
'scalar' => [
'view' => 'scramble::scalar',
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
'theme' => 'laravel',
'proxyUrl' => 'https://proxy.scalar.com',
'darkMode' => false,
'showDeveloperTools' => 'never',
'agent' => ['disabled' => true],
'credentials' => 'include',
],
],
/*
* The list of servers of the API. By default, when `null`, server URL will be created from
* `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you
* will need to specify the local server URL manually (if needed).
*
* Example of non-default config (final URLs are generated using Laravel `url` helper):
*
* ```php
* 'servers' => [
* 'Live' => 'api',
* 'Prod' => 'https://scramble.dedoc.co/api',
* ],
* ```
*/
'servers' => null,
/**
* Determines how Scramble stores the descriptions of enum cases.
* Available options:
* - 'description' – Case descriptions are stored as the enum schema's description using table formatting.
* - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension.
*
* @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions
* - false - Case descriptions are ignored.
*/
'enum_cases_description_strategy' => 'description',
/**
* Determines how Scramble stores the names of enum cases.
* Available options:
* - 'names' – Case names are stored in the `x-enumNames` enum schema extension.
* - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension.
* - false - Case names are not stored.
*/
'enum_cases_names_strategy' => false,
/**
* When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated
* OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until
* OpenAPI 3.2 is released and query string structure can be described properly.
*
* For example, this nested validation rule describes the object with `bar` property:
* `['foo.bar' => ['required', 'int']]`.
*
* When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so:
* `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`.
*
* When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so:
* `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`.
*/
'flatten_deep_query_parameters' => true,
'middleware' => [
'web',
RestrictedDocsAccess::class,
],
'extensions' => [],
/*
* Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route
* middleware.
*
* Disabled by default. Uncomment the line below to enable `MiddlewareAuthSecurityStrategy`.
* When at least one documented route uses middleware matching the configured patterns (by default
* `auth` and `auth:*`), bearer auth is applied globally. Routes without matching middleware are
* marked as public (`security: []`).
*
* Set to `null` explicitly to disable. If you already configure security manually via
* `afterOpenApiGenerated` / `extendOpenApi`, keep this disabled to avoid duplicate schemes.
*
* Customize with a class-string or [class, options]:
*
* 'security_strategy' => [
* \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
* [
* 'middleware' => ['auth', 'auth:*'],
* 'scheme' => \Dedoc\Scramble\Support\Generator\SecurityScheme::http('bearer'),
* ],
* ],
*/
'security_strategy' => \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
// 'security_strategy' => null,
];
@@ -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');
}
};
+13
View File
@@ -0,0 +1,13 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/login', [\App\Http\Controllers\Api\Auth\AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/me', [AuthController::class, 'me']);
Route::resource('taxonomies',\App\Http\Controllers\Api\Content\TaxonomyController::class)->only(['index', 'show']);
});
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
class AuthControllerTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_login_and_logout_with_sanctum_token(): void
{
$user = User::factory()->create([
'username' => 'tester',
'password' => Hash::make('secret123'),
]);
$loginResponse = $this->postJson('/api/login', [
'username' => 'tester',
'password' => 'secret123',
]);
$loginResponse->assertStatus(200)
->assertJsonPath('message', 'Login successful')
->assertJsonStructure([
'message',
'user' => ['id', 'name', 'username'],
'token',
]);
$this->assertDatabaseHas('personal_access_tokens', [
'tokenable_id' => $user->id,
'tokenable_type' => User::class,
]);
$token = $loginResponse->json('token');
$logoutResponse = $this->withHeader('Authorization', 'Bearer ' . $token)
->postJson('/api/logout');
$logoutResponse->assertStatus(200)
->assertJsonPath('message', 'Logout successful');
$this->assertDatabaseCount('personal_access_tokens', 0);
}
}