Compare commits

..

8 Commits

32 changed files with 7962 additions and 59 deletions

28
.dockerignore Normal file
View File

@@ -0,0 +1,28 @@
.git
.github
.idea
.vscode
.env
.env.*
!.env.example
!.env.production.example
node_modules
vendor
npm-debug.log
yarn-error.log
storage/logs/*
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*.php
tests
coverage
.phpunit.cache
Dockerfile
docker-compose*.yml
README.md

68
.env.production.example Normal file
View File

@@ -0,0 +1,68 @@
APP_NAME=Hoshpoint
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://example.com
HTTP_PORT=8080
APP_LOCALE=fa
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=fa_IR
APP_MAINTENANCE_DRIVER=file
BCRYPT_ROUNDS=12
LOG_CHANNEL=stderr
LOG_STACK=stderr
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=info
DB_CONNECTION=mariadb
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=hoshpoint_backend
DB_USERNAME=hoshpoint
DB_PASSWORD=change-me
MARIADB_DATABASE=hoshpoint_backend
MARIADB_USER=hoshpoint
MARIADB_PASSWORD=change-me
MARIADB_ROOT_PASSWORD=change-root-password
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
MEMCACHED_HOST=memcached
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

101
Dockerfile Normal file
View File

@@ -0,0 +1,101 @@
# syntax=docker/dockerfile:1.7
ARG DOCKER_REGISTRY=docker.arvancloud.ir
ARG PHP_VERSION=8.4
FROM ${DOCKER_REGISTRY}/node:24-alpine AS assets
WORKDIR /app
RUN sed -i 's|https://dl-cdn.alpinelinux.org|https://mirror.arvancloud.ir/alpine|g' /etc/apk/repositories
COPY package.json package-lock.json ./
RUN npm ci
COPY resources ./resources
COPY public ./public
COPY vite.config.js ./
RUN npm run build
ARG DOCKER_REGISTRY
ARG PHP_VERSION
FROM ${DOCKER_REGISTRY}/php:${PHP_VERSION}-fpm-bookworm AS app
WORKDIR /var/www/html
ENV COMPOSER_ALLOW_SUPERUSER=1
COPY docker/php/install-php-extensions /usr/local/bin/install-php-extensions
RUN if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
sed -i \
-e 's|https\?://deb\.debian\.org/debian|http://mirror.arvancloud.ir/debian|g' \
-e 's|https\?://security\.debian\.org/debian-security|http://mirror.arvancloud.ir/debian|g' \
-e 's| bookworm-updates||g' \
/etc/apt/sources.list.d/debian.sources; \
fi \
&& apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
curl \
default-mysql-client \
unzip \
&& install-php-extensions \
bcmath \
dom \
intl \
mbstring \
opcache \
pcntl \
pdo_mysql \
xml \
xmlreader \
zip \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/*
COPY docker/php/php.ini /usr/local/etc/php/conf.d/99-production.ini
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/99-opcache.ini
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-autoloader \
--no-scripts \
--prefer-dist \
--no-interaction \
&& composer check-platform-reqs --no-dev
COPY . .
COPY --from=assets /app/public/build ./public/build
RUN composer dump-autoload --no-dev --optimize --no-scripts \
&& php artisan package:discover --ansi \
&& APP_KEY=base64:dGVtcG9yYXJ5a2V5Zm9yZG9ja2VyYnVpbGRvbmx5= php artisan l5-swagger:generate --no-interaction \
&& mkdir -p \
storage/app/public \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
storage/api-docs \
bootstrap/cache \
&& rm -rf public/storage \
&& ln -s ../storage/app/public public/storage \
&& chown -R www-data:www-data storage bootstrap/cache
COPY docker/entrypoint.sh /usr/local/bin/docker-entrypoint
RUN chmod +x /usr/local/bin/docker-entrypoint
EXPOSE 9000
ENTRYPOINT ["docker-entrypoint"]
CMD ["php-fpm"]
ARG DOCKER_REGISTRY
FROM ${DOCKER_REGISTRY}/nginx:1.27-alpine AS nginx
RUN sed -i 's|https://dl-cdn.alpinelinux.org|https://mirror.arvancloud.ir/alpine|g' /etc/apk/repositories
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=app /var/www/html/public /var/www/html/public
EXPOSE 80

View File

@@ -1 +1,95 @@
### Hoshpoint
### Hoshpoint
## اجرای پروژه با Docker در Production
ابتدا Docker Desktop را اجرا کنید و فایل env پروداکشن را بسازید:
```bash
cp .env.production.example .env.production
php artisan key:generate --show
```
خروجی را کپی کنید و داخل `.env.production` بگذارید (خط `APP_KEY` نباید خالی بماند):
```env
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
```
سپس `APP_URL`، `HTTP_PORT`، `DB_PASSWORD`، `MARIADB_PASSWORD` و `MARIADB_ROOT_PASSWORD` را هم تغییر دهید. مقدار پیش‌فرض `HTTP_PORT=8080` است.
برای build کردن imageها:
```bash
docker compose --env-file .env.production build
```
اول دیتابیس را بالا بیاورید:
```bash
docker compose --env-file .env.production up -d mariadb
```
بعد migrationها را اجرا کنید:
```bash
docker compose --env-file .env.production run --rm app php artisan migrate --force --seed
```
حالا همه سرویس‌ها را بالا بیاورید:
```bash
docker compose --env-file .env.production up -d
```
سایت از طریق پورت تنظیم‌شده در `HTTP_PORT` در دسترس است. مقدار پیش‌فرض:
```text
http://localhost:8080
```
Swagger:
```text
http://localhost:8080/api/documentation
```
برای دیدن وضعیت سرویس‌ها:
```bash
docker compose --env-file .env.production ps
```
برای دیدن لاگ‌ها:
```bash
docker compose --env-file .env.production logs -f
```
برای اجرای دستورهای Artisan داخل کانتینر:
```bash
docker compose --env-file .env.production exec app php artisan about
```
برای متوقف کردن سرویس‌ها:
```bash
docker compose --env-file .env.production down
```
برای حذف کامل دیتای دیتابیس و volumeها، فقط وقتی مطمئن هستید:
```bash
docker compose --env-file .env.production down -v
```
## خطای `No application encryption key has been specified`
یعنی `APP_KEY` در `.env.production` خالی است.
1. مقدار `APP_KEY` را در `.env.production` تنظیم کنید.
2. سرویس‌ها را restart کنید:
```bash
docker compose --env-file .env.production restart app queue scheduler
```

View File

@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\RegisterRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
class AuthController extends Controller
{
#[OA\Post(
path: '/auth/register',
description: 'Register a new user account',
summary: 'Register',
tags: ['Auth'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name', 'mobile', 'email', 'password'],
properties: [
new OA\Property(property: 'name', type: 'string', example: 'علی رضایی'),
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'ali@example.com'),
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
]
)
),
responses: [
new OA\Response(
response: 201,
description: 'Registered successfully',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'message', type: 'string', example: 'ثبت‌نام با موفقیت انجام شد.'),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'user', ref: '#/components/schemas/User'),
new OA\Property(property: 'token', type: 'string'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 422, description: 'Validation error'),
]
)]
public function register(RegisterRequest $request): JsonResponse
{
$user = User::query()->create([
'name' => $request->validated('name'),
'username' => $this->generateUsername($request->validated('email')),
'mobile' => $request->validated('mobile'),
'email' => $request->validated('email'),
'password' => $request->validated('password'),
]);
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'message' => 'ثبت‌نام با موفقیت انجام شد.',
'data' => [
'user' => new UserResource($user),
'token' => $token,
],
], 201);
}
#[OA\Post(
path: '/auth/login',
description: 'Login with email or username',
summary: 'Login',
tags: ['Auth'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['login', 'password'],
properties: [
new OA\Property(property: 'login', description: 'Email or username', type: 'string', example: 'ali@example.com'),
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
]
)
),
responses: [
new OA\Response(
response: 200,
description: 'Logged in successfully',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'message', type: 'string', example: 'ورود با موفقیت انجام شد.'),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'user', ref: '#/components/schemas/User'),
new OA\Property(property: 'token', type: 'string'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 422, description: 'Invalid credentials'),
]
)]
public function login(LoginRequest $request): JsonResponse
{
$login = $request->validated('login');
$user = User::query()
->where(function ($query) use ($login): void {
$query->where('email', $login)
->orWhere('username', $login);
})
->first();
if (! $user || ! Hash::check($request->validated('password'), $user->password)) {
return response()->json([
'message' => 'اطلاعات ورود نادرست است.',
'errors' => [
'login' => ['ایمیل یا نام کاربری یا رمز عبور اشتباه است.'],
],
], 422);
}
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'message' => 'ورود با موفقیت انجام شد.',
'data' => [
'user' => new UserResource($user),
'token' => $token,
],
]);
}
private function generateUsername(string $email): string
{
$base = Str::slug(Str::before($email, '@'), '') ?: 'user';
$username = $base;
$counter = 1;
while (User::query()->where('username', $username)->exists()) {
$username = $base.$counter;
$counter++;
}
return $username;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'login' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'login.required' => 'ایمیل یا نام کاربری الزامی است.',
'password.required' => 'رمز عبور الزامی است.',
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'mobile' => ['required', 'string', 'regex:/^09\d{9}$/', 'unique:users,mobile'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.required' => 'نام و نام خانوادگی الزامی است.',
'mobile.required' => 'شماره موبایل الزامی است.',
'mobile.regex' => 'فرمت شماره موبایل صحیح نیست.',
'mobile.unique' => 'این شماره موبایل قبلاً ثبت شده است.',
'email.required' => 'ایمیل الزامی است.',
'email.email' => 'فرمت ایمیل صحیح نیست.',
'email.unique' => 'این ایمیل قبلاً ثبت شده است.',
'password.required' => 'رمز عبور الزامی است.',
'password.min' => 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Resources;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use OpenApi\Attributes as OA;
#[OA\Schema(
schema: 'User',
properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'علی رضایی'),
new OA\Property(property: 'username', type: 'string', example: 'ali'),
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'ali@example.com'),
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
]
)]
/** @mixin User */
class UserResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'username' => $this->username,
'email' => $this->email,
'mobile' => $this->mobile,
'created_at' => $this->created_at?->toIso8601String(),
];
}
}

View File

@@ -12,13 +12,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
#[Fillable(['name', 'email', 'mobile', 'password'])]
#[Fillable(['name', 'username', 'email', 'mobile', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable implements FilamentUser
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**
* @return BelongsToMany<Role, $this>

14
app/OpenApi.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace App;
use OpenApi\Attributes as OA;
#[OA\Info(
version: '1.0.0',
title: 'Hoshpoint API',
description: 'API documentation for Hoshpoint backend',
)]
#[OA\Server(url: '/api', description: 'API server')]
#[OA\Tag(name: 'Auth', description: 'Authentication endpoints')]
class OpenApi {}

View File

@@ -8,6 +8,7 @@ use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)

View File

@@ -10,8 +10,10 @@
"license": "MIT",
"require": {
"php": "^8.3",
"darkaonline/l5-swagger": "^11.0",
"filament/filament": "^4.0",
"laravel/framework": "^13.8",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^3.0"
},
"require-dev": {

610
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": "e4b0461e1d4ecb470dd48b6465d450e9",
"content-hash": "c479d2908801be9c7f146d6a1c4662b8",
"packages": [
{
"name": "blade-ui-kit/blade-heroicons",
@@ -552,6 +552,86 @@
],
"time": "2026-03-16T11:29:23+00:00"
},
{
"name": "darkaonline/l5-swagger",
"version": "11.0.1",
"source": {
"type": "git",
"url": "https://github.com/DarkaOnLine/L5-Swagger.git",
"reference": "63d737e841533cac6e8c04a007561aa833f69f3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DarkaOnLine/L5-Swagger/zipball/63d737e841533cac6e8c04a007561aa833f69f3a",
"reference": "63d737e841533cac6e8c04a007561aa833f69f3a",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^13.0 || ^12.1 || ^11.44",
"php": "^8.2",
"swagger-api/swagger-ui": ">=5.18.3",
"symfony/yaml": "^5.0 || ^6.0 || ^7.0 || ^8.0",
"zircote/swagger-php": "^6.0"
},
"require-dev": {
"mockery/mockery": "1.*",
"orchestra/testbench": "^11.0 || ^10.0 || ^9.0 || ^8.0 || 7.* || ^6.15 || 5.*",
"php-coveralls/php-coveralls": "^2.0",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"L5Swagger": "L5Swagger\\L5SwaggerFacade"
},
"providers": [
"L5Swagger\\L5SwaggerServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"L5Swagger\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Darius Matulionis",
"email": "darius@matulionis.lt"
}
],
"description": "OpenApi or Swagger integration to Laravel",
"keywords": [
"api",
"documentation",
"laravel",
"openapi",
"specification",
"swagger",
"ui"
],
"support": {
"issues": "https://github.com/DarkaOnLine/L5-Swagger/issues",
"source": "https://github.com/DarkaOnLine/L5-Swagger/tree/11.0.1"
},
"funding": [
{
"url": "https://github.com/DarkaOnLine",
"type": "github"
}
],
"time": "2026-04-08T13:14:00+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
@@ -2303,6 +2383,69 @@
},
"time": "2026-05-19T00:47:18+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.3.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "2a9bccc18e9907808e0018dd15fa643937886b1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e",
"reference": "2a9bccc18e9907808e0018dd15fa643937886b1e",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2026-04-30T11:46:25+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.13",
@@ -4065,6 +4208,53 @@
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2"
},
"time": "2026-01-25T14:56:51+00:00"
},
{
"name": "pragmarx/google2fa",
"version": "v9.0.0",
@@ -4676,6 +4866,68 @@
},
"time": "2026-05-23T13:41:31+00:00"
},
{
"name": "radebatz/type-info-extras",
"version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/DerManoMann/type-info-extras.git",
"reference": "95a524a74a61648b44e355cb33d38db4b17ef5ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DerManoMann/type-info-extras/zipball/95a524a74a61648b44e355cb33d38db4b17ef5ce",
"reference": "95a524a74a61648b44e355cb33d38db4b17ef5ce",
"shasum": ""
},
"require": {
"php": ">=8.2",
"phpstan/phpdoc-parser": "^2.0",
"symfony/type-info": "^7.3.8 || ^7.4.1 || ^8.0 || ^8.1-@dev"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.70",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Radebatz\\TypeInfoExtras\\": "src"
},
"exclude-from-classmap": [
"/tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Martin Rademacher",
"email": "mano@radebatz.org"
}
],
"description": "Extras for symfony/type-info",
"homepage": "http://radebatz.net/mano/",
"keywords": [
"component",
"symfony",
"type-info",
"types"
],
"support": {
"issues": "https://github.com/DerManoMann/type-info-extras/issues",
"source": "https://github.com/DerManoMann/type-info-extras/tree/1.0.7"
},
"time": "2026-03-06T22:40:29+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@@ -5215,6 +5467,67 @@
],
"time": "2026-04-27T14:27:52+00:00"
},
{
"name": "swagger-api/swagger-ui",
"version": "v5.32.6",
"source": {
"type": "git",
"url": "https://github.com/swagger-api/swagger-ui.git",
"reference": "dcdca62c8b64a0a54e4decd4e1a6c3c712fdcc60"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/dcdca62c8b64a0a54e4decd4e1a6c3c712fdcc60",
"reference": "dcdca62c8b64a0a54e4decd4e1a6c3c712fdcc60",
"shasum": ""
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Anna Bodnia",
"email": "anna.bodnia@gmail.com"
},
{
"name": "Buu Nguyen",
"email": "buunguyen@gmail.com"
},
{
"name": "Josh Ponelat",
"email": "jponelat@gmail.com"
},
{
"name": "Kyle Shockey",
"email": "kyleshockey1@gmail.com"
},
{
"name": "Robert Barnwell",
"email": "robert@robertismy.name"
},
{
"name": "Sahar Jafari",
"email": "shr.jafari@gmail.com"
}
],
"description": " Swagger UI is a collection of HTML, Javascript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.",
"homepage": "http://swagger.io",
"keywords": [
"api",
"documentation",
"openapi",
"specification",
"swagger",
"ui"
],
"support": {
"issues": "https://github.com/swagger-api/swagger-ui/issues",
"source": "https://github.com/swagger-api/swagger-ui/tree/v5.32.6"
},
"time": "2026-05-12T09:35:37+00:00"
},
{
"name": "symfony/clock",
"version": "v8.0.8",
@@ -7584,6 +7897,88 @@
],
"time": "2026-01-05T13:30:16+00:00"
},
{
"name": "symfony/type-info",
"version": "v8.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/type-info.git",
"reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/type-info/zipball/9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7",
"reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7",
"shasum": ""
},
"require": {
"php": ">=8.4.1",
"psr/container": "^1.1|^2.0"
},
"conflict": {
"phpstan/phpdoc-parser": "<1.30"
},
"require-dev": {
"phpstan/phpdoc-parser": "^1.30|^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\TypeInfo\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mathias Arlaud",
"email": "mathias.arlaud@gmail.com"
},
{
"name": "Baptiste LEDUC",
"email": "baptiste.leduc@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Extracts PHP types information.",
"homepage": "https://symfony.com",
"keywords": [
"PHPStan",
"phpdoc",
"symfony",
"type"
],
"support": {
"source": "https://github.com/symfony/type-info/tree/v8.1.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-29T05:06:50+00:00"
},
{
"name": "symfony/uid",
"version": "v8.0.9",
@@ -7749,6 +8144,82 @@
],
"time": "2026-03-31T07:15:36+00:00"
},
{
"name": "symfony/yaml",
"version": "v8.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0",
"reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0",
"shasum": ""
},
"require": {
"php": ">=8.4.1",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"symfony/console": "<7.4"
},
"require-dev": {
"symfony/console": "^7.4|^8.0",
"yaml/yaml-test-suite": "*"
},
"bin": [
"Resources/bin/yaml-lint"
],
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v8.1.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-29T05:06:50+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.4.0",
@@ -8030,6 +8501,96 @@
}
],
"time": "2026-04-26T05:33:54+00:00"
},
{
"name": "zircote/swagger-php",
"version": "6.1.2",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
"reference": "f66289ab9c9c3a1cf70222e0bebbe7c6c7109f2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/f66289ab9c9c3a1cf70222e0bebbe7c6c7109f2f",
"reference": "f66289ab9c9c3a1cf70222e0bebbe7c6c7109f2f",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.19 || ^5.0",
"php": ">=8.2",
"phpstan/phpdoc-parser": "^2.0",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"radebatz/type-info-extras": "^1.0.2",
"symfony/console": "^7.4 || ^8.0",
"symfony/deprecation-contracts": "^2 || ^3",
"symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"conflict": {
"symfony/process": ">=6, <6.4.14"
},
"require-dev": {
"composer/package-versions-deprecated": "^1.11",
"doctrine/annotations": "^2.0",
"friendsofphp/php-cs-fixer": "^3.62.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.5 || >=12.5.22",
"rector/rector": "^2.3.1"
},
"bin": [
"bin/openapi"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"autoload": {
"psr-4": {
"OpenApi\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Robert Allen",
"email": "zircote@gmail.com"
},
{
"name": "Bob Fanger",
"email": "bfanger@gmail.com",
"homepage": "https://bfanger.nl"
},
{
"name": "Martin Rademacher",
"email": "mano@radebatz.net",
"homepage": "https://radebatz.net"
}
],
"description": "Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations",
"homepage": "https://github.com/zircote/swagger-php",
"keywords": [
"api",
"json",
"rest",
"service discovery"
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/6.1.2"
},
"funding": [
{
"url": "https://github.com/zircote",
"type": "github"
}
],
"time": "2026-04-28T04:47:53+00:00"
}
],
"packages-dev": [
@@ -9917,53 +10478,6 @@
},
"time": "2026-01-06T21:53:42+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
"reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2"
},
"time": "2026-01-25T14:56:51+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "12.5.6",

View File

@@ -42,6 +42,10 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'sanctum' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],
/*

321
config/l5-swagger.php Normal file
View File

@@ -0,0 +1,321 @@
<?php
use L5Swagger\Generator;
use OpenApi\scan;
return [
'default' => 'default',
'documentations' => [
'default' => [
'api' => [
'title' => 'Hoshpoint API',
],
'routes' => [
/*
* Route for accessing api documentation interface
*/
'api' => 'api/documentation',
],
'paths' => [
/*
* Edit to include full URL in ui for assets
*/
'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true),
/*
* Edit to set path where swagger ui assets should be stored
*/
'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'),
/*
* File name of the generated json documentation file
*/
'docs_json' => 'api-docs.json',
/*
* File name of the generated YAML documentation file
*/
'docs_yaml' => 'api-docs.yaml',
/*
* Set this to `json` or `yaml` to determine which documentation file to use in UI
*/
'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'),
/*
* Absolute paths to directory containing the swagger annotations are stored.
*/
'annotations' => [
base_path('app'),
],
],
],
],
'defaults' => [
'routes' => [
/*
* Route for accessing parsed swagger annotations.
*/
'docs' => 'docs',
/*
* Route for Oauth2 authentication callback.
*/
'oauth2_callback' => 'api/oauth2-callback',
/*
* Middleware allows to prevent unexpected access to API documentation
*/
'middleware' => [
'api' => [],
'asset' => [],
'docs' => [],
'oauth2_callback' => [],
],
/*
* Route Group options
*/
'group_options' => [],
],
'paths' => [
/*
* Absolute path to location where parsed annotations will be stored
*/
'docs' => storage_path('api-docs'),
/*
* Absolute path to directory where to export views
*/
'views' => base_path('resources/views/vendor/l5-swagger'),
/*
* Edit to set the api's base path
*/
'base' => env('L5_SWAGGER_BASE_PATH', null),
/*
* Absolute path to directories that should be excluded from scanning
* @deprecated Please use `scanOptions.exclude`
* `scanOptions.exclude` overwrites this
*/
'excludes' => [],
],
'scanOptions' => [
/**
* Configuration for default processors. Allows to pass processors configuration to swagger-php.
*
* @link https://zircote.github.io/swagger-php/reference/processors.html
*/
'default_processors_configuration' => [
/** Example */
/**
* 'operationId.hash' => true,
* 'pathFilter' => [
* 'tags' => [
* '/pets/',
* '/store/',
* ],
* ],.
*/
],
/**
* analyser: defaults to \OpenApi\StaticAnalyser .
*
* @see scan
*/
'analyser' => null,
/**
* analysis: defaults to a new \OpenApi\Analysis .
*
* @see scan
*/
'analysis' => null,
/**
* Custom query path processors classes.
*
* @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter
* @see scan
*/
'processors' => [
// new \App\SwaggerProcessors\SchemaQueryParameter(),
],
/**
* pattern: string $pattern File pattern(s) to scan (default: *.php) .
*
* @see scan
*/
'pattern' => null,
/*
* Absolute path to directories that should be excluded from scanning
* @note This option overwrites `paths.excludes`
* @see \OpenApi\scan
*/
'exclude' => [],
/*
* Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0.
* By default the spec will be in version 3.0.0
*/
'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', Generator::OPEN_API_DEFAULT_SPEC_VERSION),
],
/*
* API security definitions. Will be generated into documentation file.
*/
'securityDefinitions' => [
'securitySchemes' => [
/*
* Examples of Security schemes
*/
/*
'api_key_security_example' => [ // Unique name of security
'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'A short description for security scheme',
'name' => 'api_key', // The name of the header or query parameter to be used.
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
],
'oauth2_security_example' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'A short description for oauth2 security scheme.',
'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode".
'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode)
//'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode)
'scopes' => [
'read:projects' => 'read your projects',
'write:projects' => 'modify projects in your account',
]
],
*/
/* Open API 3.0 support
'passport' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Laravel passport oauth2 security.',
'in' => 'header',
'scheme' => 'https',
'flows' => [
"password" => [
"authorizationUrl" => config('app.url') . '/oauth/authorize',
"tokenUrl" => config('app.url') . '/oauth/token',
"refreshUrl" => config('app.url') . '/token/refresh',
"scopes" => []
],
],
],
'sanctum' => [ // Unique name of security
'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Enter token in format (Bearer <token>)',
'name' => 'Authorization', // The name of the header or query parameter to be used.
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
],
*/
],
'security' => [
/*
* Examples of Securities
*/
[
/*
'oauth2_security_example' => [
'read',
'write'
],
'passport' => []
*/
],
],
],
/*
* Set this to `true` in development mode so that docs would be regenerated on each request
* Set this to `false` to disable swagger generation on production
*/
'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false),
/*
* Set this to `true` to generate a copy of documentation in yaml format
*/
'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false),
/*
* Edit to trust the proxy's ip address - needed for AWS Load Balancer
* string[]
*/
'proxy' => false,
/*
* Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle.
* See more at: https://github.com/swagger-api/swagger-ui#configs-plugin
*/
'additional_config_url' => null,
/*
* Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically),
* 'method' (sort by HTTP method).
* Default is the order returned by the server unchanged.
*/
'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null),
/*
* Pass the validatorUrl parameter to SwaggerUi init on the JS side.
* A null value here disables validation.
*/
'validator_url' => null,
/*
* Swagger UI configuration parameters
*/
'ui' => [
'display' => [
'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false),
/*
* Controls the default expansion setting for the operations and tags. It can be :
* 'list' (expands only the tags),
* 'full' (expands the tags and operations),
* 'none' (expands nothing).
*/
'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'),
/**
* If set, enables filtering. The top bar will show an edit box that
* you can use to filter the tagged operations that are shown. Can be
* Boolean to enable or disable, or a string, in which case filtering
* will be enabled using that string as the filter expression. Filtering
* is case-sensitive matching the filter expression anywhere inside
* the tag.
*/
'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false
],
'authorization' => [
/*
* If set to true, it persists authorization data, and it would not be lost on browser close/refresh
*/
'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false),
'oauth2' => [
/*
* If set to true, adds PKCE to AuthorizationCodeGrant flow
*/
'use_pkce_with_authorization_code_grant' => false,
],
],
],
/*
* Constants which can be used in annotations
*/
'constants' => [
'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'),
],
],
];

87
config/sanctum.php Normal file
View File

@@ -0,0 +1,87 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];

View File

@@ -24,9 +24,12 @@ class UserFactory extends Factory
*/
public function definition(): array
{
$email = fake()->unique()->safeEmail();
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'username' => fake()->unique()->userName(),
'email' => $email,
'mobile' => fake()->unique()->numerify('09#########'),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),

View File

@@ -0,0 +1,33 @@
<?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('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('username')->nullable()->unique()->after('name');
});
foreach (DB::table('users')->orderBy('id')->get() as $user) {
$base = Str::slug(Str::before($user->email, '@'), '') ?: 'user';
$username = $base;
$counter = 1;
while (DB::table('users')->where('username', $username)->where('id', '!=', $user->id)->exists()) {
$username = $base.$counter;
$counter++;
}
DB::table('users')->where('id', $user->id)->update(['username' => $username]);
}
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropUnique(['username']);
$table->dropColumn('username');
});
}
};

104
docker-compose.yml Normal file
View File

@@ -0,0 +1,104 @@
services:
app:
build:
context: .
target: app
args:
DOCKER_REGISTRY: docker.arvancloud.ir
image: hoshpoint-backend-app:production
restart: unless-stopped
env_file:
- ${APP_ENV_FILE:-.env.production}
environment:
APP_ENV: production
APP_DEBUG: "false"
LOG_CHANNEL: stderr
DB_HOST: mariadb
DB_PORT: 3306
depends_on:
mariadb:
condition: service_healthy
volumes:
- app-storage:/var/www/html/storage
networks:
- hoshpoint
nginx:
build:
context: .
target: nginx
args:
DOCKER_REGISTRY: docker.arvancloud.ir
image: hoshpoint-backend-nginx:production
restart: unless-stopped
depends_on:
- app
ports:
- "${HTTP_PORT:-8080}:80"
volumes:
- app-storage:/var/www/html/storage:ro
networks:
- hoshpoint
queue:
image: hoshpoint-backend-app:production
restart: unless-stopped
env_file:
- ${APP_ENV_FILE:-.env.production}
environment:
APP_ENV: production
APP_DEBUG: "false"
LOG_CHANNEL: stderr
DB_HOST: mariadb
DB_PORT: 3306
command: php artisan queue:work database --sleep=3 --tries=3 --timeout=90
depends_on:
mariadb:
condition: service_healthy
volumes:
- app-storage:/var/www/html/storage
networks:
- hoshpoint
scheduler:
image: hoshpoint-backend-app:production
restart: unless-stopped
env_file:
- ${APP_ENV_FILE:-.env.production}
environment:
APP_ENV: production
APP_DEBUG: "false"
LOG_CHANNEL: stderr
DB_HOST: mariadb
DB_PORT: 3306
command: sh -c "while true; do php artisan schedule:run --no-interaction; sleep 60; done"
depends_on:
mariadb:
condition: service_healthy
volumes:
- app-storage:/var/www/html/storage
networks:
- hoshpoint
mariadb:
image: mariadb:11.4
restart: unless-stopped
env_file:
- ${APP_ENV_FILE:-.env.production}
volumes:
- mariadb-data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- hoshpoint
volumes:
app-storage:
mariadb-data:
networks:
hoshpoint:
driver: bridge

32
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env sh
set -eu
mkdir -p \
storage/app/public \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
storage/api-docs \
bootstrap/cache
if [ ! -L public/storage ]; then
rm -rf public/storage
ln -s ../storage/app/public public/storage
fi
if [ "$(id -u)" = "0" ]; then
chown -R www-data:www-data storage bootstrap/cache
fi
if [ -z "${APP_KEY:-}" ]; then
echo "ERROR: APP_KEY is empty. Set it in .env.production, then restart containers." >&2
echo " php artisan key:generate --show" >&2
exit 1
fi
if php artisan list --raw 2>/dev/null | grep -q '^l5-swagger:generate'; then
php artisan l5-swagger:generate --no-interaction
fi
exec "$@"

49
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,49 @@
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
charset utf-8;
client_max_body_size 20M;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

5736
docker/php/install-php-extensions Executable file

File diff suppressed because it is too large Load Diff

8
docker/php/opcache.ini Normal file
View File

@@ -0,0 +1,8 @@
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 192
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.fast_shutdown = 1

12
docker/php/php.ini Normal file
View File

@@ -0,0 +1,12 @@
expose_php = Off
memory_limit = 256M
max_execution_time = 60
max_input_time = 60
upload_max_filesize = 20M
post_max_size = 20M
variables_order = EGPCS
realpath_cache_size = 4096K
realpath_cache_ttl = 600
log_errors = On
error_log = /proc/self/fd/2
date.timezone = UTC

View File

@@ -4,6 +4,6 @@
@source '../../storage/framework/views/*.php';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

View File

@@ -1 +1,2 @@
// JavaScript entry point required by vite.config.js.
//

View File

View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ $documentationTitle }}</title>
<link rel="stylesheet" type="text/css" href="{{ l5_swagger_asset($documentation, 'swagger-ui.css') }}">
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-32x32.png') }}" sizes="32x32"/>
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-16x16.png') }}" sizes="16x16"/>
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
@if(config('l5-swagger.defaults.ui.display.dark_mode'))
<style>
body#dark-mode,
#dark-mode .scheme-container {
background: #1b1b1b;
}
#dark-mode .scheme-container,
#dark-mode .opblock .opblock-section-header{
box-shadow: 0 1px 2px 0 rgba(255, 255, 255, 0.15);
}
#dark-mode .operation-filter-input,
#dark-mode .dialog-ux .modal-ux,
#dark-mode input[type=email],
#dark-mode input[type=file],
#dark-mode input[type=password],
#dark-mode input[type=search],
#dark-mode input[type=text],
#dark-mode textarea{
background: #343434;
color: #e7e7e7;
}
#dark-mode .title,
#dark-mode li,
#dark-mode p,
#dark-mode table,
#dark-mode label,
#dark-mode .opblock-tag,
#dark-mode .opblock .opblock-summary-operation-id,
#dark-mode .opblock .opblock-summary-path,
#dark-mode .opblock .opblock-summary-path__deprecated,
#dark-mode h1,
#dark-mode h2,
#dark-mode h3,
#dark-mode h4,
#dark-mode h5,
#dark-mode .btn,
#dark-mode .tab li,
#dark-mode .parameter__name,
#dark-mode .parameter__type,
#dark-mode .prop-format,
#dark-mode .loading-container .loading:after{
color: #e7e7e7;
}
#dark-mode .opblock-description-wrapper p,
#dark-mode .opblock-external-docs-wrapper p,
#dark-mode .opblock-title_normal p,
#dark-mode .response-col_status,
#dark-mode table thead tr td,
#dark-mode table thead tr th,
#dark-mode .response-col_links,
#dark-mode .swagger-ui{
color: wheat;
}
#dark-mode .parameter__extension,
#dark-mode .parameter__in,
#dark-mode .model-title{
color: #949494;
}
#dark-mode table thead tr td,
#dark-mode table thead tr th{
border-color: rgba(120,120,120,.2);
}
#dark-mode .opblock .opblock-section-header{
background: transparent;
}
#dark-mode .opblock.opblock-post{
background: rgba(73,204,144,.25);
}
#dark-mode .opblock.opblock-get{
background: rgba(97,175,254,.25);
}
#dark-mode .opblock.opblock-put{
background: rgba(252,161,48,.25);
}
#dark-mode .opblock.opblock-delete{
background: rgba(249,62,62,.25);
}
#dark-mode .loading-container .loading:before{
border-color: rgba(255,255,255,10%);
border-top-color: rgba(255,255,255,.6);
}
#dark-mode svg:not(:root){
fill: #e7e7e7;
}
#dark-mode .opblock-summary-description {
color: #fafafa;
}
</style>
@endif
</head>
<body @if(config('l5-swagger.defaults.ui.display.dark_mode')) id="dark-mode" @endif>
<div id="swagger-ui"></div>
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-bundle.js') }}"></script>
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-standalone-preset.js') }}"></script>
<script>
window.onload = function() {
const urls = [];
@foreach($urlsToDocs as $title => $url)
urls.push({name: "{{ $title }}", url: "{{ $url }}"});
@endforeach
// Build a system
const ui = SwaggerUIBundle({
dom_id: '#swagger-ui',
urls: urls,
"urls.primaryName": "{{ $documentationTitle }}",
operationsSorter: {!! isset($operationsSorter) ? '"' . $operationsSorter . '"' : 'null' !!},
configUrl: {!! isset($configUrl) ? '"' . $configUrl . '"' : 'null' !!},
validatorUrl: {!! isset($validatorUrl) ? '"' . $validatorUrl . '"' : 'null' !!},
oauth2RedirectUrl: "{{ route('l5-swagger.'.$documentation.'.oauth2_callback', [], $useAbsolutePath) }}",
requestInterceptor: function(request) {
request.headers['X-CSRF-TOKEN'] = '{{ csrf_token() }}';
return request;
},
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
docExpansion : "{!! config('l5-swagger.defaults.ui.display.doc_expansion', 'none') !!}",
deepLinking: true,
filter: {!! config('l5-swagger.defaults.ui.display.filter') ? 'true' : 'false' !!},
persistAuthorization: "{!! config('l5-swagger.defaults.ui.authorization.persist_authorization') ? 'true' : 'false' !!}",
})
window.ui = ui
@if(in_array('oauth2', array_column(config('l5-swagger.defaults.securityDefinitions.securitySchemes'), 'type')))
ui.initOAuth({
usePkceWithAuthorizationCodeGrant: "{!! (bool)config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant') !!}"
})
@endif
}
</script>
</body>
</html>

9
routes/api.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
Route::prefix('auth')->group(function (): void {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
});

View File

@@ -0,0 +1,202 @@
{
"openapi": "3.0.0",
"info": {
"title": "Hoshpoint API",
"description": "API documentation for Hoshpoint backend",
"version": "1.0.0"
},
"servers": [
{
"url": "/api",
"description": "API server"
}
],
"paths": {
"/auth/register": {
"post": {
"tags": [
"Auth"
],
"summary": "Register",
"description": "Register a new user account",
"operationId": "08136088c4862c313b35d6518bab8d3a",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"required": [
"name",
"mobile",
"email",
"password"
],
"properties": {
"name": {
"type": "string",
"example": "علی رضایی"
},
"mobile": {
"type": "string",
"example": "09123456789"
},
"email": {
"type": "string",
"format": "email",
"example": "ali@example.com"
},
"password": {
"type": "string",
"format": "password",
"example": "password123"
}
},
"type": "object"
}
}
}
},
"responses": {
"201": {
"description": "Registered successfully",
"content": {
"application/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "ثبت‌نام با موفقیت انجام شد."
},
"data": {
"properties": {
"user": {
"$ref": "#/components/schemas/User"
},
"token": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
}
}
},
"422": {
"description": "Validation error"
}
}
}
},
"/auth/login": {
"post": {
"tags": [
"Auth"
],
"summary": "Login",
"description": "Login with email or username",
"operationId": "99d66635c4992aeaa6aa44ff653d0563",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"required": [
"login",
"password"
],
"properties": {
"login": {
"description": "Email or username",
"type": "string",
"example": "ali@example.com"
},
"password": {
"type": "string",
"format": "password",
"example": "password123"
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Logged in successfully",
"content": {
"application/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "ورود با موفقیت انجام شد."
},
"data": {
"properties": {
"user": {
"$ref": "#/components/schemas/User"
},
"token": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
}
}
},
"422": {
"description": "Invalid credentials"
}
}
}
}
},
"components": {
"schemas": {
"User": {
"properties": {
"id": {
"type": "integer",
"example": 1
},
"name": {
"type": "string",
"example": "علی رضایی"
},
"username": {
"type": "string",
"example": "ali"
},
"email": {
"type": "string",
"format": "email",
"example": "ali@example.com"
},
"mobile": {
"type": "string",
"example": "09123456789"
},
"created_at": {
"type": "string",
"format": "date-time"
}
},
"type": "object"
}
}
},
"tags": [
{
"name": "Auth",
"description": "Authentication endpoints"
}
]
}

View File

@@ -1,6 +1,5 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
@@ -8,11 +7,6 @@ export default defineConfig({
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
fonts: [
bunny('Instrument Sans', {
weights: [400, 500, 600],
}),
],
}),
tailwindcss(),
],