Questions

This commit is contained in:
Stepan 2025-11-11 19:41:58 +01:00
parent 13dd3b8b6d
commit 7dd13799e5
10 changed files with 1182 additions and 2 deletions

View File

@ -106,7 +106,7 @@ class HitcountController extends Controller
{ {
$this->authorize('delete', $hitcount); $this->authorize('delete', $hitcount);
$hitcount->delete(); $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']; return ['message' => 'The hitcount was deleted'];
} }

View File

@ -0,0 +1,315 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\QuestionResource;
use App\Models\Log;
use App\Models\Question;
use App\Rules\ValidVariants;
use Illuminate\Http\Request;
class QuestionController extends Controller
{
const PAGINATION_COUNT = 20;
private static function get_field_rules(): array
{
return [
'title' => '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'];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class QuestionResource extends JsonResource
{
/**
* @OA\Schema(
* schema="QuestionResource",
* type="object",
* title="Question",
* description="Question resource",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="title", type="string", example="What is the capital of Japan?"),
* @OA\Property(property="description", type="string", nullable=true, example="Choose the correct option"),
* @OA\Property(property="type", type="string", enum={"single","multiply","text"}, example="single"),
* @OA\Property(property="difficulty", type="integer", example=3),
* @OA\Property(
* property="variants",
* type="array",
* nullable=true,
* @OA\Items(
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="text", type="string", example="Tokyo")
* )
* ),
* @OA\Property(
* property="correct_answers",
* type="array",
* @OA\Items(type="integer", example=1)
* ),
* @OA\Property(property="is_pending_question", type="integer", example=0),
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
* @OA\Property(property="category", ref="#/components/schemas/CategoryResource"),
* @OA\Property(property="author_id", type="integer", nullable=true, example=2),
* @OA\Property(property="author", ref="#/components/schemas/UserResource"),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-11-11T10:00:00.000000Z"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-11-11T10:05:00.000000Z")
* )
*/
public function toArray(Request $request): array
{
return [
'id' => $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,
];
}
}

56
app/Models/Question.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'description',
'type',
'difficulty',
'category_id',
'author_id',
'variants',
'is_pending_question',
'correct_answers',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
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');
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Question;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class QuestionPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Question $question): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->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;
}
}

View File

@ -8,6 +8,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"darkaonline/l5-swagger": "^9.0", "darkaonline/l5-swagger": "^9.0",
"justinrainbow/json-schema": "^6.6",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",

150
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e96a8ca5efc08a222af10a6a43726369", "content-hash": "a58d7ff4f8de3898bf7fa6350013d42a",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -1210,6 +1210,81 @@
], ],
"time": "2025-08-22T14:27:06+00:00" "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", "name": "laravel/framework",
"version": "v12.37.0", "version": "v12.37.0",
@ -2230,6 +2305,79 @@
], ],
"time": "2024-12-08T08:18:47+00:00" "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", "name": "mobiledetect/mobiledetectlib",
"version": "4.8.09", "version": "4.8.09",

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('questions', function (Blueprint $table) {
$table->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');
}
};

View File

@ -4,9 +4,20 @@ use App\Http\Controllers\AuthController;
use App\Http\Controllers\CategoryController; use App\Http\Controllers\CategoryController;
use App\Http\Controllers\HitcountController; use App\Http\Controllers\HitcountController;
use App\Http\Controllers\LogController; use App\Http\Controllers\LogController;
use App\Http\Controllers\QuestionController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route; 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 // HitcountController
Route::post('hit', [HitcountController::class, 'callHit']); Route::post('hit', [HitcountController::class, 'callHit']);
Route::apiResource('hitcounts', HitcountController::class)->middleware('auth:sanctum'); Route::apiResource('hitcounts', HitcountController::class)->middleware('auth:sanctum');

View File

@ -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": { "/api/users": {
"get": { "get": {
"tags": [ "tags": [
@ -1538,6 +1924,93 @@
}, },
"type": "object" "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": { "UserResource": {
"properties": { "properties": {
"id": { "id": {
@ -1597,6 +2070,10 @@
"name": "Logs", "name": "Logs",
"description": "Logs" "description": "Logs"
}, },
{
"name": "Questions",
"description": "Questions"
},
{ {
"name": "Users", "name": "Users",
"description": "Users" "description": "Users"