diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 1579edf..adba8ff 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Http\Resources\UserResource; use App\Mail\EmailActivation; +use App\Models\Log; use App\Models\Token; use App\Models\User; use Illuminate\Http\Request; @@ -59,6 +60,8 @@ class AuthController extends Controller $user = User::create($fields); + Log::writeLog("User '" . $user->username . "' is registered"); + $token = Token::existsToken($user, 'email_verification'); if(!$token) { $token = Token::createToken($user, 'email_verification'); @@ -126,6 +129,8 @@ class AuthController extends Controller } $token = $user->createToken('auth_token')->plainTextToken; + Log::writeLog("User '" . $user->username . "' is logged into his account"); + return response()->json([ 'access_token' => $token, 'token_type' => 'Bearer', @@ -297,6 +302,8 @@ class AuthController extends Controller $user->password = $new_password; $user->save(); + Log::writeLog("User '" . $user->username . "' is resetted his password"); + return response()->json([ 'message' => 'Your password was successfully changed!' diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index f825fe3..44daeab 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -2,62 +2,185 @@ namespace App\Http\Controllers; -use App\Http\Requests\StoreCategoryRequest; -use App\Http\Requests\UpdateCategoryRequest; +use App\Http\Resources\CategoryResource; use App\Models\Category; +use App\Models\Log; use Illuminate\Http\Request; class CategoryController extends Controller { /** - * Display a listing of the resource. + * @OA\Get( + * path="/api/categories", + * summary="Get all categories", + * tags={"Categories"}, + * security={{"bearerAuth": {}}}, + * @OA\Response( + * response=200, + * description="List of categories", + * @OA\JsonContent( + * type="array", + * @OA\Items(ref="#/components/schemas/CategoryResource") + * ) + * ) + * ) */ public function index() { - return Category::all(); + return CategoryResource::collection(Category::all()); } /** - * Store a newly created resource in storage. + * @OA\Post( + * path="/api/categories", + * summary="Create a new category (only admin)", + * tags={"Categories"}, + * security={{"bearerAuth": {}}}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"name"}, + * @OA\Property(property="name", type="string", example="Physics") + * ) + * ), + * @OA\Response( + * response=200, + * description="Category created successfully", + * @OA\JsonContent(ref="#/components/schemas/CategoryResource") + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ) + * ) */ public function store(Request $request) { + $this->authorize('create', Category::class); $fields = $request->validate([ 'name' => 'required|max:150' ]); $category = Category::create($fields); - return $category; + Log::writeLog("Category '" . $category->name . "' is created"); + + return new CategoryResource($category); } /** - * Display the specified resource. + * @OA\Get( + * path="/api/categories/{id}", + * summary="Get a specific category", + * tags={"Categories"}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the category", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Category retrieved successfully", + * @OA\JsonContent(ref="#/components/schemas/CategoryResource") + * ), + * @OA\Response( + * response=404, + * description="Category not found" + * ) + * ) */ public function show(Category $category) { - return $category; + return new CategoryResource($category); } /** - * Update the specified resource in storage. + * @OA\Put( + * path="/api/categories/{id}", + * summary="Update a category (only admin)", + * tags={"Categories"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the category", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"name"}, + * @OA\Property(property="name", type="string", example="Physics Updated") + * ) + * ), + * @OA\Response( + * response=200, + * description="Category updated successfully", + * @OA\JsonContent(ref="#/components/schemas/CategoryResource") + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="Category not found" + * ) + * ) */ public function update(Request $request, Category $category) { + $this->authorize('update', $category); $fields = $request->validate([ 'name' => 'required|max:150' ]); + $old_category_name = $category->name; $category->update($fields); - return $category; + Log::writeLog("Category '$old_category_name' is renamed to '" . $category->name ."'"); + + return new CategoryResource($category); } /** - * Remove the specified resource from storage. + * @OA\Delete( + * path="/api/categories/{id}", + * summary="Delete a category (only admin)", + * tags={"Categories"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the category", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Category deleted successfully", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="The category was deleted") + * ) + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="Category not found" + * ) + * ) */ public function destroy(Category $category) { + $this->authorize('delete', $category); $category->delete(); + Log::writeLog("Category '" . $category->name . "' is deleted"); + return ['message' => 'The category was deleted']; } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 3b19cdb..a22992a 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,6 +2,10 @@ namespace App\Http\Controllers; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Foundation\Validation\ValidatesRequests; + /** * @OA\Info( * title="HoshiAI API", @@ -11,5 +15,5 @@ namespace App\Http\Controllers; */ abstract class Controller { - // + use AuthorizesRequests, DispatchesJobs, ValidatesRequests; } diff --git a/app/Http/Controllers/LogController.php b/app/Http/Controllers/LogController.php new file mode 100644 index 0000000..28f17d0 --- /dev/null +++ b/app/Http/Controllers/LogController.php @@ -0,0 +1,114 @@ +authorize('viewAny', Log::class); + return LogResource::collection(Log::paginate()); + } + + /** + * @OA\Get( + * path="/api/logs/{id}", + * summary="Get a specific log (only admin)", + * tags={"Logs"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the log", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Log retrieved successfully", + * @OA\JsonContent(ref="#/components/schemas/LogResource") + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="Log not found" + * ) + * ) + */ + public function show(Log $log) + { + $this->authorize('view', $log); + return $log; + } + + /** + * @OA\Delete( + * path="/api/logs/{id}", + * summary="Delete a specific log (only admin)", + * tags={"Logs"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="ID of the log", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Log deleted successfully", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="This log is successfully deleted") + * ) + * ), + * @OA\Response( + * response=403, + * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="Log not found" + * ) + * ) + */ + public function destroy(Log $log) + { + $this->authorize('delete', $log); + + $log->delete(); + return response()->json([ + 'message' => 'This log is successfully deleted' + ]); + } +} diff --git a/app/Http/Resources/CategoryResource.php b/app/Http/Resources/CategoryResource.php new file mode 100644 index 0000000..e71515a --- /dev/null +++ b/app/Http/Resources/CategoryResource.php @@ -0,0 +1,54 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'created_at' => $this->created_at, + 'count' => $this->count ?? 0 + ]; + } +} diff --git a/app/Http/Resources/LogResource.php b/app/Http/Resources/LogResource.php new file mode 100644 index 0000000..6a96e66 --- /dev/null +++ b/app/Http/Resources/LogResource.php @@ -0,0 +1,47 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'description' => $this->description, + 'created_at' => $this->created_at + ]; + } +} diff --git a/app/Models/Log.php b/app/Models/Log.php new file mode 100644 index 0000000..6494935 --- /dev/null +++ b/app/Models/Log.php @@ -0,0 +1,40 @@ + + */ + protected $fillable = [ + 'description', + 'type' + ]; + + /** + * Creating new log + * @param string $descpr + * @return void + */ + public static function writeLog(string $descpr, string $type = 'access') + { + self::create(['description' => $descpr, 'type' => $type]); + } + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'created_at' => 'datetime' + ]; + } +} diff --git a/app/Policies/CategoryPolicy.php b/app/Policies/CategoryPolicy.php index 516746c..ca91e9c 100644 --- a/app/Policies/CategoryPolicy.php +++ b/app/Policies/CategoryPolicy.php @@ -8,22 +8,6 @@ use Illuminate\Auth\Access\Response; class CategoryPolicy { - /** - * 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, Category $category): bool - { - return false; - } - /** * Determine whether the user can create models. */ @@ -47,20 +31,4 @@ class CategoryPolicy { return $user->type === 'admin'; } - - /** - * Determine whether the user can restore the model. - */ - public function restore(User $user, Category $category): bool - { - return false; - } - - /** - * Determine whether the user can permanently delete the model. - */ - public function forceDelete(User $user, Category $category): bool - { - return false; - } } diff --git a/app/Policies/LogPolicy.php b/app/Policies/LogPolicy.php new file mode 100644 index 0000000..2d511a3 --- /dev/null +++ b/app/Policies/LogPolicy.php @@ -0,0 +1,34 @@ +type === 'admin'; + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Log $log): bool + { + return $user->type === 'admin'; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Log $log): bool + { + return $user->type === 'admin'; + } +} diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..b774d91 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,11 @@ return [ 'driver' => 'session', 'provider' => 'users', ], + + 'sanctum' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + ], ], /* diff --git a/config/l5-swagger.php b/config/l5-swagger.php index 8f46faf..1100429 100644 --- a/config/l5-swagger.php +++ b/config/l5-swagger.php @@ -171,10 +171,10 @@ return [ 'securityDefinitions' => [ 'securitySchemes' => [ 'bearerAuth' => [ - 'type' => 'http', - 'scheme' => 'bearer', - 'bearerFormat' => 'JWT' - ], + 'type' => 'http', + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT' + ], /* * Examples of Security schemes */ diff --git a/database/migrations/2025_11_10_200607_create_logs_table.php b/database/migrations/2025_11_10_200607_create_logs_table.php new file mode 100644 index 0000000..5f57290 --- /dev/null +++ b/database/migrations/2025_11_10_200607_create_logs_table.php @@ -0,0 +1,29 @@ +id(); + $table->string("description", 255); + $table->enum('type', ['error', 'access'])->default('access'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('logs'); + } +}; diff --git a/routes/api.php b/routes/api.php index 35eb34f..6d8e10f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,14 +2,24 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\CategoryController; +use App\Http\Controllers\LogController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -// Route::get('/user', function (Request $request) { -// return $request->user(); -// })->middleware('auth:sanctum'); -Route::apiResource('categories', CategoryController::class); + +// CategoryController +Route::get('categories', [CategoryController::class, 'index']); +Route::get('categories/{category}', [CategoryController::class, 'show']); + +Route::middleware('auth:sanctum')->group(function () { + Route::post('categories', [CategoryController::class, 'store']); + Route::put('categories/{category}', [CategoryController::class, 'update']); + Route::delete('categories/{category}', [CategoryController::class, 'destroy']); +}); + +Route::apiResource('logs', LogController::class) + ->only(['index', 'show', 'destroy'])->middleware('auth:sanctum'); // AuthController Route::post('/auth/register', [ AuthController::class, 'register' ]); @@ -18,4 +28,8 @@ Route::post('/auth/logout', [ AuthController::class, 'logout' ])->middleware('au Route::post('/auth/forgot-password', [ AuthController::class, 'forgotPassword' ]); Route::post('/auth/reset-password', [ AuthController::class, 'resetPassword' ]); Route::post('/auth/activate-account', [ AuthController::class, 'confirmationAccount' ]); -Route::get('/auth/me', [ AuthController::class, 'me' ])->middleware('auth:sanctum'); \ No newline at end of file + +Route::middleware('auth:sanctum')->group(function () { + Route::post('/auth/logout', [ AuthController::class, 'logout' ]); + Route::get('/auth/me', [ AuthController::class, 'me' ])->middleware('auth:sanctum'); +}); \ No newline at end of file diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index a30eba1..c734f9c 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -546,10 +546,399 @@ } ] } + }, + "/api/categories": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Get all categories", + "operationId": "3f5817a34833d0a1f4af4548dd3aeaba", + "responses": { + "200": { + "description": "List of categories", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CategoryResource" + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Categories" + ], + "summary": "Create a new category (only admin)", + "operationId": "71fcad552bb0eaba9fb191fd8d8dcab0", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "example": "Physics" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Category created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryResource" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/categories/{id}": { + "get": { + "tags": [ + "Categories" + ], + "summary": "Get a specific category", + "operationId": "c68e76d323c008827a9e47398b1583de", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the category", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Category retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryResource" + } + } + } + }, + "404": { + "description": "Category not found" + } + } + }, + "put": { + "tags": [ + "Categories" + ], + "summary": "Update a category (only admin)", + "operationId": "0e686b2748211cc688333ed705dc111f", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the category", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "example": "Physics Updated" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Category updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryResource" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Category not found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Categories" + ], + "summary": "Delete a category (only admin)", + "operationId": "4c12e22a7f8c617bd83bfa1fdc05428d", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the category", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Category deleted successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The category was deleted" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Category not found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/logs": { + "get": { + "tags": [ + "Logs" + ], + "summary": "Get all logs (paginated, only admin)", + "operationId": "07258c00ce1b2cbc7c7151a7cc8ca986", + "responses": { + "200": { + "description": "List of logs", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogResource" + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/logs/{id}": { + "get": { + "tags": [ + "Logs" + ], + "summary": "Get a specific log (only admin)", + "operationId": "caa09131dde473dca25ea025d181146a", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the log", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Log retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogResource" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Log not found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Logs" + ], + "summary": "Delete a specific log (only admin)", + "operationId": "2a0e57b9168eaca7e207f5b35f469666", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of the log", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Log deleted successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "This log is successfully deleted" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Log not found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { "schemas": { + "CategoryResource": { + "title": "Category", + "description": "Category resource with dynamic count", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "Books" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2025-11-10T18:00:00Z" + }, + "count": { + "description": "Dynamic count of related items", + "type": "integer", + "example": 5 + } + }, + "type": "object" + }, + "LogResource": { + "title": "Log", + "description": "Log resource", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "description": { + "type": "string", + "example": "User logged in" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2025-11-10T17:45:00.000000Z" + } + }, + "type": "object" + }, "UserResource": { "properties": { "id": { @@ -596,6 +985,14 @@ { "name": "Auth", "description": "Auth" + }, + { + "name": "Categories", + "description": "Categories" + }, + { + "name": "Logs", + "description": "Logs" } ] } \ No newline at end of file