From d8a87595045942e40a68a7af0cd6b81c89eaae7a Mon Sep 17 00:00:00 2001 From: dimon8 Date: Wed, 7 Jan 2026 19:29:49 +0000 Subject: [PATCH] crud --- app/Http/Controllers/AuthController.php | 74 +++++++++++++ app/Http/Controllers/ComponentsController.php | 102 +++++++++++------- app/Http/Controllers/UsersController.php | 3 - app/Models/AiTask.php | 42 ++++++++ app/Models/Component.php | 21 ++-- app/Models/PCBuildComponent.php | 35 ++++++ app/Models/User.php | 1 + composer.json | 2 +- composer.lock | 19 ++-- ...5_10_29_154421_create_components_table.php | 23 ++-- ...170250_add_custom_field_to_users_table.php | 2 +- ...64803_create_pc_build_components_table.php | 30 ++++++ ...026_01_07_071225_create_ai_tasks_table.php | 61 +++++++++++ routes/api.php | 26 ++++- 14 files changed, 363 insertions(+), 78 deletions(-) create mode 100644 app/Http/Controllers/AuthController.php create mode 100644 app/Models/AiTask.php create mode 100644 app/Models/PCBuildComponent.php create mode 100644 database/migrations/2026_01_07_064803_create_pc_build_components_table.php create mode 100644 database/migrations/2026_01_07_071225_create_ai_tasks_table.php diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..0e56944 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,74 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:8|confirmed', + 'custom_field' => 'required|string|min:2' + ]); + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => Hash::make($validated['password']), + 'custom_field' => $validated['custom_field'], + ]); + + return response()->json([ + 'message' => 'Пользователь зарегистрирован.', + 'user' => $user, + 'token' => $user->createToken('auth_token')->plainTextToken + ], 201); + } + + /** + * Вход пользователя. + */ + public function login(Request $request) + { + $request->validate([ + 'email' => 'required|email', + 'password' => 'required', + ]); + + $user = User::where('email', $request->email)->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['Неверные учётные данные.'], + ]); + } + + return response()->json([ + 'message' => 'Успешный вход.', + 'user' => $user, + 'token' => $user->createToken('auth_token')->plainTextToken + ]); + } + + /** + * Выход (инвалидация токена). + */ + public function logout(Request $request) + { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'message' => 'Вы успешно вышли из системы.' + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ComponentsController.php b/app/Http/Controllers/ComponentsController.php index 700515a..fa2b3f7 100644 --- a/app/Http/Controllers/ComponentsController.php +++ b/app/Http/Controllers/ComponentsController.php @@ -9,9 +9,15 @@ use Illuminate\Http\Response; class ComponentsController extends Controller { - public function index(){ - return response()->json(Component::all()->toJson()); - } +public function index() +{ + $components = Component::with('user', 'componentType') + ->where('is_official', true) + ->orWhere('created_by_user_id', auth()->id()) + ->get(); + + return response()->json($components); +} @@ -26,49 +32,73 @@ class ComponentsController extends Controller return response()->json($component); } - public function create(Request $request) - { - $name = $request->get(key:'name'); - $type = $request->get(key:'type'); - $brand = $request->get(key:'brand'); - $model = $request->get(key:'model'); - $price = $request->get(key:'price'); +public function store(Request $request) +{ + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|numeric|min:0', + 'component_type_id' => 'required|exists:component_types,id', + 'specifications' => 'nullable|array', + ]); - $component = new Component(); - $component->name = $name; - $component->type = $type; - $component->brand = $brand; - $component->model = $model; - $component->price = $price; - - $component->save(); + $component = Component::create([ + 'name' => $validated['name'], + 'price' => $validated['price'], + 'component_type_id' => $validated['component_type_id'], + 'specifications' => $validated['specifications'] ?? null, + 'is_official' => false, // всегда false для пользователя + 'created_by_user_id' => auth()->id(), // автоматически привязываем к пользователю + ]); + return response()->json([ + 'message' => 'Компонент успешно создан.', + 'component' => $component + ], 201); +} +public function update(Request $request, $id) +{ + $component = Component::findOrFail($id); - - return response()->json($component->toJson()); + // Проверяем, что компонент принадлежит пользователю и не официальный + if ($component->created_by_user_id !== auth()->id() || $component->is_official) { + return response()->json([ + 'message' => 'Вы не можете редактировать этот компонент.' + ], 403); } - public function update(Request $request, int $id): JsonResponse{ + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|numeric|min:0', + 'component_type_id' => 'required|exists:component_types,id', + 'specifications' => 'nullable|array', + ]); - return response()->json([ - 'name' => $request->get('name'), - 'type' => $request->get('type'), - 'brand' => $request->get('brand'), - 'model' => $request->get('model'), - 'price' => $request->get('price'), - ], Response::HTTP_ACCEPTED); - - } + $component->update($validated); - public function destroy(int $id): JsonResponse - { - // мы бы здесь написали вызов запроса delete из БД + return response()->json([ + 'message' => 'Компонент обновлён.', + 'component' => $component + ]); +} + +public function destroy($id) +{ + $component = Component::findOrFail($id); + + // Проверяем, что компонент принадлежит пользователю и не официальный + if ($component->created_by_user_id !== auth()->id() || $component->is_official) { return response()->json([ - 'success' => true, - ], Response::HTTP_ACCEPTED); + 'message' => 'Вы не можете удалить этот компонент.' + ], 403); } - + + $component->delete(); + + return response()->json([ + 'message' => 'Компонент удалён.' + ]); +} diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php index e6e3e72..5644c0f 100644 --- a/app/Http/Controllers/UsersController.php +++ b/app/Http/Controllers/UsersController.php @@ -33,8 +33,5 @@ class UsersController extends Controller return ['token' => $user->createToken('frontend')->plainTextToken]; - - - } } diff --git a/app/Models/AiTask.php b/app/Models/AiTask.php new file mode 100644 index 0000000..f912015 --- /dev/null +++ b/app/Models/AiTask.php @@ -0,0 +1,42 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + // Связь с пользователем (если шаблон пользовательский) + public function user() + { + return $this->belongsTo(User::class); + } + + // Общие (глобальные) шаблоны — где user_id IS NULL + public function scopeGlobal($query) + { + return $query->whereNull('user_id'); + } + + // Активные шаблоны + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} \ No newline at end of file diff --git a/app/Models/Component.php b/app/Models/Component.php index c08b33a..a0311d6 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -11,29 +11,24 @@ class Component extends Model protected $fillable = [ 'name', - 'component_type_id', 'price', + 'component_type_id', 'specifications', 'is_official', 'created_by_user_id', ]; protected $casts = [ - 'specifications' => 'array', // Автоматически преобразует JSON в массив - 'is_official' => 'boolean', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'specifications' => 'array', // автоматически преобразует JSON в массив ]; - // Связь с типом компонента - public function type() - { - return $this->belongsTo(ComponentType::class, 'component_type_id'); - } - - // Связь с пользователем (если добавил пользователь) - public function createdBy() + public function user() { return $this->belongsTo(User::class, 'created_by_user_id'); } + + public function componentType() + { + return $this->belongsTo(ComponentType::class, 'component_type_id'); + } } \ No newline at end of file diff --git a/app/Models/PCBuildComponent.php b/app/Models/PCBuildComponent.php new file mode 100644 index 0000000..da43944 --- /dev/null +++ b/app/Models/PCBuildComponent.php @@ -0,0 +1,35 @@ + 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + // Связь с сборкой + public function build() + { + return $this->belongsTo(PcBuild::class); + } + + // Связь с компонентом + public function component() + { + return $this->belongsTo(Component::class); + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index d0ad225..2c5b5a0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -24,6 +24,7 @@ class User extends Authenticatable 'name', 'email', 'password', + 'custom_field' ]; /** diff --git a/composer.json b/composer.json index 6ede6a4..bdebea5 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", - "laravel/sanctum": "^4.0", + "laravel/sanctum": "^4.2", "laravel/tinker": "^2.10.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 206bb00..304769f 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": "d3c16cb86c42230c6c023d9a5d9bcf42", + "content-hash": "8f387a0734f3bf879214e4aa2fca6e2f", "packages": [ { "name": "brick/math", @@ -1333,16 +1333,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.2.0", + "version": "v4.2.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + "reference": "fd447754d2d3f56950d53b930128af2e3b617de9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9", + "reference": "fd447754d2d3f56950d53b930128af2e3b617de9", "shasum": "" }, "require": { @@ -1356,9 +1356,8 @@ }, "require-dev": { "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^11.3" + "orchestra/testbench": "^9.15|^10.8", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -1393,7 +1392,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-07-09T19:45:24+00:00" + "time": "2026-01-06T23:11:51+00:00" }, { "name": "laravel/serializable-closure", @@ -8463,5 +8462,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/database/migrations/2025_10_29_154421_create_components_table.php b/database/migrations/2025_10_29_154421_create_components_table.php index 8dc9a76..ac06065 100644 --- a/database/migrations/2025_10_29_154421_create_components_table.php +++ b/database/migrations/2025_10_29_154421_create_components_table.php @@ -11,16 +11,19 @@ return new class extends Migration */ public function up(): void { - Schema::create('components', function (Blueprint $table) { - $table->id(); - $table->string('name'); // Например: "Intel Core i5-12400F" - $table->foreignId('component_type_id')->constrained()->onDelete('cascade'); - $table->decimal('price', 10, 2); - $table->json('specifications')->nullable(); // Для хранения характеристик - $table->boolean('is_official')->default(true); // true = админ, false = пользователь - $table->foreignId('created_by_user_id')->nullable()->constrained('users')->onDelete('set null'); - $table->timestamps(); - }); + Schema::create('components', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->decimal('price', 10, 2); + $table->unsignedBigInteger('component_type_id'); // ссылка на тип компонента + $table->json('specifications')->nullable(); // JSON-поле для характеристик + $table->boolean('is_official')->default(false); // официальный или нет + $table->unsignedBigInteger('created_by_user_id')->nullable(); // кто создал + $table->timestamps(); + + $table->foreign('component_type_id')->references('id')->on('component_types'); + $table->foreign('created_by_user_id')->references('id')->on('users')->onDelete('set null'); +}); } diff --git a/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php b/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php index c5d2643..7035542 100644 --- a/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php +++ b/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->text('custom_field'); + $table->text('custom_field')->nullable()->default(null); // }); } diff --git a/database/migrations/2026_01_07_064803_create_pc_build_components_table.php b/database/migrations/2026_01_07_064803_create_pc_build_components_table.php new file mode 100644 index 0000000..5204988 --- /dev/null +++ b/database/migrations/2026_01_07_064803_create_pc_build_components_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedBigInteger('pc_build_id'); + $table->unsignedBigInteger('component_id'); + $table->timestamps(); + + // Внешние ключи + $table->foreign('pc_build_id')->references('id')->on('pc_builds')->onDelete('cascade'); + $table->foreign('component_id')->references('id')->on('components')->onDelete('cascade'); + + // Уникальность пары (build + component), если нужно + $table->unique(['pc_build_id', 'component_id']); + }); + } + + public function down() + { + Schema::dropIfExists('pc_build_components'); + } +} \ No newline at end of file diff --git a/database/migrations/2026_01_07_071225_create_ai_tasks_table.php b/database/migrations/2026_01_07_071225_create_ai_tasks_table.php new file mode 100644 index 0000000..00443be --- /dev/null +++ b/database/migrations/2026_01_07_071225_create_ai_tasks_table.php @@ -0,0 +1,61 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); + $table->string('name'); + $table->text('prompt_template'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + // Индекс для поиска активных шаблонов + $table->index(['is_active']); + }); + + // Заполняем базовыми шаблонами от админа (user_id = NULL) + DB::table('ai_tasks')->insert([ + [ + 'name' => 'Игровой ПК до 50 000 ₽', + 'prompt_template' => 'Собери бюджетный игровой ПК до 50000 рублей. Цель: игры на средних настройках в 1080p.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Игровой ПК до 100 000 ₽', + 'prompt_template' => 'Собери мощный игровой ПК до 100000 рублей. Цель: игры на ультра в 1440p, 60+ FPS.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'Офисный ПК', + 'prompt_template' => 'Собери надёжный ПК для офиса и учёбы. Бюджет до 40000 рублей. Важна тишина и энергоэффективность.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + } + } + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ai_tasks'); + } +}; diff --git a/routes/api.php b/routes/api.php index 25cb6f5..1a724c4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,21 +4,39 @@ use App\Http\Controllers\UsersController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\ComponentsController; +use App\Http\Controllers\AuthController; Route::get('/users', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); -Route::get('components', [ComponentsController::class, 'index']); + Route::get('components/{id}', [ComponentsController::class, 'show']); -Route::post('components', [ComponentsController::class, 'create']); + Route::post('users', [UsersController::class, 'create']); -Route::put('/components', [ComponentsController::class, 'update']); -Route::delete('/components', [ComponentsController::class, 'destroy']); + + + +Route::post('/register', [AuthController::class, 'register']); +Route::post('/login', [AuthController::class, 'login']); + +// Защита маршрутов — только для авторизованных +Route::middleware('auth:sanctum')->group(function () { + Route::post('/logout', [AuthController::class, 'logout']); +}); + + + +Route::middleware('auth:sanctum')->group(function () { + Route::get('/components', [ComponentsController::class, 'index']); + Route::post('/components', [ComponentsController::class, 'store']); + Route::put('/components/{id}', [ComponentsController::class, 'update']); + Route::delete('/components/{id}', [ComponentsController::class, 'destroy']); +});