From 52da5b0e1d8ce10873ca99197ad1277238dee1a9 Mon Sep 17 00:00:00 2001 From: MadHowl Date: Fri, 20 Mar 2026 14:35:58 +1000 Subject: [PATCH] minimax --- SANCTUM_SETUP_GUIDE.md | 938 ++++++++++++++++++ app/Console/Commands/GenerateSwagger.php | 252 +++++ app/Http/Controllers/Api/AuthController.php | 80 ++ app/Http/Controllers/Api/PostController.php | 83 ++ app/Http/Requests/LoginRequest.php | 29 + app/Http/Requests/RegisterRequest.php | 30 + app/Http/Requests/StorePostRequest.php | 29 + app/Http/Requests/UpdatePostRequest.php | 29 + app/Http/Resources/PostCollection.php | 28 + app/Http/Resources/PostResource.php | 27 + app/Http/Resources/UserResource.php | 25 + app/Models/User.php | 5 +- app/Policies/PostPolicy.php | 25 + bootstrap/app.php | 1 + composer.json | 4 +- composer.lock | 328 +++++- config/sanctum.php | 84 ++ config/spectrum.php | 50 + ...32_create_personal_access_tokens_table.php | 33 + public/docs/index.html | 26 + public/docs/openapi.yaml | 79 ++ routes/api.php | 39 + 22 files changed, 2219 insertions(+), 5 deletions(-) create mode 100644 SANCTUM_SETUP_GUIDE.md create mode 100644 app/Console/Commands/GenerateSwagger.php create mode 100644 app/Http/Controllers/Api/AuthController.php create mode 100644 app/Http/Controllers/Api/PostController.php create mode 100644 app/Http/Requests/LoginRequest.php create mode 100644 app/Http/Requests/RegisterRequest.php create mode 100644 app/Http/Requests/StorePostRequest.php create mode 100644 app/Http/Requests/UpdatePostRequest.php create mode 100644 app/Http/Resources/PostCollection.php create mode 100644 app/Http/Resources/PostResource.php create mode 100644 app/Http/Resources/UserResource.php create mode 100644 app/Policies/PostPolicy.php create mode 100644 config/sanctum.php create mode 100644 config/spectrum.php create mode 100644 database/migrations/2026_03_20_032532_create_personal_access_tokens_table.php create mode 100644 public/docs/index.html create mode 100644 public/docs/openapi.yaml create mode 100644 routes/api.php diff --git a/SANCTUM_SETUP_GUIDE.md b/SANCTUM_SETUP_GUIDE.md new file mode 100644 index 0000000..93a1ec5 --- /dev/null +++ b/SANCTUM_SETUP_GUIDE.md @@ -0,0 +1,938 @@ +# Руководство по установке и настройке Laravel Sanctum с API авторизацией + +## Описание + +Данное руководство описывает процесс установки и настройки **Laravel Sanctum** для API аутентификации с использованием **wadakatu/laravel-spectrum** для генерации Swagger документации. + +--- + +## Содержание + +1. [Установка пакетов](#1-установка-пакетов) +2. [Настройка Sanctum](#2-настройка-sanctum) +3. [Структура API](#3-структура-api) +4. [API Endpoints](#4-api-endpoints) +5. [Использование API](#5-использование-api) +6. [Swagger документация](#6-swagger-документация) +7. [Команды Artisan](#7-команды-artisan) + +--- + +## 1. Установка пакетов + +### 1.1 Установка Laravel Sanctum + +```bash +composer require laravel/sanctum +``` + +### 1.2 Установка wadakatu/laravel-spectrum + +```bash +composer require wadakatu/laravel-spectrum +``` + +### 1.3 Публикация конфигураций + +```bash +# Публикация Sanctum +php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" + +# Публикация Spectrum +php artisan vendor:publish --provider="Wadakatu\LaravelSpectrum\LaravelSpectrumServiceProvider" +``` + +### 1.4 Запуск миграций + +```bash +php artisan migrate +``` + +> **Примечание:** Если таблица `personal_access_tokens` уже существует, пропустите этот шаг. + +--- + +## 2. Настройка Sanctum + +### 2.1 Обновление модели User + +Добавьте трейт `HasApiTokens` в модель `User`: + +```php +// app/Models/User.php + +namespace App\Models; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; + +class User extends Authenticatable +{ + use HasApiTokens, HasFactory, Notifiable; + + // ... +} +``` + +### 2.2 Настройка middleware + +Обновите файл `bootstrap/app.php`: + +```php +// bootstrap/app.php + +return Application::configure(basePath: dirname(__DIR__)) + ->withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->statefulApi(); + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); +``` + +### 2.3 Конфигурация Sanctum + +Конфигурация находится в файле `config/sanctum.php`. Основные настройки: + +```php +// config/sanctum.php + +return [ + 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' + ))), + + 'guard' => ['web'], + + 'expiration' => null, + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + 'middleware' => [ + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + ], +]; +``` + +--- + +## 3. Структура API + +### 3.1 Директории + +``` +app/ +├── Http/ +│ ├── Controllers/ +│ │ └── Api/ +│ │ ├── AuthController.php +│ │ └── PostController.php +│ ├── Requests/ +│ │ ├── LoginRequest.php +│ │ ├── RegisterRequest.php +│ │ ├── StorePostRequest.php +│ │ └── UpdatePostRequest.php +│ └── Resources/ +│ ├── PostCollection.php +│ ├── PostResource.php +│ └── UserResource.php +└── Policies/ + └── PostPolicy.php +``` + +### 3.2 API Resources + +**UserResource** - форматирование данных пользователя: + +```php +// app/Http/Resources/UserResource.php + +namespace App\Http\Resources; + +use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResource; + +class UserResource extends JsonResource +{ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'role' => $this->role, + 'created_at' => $this->created_at, + ]; + } +} +``` + +**PostResource** - форматирование данных поста: + +```php +// app/Http/Resources/PostResource.php + +namespace App\Http\Resources; + +use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResource; + +class PostResource extends JsonResource +{ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'content' => $this->content, + 'user_id' => $this->user_id, + 'user' => new UserResource($this->whenLoaded('user')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} +``` + +### 3.3 Form Requests + +**RegisterRequest** - валидация регистрации: + +```php +// app/Http/Requests/RegisterRequest.php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class RegisterRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]; + } +} +``` + +**LoginRequest** - валидация входа: + +```php +// app/Http/Requests/LoginRequest.php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class LoginRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } +} +``` + +**StorePostRequest** - валидация создания поста: + +```php +// app/Http/Requests/StorePostRequest.php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class StorePostRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]; + } +} +``` + +**UpdatePostRequest** - валидация обновления поста: + +```php +// app/Http/Requests/UpdatePostRequest.php + +namespace App\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class UpdatePostRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + public function rules(): array + { + return [ + 'title' => ['sometimes', 'string', 'max:255'], + 'content' => ['sometimes', 'string'], + ]; + } +} +``` + +--- + +## 4. API Endpoints + +### 4.1 Маршруты + +```php +// routes/api.php + +use App\Http\Controllers\Api\AuthController; +use App\Http\Controllers\Api\PostController; +use Illuminate\Support\Facades\Route; + +// Swagger Documentation +Route::get('/docs', function () { + return redirect()->to('/docs/index.html'); +}); + +// Public routes +Route::post('/register', [AuthController::class, 'register'])->name('api.register'); +Route::post('/login', [AuthController::class, 'login'])->name('api.login'); + +// Protected routes +Route::middleware('auth:sanctum')->group(function () { + // Auth + Route::post('/logout', [AuthController::class, 'logout'])->name('api.logout'); + Route::get('/user', [AuthController::class, 'user'])->name('api.user'); + + // Posts + Route::get('/posts', [PostController::class, 'index'])->name('api.posts.index'); + Route::post('/posts', [PostController::class, 'store'])->name('api.posts.store'); + Route::get('/posts/{post}', [PostController::class, 'show'])->name('api.posts.show'); + Route::put('/posts/{post}', [PostController::class, 'update'])->name('api.posts.update'); + Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('api.posts.destroy'); +}); +``` + +### 4.2 Список Endpoints + +| Метод | Endpoint | Описание | Авторизация | +|-------|----------|----------|-------------| +| POST | `/api/register` | Регистрация | Нет | +| POST | `/api/login` | Вход | Нет | +| POST | `/api/logout` | Выход | Да | +| GET | `/api/user` | Текущий пользователь | Да | +| GET | `/api/posts` | Список постов | Да | +| POST | `/api/posts` | Создание поста | Да | +| GET | `/api/posts/{id}` | Просмотр поста | Да | +| PUT | `/api/posts/{id}` | Обновление поста | Да | +| DELETE | `/api/posts/{id}` | Удаление поста | Да | +| GET | `/api/docs` | Swagger UI | Нет | + +--- + +## 5. Использование API + +### 5.1 Регистрация + +**Запрос:** + +```bash +curl -X POST http://la.test/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + "password_confirmation": "password123" + }' +``` + +**Ответ:** + +```json +{ + "message": "User registered successfully", + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "role": null, + "created_at": "2026-03-20T03:45:00.000000Z" + }, + "token": "1|abc123..." +} +``` + +### 5.2 Вход + +**Запрос:** + +```bash +curl -X POST http://la.test/api/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "password": "password123" + }' +``` + +**Ответ:** + +```json +{ + "message": "Login successful", + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "role": null, + "created_at": "2026-03-20T03:45:00.000000Z" + }, + "token": "2|def456..." +} +``` + +### 5.3 Создание поста + +**Запрос:** + +```bash +curl -X POST http://la.test/api/posts \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {token}" \ + -d '{ + "title": "My First Post", + "content": "This is the content of my post." + }' +``` + +**Ответ:** + +```json +{ + "message": "Post created successfully", + "post": { + "id": 1, + "title": "My First Post", + "content": "This is the content of my post.", + "user_id": 1, + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com" + }, + "created_at": "2026-03-20T03:50:00.000000Z", + "updated_at": "2026-03-20T03:50:00.000000Z" + } +} +``` + +### 5.4 Обновление поста + +**Запрос:** + +```bash +curl -X PUT http://la.test/api/posts/1 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {token}" \ + -d '{ + "title": "Updated Title" + }' +``` + +### 5.5 Удаление поста + +**Запрос:** + +```bash +curl -X DELETE http://la.test/api/posts/1 \ + -H "Authorization: Bearer {token}" +``` + +### 5.6 Выход + +**Запрос:** + +```bash +curl -X POST http://la.test/api/logout \ + -H "Authorization: Bearer {token}" +``` + +--- + +## 6. Swagger документация + +### 6.1 Доступ к документации + +Swagger UI доступен по адресу: `{APP_URL}/api/docs` + +Например: `http://la.test/api/docs` + +### 6.2 Структура файлов документации + +``` +public/ +└── docs/ + ├── index.html # Swagger UI + └── openapi.yaml # OpenAPI спецификация +``` + +### 6.3 Обновление документации + +После добавления новых endpointов перегенерируйте документацию: + +```bash +php artisan swagger:generate +``` + +### 6.4 Конфигурация Spectrum + +```php +// config/spectrum.php + +return [ + 'output' => 'public/docs', + 'title' => env('APP_NAME', 'Laravel API'), + 'description' => 'API Documentation', + 'version' => '1.0.0', + 'server_url' => env('APP_URL'), + 'servers' => [ + ['url' => env('APP_URL'), 'description' => 'Local server'], + ], + 'security_schemes' => [ + 'bearerAuth' => [ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ], + ], + 'default_security_scheme' => 'bearerAuth', + 'paths' => [ + 'controllers' => ['App\\Http\\Controllers\\Api\\'], + ], +]; +``` + +--- + +## 7. Команды Artisan + +### 7.1 Создание пользовательской команды + +#### Структура директорий + +Команды Artisan располагаются в директории `app/Console/Commands/`: + +``` +app/ +├── Console/ +│ └── Commands/ +│ └── GenerateSwagger.php # Наша команда +``` + +#### Создание команды + +**1. Создайте директорию Commands:** + +```bash +mkdir -p app/Console/Commands +``` + +**2. Создайте файл команды:** + +```bash +# app/Console/Commands/GenerateSwagger.php +``` + +#### Пример кода команды + +```php +info('Generating OpenAPI specification...'); + + // Создание директории для документации + $outputDir = public_path('docs'); + if (!File::exists($outputDir)) { + File::makeDirectory($outputDir, 0755, true); + } + + // Генерация спецификации + $spec = $this->generateSpec(); + + // Конвертация в YAML формат + $yaml = Yaml::dump($spec, 4, 2); + + // Сохранение файла + File::put($outputDir . '/openapi.yaml', $yaml); + $this->info('OpenAPI specification saved to public/docs/openapi.yaml'); + + // Возврат кода успеха + return Command::SUCCESS; + } + + /** + * Метод для генерации структуры OpenAPI спецификации. + * Можно создавать YAML или JSON вручную. + */ + protected function generateSpec(): array + { + $appUrl = config('app.url', 'http://localhost'); + + return [ + 'openapi' => '3.0.0', + 'info' => [ + 'title' => config('app.name', 'Laravel API'), + 'description' => 'API Documentation', + 'version' => '1.0.0', + ], + 'servers' => [ + ['url' => $appUrl, 'description' => 'Local server'], + ], + 'paths' => $this->generatePaths(), + 'components' => [ + 'securitySchemes' => [ + 'bearerAuth' => [ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ], + ], + ], + ]; + } + + /** + * Генерация секции paths для OpenAPI. + */ + protected function generatePaths(): array + { + return [ + '/api/register' => [ + 'post' => [ + 'tags' => ['Auth'], + 'summary' => 'Register a new user', + // ... описание endpoint + ], + ], + // ... другие endpoints + ]; + } +} +``` + +#### Основные компоненты команды + +| Компонент | Описание | +|----------|----------| +| `$signature` | Имя команды (например, `swagger:generate`) | +| `$description` | Описание для списка команд | +| `handle()` | Основной метод выполнения команды | +| `$this->info()` | Вывод информационного сообщения | +| `$this->error()` | Вывод сообщения об ошибке | +| `$this->warn()` | Вывод предупреждения | +| `$this->question()` | Вывод вопроса | +| `Command::SUCCESS` | Код успешного завершения | +| `Command::FAILURE` | Код неудачного завершения | + +#### Методы для взаимодействия с пользователем + +```php +// Вывод текста +$this->info('Сообщение'); // Зеленый текст +$this->error('Ошибка'); // Красный текст +$this->warn('Внимание'); // Желтый текст +$this->line('Текст'); // Обычный текст + +// Запрос подтверждения +if ($this->confirm('Продолжить?')) { + // пользователь ответил "да" +} + +// Выбор из списка +$choice = $this->choice('Выберите:', ['opt1', 'opt2', 'opt3'], 0); + +// Ввод текста +$name = $this->ask('Введите имя:'); +$password = $this->secret('Введите пароль:'); // Скрытый ввод + +// Прогресс-бар +$bar = $this->output->createProgressBar(100); +$bar->start(); +foreach ($items as $item) { + // обработка + $bar->advance(); +} +$bar->finish(); +``` + +### 7.2 Список команд + +| Команда | Описание | +|---------|----------| +| `php artisan swagger:generate` | Генерация OpenAPI спецификации | +| `php artisan list` | Список всех команд | +| `php artisan list api` | Список команд содержащих "api" | + +### 7.3 Пример использования + +```bash +# Генерация документации +php artisan swagger:generate + +# Очистка кэша конфигурации +php artisan config:clear + +# Очистка кэша маршрутов +php artisan route:clear + +# Просмотр списка команд +php artisan list + +# Справка по команде +php artisan swagger:generate --help +``` + +### 7.4 Автоматическая регистрация + +Laravel автоматически обнаруживает команды в директории `app/Console/Commands`. +Для ручной регистрации добавьте в `app/Console/Kernel.php`: + +```php +// app/Console/Kernel.php + +namespace App\Console; + +use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Foundation\Console\Kernel as ConsoleKernel; + +class Kernel extends ConsoleKernel +{ + /** + * The Artisan commands provided by your application. + * + * @var array + */ + protected $commands = [ + Commands\GenerateSwagger::class, + ]; + + /** + * Define the application's command schedule. + */ + protected function schedule(Schedule $schedule): void + { + // $schedule->command('swagger:generate')->daily(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__.'/Commands'); + } +} +``` + +--- + +## 8. Политики доступа + +### 8.1 PostPolicy + +```php +// app/Policies/PostPolicy.php + +namespace App\Policies; + +use App\Models\Post; +use App\Models\User; + +class PostPolicy +{ + public function update(User $user, Post $post): bool + { + return $user->id === $post->user_id; + } + + public function delete(User $user, Post $post): bool + { + return $user->id === $post->user_id; + } +} +``` + +### 8.2 Регистрация политики + +```php +// app/Providers/AuthServiceProvider.php + +namespace App\Providers; + +use App\Models\Post; +use App\Models\User; +use App\Policies\PostPolicy; +use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; + +class AuthServiceProvider extends ServiceProvider +{ + protected $policies = [ + Post::class => PostPolicy::class, + ]; + + public function boot(): void + { + $this->registerPolicies(); + } +} +``` + +--- + +## 9. Конфигурация .env + +Убедитесь, что в файле `.env` указаны правильные настройки: + +```env +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:... +APP_URL=http://la.test + +DB_CONNECTION=sqlite + +SESSION_DRIVER=database +SANCTUM_STATEFUL_DOMAINS=la.test,localhost,127.0.0.1 +``` + +--- + +## 10. Тестирование API + +### 10.1 Примеры запросов в Swagger UI + +1. Откройте `http://la.test/api/docs` +2. Нажмите **Authorize** и введите ваш token +3. Тестируйте endpoints прямо в браузере + +### 10.2 Ручное тестирование + +```bash +# Проверка списка маршрутов +php artisan route:list + +# Запуск сервера +php artisan serve --host=la.test + +# Запуск тестов +php artisan test +``` + +--- + +## Troubleshooting + +### Проблема: 401 Unauthorized + +**Решение:** +1. Убедитесь, что передаёте правильный токен в заголовке `Authorization` +2. Проверьте срок действия токена +3. Убедитесь, что используете префикс `Bearer` + +### Проблема: CSRF token mismatch + +**Решение:** +Для SPA приложений добавьте домен в `SANCTUM_STATEFUL_DOMAINS`: + +```env +SANCTUM_STATEFUL_DOMAINS=la.test,localhost,127.0.0.1 +``` + +### Проблема: Таблица уже существует + +**Решение:** +Если миграция `personal_access_tokens` уже выполнена, пропустите её: + +```bash +php artisan migrate --path=/database/migrations/2026_03_20_032532_create_personal_access_tokens_table.php --skip +``` + +--- + +## Заключение + +Теперь у вас есть полностью настроенный API с: +- ✅ Laravel Sanctum для аутентификации +- ✅ Token-based авторизация +- ✅ API Resources для форматирования ответов +- ✅ Form Requests для валидации +- ✅ Swagger документация по адресу `/api/docs` +- ✅ Политики доступа для постов + +Для получения дополнительной информации обратитесь к официальной документации: +- [Laravel Sanctum](https://laravel.com/docs/sanctum) +- [wadakatu/laravel-spectrum](https://github.com/wadakatu/laravel-spectrum) diff --git a/app/Console/Commands/GenerateSwagger.php b/app/Console/Commands/GenerateSwagger.php new file mode 100644 index 0000000..6115896 --- /dev/null +++ b/app/Console/Commands/GenerateSwagger.php @@ -0,0 +1,252 @@ +info('Generating OpenAPI specification...'); + + $outputDir = public_path('docs'); + if (!File::exists($outputDir)) { + File::makeDirectory($outputDir, 0755, true); + } + + $spec = $this->generateSpec(); + $yaml = Yaml::dump($spec, 4, 2); + + File::put($outputDir . '/openapi.yaml', $yaml); + $this->info('OpenAPI specification saved to public/docs/openapi.yaml'); + + return Command::SUCCESS; + } + + protected function generateSpec(): array + { + $appUrl = config('app.url', 'http://localhost'); + + return [ + 'openapi' => '3.0.0', + 'info' => [ + 'title' => config('app.name', 'Laravel API'), + 'description' => 'API Documentation', + 'version' => '1.0.0', + ], + 'servers' => [ + ['url' => $appUrl, 'description' => 'Local server'], + ], + 'paths' => $this->generatePaths(), + 'components' => [ + 'securitySchemes' => [ + 'bearerAuth' => [ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ], + ], + 'schemas' => $this->generateSchemas(), + ], + ]; + } + + protected function generatePaths(): array + { + return [ + '/api/register' => [ + 'post' => [ + 'tags' => ['Auth'], + 'summary' => 'Register a new user', + 'requestBody' => [ + 'required' => true, + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'required' => ['name', 'email', 'password', 'password_confirmation'], + 'properties' => [ + 'name' => ['type' => 'string', 'example' => 'John Doe'], + 'email' => ['type' => 'string', 'format' => 'email', 'example' => 'john@example.com'], + 'password' => ['type' => 'string', 'format' => 'password', 'example' => 'password123'], + 'password_confirmation' => ['type' => 'string', 'format' => 'password', 'example' => 'password123'], + ], + ], + ], + ], + ], + 'responses' => [ + '201' => ['description' => 'User registered successfully'], + '422' => ['description' => 'Validation error'], + ], + ], + ], + '/api/login' => [ + 'post' => [ + 'tags' => ['Auth'], + 'summary' => 'Login user', + 'requestBody' => [ + 'required' => true, + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'required' => ['email', 'password'], + 'properties' => [ + 'email' => ['type' => 'string', 'format' => 'email', 'example' => 'john@example.com'], + 'password' => ['type' => 'string', 'format' => 'password', 'example' => 'password123'], + ], + ], + ], + ], + ], + 'responses' => [ + '200' => ['description' => 'Login successful'], + '401' => ['description' => 'Invalid credentials'], + ], + ], + ], + '/api/logout' => [ + 'post' => [ + 'tags' => ['Auth'], + 'summary' => 'Logout user', + 'security' => [['bearerAuth' => []]], + 'responses' => [ + '200' => ['description' => 'Logged out successfully'], + ], + ], + ], + '/api/user' => [ + 'get' => [ + 'tags' => ['Auth'], + 'summary' => 'Get authenticated user', + 'security' => [['bearerAuth' => []]], + 'responses' => [ + '200' => ['description' => 'User data'], + ], + ], + ], + '/api/posts' => [ + 'get' => [ + 'tags' => ['Posts'], + 'summary' => 'Get all posts', + 'security' => [['bearerAuth' => []]], + 'responses' => [ + '200' => ['description' => 'List of posts'], + ], + ], + 'post' => [ + 'tags' => ['Posts'], + 'summary' => 'Create a new post', + 'security' => [['bearerAuth' => []]], + 'requestBody' => [ + 'required' => true, + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'required' => ['title', 'content'], + 'properties' => [ + 'title' => ['type' => 'string', 'example' => 'My Post Title'], + 'content' => ['type' => 'string', 'example' => 'Post content here...'], + ], + ], + ], + ], + ], + 'responses' => [ + '201' => ['description' => 'Post created successfully'], + '422' => ['description' => 'Validation error'], + ], + ], + ], + '/api/posts/{id}' => [ + 'get' => [ + 'tags' => ['Posts'], + 'summary' => 'Get a post', + 'security' => [['bearerAuth' => []]], + 'parameters' => [ + ['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'integer']], + ], + 'responses' => [ + '200' => ['description' => 'Post data'], + '404' => ['description' => 'Post not found'], + ], + ], + 'put' => [ + 'tags' => ['Posts'], + 'summary' => 'Update a post', + 'security' => [['bearerAuth' => []]], + 'parameters' => [ + ['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'integer']], + ], + 'requestBody' => [ + 'required' => true, + 'content' => [ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'title' => ['type' => 'string', 'example' => 'Updated Title'], + 'content' => ['type' => 'string', 'example' => 'Updated content...'], + ], + ], + ], + ], + ], + 'responses' => [ + '200' => ['description' => 'Post updated successfully'], + '404' => ['description' => 'Post not found'], + '422' => ['description' => 'Validation error'], + ], + ], + 'delete' => [ + 'tags' => ['Posts'], + 'summary' => 'Delete a post', + 'security' => [['bearerAuth' => []]], + 'parameters' => [ + ['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'integer']], + ], + 'responses' => [ + '200' => ['description' => 'Post deleted successfully'], + '404' => ['description' => 'Post not found'], + ], + ], + ], + ]; + } + + protected function generateSchemas(): array + { + return [ + 'User' => [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'email' => ['type' => 'string', 'format' => 'email'], + 'role' => ['type' => 'string'], + 'created_at' => ['type' => 'string', 'format' => 'date-time'], + ], + ], + 'Post' => [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'title' => ['type' => 'string'], + 'content' => ['type' => 'string'], + 'user_id' => ['type' => 'integer'], + 'user' => ['$ref' => '#/components/schemas/User'], + 'created_at' => ['type' => 'string', 'format' => 'date-time'], + 'updated_at' => ['type' => 'string', 'format' => 'date-time'], + ], + ], + ]; + } +} diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..f117082 --- /dev/null +++ b/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,80 @@ + $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + ]); + + $token = $user->createToken('auth-token')->plainTextToken; + + return response()->json([ + 'message' => 'User registered successfully', + 'user' => new UserResource($user), + 'token' => $token, + ], 201); + } + + /** + * Login user. + */ + public function login(LoginRequest $request): JsonResponse + { + $user = User::where('email', $request->email)->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + return response()->json([ + 'message' => 'Invalid credentials', + ], 401); + } + + $token = $user->createToken('auth-token')->plainTextToken; + + return response()->json([ + 'message' => 'Login successful', + 'user' => new UserResource($user), + 'token' => $token, + ]); + } + + /** + * Logout user. + */ + public function logout(Request $request): JsonResponse + { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'message' => 'Logged out successfully', + ]); + } + + /** + * Get authenticated user. + */ + public function user(Request $request): JsonResponse + { + return response()->json([ + 'user' => new UserResource($request->user()), + ]); + } +} diff --git a/app/Http/Controllers/Api/PostController.php b/app/Http/Controllers/Api/PostController.php new file mode 100644 index 0000000..1b62c91 --- /dev/null +++ b/app/Http/Controllers/Api/PostController.php @@ -0,0 +1,83 @@ +latest()->paginate(15); + return new PostCollection($posts); + } + + /** + * Store a newly created post. + */ + public function store(StorePostRequest $request): JsonResponse + { + $post = Post::create([ + 'title' => $request->title, + 'content' => $request->content, + 'user_id' => $request->user()->id, + ]); + + $post->load('user'); + + return response()->json([ + 'message' => 'Post created successfully', + 'post' => new PostResource($post), + ], 201); + } + + /** + * Display the specified post. + */ + public function show(Post $post): JsonResponse + { + $post->load('user'); + return response()->json([ + 'post' => new PostResource($post), + ]); + } + + /** + * Update the specified post. + */ + public function update(UpdatePostRequest $request, Post $post): JsonResponse + { + $this->authorize('update', $post); + + $post->update($request->validated()); + + return response()->json([ + 'message' => 'Post updated successfully', + 'post' => new PostResource($post), + ]); + } + + /** + * Remove the specified post. + */ + public function destroy(Post $post): JsonResponse + { + $this->authorize('delete', $post); + + $post->delete(); + + return response()->json([ + 'message' => 'Post deleted successfully', + ]); + } +} diff --git a/app/Http/Requests/LoginRequest.php b/app/Http/Requests/LoginRequest.php new file mode 100644 index 0000000..07c07c1 --- /dev/null +++ b/app/Http/Requests/LoginRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/RegisterRequest.php b/app/Http/Requests/RegisterRequest.php new file mode 100644 index 0000000..79918da --- /dev/null +++ b/app/Http/Requests/RegisterRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]; + } +} diff --git a/app/Http/Requests/StorePostRequest.php b/app/Http/Requests/StorePostRequest.php new file mode 100644 index 0000000..e9b11e1 --- /dev/null +++ b/app/Http/Requests/StorePostRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/UpdatePostRequest.php b/app/Http/Requests/UpdatePostRequest.php new file mode 100644 index 0000000..98d258b --- /dev/null +++ b/app/Http/Requests/UpdatePostRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => ['sometimes', 'string', 'max:255'], + 'content' => ['sometimes', 'string'], + ]; + } +} diff --git a/app/Http/Resources/PostCollection.php b/app/Http/Resources/PostCollection.php new file mode 100644 index 0000000..3b4019e --- /dev/null +++ b/app/Http/Resources/PostCollection.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + ]; + } +} diff --git a/app/Http/Resources/PostResource.php b/app/Http/Resources/PostResource.php new file mode 100644 index 0000000..2605475 --- /dev/null +++ b/app/Http/Resources/PostResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'content' => $this->content, + 'user_id' => $this->user_id, + 'user' => new UserResource($this->whenLoaded('user')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..8f89ec8 --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'role' => $this->role, + 'created_at' => $this->created_at, + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3950f6c..e2ae233 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,15 +2,14 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { - /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. diff --git a/app/Policies/PostPolicy.php b/app/Policies/PostPolicy.php new file mode 100644 index 0000000..bbdf927 --- /dev/null +++ b/app/Policies/PostPolicy.php @@ -0,0 +1,25 @@ +id === $post->user_id; + } + + /** + * Determine whether the user can delete the post. + */ + public function delete(User $user, Post $post): bool + { + return $user->id === $post->user_id; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..c3928c5 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', health: '/up', ) diff --git a/composer.json b/composer.json index 3f4f746..1f0c40d 100644 --- a/composer.json +++ b/composer.json @@ -8,8 +8,10 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", + "laravel/sanctum": "^4.3", "laravel/tinker": "^2.10.1", - "laravel/ui": "^4.6" + "laravel/ui": "^4.6", + "wadakatu/laravel-spectrum": "^1.1" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 2e30ee5..267e91b 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "4776810dc1f9cc2ee65b6526f0091af8", + "content-hash": "71b27411bdfa32f8d8a868f991c3dccb", "packages": [ { "name": "brick/math", @@ -1333,6 +1333,69 @@ }, "time": "2026-02-06T12:17:10+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "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-02-07T17:19:31+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.10", @@ -3357,6 +3420,71 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "spatie/fork", + "version": "1.2.5", + "source": { + "type": "git", + "url": "https://github.com/spatie/fork.git", + "reference": "bde768a99be8cff41b8ec4991b016dcb58f414e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/fork/zipball/bde768a99be8cff41b8ec4991b016dcb58f414e8", + "reference": "bde768a99be8cff41b8ec4991b016dcb58f414e8", + "shasum": "" + }, + "require": { + "ext-pcntl": "*", + "ext-sockets": "*", + "php": "^8.0" + }, + "require-dev": { + "nesbot/carbon": "^2.66", + "pestphp/pest": "^1.23", + "phpunit/phpunit": "^9.5", + "spatie/ray": "^1.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Fork\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brent Roose", + "email": "brent@spatie.be", + "role": "Developer" + }, + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "A lightweight solution for running code concurrently in PHP", + "homepage": "https://github.com/spatie/fork", + "keywords": [ + "fork", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/fork/issues", + "source": "https://github.com/spatie/fork/tree/1.2.5" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-04-24T08:58:04+00:00" + }, { "name": "symfony/clock", "version": "v8.0.0", @@ -6061,6 +6189,204 @@ } ], "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "wadakatu/laravel-spectrum", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/wadakatu/laravel-spectrum.git", + "reference": "0c80efc8456a7dcb910f62e37f15cee95b4d0683" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wadakatu/laravel-spectrum/zipball/0c80efc8456a7dcb910f62e37f15cee95b4d0683", + "reference": "0c80efc8456a7dcb910f62e37f15cee95b4d0683", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0", + "illuminate/routing": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "nikic/php-parser": "^5.5", + "php": "^8.2", + "spatie/fork": "^1.2", + "symfony/finder": "^6.0|^7.0", + "workerman/workerman": "^5.1" + }, + "require-dev": { + "devizzent/cebe-php-openapi": "^1.1", + "fakerphp/faker": "^1.23", + "infection/infection": "*", + "laravel/pint": "^1.23", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0|^11.0|^12.0", + "spatie/phpunit-snapshot-assertions": "^5.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LaravelSpectrum\\SpectrumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LaravelSpectrum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "wadakatu", + "email": "wadakatukoyo330@gmail.com" + } + ], + "description": "Zero-annotation OpenAPI/Swagger documentation generator for Laravel - automatic API docs from your existing code", + "homepage": "https://github.com/wadakatu/laravel-spectrum", + "keywords": [ + "api", + "api-documentation", + "automatic", + "documentation", + "generator", + "laravel", + "mock-server", + "no-annotation", + "openapi", + "rest-api", + "swagger", + "swagger-alternative", + "zero-annotation" + ], + "support": { + "changelog": "https://github.com/wadakatu/laravel-spectrum/releases", + "docs": "https://github.com/wadakatu/laravel-spectrum#readme", + "issues": "https://github.com/wadakatu/laravel-spectrum/issues", + "source": "https://github.com/wadakatu/laravel-spectrum" + }, + "time": "2026-02-16T10:52:56+00:00" + }, + { + "name": "workerman/coroutine", + "version": "v1.1.5", + "source": { + "type": "git", + "url": "https://github.com/workerman-php/coroutine.git", + "reference": "b60e44267b90d398dbfa7a320f3e97b46357ac9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b60e44267b90d398dbfa7a320f3e97b46357ac9f", + "reference": "b60e44267b90d398dbfa7a320f3e97b46357ac9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "workerman/workerman": "^5.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "psr/log": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "src", + "Workerman\\Coroutine\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Workerman coroutine", + "support": { + "issues": "https://github.com/workerman-php/coroutine/issues", + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.5" + }, + "time": "2026-03-12T02:07:37+00:00" + }, + { + "name": "workerman/workerman", + "version": "v5.1.9", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "fff0954628f8ceeccfe29d3e817f0fad87cfdbf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/fff0954628f8ceeccfe29d3e817f0fad87cfdbf2", + "reference": "fff0954628f8ceeccfe29d3e817f0fad87cfdbf2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "workerman/coroutine": "^1.1 || dev-main" + }, + "conflict": { + "ext-swow": " 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, + ], + +]; diff --git a/config/spectrum.php b/config/spectrum.php new file mode 100644 index 0000000..3bbb5df --- /dev/null +++ b/config/spectrum.php @@ -0,0 +1,50 @@ + 'public/docs', + + // Base API documentation information + 'title' => env('APP_NAME', 'Laravel API'), + 'description' => 'API Documentation', + 'version' => '1.0.0', + + // Server URL (uses APP_URL from .env) + 'server_url' => env('APP_URL'), + + // Additional servers for documentation + 'servers' => [ + [ + 'url' => env('APP_URL'), + 'description' => 'Local server', + ], + ], + + // Security schemes + 'security_schemes' => [ + 'bearerAuth' => [ + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ], + ], + + // Default security scheme to apply + 'default_security_scheme' => 'bearerAuth', + + // Paths to scan for OpenAPI attributes + 'paths' => [ + 'controllers' => [ + 'App\\Http\\Controllers\\Api\\', + ], + ], +]; diff --git a/database/migrations/2026_03_20_032532_create_personal_access_tokens_table.php b/database/migrations/2026_03_20_032532_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2026_03_20_032532_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/public/docs/index.html b/public/docs/index.html new file mode 100644 index 0000000..079545e --- /dev/null +++ b/public/docs/index.html @@ -0,0 +1,26 @@ + + + + + + API Documentation - Swagger UI + + + +
+ + + + diff --git a/public/docs/openapi.yaml b/public/docs/openapi.yaml new file mode 100644 index 0000000..9fe1ed3 --- /dev/null +++ b/public/docs/openapi.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.0 +info: + title: Laravel + description: 'API Documentation' + version: 1.0.0 +servers: + - + url: 'http://la.test' + description: 'Local server' +paths: + /api/register: + post: + tags: [Auth] + summary: 'Register a new user' + requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, email, password, password_confirmation], properties: { name: { type: string, example: 'John Doe' }, email: { type: string, format: email, example: john@example.com }, password: { type: string, format: password, example: password123 }, password_confirmation: { type: string, format: password, example: password123 } } } } } } + responses: { 201: { description: 'User registered successfully' }, 422: { description: 'Validation error' } } + /api/login: + post: + tags: [Auth] + summary: 'Login user' + requestBody: { required: true, content: { application/json: { schema: { type: object, required: [email, password], properties: { email: { type: string, format: email, example: john@example.com }, password: { type: string, format: password, example: password123 } } } } } } + responses: { 200: { description: 'Login successful' }, 401: { description: 'Invalid credentials' } } + /api/logout: + post: + tags: [Auth] + summary: 'Logout user' + security: [{ bearerAuth: { } }] + responses: { 200: { description: 'Logged out successfully' } } + /api/user: + get: + tags: [Auth] + summary: 'Get authenticated user' + security: [{ bearerAuth: { } }] + responses: { 200: { description: 'User data' } } + /api/posts: + get: + tags: [Posts] + summary: 'Get all posts' + security: [{ bearerAuth: { } }] + responses: { 200: { description: 'List of posts' } } + post: + tags: [Posts] + summary: 'Create a new post' + security: [{ bearerAuth: { } }] + requestBody: { required: true, content: { application/json: { schema: { type: object, required: [title, content], properties: { title: { type: string, example: 'My Post Title' }, content: { type: string, example: 'Post content here...' } } } } } } + responses: { 201: { description: 'Post created successfully' }, 422: { description: 'Validation error' } } + '/api/posts/{id}': + get: + tags: [Posts] + summary: 'Get a post' + security: [{ bearerAuth: { } }] + parameters: [{ name: id, in: path, required: true, schema: { type: integer } }] + responses: { 200: { description: 'Post data' }, 404: { description: 'Post not found' } } + put: + tags: [Posts] + summary: 'Update a post' + security: [{ bearerAuth: { } }] + parameters: [{ name: id, in: path, required: true, schema: { type: integer } }] + requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { title: { type: string, example: 'Updated Title' }, content: { type: string, example: 'Updated content...' } } } } } } + responses: { 200: { description: 'Post updated successfully' }, 404: { description: 'Post not found' }, 422: { description: 'Validation error' } } + delete: + tags: [Posts] + summary: 'Delete a post' + security: [{ bearerAuth: { } }] + parameters: [{ name: id, in: path, required: true, schema: { type: integer } }] + responses: { 200: { description: 'Post deleted successfully' }, 404: { description: 'Post not found' } } +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + User: + type: object + properties: { id: { type: integer }, name: { type: string }, email: { type: string, format: email }, role: { type: string }, created_at: { type: string, format: date-time } } + Post: + type: object + properties: { id: { type: integer }, title: { type: string }, content: { type: string }, user_id: { type: integer }, user: { $ref: '#/components/schemas/User' }, created_at: { type: string, format: date-time }, updated_at: { type: string, format: date-time } } diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..0469f20 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,39 @@ +to('/docs/index.html'); +}); + +// Public routes +Route::post('/register', [AuthController::class, 'register'])->name('api.register'); +Route::post('/login', [AuthController::class, 'login'])->name('api.login'); + +// Protected routes +Route::middleware('auth:sanctum')->group(function () { + // Auth + Route::post('/logout', [AuthController::class, 'logout'])->name('api.logout'); + Route::get('/user', [AuthController::class, 'user'])->name('api.user'); + + // Posts + Route::get('/posts', [PostController::class, 'index'])->name('api.posts.index'); + Route::post('/posts', [PostController::class, 'store'])->name('api.posts.store'); + Route::get('/posts/{post}', [PostController::class, 'show'])->name('api.posts.show'); + Route::put('/posts/{post}', [PostController::class, 'update'])->name('api.posts.update'); + Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('api.posts.destroy'); +});