Начальный коммит: рабочая версия с исправленной авторизацией
This commit is contained in:
54
app/Http/Controllers/AiController.php
Normal file
54
app/Http/Controllers/AiController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\AiSuggestorService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AiController extends Controller
|
||||
{
|
||||
protected $aiService;
|
||||
|
||||
// ✅ Используем DI — Laravel сам передаст сервис
|
||||
public function __construct(AiSuggestorService $aiService)
|
||||
{
|
||||
$this->aiService = $aiService;
|
||||
}
|
||||
|
||||
public function suggest(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'task_id' => 'nullable|string',
|
||||
'custom_prompt' => 'nullable|string',
|
||||
'budget' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
try {
|
||||
// ✅ Вызываем через DI
|
||||
$result = $this->aiService->suggest(
|
||||
$request->input('task_id'),
|
||||
$request->input('custom_prompt'),
|
||||
$request->input('budget')
|
||||
);
|
||||
|
||||
// ✅ Возвращаем ТОЛЬКО ID — быстро и безопасно
|
||||
return response()->json([
|
||||
'message' => 'Сборка успешно сгенерирована ИИ.',
|
||||
'build_id' => $result['build']->id,
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Ошибка в AI-сервисе', [
|
||||
'user_id' => auth()->id(),
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Не удалось сгенерировать сборку.',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
app/Http/Controllers/AuthController.php
Normal file
82
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Регистрация пользователя.
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
'custom_field' => 'required|string|min:2'
|
||||
>>>>>>> origin/main
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
<<<<<<< HEAD
|
||||
'custom_field' => $request->custom_field ?? 'user', // ← ключевая строка
|
||||
=======
|
||||
'custom_field' => $validated['custom_field'],
|
||||
>>>>>>> origin/main
|
||||
]);
|
||||
|
||||
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' => 'Вы успешно вышли из системы.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
213
app/Http/Controllers/ComponentsController.php
Normal file
213
app/Http/Controllers/ComponentsController.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ComponentsController extends Controller
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
public function index(Request $request)
|
||||
{
|
||||
// Начинаем с базового запроса
|
||||
$query = Component::with('componentType'); // ← важно! Загружаем связь
|
||||
|
||||
// Фильтр по пользователю и статусу
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_official', true)
|
||||
->orWhere('created_by_user_id', auth()->id());
|
||||
});
|
||||
|
||||
// Если передан параметр type — фильтруем по code
|
||||
if ($request->has('type')) {
|
||||
$typeCode = $request->input('type');
|
||||
|
||||
// Убедимся, что такой тип существует
|
||||
$type = \App\Models\ComponentType::where('code', $typeCode)->first();
|
||||
if (!$type) {
|
||||
return response()->json([], 200); // пустой массив, если тип не найден
|
||||
}
|
||||
|
||||
// Фильтруем компоненты по component_type_id
|
||||
$query->where('component_type_id', $type->id);
|
||||
}
|
||||
|
||||
$components = $query->get();
|
||||
|
||||
// Добавляем поле "type" для удобства фронта
|
||||
$components = $components->map(function ($comp) {
|
||||
$comp->type = $comp->componentType?->code ?? 'unknown';
|
||||
return $comp;
|
||||
});
|
||||
|
||||
return response()->json($components);
|
||||
}
|
||||
=======
|
||||
public function index()
|
||||
{
|
||||
$components = Component::with('user', 'componentType')
|
||||
->where('is_official', true)
|
||||
->orWhere('created_by_user_id', auth()->id())
|
||||
->get();
|
||||
|
||||
return response()->json($components);
|
||||
}
|
||||
|
||||
|
||||
|
||||
>>>>>>> origin/main
|
||||
public function show($id)
|
||||
{
|
||||
$component = Component::find($id);
|
||||
|
||||
if (!$component) {
|
||||
return response()->json(['message' => 'Component not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json($component);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
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 = Component::create([
|
||||
'name' => $validated['name'],
|
||||
'price' => $validated['price'],
|
||||
'component_type_id' => $validated['component_type_id'],
|
||||
'specifications' => $validated['specifications'] ?? null,
|
||||
'is_official' => false,
|
||||
'created_by_user_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Компонент успешно создан.',
|
||||
'component' => $component
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$component = Component::findOrFail($id);
|
||||
|
||||
if ($component->created_by_user_id !== auth()->id() || $component->is_official) {
|
||||
return response()->json([
|
||||
'message' => 'Вы не можете редактировать этот компонент.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$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->update($validated);
|
||||
|
||||
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([
|
||||
'message' => 'Вы не можете удалить этот компонент.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$component->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Компонент удалён.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
=======
|
||||
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 = 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);
|
||||
|
||||
// Проверяем, что компонент принадлежит пользователю и не официальный
|
||||
if ($component->created_by_user_id !== auth()->id() || $component->is_official) {
|
||||
return response()->json([
|
||||
'message' => 'Вы не можете редактировать этот компонент.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$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->update($validated);
|
||||
|
||||
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([
|
||||
'message' => 'Вы не можете удалить этот компонент.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$component->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Компонент удалён.'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
>>>>>>> origin/main
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
246
app/Http/Controllers/PCBuildsController.php
Normal file
246
app/Http/Controllers/PCBuildsController.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PCBuild;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Services\BuildValidator;
|
||||
|
||||
class PCBuildsController extends Controller
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/builds",
|
||||
* summary="Получить список своих сборок",
|
||||
* tags={"PC Builds"},
|
||||
* @OA\Response(response=200, description="Список сборок"),
|
||||
* @OA\Response(response=401, description="Неавторизован")
|
||||
* )
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$builds = PCBuild::where('user_id', auth()->id())
|
||||
->with('components')
|
||||
->get();
|
||||
|
||||
return response()->json($builds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/builds",
|
||||
* summary="Создать новую сборку",
|
||||
* tags={"PC Builds"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"name", "component_ids"},
|
||||
* @OA\Property(property="name", type="string", example="Игровой ПК 2025"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="Для игр в 1440p"),
|
||||
* @OA\Property(property="component_ids", type="array", @OA\Items(type="integer", example=1)),
|
||||
* @OA\Property(property="is_ai_generated", type="boolean", default=false),
|
||||
* @OA\Property(property="ai_prompt", type="string", nullable=true, example="Сборка до 1000$ для игр")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=201, description="Сборка создана"),
|
||||
* @OA\Response(response=400, description="Ошибка валидации"),
|
||||
* @OA\Response(response=403, description="Запрещено: чужие компоненты")
|
||||
* )
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'component_ids' => 'required|array|min:1',
|
||||
'component_ids.*' => 'exists:components,id',
|
||||
'is_ai_generated' => 'nullable|boolean',
|
||||
'ai_prompt' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Проверяем: все компоненты — либо официальные, либо ваши
|
||||
$invalidComponents = Component::whereIn('id', $validated['component_ids'])
|
||||
->where(function ($query) {
|
||||
$query->where('is_official', false)
|
||||
->where('created_by_user_id', '!=', auth()->id());
|
||||
})
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
if (!empty($invalidComponents)) {
|
||||
return response()->json([
|
||||
'message' => 'Запрещено использовать неофициальные компоненты, созданные другими пользователями.',
|
||||
'invalid_components' => $invalidComponents
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 👇 ВСТАВЛЯЕМ ПРОВЕРКУ СОВМЕСТИМОСТИ ЗДЕСЬ — ПЕРЕД СОЗДАНИЕМ СБОРКИ
|
||||
$validator = new BuildValidator();
|
||||
$compatibility = $validator->validateCompatibility($validated['component_ids']);
|
||||
|
||||
if (!$compatibility['valid']) {
|
||||
return response()->json([
|
||||
'message' => 'Сборка содержит несовместимые компоненты.',
|
||||
'errors' => $compatibility['errors'],
|
||||
'warnings' => $compatibility['warnings']
|
||||
], 422); // 422 Unprocessable Entity
|
||||
}
|
||||
|
||||
// ✅ Только если совместимость OK — создаём сборку
|
||||
$build = PCBuild::create([
|
||||
'user_id' => auth()->id(),
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'is_ai_generated' => $validated['is_ai_generated'] ?? false,
|
||||
'ai_prompt' => $validated['ai_prompt'] ?? null,
|
||||
]);
|
||||
|
||||
$build->components()->attach($validated['component_ids']);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Сборка успешно создана.',
|
||||
'build' => $build->load('components'),
|
||||
'compatibility' => $compatibility // опционально — для отладки
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/builds/{id}",
|
||||
* summary="Получить одну сборку по ID",
|
||||
* tags={"PC Builds"},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Response(response=200, description="Сборка найдена"),
|
||||
* @OA\Response(response=403, description="Запрещено: не ваша сборка"),
|
||||
* @OA\Response(response=404, description="Не найдено")
|
||||
* )
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$build = PCBuild::with('components')->findOrFail($id);
|
||||
|
||||
if ($build->user_id !== auth()->id()) {
|
||||
return response()->json([
|
||||
'message' => 'Вы не можете просматривать эту сборку.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
return response()->json($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/builds/{id}",
|
||||
* summary="Обновить сборку",
|
||||
* tags={"PC Builds"},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"name", "component_ids"},
|
||||
* @OA\Property(property="name", type="string"),
|
||||
* @OA\Property(property="description", type="string", nullable=true),
|
||||
* @OA\Property(property="component_ids", type="array", @OA\Items(type="integer")),
|
||||
* @OA\Property(property="is_ai_generated", type="boolean"),
|
||||
* @OA\Property(property="ai_prompt", type="string", nullable=true)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Сборка обновлена"),
|
||||
* @OA\Response(response=403, description="Запрещено")
|
||||
* )
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$build = PCBuild::findOrFail($id);
|
||||
|
||||
if ($build->user_id !== auth()->id()) {
|
||||
return response()->json([
|
||||
'message' => 'Вы не можете редактировать эту сборку.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'component_ids' => 'required|array|min:1',
|
||||
'component_ids.*' => 'exists:components,id',
|
||||
'is_ai_generated' => 'nullable|boolean',
|
||||
'ai_prompt' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Проверка: все компоненты — либо официальные, либо ваши
|
||||
$invalidComponents = Component::whereIn('id', $validated['component_ids'])
|
||||
->where(function ($query) {
|
||||
$query->where('is_official', false)
|
||||
->where('created_by_user_id', '!=', auth()->id());
|
||||
})
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
|
||||
if (!empty($invalidComponents)) {
|
||||
return response()->json([
|
||||
'message' => 'Запрещено использовать неофициальные компоненты, созданные другими пользователями.',
|
||||
'invalid_components' => $invalidComponents
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 👇 ВСТАВЛЯЕМ ПРОВЕРКУ СОВМЕСТИМОСТИ ЗДЕСЬ — ПЕРЕД ОБНОВЛЕНИЕМ
|
||||
$validator = new BuildValidator();
|
||||
$compatibility = $validator->validateCompatibility($validated['component_ids']);
|
||||
|
||||
if (!$compatibility['valid']) {
|
||||
return response()->json([
|
||||
'message' => 'Сборка содержит несовместимые компоненты.',
|
||||
'errors' => $compatibility['errors'],
|
||||
'warnings' => $compatibility['warnings']
|
||||
], 422);
|
||||
}
|
||||
|
||||
// ✅ Обновляем сборку
|
||||
DB::transaction(function () use ($build, $validated) {
|
||||
$build->update([
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'is_ai_generated' => $validated['is_ai_generated'] ?? $build->is_ai_generated,
|
||||
'ai_prompt' => $validated['ai_prompt'] ?? $build->ai_prompt,
|
||||
]);
|
||||
|
||||
$build->components()->sync($validated['component_ids']);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Сборка обновлена.',
|
||||
'build' => $build->load('components'),
|
||||
'compatibility' => $compatibility // опционально
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/builds/{id}",
|
||||
* summary="Удалить сборку",
|
||||
* tags={"PC Builds"},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Response(response=200, description="Сборка удалена"),
|
||||
* @OA\Response(response=403, description="Запрещено")
|
||||
* )
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$build = PCBuild::findOrFail($id);
|
||||
|
||||
if ($build->user_id !== auth()->id()) {
|
||||
return response()->json([
|
||||
'message' => 'Вы не можете удалить эту сборку.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$build->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Сборка удалена.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
37
app/Http/Controllers/UsersController.php
Normal file
37
app/Http/Controllers/UsersController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
use App\Mail\RegisterUserMail;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
class UsersController extends Controller
|
||||
{
|
||||
public function create(Request $request){
|
||||
$user = new User();
|
||||
$name = $request->get('name');
|
||||
$email = $request->get('email');
|
||||
$password = Hash::make($request->get('password'));
|
||||
|
||||
$user->name = $name;
|
||||
$user->email = $email;
|
||||
$user->password = $password;
|
||||
|
||||
$user->save();
|
||||
|
||||
$adminEmail = 'dimon.cozlow2017@yandex.ru';
|
||||
|
||||
dispatch(function () use ($user, $adminEmail) {
|
||||
Mail::to($adminEmail)->send(new RegisterUserMail($user->name));
|
||||
});
|
||||
|
||||
return ['token' => $user->createToken('frontend')->plainTextToken];
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user