Compare commits
25 Commits
1c408130d0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cf0e6f82fd | |||
| ca43502ffb | |||
| d1d42b38d1 | |||
| c2319a55cb | |||
| 12d149e5d9 | |||
| 3029f80d16 | |||
| 97fc7855ca | |||
| f8de292f12 | |||
| c57cf9bcb9 | |||
| 0607b45491 | |||
| 34ce6d810f | |||
| 8ddce5e792 | |||
| e2a6d50cb6 | |||
| 12b10db063 | |||
| e34744ad99 | |||
| 10a63b46bf | |||
| 6fff2d0285 | |||
| ca44e5b0db | |||
| dee1c3ab16 | |||
| 7cb4459fd4 | |||
| 0ddd54dc66 | |||
| 802326336a | |||
| 07d80b8740 | |||
| 0b99ab2119 | |||
| 6eae917540 |
28
.dockerignore
Normal file
28
.dockerignore
Normal 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
68
.env.production.example
Normal file
@@ -0,0 +1,68 @@
|
||||
APP_NAME=Hoshpoint
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://hoshpoint-api.treenix.ir
|
||||
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}"
|
||||
71
Dockerfile
Normal file
71
Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
FROM php:8.4-fpm-bookworm AS app
|
||||
WORKDIR /var/www/html
|
||||
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
ENV COMPOSER_PROCESS_TIMEOUT=600
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
default-mysql-client \
|
||||
default-libmysqlclient-dev \
|
||||
git \
|
||||
libicu-dev \
|
||||
libzip-dev \
|
||||
unzip \
|
||||
&& docker-php-ext-install -j"$(nproc)" bcmath intl pcntl pdo_mysql zip \
|
||||
&& apt-mark manual libzip4 libicu72 \
|
||||
&& apt-get purge -y --auto-remove \
|
||||
default-libmysqlclient-dev \
|
||||
libicu-dev \
|
||||
libzip-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/php/composer.phar /usr/local/bin/composer
|
||||
RUN chmod +x /usr/local/bin/composer
|
||||
|
||||
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 . .
|
||||
|
||||
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"]
|
||||
|
||||
FROM nginx:1.27-alpine AS nginx
|
||||
|
||||
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=app /var/www/html/public /var/www/html/public
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# npm build (run locally before docker build):
|
||||
# npm ci && npm run build
|
||||
95
README.md
95
README.md
@@ -1 +1,96 @@
|
||||
### 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
|
||||
```
|
||||
|
||||
|
||||
17
app/Enums/WalletTransactionType.php
Normal file
17
app/Enums/WalletTransactionType.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum WalletTransactionType: string
|
||||
{
|
||||
case Credit = 'credit';
|
||||
case Debit = 'debit';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Credit => 'واریز',
|
||||
self::Debit => 'برداشت',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions\Pages;
|
||||
|
||||
use App\Filament\Resources\WalletTransactions\WalletTransactionResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListWalletTransactions extends ListRecords
|
||||
{
|
||||
protected static string $resource = WalletTransactionResource::class;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions\Pages;
|
||||
|
||||
use App\Filament\Resources\WalletTransactions\WalletTransactionResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewWalletTransaction extends ViewRecord
|
||||
{
|
||||
protected static string $resource = WalletTransactionResource::class;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions\Tables;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class WalletTransactionsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('#')
|
||||
->sortable(),
|
||||
TextColumn::make('wallet.user.name')
|
||||
->label('کاربر')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('wallet.user.mobile')
|
||||
->label('موبایل')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('type')
|
||||
->label('نوع')
|
||||
->badge()
|
||||
->formatStateUsing(fn (WalletTransactionType $state): string => $state->label())
|
||||
->color(fn (WalletTransactionType $state): string => $state === WalletTransactionType::Credit ? 'success' : 'danger'),
|
||||
TextColumn::make('amount')
|
||||
->label('مبلغ')
|
||||
->sortable()
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
TextColumn::make('balance_after')
|
||||
->label('موجودی بعد')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
TextColumn::make('description')
|
||||
->label('توضیحات')
|
||||
->limit(50)
|
||||
->searchable(),
|
||||
TextColumn::make('creator.name')
|
||||
->label('ثبتکننده')
|
||||
->placeholder('سیستم'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->label('نوع')
|
||||
->options([
|
||||
WalletTransactionType::Credit->value => WalletTransactionType::Credit->label(),
|
||||
WalletTransactionType::Debit->value => WalletTransactionType::Debit->label(),
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions;
|
||||
|
||||
use App\Filament\Resources\WalletTransactions\Pages\ListWalletTransactions;
|
||||
use App\Filament\Resources\WalletTransactions\Pages\ViewWalletTransaction;
|
||||
use App\Filament\Resources\WalletTransactions\Tables\WalletTransactionsTable;
|
||||
use App\Models\WalletTransaction;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
|
||||
class WalletTransactionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = WalletTransaction::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedArrowsRightLeft;
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'مالی';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'id';
|
||||
|
||||
protected static ?string $modelLabel = 'تراکنش';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'تراکنشها';
|
||||
|
||||
protected static ?string $navigationLabel = 'تراکنشها';
|
||||
|
||||
protected static ?int $navigationSort = 11;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return WalletTransactionsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListWalletTransactions::route('/'),
|
||||
'view' => ViewWalletTransaction::route('/{record}'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canDelete($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Wallets/Pages/EditWallet.php
Normal file
19
app/Filament/Resources/Wallets/Pages/EditWallet.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Pages;
|
||||
|
||||
use App\Filament\Resources\Wallets\WalletResource;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditWallet extends EditRecord
|
||||
{
|
||||
protected static string $resource = WalletResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Wallets/Pages/ListWallets.php
Normal file
11
app/Filament/Resources/Wallets/Pages/ListWallets.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Pages;
|
||||
|
||||
use App\Filament\Resources\Wallets\WalletResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListWallets extends ListRecords
|
||||
{
|
||||
protected static string $resource = WalletResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Wallets/Pages/ViewWallet.php
Normal file
19
app/Filament/Resources/Wallets/Pages/ViewWallet.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Pages;
|
||||
|
||||
use App\Filament\Resources\Wallets\WalletResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewWallet extends ViewRecord
|
||||
{
|
||||
protected static string $resource = WalletResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\RelationManagers;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TransactionsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'transactions';
|
||||
|
||||
protected static ?string $title = 'تراکنشها';
|
||||
|
||||
protected static ?string $modelLabel = 'تراکنش';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'تراکنشها';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('type')
|
||||
->label('نوع')
|
||||
->badge()
|
||||
->formatStateUsing(fn (WalletTransactionType $state): string => $state->label())
|
||||
->color(fn (WalletTransactionType $state): string => $state === WalletTransactionType::Credit ? 'success' : 'danger'),
|
||||
TextColumn::make('amount')
|
||||
->label('مبلغ')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
TextColumn::make('balance_before')
|
||||
->label('قبل')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state)),
|
||||
TextColumn::make('balance_after')
|
||||
->label('بعد')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state)),
|
||||
TextColumn::make('description')
|
||||
->label('توضیحات')
|
||||
->limit(40),
|
||||
TextColumn::make('creator.name')
|
||||
->label('ثبتکننده')
|
||||
->placeholder('سیستم'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->label('نوع')
|
||||
->options([
|
||||
WalletTransactionType::Credit->value => WalletTransactionType::Credit->label(),
|
||||
WalletTransactionType::Debit->value => WalletTransactionType::Debit->label(),
|
||||
]),
|
||||
])
|
||||
->recordActions([])
|
||||
->toolbarActions([]);
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Wallets/Schemas/WalletForm.php
Normal file
19
app/Filament/Resources/Wallets/Schemas/WalletForm.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class WalletForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Toggle::make('is_active')
|
||||
->label('فعال')
|
||||
->default(true),
|
||||
]);
|
||||
}
|
||||
}
|
||||
106
app/Filament/Resources/Wallets/Tables/WalletsTable.php
Normal file
106
app/Filament/Resources/Wallets/Tables/WalletsTable.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Tables;
|
||||
|
||||
use App\Models\Wallet;
|
||||
use App\Services\WalletService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use RuntimeException;
|
||||
|
||||
class WalletsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.name')
|
||||
->label('کاربر')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('user.email')
|
||||
->label('ایمیل')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('user.mobile')
|
||||
->label('موبایل')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('balance')
|
||||
->label('موجودی (تومان)')
|
||||
->numeric()
|
||||
->sortable()
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
IconColumn::make('is_active')
|
||||
->label('فعال')
|
||||
->boolean(),
|
||||
TextColumn::make('updated_at')
|
||||
->label('آخرین بروزرسانی')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->defaultSort('balance', 'desc')
|
||||
->filters([
|
||||
TernaryFilter::make('is_active')
|
||||
->label('وضعیت')
|
||||
->placeholder('همه')
|
||||
->trueLabel('فعال')
|
||||
->falseLabel('غیرفعال'),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
self::adjustAction('credit', 'واریز', Heroicon::OutlinedPlusCircle, 'success'),
|
||||
self::adjustAction('debit', 'برداشت', Heroicon::OutlinedMinusCircle, 'danger'),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function adjustAction(string $type, string $label, Heroicon $icon, string $color): Action
|
||||
{
|
||||
return Action::make($type)
|
||||
->label($label)
|
||||
->icon($icon)
|
||||
->color($color)
|
||||
->schema([
|
||||
TextInput::make('amount')
|
||||
->label('مبلغ (تومان)')
|
||||
->numeric()
|
||||
->required()
|
||||
->minValue(1)
|
||||
->integer(),
|
||||
Textarea::make('description')
|
||||
->label('توضیحات')
|
||||
->maxLength(500)
|
||||
->rows(2),
|
||||
])
|
||||
->action(function (Wallet $record, array $data, WalletService $walletService): void {
|
||||
try {
|
||||
$transaction = $type === 'credit'
|
||||
? $walletService->credit($record, (int) $data['amount'], $data['description'] ?? null, auth()->user())
|
||||
: $walletService->debit($record, (int) $data['amount'], $data['description'] ?? null, auth()->user());
|
||||
|
||||
Notification::make()
|
||||
->title($type === 'credit' ? 'واریز انجام شد' : 'برداشت انجام شد')
|
||||
->body('موجودی جدید: '.number_format($transaction->balance_after).' تومان')
|
||||
->success()
|
||||
->send();
|
||||
} catch (RuntimeException $exception) {
|
||||
Notification::make()
|
||||
->title('خطا')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
67
app/Filament/Resources/Wallets/WalletResource.php
Normal file
67
app/Filament/Resources/Wallets/WalletResource.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets;
|
||||
|
||||
use App\Filament\Resources\Wallets\Pages\EditWallet;
|
||||
use App\Filament\Resources\Wallets\Pages\ListWallets;
|
||||
use App\Filament\Resources\Wallets\Pages\ViewWallet;
|
||||
use App\Filament\Resources\Wallets\RelationManagers\TransactionsRelationManager;
|
||||
use App\Filament\Resources\Wallets\Schemas\WalletForm;
|
||||
use App\Filament\Resources\Wallets\Tables\WalletsTable;
|
||||
use App\Models\Wallet;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
|
||||
class WalletResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Wallet::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedWallet;
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'مالی';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'id';
|
||||
|
||||
protected static ?string $modelLabel = 'کیف پول';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'کیف پولها';
|
||||
|
||||
protected static ?string $navigationLabel = 'کیف پولها';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return WalletForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return WalletsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
TransactionsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListWallets::route('/'),
|
||||
'view' => ViewWallet::route('/{record}'),
|
||||
'edit' => EditWallet::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
156
app/Http/Controllers/Api/AuthController.php
Normal file
156
app/Http/Controllers/Api/AuthController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
35
app/Http/Requests/Auth/LoginRequest.php
Normal file
35
app/Http/Requests/Auth/LoginRequest.php
Normal 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' => 'رمز عبور الزامی است.',
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Http/Requests/Auth/RegisterRequest.php
Normal file
44
app/Http/Requests/Auth/RegisterRequest.php
Normal 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' => 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Http/Resources/UserResource.php
Normal file
38
app/Http/Resources/UserResource.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,18 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
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>
|
||||
@@ -28,6 +31,22 @@ class User extends Authenticatable implements FilamentUser
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasOne<Wallet, $this>
|
||||
*/
|
||||
public function wallet(): HasOne
|
||||
{
|
||||
return $this->hasOne(Wallet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasManyThrough<WalletTransaction, Wallet, $this>
|
||||
*/
|
||||
public function walletTransactions(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(WalletTransaction::class, Wallet::class);
|
||||
}
|
||||
|
||||
public function hasRole(string|array $roles): bool
|
||||
{
|
||||
$roles = (array) $roles;
|
||||
|
||||
39
app/Models/Wallet.php
Normal file
39
app/Models/Wallet.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['user_id', 'balance', 'is_active'])]
|
||||
class Wallet extends Model
|
||||
{
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<WalletTransaction, $this>
|
||||
*/
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletTransaction::class)->latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'balance' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Models/WalletTransaction.php
Normal file
49
app/Models/WalletTransaction.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable([
|
||||
'wallet_id',
|
||||
'type',
|
||||
'amount',
|
||||
'balance_before',
|
||||
'balance_after',
|
||||
'description',
|
||||
'created_by',
|
||||
])]
|
||||
class WalletTransaction extends Model
|
||||
{
|
||||
/**
|
||||
* @return BelongsTo<Wallet, $this>
|
||||
*/
|
||||
public function wallet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => WalletTransactionType::class,
|
||||
'amount' => 'integer',
|
||||
'balance_before' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
16
app/Observers/UserObserver.php
Normal file
16
app/Observers/UserObserver.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\WalletService;
|
||||
|
||||
class UserObserver
|
||||
{
|
||||
public function __construct(private WalletService $walletService) {}
|
||||
|
||||
public function created(User $user): void
|
||||
{
|
||||
$this->walletService->createForUser($user);
|
||||
}
|
||||
}
|
||||
14
app/OpenApi.php
Normal file
14
app/OpenApi.php
Normal 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 {}
|
||||
29
app/Policies/WalletPolicy.php
Normal file
29
app/Policies/WalletPolicy.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
|
||||
class WalletPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function view(User $user, Wallet $wallet): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function update(User $user, Wallet $wallet): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
private function canManage(User $user): bool
|
||||
{
|
||||
return $user->hasRole('admin') || $user->hasPermission('wallets.manage');
|
||||
}
|
||||
}
|
||||
24
app/Policies/WalletTransactionPolicy.php
Normal file
24
app/Policies/WalletTransactionPolicy.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WalletTransaction;
|
||||
|
||||
class WalletTransactionPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function view(User $user, WalletTransaction $transaction): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
private function canManage(User $user): bool
|
||||
{
|
||||
return $user->hasRole('admin') || $user->hasPermission('wallets.manage');
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,16 @@ namespace App\Providers;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use App\Observers\UserObserver;
|
||||
use App\Policies\PermissionPolicy;
|
||||
use App\Policies\RolePolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use App\Policies\WalletPolicy;
|
||||
use App\Policies\WalletTransactionPolicy;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -26,8 +32,16 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if (app()->environment('production') || str_starts_with((string) config('app.url'), 'https://')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
User::observe(UserObserver::class);
|
||||
|
||||
Gate::policy(User::class, UserPolicy::class);
|
||||
Gate::policy(Role::class, RolePolicy::class);
|
||||
Gate::policy(Permission::class, PermissionPolicy::class);
|
||||
Gate::policy(Wallet::class, WalletPolicy::class);
|
||||
Gate::policy(WalletTransaction::class, WalletTransactionPolicy::class);
|
||||
}
|
||||
}
|
||||
|
||||
81
app/Services/WalletService.php
Normal file
81
app/Services/WalletService.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
public function createForUser(User $user): Wallet
|
||||
{
|
||||
return Wallet::query()->firstOrCreate(
|
||||
['user_id' => $user->id],
|
||||
['balance' => 0, 'is_active' => true],
|
||||
);
|
||||
}
|
||||
|
||||
public function credit(
|
||||
Wallet $wallet,
|
||||
int $amount,
|
||||
?string $description = null,
|
||||
?User $performedBy = null,
|
||||
): WalletTransaction {
|
||||
return $this->apply($wallet, WalletTransactionType::Credit, $amount, $description, $performedBy);
|
||||
}
|
||||
|
||||
public function debit(
|
||||
Wallet $wallet,
|
||||
int $amount,
|
||||
?string $description = null,
|
||||
?User $performedBy = null,
|
||||
): WalletTransaction {
|
||||
return $this->apply($wallet, WalletTransactionType::Debit, $amount, $description, $performedBy);
|
||||
}
|
||||
|
||||
private function apply(
|
||||
Wallet $wallet,
|
||||
WalletTransactionType $type,
|
||||
int $amount,
|
||||
?string $description,
|
||||
?User $performedBy,
|
||||
): WalletTransaction {
|
||||
if ($amount <= 0) {
|
||||
throw new InvalidArgumentException('مبلغ باید بزرگتر از صفر باشد.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($wallet, $type, $amount, $description, $performedBy): WalletTransaction {
|
||||
$wallet = Wallet::query()->lockForUpdate()->findOrFail($wallet->id);
|
||||
|
||||
if (! $wallet->is_active) {
|
||||
throw new RuntimeException('کیف پول غیرفعال است.');
|
||||
}
|
||||
|
||||
$balanceBefore = $wallet->balance;
|
||||
$balanceAfter = $type === WalletTransactionType::Credit
|
||||
? $balanceBefore + $amount
|
||||
: $balanceBefore - $amount;
|
||||
|
||||
if ($balanceAfter < 0) {
|
||||
throw new RuntimeException('موجودی کیف پول کافی نیست.');
|
||||
}
|
||||
|
||||
$wallet->update(['balance' => $balanceAfter]);
|
||||
|
||||
return WalletTransaction::query()->create([
|
||||
'wallet_id' => $wallet->id,
|
||||
'type' => $type,
|
||||
'amount' => $amount,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'description' => $description,
|
||||
'created_by' => $performedBy?->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,19 @@ 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',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->trustProxies(
|
||||
at: '*',
|
||||
headers: Request::HEADER_X_FORWARDED_FOR
|
||||
| Request::HEADER_X_FORWARDED_HOST
|
||||
| Request::HEADER_X_FORWARDED_PORT
|
||||
| Request::HEADER_X_FORWARDED_PROTO
|
||||
| Request::HEADER_X_FORWARDED_AWS_ELB,
|
||||
);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
|
||||
@@ -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
610
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -42,6 +42,10 @@ return [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
'sanctum' => [
|
||||
'driver' => 'sanctum',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
321
config/l5-swagger.php
Normal file
321
config/l5-swagger.php
Normal 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' => env('L5_SWAGGER_BEHIND_PROXY', '*'),
|
||||
|
||||
/*
|
||||
* 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', env('APP_URL', 'http://localhost')),
|
||||
],
|
||||
],
|
||||
];
|
||||
87
config/sanctum.php
Normal file
87
config/sanctum.php
Normal 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,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -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'),
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
|
||||
$table->unsignedBigInteger('balance')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallet_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('wallet_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('type');
|
||||
$table->unsignedBigInteger('amount');
|
||||
$table->unsignedBigInteger('balance_before');
|
||||
$table->unsignedBigInteger('balance_after');
|
||||
$table->string('description')->nullable();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['wallet_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallet_transactions');
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ namespace Database\Seeders;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Services\WalletService;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
@@ -24,6 +25,7 @@ class DatabaseSeeder extends Seeder
|
||||
['name' => 'مدیریت کاربران', 'slug' => 'users.manage'],
|
||||
['name' => 'مدیریت نقشها', 'slug' => 'roles.manage'],
|
||||
['name' => 'مدیریت دسترسیها', 'slug' => 'permissions.manage'],
|
||||
['name' => 'مدیریت کیف پول', 'slug' => 'wallets.manage'],
|
||||
])->map(fn (array $permission) => Permission::query()->updateOrCreate(
|
||||
['slug' => $permission['slug']],
|
||||
['name' => $permission['name']],
|
||||
@@ -47,5 +49,7 @@ class DatabaseSeeder extends Seeder
|
||||
);
|
||||
|
||||
$user->roles()->sync([$adminRole->id]);
|
||||
|
||||
User::query()->each(fn (User $user) => app(WalletService::class)->createForUser($user));
|
||||
}
|
||||
}
|
||||
|
||||
100
docker-compose.yml
Normal file
100
docker-compose.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: app
|
||||
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
|
||||
image: hoshpoint-backend-nginx:production
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- app
|
||||
ports:
|
||||
- "127.0.0.1:${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:
|
||||
- app
|
||||
- mariadb
|
||||
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:
|
||||
- app
|
||||
- mariadb
|
||||
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
32
docker/entrypoint.sh
Normal 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 "$@"
|
||||
68
docker/nginx/default.conf
Normal file
68
docker/nginx/default.conf
Normal file
@@ -0,0 +1,68 @@
|
||||
map $http_x_forwarded_proto $forwarded_proto {
|
||||
"" $scheme;
|
||||
default $http_x_forwarded_proto;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $forwarded_host {
|
||||
"" $http_host;
|
||||
default $http_x_forwarded_host;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_for $forwarded_for {
|
||||
"" $remote_addr;
|
||||
default $http_x_forwarded_for;
|
||||
}
|
||||
|
||||
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;
|
||||
fastcgi_param HTTP_HOST $http_host;
|
||||
fastcgi_param HTTP_X_FORWARDED_PROTO $forwarded_proto;
|
||||
fastcgi_param HTTP_X_FORWARDED_FOR $forwarded_for;
|
||||
fastcgi_param HTTP_X_FORWARDED_HOST $forwarded_host;
|
||||
include fastcgi_params;
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
BIN
docker/php/composer.phar
Normal file
BIN
docker/php/composer.phar
Normal file
Binary file not shown.
8
docker/php/opcache.ini
Normal file
8
docker/php/opcache.ini
Normal 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
12
docker/php/php.ini
Normal 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
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
// JavaScript entry point required by vite.config.js.
|
||||
//
|
||||
|
||||
0
resources/views/vendor/l5-swagger/.gitkeep
vendored
Normal file
0
resources/views/vendor/l5-swagger/.gitkeep
vendored
Normal file
174
resources/views/vendor/l5-swagger/index.blade.php
vendored
Normal file
174
resources/views/vendor/l5-swagger/index.blade.php
vendored
Normal 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
9
routes/api.php
Normal 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']);
|
||||
});
|
||||
202
storage/api-docs/api-docs.json
Normal file
202
storage/api-docs/api-docs.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user