Hitcount, changed categories and updated README

This commit is contained in:
Stepan 2025-11-11 14:31:56 +01:00
parent 8ca7fa0fd0
commit 13dd3b8b6d
16 changed files with 1463 additions and 55 deletions

View File

@ -1,4 +1,4 @@
APP_NAME=Laravel
APP_NAME=HoshiAI
APP_ENV=local
APP_KEY=
APP_DEBUG=true

138
README.md
View File

@ -1,59 +1,107 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
# HoshiAI Laravel (backend)
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
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 projects 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).

View File

@ -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'];
}

View File

@ -0,0 +1,177 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\HitcountResource;
use App\Models\Hitcount;
use App\Models\Log;
use Detection\MobileDetect;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class HitcountController extends Controller
{
/**
* @OA\Get(
* path="/api/hitcounts",
* summary="Get list of hitcounts (paginated, only admin)",
* description="Returns a paginated list of all recorded hitcounts. Requires admin privileges.",
* operationId="getHitcounts",
* tags={"Hitcounts"},
* security={{"bearerAuth":{}}},
* @OA\Response(
* response=200,
* description="List of hitcounts",
* @OA\JsonContent(
* type="object",
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/HitcountResource")
* ),
* @OA\Property(property="links", type="object"),
* @OA\Property(property="meta", type="object")
* )
* ),
* @OA\Response(response=401, description="Unauthenticated"),
* @OA\Response(response=403, description="Forbidden (not admin)")
* )
*/
public function index()
{
$this->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'];
}
}

View File

@ -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));
}
/**

View File

@ -0,0 +1,259 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\UserResource;
use App\Models\Log;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
private const FIELD_RULES = [
'username' => '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);
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class HitcountResource extends JsonResource
{
/**
* @OA\Schema(
* schema="HitcountResource",
* type="object",
* title="Hitcount",
* description="Hitcount resource",
* @OA\Property(
* property="id",
* type="integer",
* example=1
* ),
* @OA\Property(
* property="ip",
* type="string",
* example="127.0.0.1"
* ),
* @OA\Property(
* property="device_type",
* type="string",
* example="desktop"
* ),
* @OA\Property(
* property="user_agent",
* type="string",
* example="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0"
* ),
* @OA\Property(
* property="country",
* type="string",
* example="Japan"
* ),
* @OA\Property(
* property="url",
* type="string",
* example="https://my-application.com"
* ),
* @OA\Property(
* property="created_at",
* type="string",
* format="date-time",
* example="2025-11-10T17:45:00.000000Z"
* )
* )
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'ip' => $this->ip,
'device_type' => $this->device_type,
'user_agent' => $this->user_agent,
'country' => $this->country,
'url' => $this->url
];
}
}

16
app/Models/Hitcount.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Hitcount extends Model
{
protected $fillable = [
'ip',
'device_type',
'user_agent',
'country',
'url'
];
}

View File

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

View File

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

View File

@ -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",

67
composer.lock generated
View File

@ -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",

View File

@ -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();
});

View File

@ -0,0 +1,32 @@
<?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('hitcounts', function (Blueprint $table) {
$table->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');
}
};

View File

@ -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');

View File

@ -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"
}
]
}