diff --git a/.env.example b/.env.example index c0660ea..654a61c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME=Laravel +APP_NAME=HoshiAI APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/README.md b/README.md index 0165a77..c9956b1 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,107 @@ -

Laravel Logo

+# HoshiAI Laravel (backend) -

-Build Status -Total Downloads -Latest Stable Version -License -

+HoshiAI-be — the backend part of the HoshiAI project for the Web Programming course at VTŠ. The project is built with Laravel and includes API documentation via Swagger. Authentication is handled using standard tokens. + +## Libraries + +* [Laravel (Sanctum for api)](https://laravel.com/) +* [L5-Swagger](https://github.com/DarkaOnLine/L5-Swagger) +* [mobiledetect/mobiledetectlib](https://packagist.org/packages/mobiledetect/mobiledetectlib) + +## Installation + +1. Install php and composer +2. Install all dependecies for this project +``` +composer install +``` +3. Copy `.env.example` and make `.env` file. Fill db, email and openai token +``` +DB_CONNECTION=mysql +DB_HOST= +DB_PORT= +DB_DATABASE= +DB_USERNAME= +DB_PASSWORD= +... +MAIL_MAILER= +MAIL_SCHEME= +MAIL_HOST= +MAIL_PORT= +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION= +MAIL_FROM_ADDRESS= +... +OPENAI_KEY= +``` +4. Make migrations to your db +``` +php artisan migrate +``` +5. Start dev server +``` +php artisan serve +``` +6. Build this project in `/public/` folder for deploy +``` +.... +``` + +*P.s. To access Swagger and view all API endpoints, go to `/swagger/`.* ## About Laravel -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: +**Laravel** is a modern PHP framework designed for building web applications quickly and efficiently. It provides an elegant syntax, powerful tools for routing, authentication, and database management, and follows the MVC (Model-View-Controller) architecture to help developers write clean and maintainable code. -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +### Sanctum -Laravel is accessible, powerful, and provides tools required for large, robust applications. +In this project we use **Laravel Sanctum**, lightweight authentication system for APIs in Laravel. It allows applications to issue and manage API tokens for users, enabling secure communication between clients (like web apps, mobile apps, or external services) and the backend. -## Learning Laravel +#### Examples +Group of routes wrapped with the auth:sanctum middleware. This means that only authenticated users with valid API tokens can access these endpoints: +```php +Route::middleware('auth:sanctum')->group(function () { + Route::get('/auth/me', [ AuthController::class, 'me' ])->middleware('auth:sanctum'); + Route::post('categories', [CategoryController::class, 'store']); + Route::put('categories/{category}', [CategoryController::class, 'update']); + Route::delete('categories/{category}', [CategoryController::class, 'destroy']); +}); +``` -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. +Below simple `me()` method that returns information about the currently authenticated user. It uses the $request->user() helper to retrieve the logged-in user +```php +public function me(Request $request) +{ + return response()->json([ + 'user' => new UserResource($request->user()) + ]); +} +``` -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. -## Laravel Sponsors +### Eloquent ORM -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). +For interacting with the database, we use `Eloquent ORM` (Object-Relational Mapper), which provides a simple and expressive syntax for working with database tables using PHP classes called models. Each model represents a table, and each model instance represents a record in that table. +```php +public function index() +{ + $this->authorize('viewAny', Log::class); + return LogResource::collection(Log::paginate(self::PAGINATED_COUNT)); +} +``` -### Premium Partners +### Base Laravel Structure for this project -- **[Vehikl](https://vehikl.com)** -- **[Tighten Co.](https://tighten.co)** -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** -- **[64 Robots](https://64robots.com)** -- **[Curotec](https://www.curotec.com/services/technologies/laravel)** -- **[DevSquad](https://devsquad.com/hire-laravel-developers)** -- **[Redberry](https://redberry.international/laravel-development)** -- **[Active Logic](https://activelogic.com)** +* `app/` — the main application directory that contains most of your project’s logic. + * `Http/` — contains controllers, middleware, and requests that handle incoming web and API requests. + * `Controllers/` - holds all controller classes that manage the logic between models and views or API responses. Controllers receive requests, process data (often using models), and return responses. + * `Resources/` - contains resource classes that transform models and data into structured JSON responses for APIs. + * `Mail/` — includes classes responsible for sending emails. + * `Models/` — holds Eloquent models that represent and interact with database tables. + * `Policies/` — defines authorization logic for various models and user actions. +* `routes/` — stores all route definitions for the application. + * `api.php` — contains routes specifically for API endpoints. +* `database/` — includes everything related to the database layer. + * `migrations/` — holds migration files used to create and modify database tables. -## Contributing - -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). - -## Code of Conduct - -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). - -## Security Vulnerabilities - -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. - -## License - -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 44daeab..8250750 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -62,7 +62,7 @@ class CategoryController extends Controller ]); $category = Category::create($fields); - Log::writeLog("Category '" . $category->name . "' is created"); + Log::writeLog("Category '" . $category->name . "' is created by " . $request->user()->username); return new CategoryResource($category); } @@ -139,7 +139,7 @@ class CategoryController extends Controller $old_category_name = $category->name; $category->update($fields); - Log::writeLog("Category '$old_category_name' is renamed to '" . $category->name ."'"); + Log::writeLog("Category '$old_category_name' is renamed to '" . $category->name ."' by " . $request->user()->username); return new CategoryResource($category); } @@ -174,12 +174,12 @@ class CategoryController extends Controller * ) * ) */ - public function destroy(Category $category) + public function destroy(Request $request, Category $category) { $this->authorize('delete', $category); $category->delete(); - Log::writeLog("Category '" . $category->name . "' is deleted"); + Log::writeLog("Category '" . $category->name . "' is deleted by " . $request->user()->username); return ['message' => 'The category was deleted']; } diff --git a/app/Http/Controllers/HitcountController.php b/app/Http/Controllers/HitcountController.php new file mode 100644 index 0000000..c6fa099 --- /dev/null +++ b/app/Http/Controllers/HitcountController.php @@ -0,0 +1,177 @@ +authorize('viewAny', Hitcount::class); + return HitcountResource::collection(Hitcount::paginate()); + } + + /** + * @OA\Get( + * path="/api/hitcounts/{id}", + * summary="Get a specific hitcount (only admin)", + * description="Returns detailed information about a specific hitcount. Requires admin privileges.", + * operationId="getHitcount", + * tags={"Hitcounts"}, + * security={{"bearerAuth":{}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="Hitcount ID", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Hitcount details", + * @OA\JsonContent(ref="#/components/schemas/HitcountResource") + * ), + * @OA\Response(response=401, description="Unauthenticated"), + * @OA\Response(response=403, description="Forbidden (not admin)"), + * @OA\Response(response=404, description="Not Found") + * ) + */ + public function show(Hitcount $hitcount) + { + $this->authorize('view', $hitcount); + return new HitcountResource($hitcount); + } + + /** + * @OA\Delete( + * path="/api/hitcounts/{id}", + * summary="Delete a hitcount (only admin)", + * description="Deletes a specific hitcount entry. Requires admin privileges.", + * operationId="deleteHitcount", + * tags={"Hitcounts"}, + * security={{"bearerAuth":{}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="Hitcount ID to delete", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="Hitcount deleted successfully", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="message", type="string", example="The hitcount was deleted") + * ) + * ), + * @OA\Response(response=401, description="Unauthenticated"), + * @OA\Response(response=403, description="Forbidden (not admin)"), + * @OA\Response(response=404, description="Not Found") + * ) + */ + public function destroy(Request $request, Hitcount $hitcount) + { + $this->authorize('delete', $hitcount); + $hitcount->delete(); + Log::writeLog("Category '" . $hitcount->id . "' is deleted by " . $request->user()->username); + return ['message' => 'The hitcount was deleted']; + } + + + /** + * @OA\Post( + * path="/api/hit", + * summary="Register a hit for a URL", + * description="Stores information about a visit, including IP, device, user agent, and country", + * tags={"Hitcounts"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"url"}, + * @OA\Property(property="url", type="string", maxLength=255, example="https://my-application.com") + * ) + * ), + * @OA\Response( + * response=200, + * description="Hitcount successfully set", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="The hitcount is set") + * ) + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="The given data was invalid."), + * @OA\Property( + * property="errors", + * type="object", + * example={"url": {"The url field is required."}} + * ) + * ) + * ) + * ) + */ + public function callHit(Request $request) + { + $fields = $request->validate([ + 'url' => 'required|max:255' + ]); + $hitcount = new Hitcount(); + $ip = $request->ip(); + $hitcount->ip = $ip; + $hitcount->user_agent = $request->userAgent(); + $hitcount->url = $fields['url']; + $detect = new MobileDetect(); + $hitcount->device_type = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'desktop'); + + $previousHitcount = Hitcount::where('ip', $ip)->first(); + if($previousHitcount) { + $hitcount->country = $previousHitcount->country; + } + else { + $response = Http::get("http://ip-api.com/json/{$hitcount->ip}?fields=country"); + if ($response->ok()) { + $hitcount->country = $response->json('country'); + } else { + $hitcount->country = null; + } + } + + $hitcount->save(); + return ['message' => 'The hitcount is set']; + } +} diff --git a/app/Http/Controllers/LogController.php b/app/Http/Controllers/LogController.php index 28f17d0..7470aaf 100644 --- a/app/Http/Controllers/LogController.php +++ b/app/Http/Controllers/LogController.php @@ -35,7 +35,7 @@ class LogController extends Controller public function index() { $this->authorize('viewAny', Log::class); - return LogResource::collection(Log::paginate()); + return LogResource::collection(Log::paginate(self::PAGINATED_COUNT)); } /** diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..64f00ee --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,259 @@ + 'required|max:100|unique:users', + 'email' => 'required|max:100|unique:users', + 'password' => 'required|min:6|confirmed', + 'type' => 'required|in:admin,user,creator,banned', + 'email_verified_at' => 'nullable|date' + ]; + private const PAGINATED_COUNT = 20; + /** + * @OA\Get( + * path="/api/users", + * summary="Get list of users (paginated, only admin)", + * tags={"Users"}, + * security={{"bearerAuth": {}}}, + * @OA\Response( + * response=200, + * description="List of users retrieved successfully", + * @OA\JsonContent( + * @OA\Property(property="data", type="array", + * @OA\Items(ref="#/components/schemas/UserResource") + * ), + * @OA\Property(property="links", type="object"), + * @OA\Property(property="meta", type="object") + * ) + * ), + * @OA\Response( + * response=403, + * description="Forbidden — user is not authorized to view users" + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated" + * ) + * ) + */ + public function index() + { + $this->authorize('viewAny', User::class); + return UserResource::collection(User::paginate(self::PAGINATED_COUNT)); + } + + /** + * @OA\Post( + * path="/api/users", + * summary="Create a new user (only admin)", + * tags={"Users"}, + * security={{"bearerAuth": {}}}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"username","email","password","type"}, + * @OA\Property(property="username", type="string", maxLength=100, example="newuser"), + * @OA\Property(property="email", type="string", format="email", maxLength=100, example="newuser@example.com"), + * @OA\Property(property="password", type="string", format="password", example="secret123"), + * @OA\Property(property="type", type="string", enum={"admin","user"}, example="user") + * ) + * ), + * @OA\Response( + * response=201, + * description="User created successfully", + * @OA\JsonContent(ref="#/components/schemas/UserResource") + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="The given data was invalid."), + * @OA\Property( + * property="errors", + * type="object", + * example={ + * "email": {"The email has already been taken."}, + * "password": {"The password must be at least 6 characters."} + * } + * ) + * ) + * ), + * @OA\Response( + * response=403, + * description="Forbidden — only admins can create users" + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated" + * ) + * ) + */ + public function store(Request $request) + { + $this->authorize('create', User::class); + $fields = $request->validate(self::FIELD_RULES); + $fields['password'] = Hash::make($fields['password']); + + $user = User::create($fields); + + Log::writeLog("User '" . $user->username . "' is created by " . $request->user()->username); + return new UserResource($user); + } + + /** + * @OA\Get( + * path="/api/users/{id}", + * summary="Get a specific user (only admin)", + * tags={"Users"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="User ID", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=200, + * description="User retrieved successfully", + * @OA\JsonContent(ref="#/components/schemas/UserResource") + * ), + * @OA\Response( + * response=404, + * description="User not found" + * ), + * @OA\Response( + * response=403, + * description="Forbidden — user not authorized to view this resource" + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated" + * ) + * ) + */ + public function show(User $user) + { + $this->authorize('view', $user); + return new UserResource($user); + } + + /** + * @OA\Put( + * path="/api/users/{id}", + * summary="Update an existing user (only admin)", + * tags={"Users"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="User ID", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"username","email","password","type"}, + * @OA\Property(property="username", type="string", maxLength=100, example="updated_user"), + * @OA\Property(property="email", type="string", format="email", maxLength=100, example="updated_user@example.com"), + * @OA\Property(property="password", type="string", format="password", example="newpassword123"), + * @OA\Property(property="type", type="string", enum={"admin","user"}, example="user") + * ) + * ), + * @OA\Response( + * response=200, + * description="User updated successfully", + * @OA\JsonContent(ref="#/components/schemas/UserResource") + * ), + * @OA\Response( + * response=404, + * description="User not found" + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="The given data was invalid."), + * @OA\Property( + * property="errors", + * type="object", + * example={ + * "email": {"The email has already been taken."}, + * "password": {"The password must be at least 6 characters."} + * } + * ) + * ) + * ), + * @OA\Response( + * response=403, + * description="Forbidden — only admins can update users" + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated" + * ) + * ) + */ + public function update(Request $request, User $user) + { + $this->authorize('update', $user); + $fields = $request->validate(self::FIELD_RULES); + + if(!Hash::check($fields['password'], $user->password)) { + $fields['password'] = Hash::make($fields['password']); + } + $user->update($fields); + + Log::writeLog("User '" . $user->username . "' is updated by " . $request->user()->username); + return new UserResource($user); + } + + /** + * @OA\Delete( + * path="/api/users/{id}", + * summary="Delete a user (only admin)", + * tags={"Users"}, + * security={{"bearerAuth": {}}}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="User ID", + * @OA\Schema(type="integer", example=1) + * ), + * @OA\Response( + * response=204, + * description="User deleted successfully (no content)" + * ), + * @OA\Response( + * response=404, + * description="User not found" + * ), + * @OA\Response( + * response=403, + * description="Forbidden — only admins can delete users" + * ), + * @OA\Response( + * response=401, + * description="Unauthenticated" + * ) + * ) + */ + public function destroy(Request $request, User $user) + { + $this->authorize('delete', $user); + $user->delete(); + + Log::writeLog("User '" . $user->username . "' is deleted by " . $request->user()->username); + } +} diff --git a/app/Http/Resources/HitcountResource.php b/app/Http/Resources/HitcountResource.php new file mode 100644 index 0000000..d11e89c --- /dev/null +++ b/app/Http/Resources/HitcountResource.php @@ -0,0 +1,65 @@ + $this->id, + 'ip' => $this->ip, + 'device_type' => $this->device_type, + 'user_agent' => $this->user_agent, + 'country' => $this->country, + 'url' => $this->url + ]; + } +} diff --git a/app/Models/Hitcount.php b/app/Models/Hitcount.php new file mode 100644 index 0000000..7c5d522 --- /dev/null +++ b/app/Models/Hitcount.php @@ -0,0 +1,16 @@ +type == 'admin'; + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Hitcount $hitcount): bool + { + return $user->type == 'admin'; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->type == 'admin'; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Hitcount $hitcount): bool + { + return $user->type == 'admin'; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Hitcount $hitcount): bool + { + return $user->type == 'admin'; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Hitcount $hitcount): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Hitcount $hitcount): bool + { + return false; + } +} diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000..84f42c4 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,65 @@ +type === 'admin'; + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, User $model): bool + { + return $user->type === 'admin'; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->type === 'admin'; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, User $model): bool + { + return $user->type === 'admin'; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, User $model): bool + { + return $user->type === 'admin'; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, User $model): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, User $model): bool + { + return false; + } +} diff --git a/composer.json b/composer.json index 917bb39..1a9ffbe 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "darkaonline/l5-swagger": "^9.0", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "mobiledetect/mobiledetectlib": "^4.8" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 0cd9061..26c00dc 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": "fe342f52ccac646a44615b0807265923", + "content-hash": "e96a8ca5efc08a222af10a6a43726369", "packages": [ { "name": "brick/math", @@ -2230,6 +2230,71 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "mobiledetect/mobiledetectlib", + "version": "4.8.09", + "source": { + "type": "git", + "url": "https://github.com/serbanghita/Mobile-Detect.git", + "reference": "a06fe2e546a06bb8c2639d6823d5250b2efb3209" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/a06fe2e546a06bb8c2639d6823d5250b2efb3209", + "reference": "a06fe2e546a06bb8c2639d6823d5250b2efb3209", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "psr/cache": "^3.0", + "psr/simple-cache": "^3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.65.0", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.12.x-dev", + "phpunit/phpunit": "^9.6.18", + "squizlabs/php_codesniffer": "^3.11.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Detection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Serban Ghita", + "email": "serbanghita@gmail.com", + "homepage": "http://mobiledetect.net", + "role": "Developer" + } + ], + "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.", + "homepage": "https://github.com/serbanghita/Mobile-Detect", + "keywords": [ + "detect mobile devices", + "mobile", + "mobile detect", + "mobile detector", + "php mobile detect" + ], + "support": { + "issues": "https://github.com/serbanghita/Mobile-Detect/issues", + "source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.09" + }, + "funding": [ + { + "url": "https://github.com/serbanghita", + "type": "github" + } + ], + "time": "2024-12-10T15:32:06+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index d321908..5b8bcb9 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -17,7 +17,7 @@ return new class extends Migration $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); - $table->enum('type', ['admin', 'user', 'moderator'])->default('user'); + $table->enum('type', ['admin', 'user', 'creator', 'banned'])->default('user'); $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2025_11_11_094046_create_hitcounts_table.php b/database/migrations/2025_11_11_094046_create_hitcounts_table.php new file mode 100644 index 0000000..68f4a08 --- /dev/null +++ b/database/migrations/2025_11_11_094046_create_hitcounts_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('ip', 40); + $table->enum('device_type', ['tablet', 'desktop', 'phone']); + $table->string('user_agent', 255); + $table->string('country', 255)->nullable(); + $table->string('url', 255); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('hitcounts'); + } +}; diff --git a/routes/api.php b/routes/api.php index 6d8e10f..b32eab1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,11 +2,17 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\CategoryController; +use App\Http\Controllers\HitcountController; use App\Http\Controllers\LogController; -use Illuminate\Http\Request; +use App\Http\Controllers\UserController; use Illuminate\Support\Facades\Route; +// HitcountController +Route::post('hit', [HitcountController::class, 'callHit']); +Route::apiResource('hitcounts', HitcountController::class)->middleware('auth:sanctum'); +// UserController +Route::apiResource('users', UserController::class)->middleware('auth:sanctum'); // CategoryController Route::get('categories', [CategoryController::class, 'index']); @@ -18,6 +24,7 @@ Route::middleware('auth:sanctum')->group(function () { Route::delete('categories/{category}', [CategoryController::class, 'destroy']); }); +// LogsController Route::apiResource('logs', LogController::class) ->only(['index', 'show', 'destroy'])->middleware('auth:sanctum'); diff --git a/storage/api-docs/api-docs.json b/storage/api-docs/api-docs.json index c734f9c..33e7318 100644 --- a/storage/api-docs/api-docs.json +++ b/storage/api-docs/api-docs.json @@ -767,6 +767,225 @@ ] } }, + "/api/hitcounts": { + "get": { + "tags": [ + "Hitcounts" + ], + "summary": "Get list of hitcounts (paginated, only admin)", + "description": "Returns a paginated list of all recorded hitcounts. Requires admin privileges.", + "operationId": "getHitcounts", + "responses": { + "200": { + "description": "List of hitcounts", + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HitcountResource" + } + }, + "links": { + "type": "object" + }, + "meta": { + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthenticated" + }, + "403": { + "description": "Forbidden (not admin)" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/hitcounts/{id}": { + "get": { + "tags": [ + "Hitcounts" + ], + "summary": "Get a specific hitcount (only admin)", + "description": "Returns detailed information about a specific hitcount. Requires admin privileges.", + "operationId": "getHitcount", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Hitcount ID", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Hitcount details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HitcountResource" + } + } + } + }, + "401": { + "description": "Unauthenticated" + }, + "403": { + "description": "Forbidden (not admin)" + }, + "404": { + "description": "Not Found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Hitcounts" + ], + "summary": "Delete a hitcount (only admin)", + "description": "Deletes a specific hitcount entry. Requires admin privileges.", + "operationId": "deleteHitcount", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Hitcount ID to delete", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "Hitcount deleted successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The hitcount was deleted" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthenticated" + }, + "403": { + "description": "Forbidden (not admin)" + }, + "404": { + "description": "Not Found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/hit": { + "post": { + "tags": [ + "Hitcounts" + ], + "summary": "Register a hit for a URL", + "description": "Stores information about a visit, including IP, device, user agent, and country", + "operationId": "8297226e4c0d09764edb0821edbf282f", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "maxLength": 255, + "example": "https://my-application.com" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Hitcount successfully set", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The hitcount is set" + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The given data was invalid." + }, + "errors": { + "type": "object", + "example": { + "url": [ + "The url field is required." + ] + } + } + }, + "type": "object" + } + } + } + } + } + } + }, "/api/logs": { "get": { "tags": [ @@ -890,6 +1109,350 @@ } ] } + }, + "/api/users": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get list of users (paginated, only admin)", + "operationId": "c457726701591d1183b53aa71fc13441", + "responses": { + "200": { + "description": "List of users retrieved successfully", + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResource" + } + }, + "links": { + "type": "object" + }, + "meta": { + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "Forbidden — user is not authorized to view users" + }, + "401": { + "description": "Unauthenticated" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Users" + ], + "summary": "Create a new user (only admin)", + "operationId": "592819a0265360b2014512d6dbfaf0e7", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username", + "email", + "password", + "type" + ], + "properties": { + "username": { + "type": "string", + "maxLength": 100, + "example": "newuser" + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 100, + "example": "newuser@example.com" + }, + "password": { + "type": "string", + "format": "password", + "example": "secret123" + }, + "type": { + "type": "string", + "enum": [ + "admin", + "user" + ], + "example": "user" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "User created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResource" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The given data was invalid." + }, + "errors": { + "type": "object", + "example": { + "email": [ + "The email has already been taken." + ], + "password": [ + "The password must be at least 6 characters." + ] + } + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "Forbidden — only admins can create users" + }, + "401": { + "description": "Unauthenticated" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/users/{id}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get a specific user (only admin)", + "operationId": "36a33ff774d5cba33c039dec2c3e0287", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "User ID", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "200": { + "description": "User retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResource" + } + } + } + }, + "404": { + "description": "User not found" + }, + "403": { + "description": "Forbidden — user not authorized to view this resource" + }, + "401": { + "description": "Unauthenticated" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": [ + "Users" + ], + "summary": "Update an existing user (only admin)", + "operationId": "b9091397c8b25f12c6adb74be6ce3a5a", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "User ID", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "username", + "email", + "password", + "type" + ], + "properties": { + "username": { + "type": "string", + "maxLength": 100, + "example": "updated_user" + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 100, + "example": "updated_user@example.com" + }, + "password": { + "type": "string", + "format": "password", + "example": "newpassword123" + }, + "type": { + "type": "string", + "enum": [ + "admin", + "user" + ], + "example": "user" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "User updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResource" + } + } + } + }, + "404": { + "description": "User not found" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "The given data was invalid." + }, + "errors": { + "type": "object", + "example": { + "email": [ + "The email has already been taken." + ], + "password": [ + "The password must be at least 6 characters." + ] + } + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "Forbidden — only admins can update users" + }, + "401": { + "description": "Unauthenticated" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Users" + ], + "summary": "Delete a user (only admin)", + "operationId": "fa56cffde745d3f152f95cbacd936c0b", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "User ID", + "required": true, + "schema": { + "type": "integer", + "example": 1 + } + } + ], + "responses": { + "204": { + "description": "User deleted successfully (no content)" + }, + "404": { + "description": "User not found" + }, + "403": { + "description": "Forbidden — only admins can delete users" + }, + "401": { + "description": "Unauthenticated" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { @@ -919,6 +1482,42 @@ }, "type": "object" }, + "HitcountResource": { + "title": "Hitcount", + "description": "Hitcount resource", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "ip": { + "type": "string", + "example": "127.0.0.1" + }, + "device_type": { + "type": "string", + "example": "desktop" + }, + "user_agent": { + "type": "string", + "example": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0" + }, + "country": { + "type": "string", + "example": "Japan" + }, + "url": { + "type": "string", + "example": "https://my-application.com" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2025-11-10T17:45:00.000000Z" + } + }, + "type": "object" + }, "LogResource": { "title": "Log", "description": "Log resource", @@ -990,9 +1589,17 @@ "name": "Categories", "description": "Categories" }, + { + "name": "Hitcounts", + "description": "Hitcounts" + }, { "name": "Logs", "description": "Logs" + }, + { + "name": "Users", + "description": "Users" } ] } \ No newline at end of file