From 7dd13799e5d15bf1a52a74ebc748b3afe4ec9a92 Mon Sep 17 00:00:00 2001 From: Stepan Date: Tue, 11 Nov 2025 19:41:58 +0100 Subject: [PATCH] Questions --- app/Http/Controllers/HitcountController.php | 2 +- app/Http/Controllers/QuestionController.php | 315 ++++++++++++ app/Http/Resources/QuestionResource.php | 67 +++ app/Models/Question.php | 56 ++ app/Policies/QuestionPolicy.php | 66 +++ composer.json | 1 + composer.lock | 150 +++++- ...25_11_11_154002_create_questions_table.php | 39 ++ routes/api.php | 11 + storage/api-docs/api-docs.json | 477 ++++++++++++++++++ 10 files changed, 1182 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/QuestionController.php create mode 100644 app/Http/Resources/QuestionResource.php create mode 100644 app/Models/Question.php create mode 100644 app/Policies/QuestionPolicy.php create mode 100644 database/migrations/2025_11_11_154002_create_questions_table.php diff --git a/app/Http/Controllers/HitcountController.php b/app/Http/Controllers/HitcountController.php index c6fa099..936bcf2 100644 --- a/app/Http/Controllers/HitcountController.php +++ b/app/Http/Controllers/HitcountController.php @@ -106,7 +106,7 @@ class HitcountController extends Controller { $this->authorize('delete', $hitcount); $hitcount->delete(); - Log::writeLog("Category '" . $hitcount->id . "' is deleted by " . $request->user()->username); + Log::writeLog("Hitcount '" . $hitcount->id . "' is deleted by " . $request->user()->username); return ['message' => 'The hitcount was deleted']; } diff --git a/app/Http/Controllers/QuestionController.php b/app/Http/Controllers/QuestionController.php new file mode 100644 index 0000000..4d4d387 --- /dev/null +++ b/app/Http/Controllers/QuestionController.php @@ -0,0 +1,315 @@ + 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|in:single,multiple,text', + 'difficulty' => 'required|integer', + 'category_id' => 'nullable|exists:categories,id', + 'variants' => ['array', 'nullable'], + 'variants.*.id' => ['required', 'integer'], + 'variants.*.text' => ['required', 'string'], + 'correct_answers' => 'required|array', + 'is_pending_question' => 'integer' + ]; + } + /** + * @OA\Get( + * path="/api/questions", + * summary="Get a paginated list of questions (paginated)", + * description="Retrieve questions. Optional filter by category_id.", + * tags={"Questions"}, + * @OA\Parameter( + * name="category_id", + * in="query", + * description="Filter questions by category ID", + * required=false, + * @OA\Schema(type="integer") + * ), + * @OA\Parameter( + * name="page", + * in="query", + * description="Page number for pagination", + * required=false, + * @OA\Schema(type="integer", default=1) + * ), + * @OA\Response( + * response=200, + * description="Paginated list of questions", + * @OA\JsonContent( + * @OA\Property( + * property="data", + * type="array", + * @OA\Items(ref="#/components/schemas/QuestionResource") + * ), + * @OA\Property( + * property="links", + * type="object", + * example={ + * "first": "http://localhost/api/questions?page=1", + * "last": "http://localhost/api/questions?page=10", + * "prev": null, + * "next": "http://localhost/api/questions?page=2" + * } + * ), + * @OA\Property( + * property="meta", + * type="object", + * example={ + * "current_page": 1, + * "from": 1, + * "last_page": 10, + * "path": "http://localhost/api/questions", + * "per_page": 15, + * "to": 15, + * "total": 150 + * } + * ) + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ) + * ) + */ + public function index(Request $request) + { + $query = Question::with(['author', 'category']); + + if ($request->has('category_id')) { + $query->where('category_id', $request->query('category_id')); + } + + $questions = $query->paginate(self::PAGINATION_COUNT); + + return QuestionResource::collection($questions); + } + + /** + * @OA\Get( + * path="/api/questions/{id}", + * summary="Get a single question", + * description="Retrieve a single question by its ID, including author and category", + * tags={"Questions"}, + * @OA\Parameter( + * name="id", + * in="path", + * description="ID of the question to retrieve", + * required=true, + * @OA\Schema(type="integer") + * ), + * @OA\Response( + * response=200, + * description="Question retrieved successfully", + * @OA\JsonContent(ref="#/components/schemas/QuestionResource") + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=404, + * description="Question not found" + * ) + * ) + */ + public function show(Question $question) + { + $question->load(['author', 'category']); + return new QuestionResource($question); + } + + /** + * @OA\Post( + * path="/api/questions", + * summary="Create a new question", + * description="Store a new question in the system (only admin or creator).", + * tags={"Questions"}, + * security={{"bearerAuth":{}}}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"title","type","difficulty","variants","correct_answers"}, + * @OA\Property(property="title", type="string", maxLength=255, example="Sample question"), + * @OA\Property(property="description", type="string", nullable=true, example="Optional description"), + * @OA\Property(property="type", type="string", enum={"single","multiply","text"}, example="single"), + * @OA\Property(property="difficulty", type="integer", example=2), + * @OA\Property( + * property="variants", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="text", type="string", example="Option 1") + * ) + * ), + * @OA\Property( + * property="correct_answers", + * type="array", + * @OA\Items( + * type="integer", + * example=1 + * ) + * ), + * @OA\Property(property="category_id", type="integer", nullable=true, example=3), + * @OA\Property(property="is_pending_question", type="integer", example=0) + * ) + * ), + * @OA\Response( + * response=200, + * description="Question created successfully", + * @OA\JsonContent(ref="#/components/schemas/QuestionResource") + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=422, + * description="Validation error" + * ) + * ) + */ + public function store(Request $request) + { + $this->authorize('create', Question::class); + $fields = $request->validate(self::get_field_rules()); + $fields['author_id'] = $request->user()->id; + + $question = Question::create($fields); + + Log::writeLog("Question '" . $question->title . "' is created by " . $request->user()->username); + + return new QuestionResource($question); + } + + /** + * @OA\Put( + * path="/api/questions/{id}", + * summary="Update a question", + * description="Update an existing question by ID (only admin or creator).", + * tags={"Questions"}, + * security={{"bearerAuth":{}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the question to update", + * @OA\Schema(type="integer") + * ), + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"title","type","difficulty","variants","correct_answers"}, + * @OA\Property(property="title", type="string", maxLength=255, example="Updated question"), + * @OA\Property(property="description", type="string", nullable=true, example="Updated description"), + * @OA\Property(property="type", type="string", enum={"single","multiply","text"}, example="multiply"), + * @OA\Property(property="difficulty", type="integer", example=3), + * @OA\Property( + * property="variants", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="id", type="integer", example=1), + * @OA\Property(property="text", type="string", example="Option 1") + * ) + * ), + * @OA\Property( + * property="correct_answers", + * type="array", + * @OA\Items( + * type="integer", + * example=1 + * ) + * ), + * @OA\Property(property="category_id", type="integer", nullable=true, example=2), + * @OA\Property(property="is_pending_question", type="integer", example=0) + * ) + * ), + * @OA\Response( + * response=200, + * description="Question updated successfully", + * @OA\JsonContent(ref="#/components/schemas/QuestionResource") + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=422, + * description="Validation error" + * ) + * ) + */ + public function update(Request $request, Question $question) + { + $this->authorize('update', $question); + $fields = $request->validate(self::get_field_rules()); + $fields['variants'] = json_encode($fields['variants']); + + $question->update($fields); + + Log::writeLog("Question '" . $question->title . "' is updated by " . $request->user()->username); + + return new QuestionResource($question); + } + + /** + * @OA\Delete( + * path="/api/questions/{id}", + * summary="Delete a question", + * description="Delete a question by ID (only admin or creator).", + * tags={"Questions"}, + * security={{"bearerAuth":{}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the question to delete", + * @OA\Schema(type="integer") + * ), + * @OA\Response( + * response=200, + * description="Question deleted successfully", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="The hitcount was deleted") + * ) + * ), + * @OA\Response( + * response=401, + * description="Unauthorized" + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="Question not found" + * ) + * ) + */ + public function destroy(Request $request, Question $question) + { + $this->authorize('delete', $question); + $question->delete(); + Log::writeLog("Question '" . $question->id . "' is deleted by " . $request->user()->username); + + return ['message' => 'The hitcount was deleted']; + } +} diff --git a/app/Http/Resources/QuestionResource.php b/app/Http/Resources/QuestionResource.php new file mode 100644 index 0000000..ceaa97b --- /dev/null +++ b/app/Http/Resources/QuestionResource.php @@ -0,0 +1,67 @@ + $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'type' => $this->type, + 'difficulty' => $this->difficulty, + 'variants' => $this->variants, + 'correct_answers' => $this->correct_answers, + 'is_pending_question' => $this->is_pending_question, + + 'category_id' => $this->category_id, + 'category' => new CategoryResource($this->whenLoaded('category')), + + 'author_id' => $this->author_id, + 'author' => new UserResource($this->whenLoaded('author')), + + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Models/Question.php b/app/Models/Question.php new file mode 100644 index 0000000..4bdb804 --- /dev/null +++ b/app/Models/Question.php @@ -0,0 +1,56 @@ + + */ + protected $fillable = [ + 'title', + 'description', + 'type', + 'difficulty', + 'category_id', + 'author_id', + 'variants', + 'is_pending_question', + 'correct_answers', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'is_pending_question' => 'boolean', + 'correct_answers' => 'array', + 'variants' => 'array', + ]; + + /** + * Category relation (nullable, on delete set null). + */ + public function category() + { + return $this->belongsTo(Category::class); + } + + /** + * Author relation (nullable, on delete set null). + */ + public function author() + { + return $this->belongsTo(User::class, 'author_id'); + } + +} diff --git a/app/Policies/QuestionPolicy.php b/app/Policies/QuestionPolicy.php new file mode 100644 index 0000000..ae30fdf --- /dev/null +++ b/app/Policies/QuestionPolicy.php @@ -0,0 +1,66 @@ +type == 'admin' || $user->type == 'creator'; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Question $question): bool + { + return $user->type == 'admin' || $question->user_id == $user->id; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Question $question): bool + { + return $user->type == 'admin' || $question->user_id == $user->id; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Question $question): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Question $question): bool + { + return false; + } +} diff --git a/composer.json b/composer.json index 1a9ffbe..3b3bc8b 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "require": { "php": "^8.2", "darkaonline/l5-swagger": "^9.0", + "justinrainbow/json-schema": "^6.6", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10.1", diff --git a/composer.lock b/composer.lock index 26c00dc..8a62556 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": "e96a8ca5efc08a222af10a6a43726369", + "content-hash": "a58d7ff4f8de3898bf7fa6350013d42a", "packages": [ { "name": "brick/math", @@ -1210,6 +1210,81 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "6.6.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" + }, + "time": "2025-11-07T18:30:29+00:00" + }, { "name": "laravel/framework", "version": "v12.37.0", @@ -2230,6 +2305,79 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, { "name": "mobiledetect/mobiledetectlib", "version": "4.8.09", diff --git a/database/migrations/2025_11_11_154002_create_questions_table.php b/database/migrations/2025_11_11_154002_create_questions_table.php new file mode 100644 index 0000000..5f451c1 --- /dev/null +++ b/database/migrations/2025_11_11_154002_create_questions_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('title', 255); + $table->text('description')->nullable(); + $table->enum('type', ['single', 'multiple', 'text']); + $table->integer('difficulty'); + $table->unsignedBigInteger('category_id')->nullable(); + $table->unsignedBigInteger('author_id')->nullable(); + $table->text('variants'); + $table->tinyInteger('is_pending_question')->unsigned()->default(0); + $table->text('correct_answers')->default('[]'); + + $table->foreign('author_id')->references('id')->on('users')->onDelete('set null'); + $table->foreign('category_id')->references('id')->on('categories')->onDelete('set null'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('questions'); + } +}; diff --git a/routes/api.php b/routes/api.php index b32eab1..fdc9c79 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,9 +4,20 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\CategoryController; use App\Http\Controllers\HitcountController; use App\Http\Controllers\LogController; +use App\Http\Controllers\QuestionController; use App\Http\Controllers\UserController; use Illuminate\Support\Facades\Route; +// Questions +Route::get('questions', [QuestionController::class, 'index']); +Route::get('questions/{question}', [QuestionController::class, 'show']); + +Route::middleware('auth:sanctum')->group(function () { + Route::post('questions', [QuestionController::class, 'store']); + Route::put('questions/{question}', [QuestionController::class, 'update']); + Route::delete('questions/{question}', [QuestionController::class, 'destroy']); +}); + // HitcountController Route::post('hit', [HitcountController::class, 'callHit']); Route::apiResource('hitcounts', HitcountController::class)->middleware('auth:sanctum'); diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index 33e7318..142bfaa 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -1110,6 +1110,392 @@ ] } }, + "/api/questions": { + "get": { + "tags": [ + "Questions" + ], + "summary": "Get a paginated list of questions (paginated)", + "description": "Retrieve questions. Optional filter by category_id.", + "operationId": "e64d3c7a745fc05662a4a1e1eb3d96ab", + "parameters": [ + { + "name": "category_id", + "in": "query", + "description": "Filter questions by category ID", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + } + ], + "responses": { + "200": { + "description": "Paginated list of questions", + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionResource" + } + }, + "links": { + "type": "object", + "example": { + "first": "http://localhost/api/questions?page=1", + "last": "http://localhost/api/questions?page=10", + "prev": null, + "next": "http://localhost/api/questions?page=2" + } + }, + "meta": { + "type": "object", + "example": { + "current_page": 1, + "from": 1, + "last_page": 10, + "path": "http://localhost/api/questions", + "per_page": 15, + "to": 15, + "total": 150 + } + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + } + }, + "post": { + "tags": [ + "Questions" + ], + "summary": "Create a new question", + "description": "Store a new question in the system (only admin or creator).", + "operationId": "788d85763184ddf1b557afb040547f32", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "type", + "difficulty", + "variants", + "correct_answers" + ], + "properties": { + "title": { + "type": "string", + "maxLength": 255, + "example": "Sample question" + }, + "description": { + "type": "string", + "example": "Optional description", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "single", + "multiply", + "text" + ], + "example": "single" + }, + "difficulty": { + "type": "integer", + "example": 2 + }, + "variants": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "text": { + "type": "string", + "example": "Option 1" + } + }, + "type": "object" + } + }, + "correct_answers": { + "type": "array", + "items": { + "type": "integer", + "example": 1 + } + }, + "category_id": { + "type": "integer", + "example": 3, + "nullable": true + }, + "is_pending_question": { + "type": "integer", + "example": 0 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Question created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionResource" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "422": { + "description": "Validation error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/questions/{id}": { + "get": { + "tags": [ + "Questions" + ], + "summary": "Get a single question", + "description": "Retrieve a single question by its ID, including author and category", + "operationId": "bea45702e58c27163e9dc928d8984dfa", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the question to retrieve", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Question retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionResource" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Question not found" + } + } + }, + "put": { + "tags": [ + "Questions" + ], + "summary": "Update a question", + "description": "Update an existing question by ID (only admin or creator).", + "operationId": "795b02e5ecdc23fd74f20e0671a40f8c", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the question to update", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "title", + "type", + "difficulty", + "variants", + "correct_answers" + ], + "properties": { + "title": { + "type": "string", + "maxLength": 255, + "example": "Updated question" + }, + "description": { + "type": "string", + "example": "Updated description", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "single", + "multiply", + "text" + ], + "example": "multiply" + }, + "difficulty": { + "type": "integer", + "example": 3 + }, + "variants": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "text": { + "type": "string", + "example": "Option 1" + } + }, + "type": "object" + } + }, + "correct_answers": { + "type": "array", + "items": { + "type": "integer", + "example": 1 + } + }, + "category_id": { + "type": "integer", + "example": 2, + "nullable": true + }, + "is_pending_question": { + "type": "integer", + "example": 0 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Question updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionResource" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "422": { + "description": "Validation error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Questions" + ], + "summary": "Delete a question", + "description": "Delete a question by ID (only admin or creator).", + "operationId": "b2cbd34337a604c75c8a94f1a6e2f252", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the question to delete", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Question deleted successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The hitcount was deleted" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Question not found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/api/users": { "get": { "tags": [ @@ -1538,6 +1924,93 @@ }, "type": "object" }, + "QuestionResource": { + "title": "Question", + "description": "Question resource", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "title": { + "type": "string", + "example": "What is the capital of Japan?" + }, + "description": { + "type": "string", + "example": "Choose the correct option", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "single", + "multiply", + "text" + ], + "example": "single" + }, + "difficulty": { + "type": "integer", + "example": 3 + }, + "variants": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "text": { + "type": "string", + "example": "Tokyo" + } + }, + "type": "object" + }, + "nullable": true + }, + "correct_answers": { + "type": "array", + "items": { + "type": "integer", + "example": 1 + } + }, + "is_pending_question": { + "type": "integer", + "example": 0 + }, + "category_id": { + "type": "integer", + "example": 3, + "nullable": true + }, + "category": { + "$ref": "#/components/schemas/CategoryResource" + }, + "author_id": { + "type": "integer", + "example": 2, + "nullable": true + }, + "author": { + "$ref": "#/components/schemas/UserResource" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2025-11-11T10:00:00.000000Z" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "example": "2025-11-11T10:05:00.000000Z" + } + }, + "type": "object" + }, "UserResource": { "properties": { "id": { @@ -1597,6 +2070,10 @@ "name": "Logs", "description": "Logs" }, + { + "name": "Questions", + "description": "Questions" + }, { "name": "Users", "description": "Users"