add captcha and top sale commodityes chart

This commit is contained in:
Hesabix 2025-03-03 16:58:19 +00:00
parent 4c5908c5c8
commit 7c58275de6
23 changed files with 1298 additions and 753 deletions

View file

@ -19,7 +19,7 @@
"melipayamak/php": "1.0.0",
"mpdf/mpdf": "^8.2",
"nelmio/api-doc-bundle": "^4.34",
"nelmio/cors-bundle": "^2.4",
"nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.3",
"phpoffice/phpspreadsheet": "^1.29",
"phpstan/phpdoc-parser": "^1.16",

View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0000709f0d53a720475e13fa5dae69ab",
"content-hash": "66bf4d4239acf870e6cf14109284dd59",
"packages": [
{
"name": "composer/pcre",
@ -1228,16 +1228,16 @@
},
{
"name": "doctrine/orm",
"version": "2.20.1",
"version": "2.20.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/orm.git",
"reference": "e3cabade99ebccc6ba078884c1c5f250866a494e"
"reference": "19912de9270fa6abb3d25a1a37784af6b818d534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/orm/zipball/e3cabade99ebccc6ba078884c1c5f250866a494e",
"reference": "e3cabade99ebccc6ba078884c1c5f250866a494e",
"url": "https://api.github.com/repos/doctrine/orm/zipball/19912de9270fa6abb3d25a1a37784af6b818d534",
"reference": "19912de9270fa6abb3d25a1a37784af6b818d534",
"shasum": ""
},
"require": {
@ -1324,9 +1324,9 @@
],
"support": {
"issues": "https://github.com/doctrine/orm/issues",
"source": "https://github.com/doctrine/orm/tree/2.20.1"
"source": "https://github.com/doctrine/orm/tree/2.20.2"
},
"time": "2024-12-19T06:48:36+00:00"
"time": "2025-02-04T19:17:01+00:00"
},
{
"name": "doctrine/persistence",
@ -1671,16 +1671,16 @@
},
{
"name": "friendsofsymfony/ckeditor-bundle",
"version": "2.5.0",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle.git",
"reference": "9d4cd4f2db4d800164b9c3051e4bfdee21acb27f"
"reference": "dc0f0dc1ba328e0adf5df0a37f1676b6072f46de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSCKEditorBundle/zipball/9d4cd4f2db4d800164b9c3051e4bfdee21acb27f",
"reference": "9d4cd4f2db4d800164b9c3051e4bfdee21acb27f",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSCKEditorBundle/zipball/dc0f0dc1ba328e0adf5df0a37f1676b6072f46de",
"reference": "dc0f0dc1ba328e0adf5df0a37f1676b6072f46de",
"shasum": ""
},
"require": {
@ -1708,6 +1708,7 @@
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.41",
"matthiasnoback/symfony-dependency-injection-test": "^4.0 || ^5.0",
"phpunit/phpunit": "^9.6",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
@ -1746,9 +1747,9 @@
],
"support": {
"issues": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/issues",
"source": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/tree/2.5.0"
"source": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/tree/2.6.0"
},
"time": "2024-01-23T15:35:55+00:00"
"time": "2025-01-13T15:11:41+00:00"
},
{
"name": "gregwar/captcha",
@ -2458,16 +2459,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.12.1",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
"reference": "024473a478be9df5fdaca2c793f2232fe788e414"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"shasum": ""
},
"require": {
@ -2506,7 +2507,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
},
"funding": [
{
@ -2514,20 +2515,20 @@
"type": "tidelift"
}
],
"time": "2024-11-08T17:47:46+00:00"
"time": "2025-02-12T12:17:51+00:00"
},
{
"name": "nelmio/api-doc-bundle",
"version": "v4.36.1",
"version": "v4.37.1",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioApiDocBundle.git",
"reference": "cdc855ef8e6a811336c3a6c72fe99fbe13a78e37"
"reference": "1497977f82d396f1dda8120434c6d29f3de683e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/cdc855ef8e6a811336c3a6c72fe99fbe13a78e37",
"reference": "cdc855ef8e6a811336c3a6c72fe99fbe13a78e37",
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/1497977f82d396f1dda8120434c6d29f3de683e6",
"reference": "1497977f82d396f1dda8120434c6d29f3de683e6",
"shasum": ""
},
"require": {
@ -2628,7 +2629,7 @@
],
"support": {
"issues": "https://github.com/nelmio/NelmioApiDocBundle/issues",
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.36.1"
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.37.1"
},
"funding": [
{
@ -2636,7 +2637,7 @@
"type": "github"
}
],
"time": "2025-01-19T17:01:30+00:00"
"time": "2025-02-14T12:47:20+00:00"
},
{
"name": "nelmio/cors-bundle",
@ -3075,20 +3076,20 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.9",
"version": "1.29.10",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "ffb47b639649fc9c8a6fa67977a27b756592ed85"
"reference": "c80041b1628c4f18030407134fe88303661d4e4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/ffb47b639649fc9c8a6fa67977a27b756592ed85",
"reference": "ffb47b639649fc9c8a6fa67977a27b756592ed85",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/c80041b1628c4f18030407134fe88303661d4e4e",
"reference": "c80041b1628c4f18030407134fe88303661d4e4e",
"shasum": ""
},
"require": {
"composer/pcre": "^3.3",
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
@ -3175,9 +3176,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.9"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.10"
},
"time": "2025-01-26T04:55:00+00:00"
"time": "2025-02-08T02:56:14+00:00"
},
{
"name": "phpstan/phpdoc-parser",
@ -3810,31 +3811,31 @@
},
{
"name": "setasign/fpdi",
"version": "v2.6.2",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/Setasign/FPDI.git",
"reference": "9e013b376939c0d4029f54150d2a16f3c67a5797"
"reference": "67c31f5e50c93c20579ca9e23035d8c540b51941"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/9e013b376939c0d4029f54150d2a16f3c67a5797",
"reference": "9e013b376939c0d4029f54150d2a16f3c67a5797",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/67c31f5e50c93c20579ca9e23035d8c540b51941",
"reference": "67c31f5e50c93c20579ca9e23035d8c540b51941",
"shasum": ""
},
"require": {
"ext-zlib": "*",
"php": "^5.6 || ^7.0 || ^8.0"
"php": "^7.1 || ^8.0"
},
"conflict": {
"setasign/tfpdf": "<1.31"
},
"require-dev": {
"phpunit/phpunit": "~5.7",
"phpunit/phpunit": "^7",
"setasign/fpdf": "~1.8.6",
"setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "~6.2"
"tecnickcom/tcpdf": "^6.2"
},
"suggest": {
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
@ -3870,7 +3871,7 @@
],
"support": {
"issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.2"
"source": "https://github.com/Setasign/FPDI/tree/v2.6.3"
},
"funding": [
{
@ -3878,7 +3879,7 @@
"type": "tidelift"
}
],
"time": "2024-12-10T13:12:19+00:00"
"time": "2025-02-05T13:22:35+00:00"
},
{
"name": "symfony/apache-pack",
@ -9039,20 +9040,20 @@
},
{
"name": "twig/extra-bundle",
"version": "v3.19.0",
"version": "v3.20.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "9746573ca4bc1cd03a767a183faadaf84e0c31fa"
"reference": "9df5e1dbb6a68c0665ae5603f6f2c20815647876"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/9746573ca4bc1cd03a767a183faadaf84e0c31fa",
"reference": "9746573ca4bc1cd03a767a183faadaf84e0c31fa",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/9df5e1dbb6a68c0665ae5603f6f2c20815647876",
"reference": "9df5e1dbb6a68c0665ae5603f6f2c20815647876",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"php": ">=8.1.0",
"symfony/framework-bundle": "^5.4|^6.4|^7.0",
"symfony/twig-bundle": "^5.4|^6.4|^7.0",
"twig/twig": "^3.2|^4.0"
@ -9097,7 +9098,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.19.0"
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.20.0"
},
"funding": [
{
@ -9109,28 +9110,27 @@
"type": "tidelift"
}
],
"time": "2024-09-26T19:22:23+00:00"
"time": "2025-02-08T09:47:15+00:00"
},
{
"name": "twig/twig",
"version": "v3.19.0",
"version": "v3.20.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e"
"reference": "3468920399451a384bef53cf7996965f7cd40183"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/d4f8c2b86374f08efc859323dbcd95c590f7124e",
"reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183",
"reference": "3468920399451a384bef53cf7996965f7cd40183",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"php": ">=8.1.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
@ -9177,7 +9177,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.19.0"
"source": "https://github.com/twigphp/Twig/tree/v3.20.0"
},
"funding": [
{
@ -9189,7 +9189,7 @@
"type": "tidelift"
}
],
"time": "2025-01-29T07:06:14+00:00"
"time": "2025-02-13T08:34:43+00:00"
},
{
"name": "webmozart/assert",
@ -9251,16 +9251,16 @@
},
{
"name": "zircote/swagger-php",
"version": "5.0.3",
"version": "5.0.5",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
"reference": "7708510b17502a416214148edaa8c9958b23b6cd"
"reference": "2eb4005840058d8844a0bcc14403932331331068"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/7708510b17502a416214148edaa8c9958b23b6cd",
"reference": "7708510b17502a416214148edaa8c9958b23b6cd",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/2eb4005840058d8844a0bcc14403932331331068",
"reference": "2eb4005840058d8844a0bcc14403932331331068",
"shasum": ""
},
"require": {
@ -9331,9 +9331,9 @@
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/5.0.3"
"source": "https://github.com/zircote/swagger-php/tree/5.0.5"
},
"time": "2025-01-15T21:02:43+00:00"
"time": "2025-02-24T00:48:00+00:00"
}
],
"packages-dev": [

View file

@ -1,7 +1,6 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '15'
@ -16,6 +15,9 @@ doctrine:
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
dql:
string_functions:
CAST: App\Doctrine\Cast
when@test:
doctrine:

View file

@ -8,10 +8,12 @@ framework:
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
enabled: true
handler_id: null
cookie_secure: auto
cookie_samesite: lax
cookie_secure: true
storage_factory_id: session.storage.factory.native
cookie_lifetime: 3600 # 1 ساعت
cookie_samesite: none # برای CORS باید Lax یا None باشه
#esi: true
#fragments: true

View file

@ -1,10 +1,19 @@
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: false
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['*']
expose_headers: ['Link']
allow_origin: [ '*' ] # اجازه به همه دامنه‌ها
allow_methods: [ 'GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE' ]
allow_headers: [ '*' ] # اجازه به همه هدرها
expose_headers: [ 'Link' ]
allow_credentials: true # اجازه ارسال کوکی‌ها
max_age: 3600
paths:
'^/': null
'^/':
# اعمال تنظیمات به همه مسیرها
allow_origin: [ '*' ]
allow_methods: [ 'GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE' ]
allow_headers: [ '*' ]
expose_headers: [ 'Link' ]
allow_credentials: true
max_age: 3600

View file

@ -22,10 +22,13 @@ security:
check_path: api_login
username_path: mobile
password_path: password
failure_handler: App\Security\AuthenticationFailureHandler
form_login:
# ...
failure_handler: App\Security\AuthenticationFailureHandler
custom_authenticators:
- App\Security\ApiKeyAuthenticator
- App\Security\ParttyAuthenticator
- App\Security\ApiKeyAuthenticator
- App\Security\ParttyAuthenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@ -44,12 +47,12 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api/acc/*, roles: ROLE_USER }
- { path: ^/hooks/*, roles: ROLE_USER }
- { path: ^/api/app/*, roles: ROLE_USER }
- { path: ^/api/admin/*, roles: ROLE_ADMIN }
- { path: ^/app/*, roles: ROLE_ADMIN }
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api/acc/*, roles: ROLE_USER }
- { path: ^/hooks/*, roles: ROLE_USER }
- { path: ^/api/app/*, roles: ROLE_USER }
- { path: ^/api/admin/*, roles: ROLE_ADMIN }
- { path: ^/app/*, roles: ROLE_ADMIN }
when@test:
security:
password_hashers:

View file

@ -12,17 +12,20 @@ parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\Security\AuthenticationFailureHandler:
arguments:
$captchaService: '@App\Service\CaptchaService'
$requestStack: '@request_stack'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
@ -48,4 +51,4 @@ services:
arguments: [ "@doctrine.orm.entity_manager" ]
Printers:
class: App\Service\Printers
arguments: [ "@doctrine.orm.entity_manager" ]
arguments: [ "@doctrine.orm.entity_manager" ]

View file

@ -0,0 +1,18 @@
<?php
// src/Controller/CaptchaController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use App\Service\CaptchaService;
class CaptchaController extends AbstractController
{
#[Route('/api/captcha/image', name: 'api_captcha_image', methods: ['GET'])]
public function generateCaptchaImage(SessionInterface $session,CaptchaService $captchaService): Response
{
return $captchaService->createCaptchaImage($session);
}
}

View file

@ -63,6 +63,7 @@ class DashboardController extends AbstractController
if(array_key_exists('persons',$params)) $setting->setPersons($params['persons']);
if(array_key_exists('notif',$params)) $setting->setNotif($params['notif']);
if(array_key_exists('sellChart',$params)) $setting->setSellChart($params['sellChart']);
if(array_key_exists('topCommodities',$params)) $setting->setTopCommoditiesChart($params['topCommodities']);
$entityManagerInterface->persist($setting);
$entityManagerInterface->flush();

View file

@ -17,6 +17,7 @@ use App\Entity\HesabdariRow;
use App\Service\Explore;
use App\Service\Jdate;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -28,11 +29,13 @@ class ReportController extends AbstractController
{
private $em;
private $provider;
function __construct(Provider $provider, EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
$this->provider = $provider;
}
#[Route('/api/report/person/buysell', name: 'app_report_person_buysell')]
public function app_report_person_buysell(Provider $provider, Jdate $jdate, Access $access, Request $request, EntityManagerInterface $entityManagerInterface): JsonResponse
{
@ -215,7 +218,6 @@ class ReportController extends AbstractController
]);
}
$commodity = $entityManagerInterface->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid']->getId(),
'code' => $params['commodity'],
@ -285,4 +287,107 @@ class ReportController extends AbstractController
}
return $this->json($response);
}
}
#[Route('/api/report/top-selling-commodities', name: 'app_report_top_selling_commodities', methods: ['POST'])]
public function app_report_top_selling_commodities(Access $access, Explore $explore, Jdate $jdate, Request $request, EntityManagerInterface $entityManager, LoggerInterface $logger): JsonResponse
{
$acc = $access->hasRole('report');
if (!$acc) {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم برای مشاهده این اطلاعات را ندارید.');
}
}
/** @var Business $business */
$business = $acc['bid'];
/** @var Year $year */
$year = $acc['year'];
$payload = $request->getPayload();
$period = $payload->get('period', 'year');
$limit = (int) $payload->get('limit', 10);
if ($limit < 3) {
$limit = 3;
}
$today = $jdate->GetTodayDate();
list($currentYear, $currentMonth, $currentDay) = explode('/', $today);
switch ($period) {
case 'today':
$dateStart = $today;
$dateEnd = $today;
break;
case 'week':
$weekDay = (int) $jdate->jdate('w', time());
$daysToSubtract = $weekDay;
$dateStart = $jdate->shamsiDate(0, 0, -$daysToSubtract);
$dateEnd = $jdate->shamsiDate(0, 0, 6 - $weekDay);
break;
case 'month':
$dateStart = "$currentYear/$currentMonth/01";
$dateEnd = "$currentYear/$currentMonth/" . $jdate->jdate('t', $jdate->jallaliToUnixTime("$currentYear/$currentMonth/01"));
break;
case 'year':
default:
$dateStart = $jdate->jdate('Y/m/d', $year->getStart());
$dateEnd = $jdate->jdate('Y/m/d', $year->getEnd());
break;
}
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder
->select('c') // Commodity
->addSelect('SUM(CAST(hr.commdityCount AS INTEGER)) as totalCount')
->addSelect('hr') // HesabdariRow
->from(HesabdariRow::class, 'hr')
->innerJoin('hr.doc', 'hd')
->innerJoin('hr.commodity', 'c')
->where('hd.bid = :business')
->andWhere('hd.type = :type')
->andWhere('hr.year = :year')
->andWhere('hd.date BETWEEN :dateStart AND :dateEnd')
->setParameter('business', $business)
->setParameter('type', 'sell')
->setParameter('year', $year)
->setParameter('dateStart', $dateStart)
->setParameter('dateEnd', $dateEnd)
->groupBy('c.id, hr.id')
->orderBy('totalCount', 'DESC')
->setMaxResults($limit);
try {
$results = $queryBuilder->getQuery()->getResult();
$logger->info('Query executed successfully', [
'sql' => $queryBuilder->getQuery()->getSQL(),
'params' => $queryBuilder->getQuery()->getParameters()->toArray(),
'results' => $results
]);
if (empty($results)) {
$logger->info('No results returned from query');
return $this->json(['message' => 'No data found'], 200);
}
$topCommodities = [];
foreach ($results as $result) {
// با توجه به لاگ، اندیس 0 الان HesabdariRow هست
$row = $result[0]; // HesabdariRow
$commodity = $row->getCommodity(); // Commodity از داخل HesabdariRow
$totalCount = (int) $result['totalCount'];
$topCommodities[] = $explore::ExploreCommodity($commodity, $totalCount);
}
return $this->json($topCommodities);
} catch (\Exception $e) {
$logger->error('Error in top-selling commodities query', [
'message' => $e->getMessage(),
'sql' => $queryBuilder->getQuery()->getSQL(),
'params' => $queryBuilder->getQuery()->getParameters()->toArray(),
'trace' => $e->getTraceAsString()
]);
return $this->json(['error' => 'An error occurred: ' . $e->getMessage()], 500);
}
}
}

View file

@ -5,9 +5,11 @@ namespace App\Controller;
use App\Entity\Business;
use App\Entity\EmailHistory;
use App\Entity\Permission;
use App\Service\CaptchaService;
use App\Service\Extractor;
use App\Service\Provider;
use App\Service\SMS;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
@ -18,6 +20,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use App\Entity\User;
@ -52,61 +55,77 @@ class UserController extends AbstractController
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
#[Route('/api/user/login', name: 'api_login')]
public function api_login(TranslatorInterface $translatorInterface, Extractor $extractor, Request $request, #[CurrentUser] ?User $user, EntityManagerInterface $entityManager): Response
{
#[Route('/api/user/login', name: 'api_login', methods: ['POST'])]
public function api_login(
TranslatorInterface $translatorInterface,
Extractor $extractor,
Request $request,
CaptchaService $captchaService,
#[CurrentUser] ?User $user,
EntityManagerInterface $entityManager
): Response {
$params = $request->getPayload()->all();
if (array_key_exists('standard', $params)) {
if (null === $user) {
$captchaAnswer = $params['captcha_answer'] ?? '';
$attemptKey = 'login_attempts';
// بررسی نیاز به کپچا
$captchaRequired = $captchaService->isCaptchaRequired($attemptKey);
if ($captchaRequired) {
if (!$captchaService->validateCaptcha($captchaAnswer)) {
return $this->json($extractor->operationFail(
$translatorInterface->trans('login_fail'),
empty($captchaAnswer) ? 'کپچا لازم است' : 'کپچا اشتباه است',
400,
['captcha_required' => true]
));
}
if ($user->isActive() == false) {
return $this->json($extractor->operationFail(
'حساب کاربری شما فعال نیست. لطفا با پشتیبانی تماس بگیرید'
,
506,
[
'user' => $user->getUserIdentifier(),
'active' => $user->isActive(),
]
));
}
$token = new UserToken();
$token->setUser($user);
$token->setToken($this->RandomString(254));
$token->setTokenID($this->RandomString(254));
$entityManager->persist($token);
$entityManager->flush();
return $this->json($extractor->operationSuccess([
'user' => $user->getUserIdentifier(),
'token' => $token->getToken(),
'tokenID' => $token->getTokenID(),
'active' => $user->isActive(),
]));
} else {
if (null === $user) {
return $this->json([
'message' => 'missing credentials',
], Response::HTTP_UNAUTHORIZED);
}
$token = new UserToken();
$token->setUser($user);
$token->setToken($this->RandomString(254));
$token->setTokenID($this->RandomString(254));
$entityManager->persist($token);
$entityManager->flush();
return $this->json([
'user' => $user->getUserIdentifier(),
'token' => $token->getToken(),
'tokenID' => $token->getTokenID(),
'active' => $user->isActive(),
]);
}
// چون #[CurrentUser] فقط در صورت احراز هویت موفق کاربر رو برمی‌گردونه،
// اینجا فقط شرایط بعد از احراز هویت موفق بررسی می‌شه
if (null === $user) {
// این خط عملاً اجرا نمی‌شه چون Security Bundle شکست رو مدیریت می‌کنه
// اما برای اطمینان نگهش داشتم
$captchaService->incrementAttempts($attemptKey);
return $this->json($extractor->operationFail(
$translatorInterface->trans('login_fail'),
400,
['captcha_required' => $captchaService->isCaptchaRequired($attemptKey)]
));
}
// بررسی وضعیت کاربر
if ($user->isActive() == false) {
$captchaService->incrementAttempts($attemptKey);
return $this->json($extractor->operationFail(
'حساب کاربری شما فعال نیست. لطفا با پشتیبانی تماس بگیرید',
506,
[
'user' => $user->getUserIdentifier(),
'active' => $user->isActive(),
'captcha_required' => $captchaService->isCaptchaRequired($attemptKey)
]
));
}
// ورود موفق
$token = new UserToken();
$token->setUser($user);
$token->setToken($this->RandomString(254));
$token->setTokenID($this->RandomString(254));
$entityManager->persist($token);
$entityManager->flush();
$captchaService->resetAttempts($attemptKey);
return $this->json($extractor->operationSuccess([
'user' => $user->getUserIdentifier(),
'token' => $token->getToken(),
'tokenID' => $token->getTokenID(),
'active' => $user->isActive(),
'captcha_required' => false
]));
}
#[Route('/api/user/has/role/{id}', name: 'api_user_has_role')]
public function api_user_has_role(Extractor $extractor, #[CurrentUser] ?User $user, EntityManagerInterface $entityManager, $id): Response
{
@ -279,23 +298,51 @@ class UserController extends AbstractController
));
}
#[Route('/api/user/register', name: 'api_user_register')]
public function api_user_register(Extractor $extractor, registryMGR $registryMGR, SMS $SMS, MailerInterface $mailer, Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response
{
#[Route('/api/user/register', name: 'api_user_register', methods: ['POST'])]
public function api_user_register(
Extractor $extractor,
registryMGR $registryMGR,
SMS $SMS,
MailerInterface $mailer,
Request $request,
UserPasswordHasherInterface $userPasswordHasher,
EntityManagerInterface $entityManager,
CaptchaService $captchaService // اضافه کردن به آرگومان‌های متد
): Response {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (array_key_exists('name', $params) && array_key_exists('email', $params) && array_key_exists('mobile', $params) && array_key_exists('password', $params)) {
// همیشه کپچا رو چک می‌کنیم
$captchaAnswer = $params['captcha_answer'] ?? '';
if (!$captchaService->validateCaptcha($captchaAnswer)) {
return $this->json($extractor->operationFail(
empty($captchaAnswer) ? 'کپچا لازم است' : 'کپچا اشتباه است',
400,
['captcha_required' => true]
));
}
// ادامه منطق عضویت
if (
array_key_exists('name', $params) && array_key_exists('email', $params) &&
array_key_exists('mobile', $params) && array_key_exists('password', $params)
) {
if ($entityManager->getRepository(User::class)->findOneBy(['email' => trim($params['email'])])) {
return $this->json($extractor->operationFail(
'پست الکترونیکی وارد شده قبلا ثبت شده است'
'پست الکترونیکی وارد شده قبلا ثبت شده است',
400,
['captcha_required' => true]
));
} elseif ($entityManager->getRepository(User::class)->findOneBy(['mobile' => trim($params['mobile'])])) {
return $this->json($extractor->operationFail(
'شماره تلفن وارد شده قبلا ثبت شده است'
'شماره تلفن وارد شده قبلا ثبت شده است',
400,
['captcha_required' => true]
));
}
$user = new User();
$user->setEmail($params['email']);
$user->setRoles(['ROLE_USER']);
@ -313,11 +360,13 @@ class UserController extends AbstractController
$user->setActive(false);
$entityManager->persist($user);
$entityManager->flush();
$SMS->send(
[$user->getVerifyCode()],
$registryMGR->get('sms', 'f2a'),
$user->getMobile()
);
try {
$email = (new Email())
->to($user->getEmail())
@ -331,16 +380,22 @@ class UserController extends AbstractController
$mailer->send($email);
} catch (Exception $exception) {
// خطای ارسال ایمیل رو می‌تونید لاگ کنید، فعلاً نادیده می‌گیره
}
return $this->json($extractor->operationSuccess([
'id' => $user->getId()
]));
}
return $this->json($extractor->operationFail(
'تمام موارد لازم را وارد کنید.'
'تمام موارد لازم را وارد کنید.',
400,
['captcha_required' => true]
));
}
#[Route('/api/user/active/code/info/{id}', name: 'api_user_active_code_info')]
public function api_user_active_code_info(registryMGR $registryMGR, MailerInterface $mailer, SMS $SMS, string $id, #[CurrentUser] ?User $user, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager, Request $request): Response
{
@ -479,35 +534,61 @@ class UserController extends AbstractController
}
return $this->json($extractor->operationFail('کد ارسالی اشتباه است.'));
}
#[Route('/api/user/forget/password/send-code', name: 'api_user_forget_password_send_code')]
public function api_user_forget_password_send_code(Extractor $extractor, registryMGR $registryMGR, #[CurrentUser] ?User $user, SMS $SMS, MailerInterface $mailer, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager, Request $request): Response
{
#[Route('/api/user/forget/password/send-code', name: 'api_user_forget_password_send_code', methods: ['POST'])]
public function api_user_forget_password_send_code(
Extractor $extractor,
registryMGR $registryMGR,
#[CurrentUser] ?User $user,
SMS $SMS,
MailerInterface $mailer,
UserPasswordHasherInterface $userPasswordHasher,
EntityManagerInterface $entityManager,
Request $request,
CaptchaService $captchaService
): Response {
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
// همیشه کپچا رو چک می‌کنیم
$captchaAnswer = $params['captcha_answer'] ?? '';
if (!$captchaService->validateCaptcha($captchaAnswer)) {
return $this->json($extractor->operationFail(
empty($captchaAnswer) ? 'کپچا لازم است' : 'کپچا اشتباه است',
400,
['captcha_required' => true]
));
}
// ادامه منطق بازیابی
if (!array_key_exists('mobile', $params)) {
return $this->json($extractor->paramsNotSend());
}
$user = $entityManager->getRepository(User::class)->findOneBy(['mobile' => $params['mobile']]);
if (!$user) {
return $this->json(data: $extractor->operationFail(
return $this->json($extractor->operationFail(
'کاربری با شماره تلفن وارد شده یافت نشد.',
404
404,
['captcha_required' => true]
));
}
if ($user->getVerifyCodeTime() > time()) {
return $this->json(data: $extractor->operationFail(
'کد بازیابی رمز عبور اخیرا ارسال شده است.لطفا چند دقیقه دیگر مجددا درخواست خود را ارسال نمایید.',
600
return $this->json($extractor->operationFail(
'کد بازیابی رمز عبور اخیرا ارسال شده است. لطفا چند دقیقه دیگر مجددا درخواست خود را ارسال نمایید.',
600,
['captcha_required' => true]
));
}
$user->setVerifyCode($this->RandomString(6, true));
$user->setVerifyCodeTime(time() + 300);
$entityManager->persist($user);
$entityManager->flush();
//send sms and email
// ارسال SMS و ایمیل
$SMS->send(
[$user->getVerifyCode()],
$registryMGR->get('sms', 'recPassword'),
@ -524,10 +605,12 @@ class UserController extends AbstractController
);
$mailer->send($email);
return $this->json($extractor->operationSuccess([
'id' => $user->getId(),
]));
}
#[Route('/api/user/save/mobile-number', name: 'api_user_save_mobile_number')]
public function api_user_save_mobile_number(MailerInterface $mailer, SMS $SMS, #[CurrentUser] ?User $user, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager, Request $request): Response
{

View file

@ -0,0 +1,28 @@
<?php
namespace App\Doctrine;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class Cast extends FunctionNode
{
private $expression;
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER); // CAST
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expression = $parser->ArithmeticExpression();
$parser->match(Lexer::T_AS);
$parser->match(Lexer::T_IDENTIFIER); // INTEGER یا هر نوع دیگه
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker)
{
// به جای استفاده از $this->type، مستقیماً SIGNED رو می‌ذاریم
return 'CAST(' . $sqlWalker->walkArithmeticPrimary($this->expression) . ' AS SIGNED)';
}
}

View file

@ -51,6 +51,9 @@ class DashboardSettings
#[ORM\Column(nullable: true)]
private ?bool $sellChart = null;
#[ORM\Column(nullable: true)]
private ?bool $topCommoditiesChart = null;
public function getId(): ?int
{
return $this->id;
@ -199,4 +202,16 @@ class DashboardSettings
return $this;
}
public function isTopCommoditiesChart(): ?bool
{
return $this->topCommoditiesChart;
}
public function setTopCommoditiesChart(?bool $topCommoditiesChart): static
{
$this->topCommoditiesChart = $topCommoditiesChart;
return $this;
}
}

View file

@ -0,0 +1,25 @@
<?php
// src/EventListener/AuthenticationFailureListener.php
namespace App\EventListener;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Http\Event\AuthenticationFailureEvent;
use App\Service\CaptchaService;
class AuthenticationFailureListener
{
private CaptchaService $captchaService;
private SessionInterface $session;
public function __construct(CaptchaService $captchaService, SessionInterface $session)
{
$this->captchaService = $captchaService;
$this->session = $session;
}
public function onAuthenticationFailure(AuthenticationFailureEvent $event): void
{
$attemptKey = 'login_attempts';
$this->captchaService->incrementAttempts($this->session, $attemptKey);
}
}

View file

@ -0,0 +1,40 @@
<?php
// src/Security/AuthenticationFailureHandler.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use App\Service\CaptchaService;
class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
private CaptchaService $captchaService;
private RequestStack $requestStack;
public function __construct(CaptchaService $captchaService, RequestStack $requestStack)
{
$this->captchaService = $captchaService;
$this->requestStack = $requestStack;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
{
$session = $this->requestStack->getSession();
$attemptKey = 'login_attempts';
$this->captchaService->incrementAttempts($attemptKey);
$attempts = $session->get($attemptKey, 0);
$sessionId = $session->getId(); // برای چک کردن اینکه سشن ثابت می‌مونه یا نه
return new JsonResponse([
'error' => 'احراز هویت نامعتبر می باشد.',
'captcha_required' => $this->captchaService->isCaptchaRequired($attemptKey),
'attempts' => $attempts,
'last_attempt_time' => $session->get($attemptKey . '_time', 0),
'session_id' => $sessionId // برای دیباگ
], 401);
}
}

View file

@ -0,0 +1,125 @@
<?php
// src/Service/CaptchaService.php
namespace App\Service;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
class CaptchaService
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function createCaptchaImage(): Response
{
$session = $this->requestStack->getSession();
// تولید کپچا که با صفر شروع نشه
$firstDigit = rand(1, 9); // رقم اول بین 1 تا 9
$remainingDigits = str_pad(rand(0, 99999), 5, '0', STR_PAD_LEFT); // ۵ رقم بعدی
$captchaCode = $firstDigit . $remainingDigits; // ترکیب به صورت ۶ رقمی
$persianNumbers = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
$englishNumbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
$captchaCodePersian = str_replace($englishNumbers, $persianNumbers, $captchaCode);
$session->set('captcha_code', $captchaCode);
$image = imagecreatetruecolor(200, 80);
$backgroundColor = imagecolorallocate($image, 255, 255, 255);
$textColor = imagecolorallocate($image, 0, 0, 0);
imagefill($image, 0, 0, $backgroundColor);
$fontPath = __DIR__ . '/../Fonts/Vazirmatn-Black.ttf';
imagettftext($image, 20, rand(-10, 10), 40, 50, $textColor, $fontPath, $captchaCodePersian);
$lineColor1 = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
$lineColor2 = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
imageline($image, rand(0, 50), rand(0, 80), rand(150, 200), rand(0, 80), $lineColor1);
imageline($image, rand(0, 50), rand(0, 80), rand(150, 200), rand(0, 80), $lineColor2);
$circleColor = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
imageellipse($image, rand(20, 180), rand(10, 70), rand(20, 50), rand(20, 50), $circleColor);
$squareColor = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
$squareX = rand(20, 160);
$squareY = rand(10, 40);
$squareSize = rand(20, 40);
imagerectangle($image, $squareX, $squareY, $squareX + $squareSize, $squareY + $squareSize, $squareColor);
imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR);
$wavedImage = imagecreatetruecolor(200, 80);
imagefill($wavedImage, 0, 0, $backgroundColor);
for ($x = 0; $x < 200; $x++) {
for ($y = 0; $y < 80; $y++) {
$newX = $x + sin($y / 10) * 5;
$newY = $y + cos($x / 10) * 5;
if ($newX >= 0 && $newX < 200 && $newY >= 0 && $newY < 80) {
$color = imagecolorat($image, $x, $y);
imagesetpixel($wavedImage, (int)$newX, (int)$newY, $color);
}
}
}
ob_start();
imagepng($wavedImage);
$imageData = ob_get_clean();
imagedestroy($image);
imagedestroy($wavedImage);
return new Response($imageData, 200, ['Content-Type' => 'image/png']);
}
public function isCaptchaRequired(string $attemptKey): bool
{
$session = $this->requestStack->getSession();
$attempts = $session->get($attemptKey, 0);
$lastAttemptTime = $session->get($attemptKey . '_time', 0);
$currentTime = time();
$halfHour = 30 * 60;
if (($currentTime - $lastAttemptTime) > $halfHour) {
$session->set($attemptKey, 0);
$session->set($attemptKey . '_time', $currentTime);
return false;
}
return $attempts >= 3;
}
public function validateCaptcha(string $captchaAnswer): bool
{
$session = $this->requestStack->getSession();
if (empty($captchaAnswer)) {
return false;
}
$storedCode = $session->get('captcha_code');
if ($captchaAnswer === $storedCode) {
$session->remove('captcha_code');
return true;
}
return false;
}
public function incrementAttempts(string $attemptKey): void
{
$session = $this->requestStack->getSession();
$attempts = $session->get($attemptKey, 0) + 1;
$session->set($attemptKey, $attempts);
$session->set($attemptKey . '_time', time());
}
public function resetAttempts(string $attemptKey): void
{
$session = $this->requestStack->getSession();
$session->set($attemptKey, 0);
$session->set($attemptKey . '_time', time());
}
}

View file

@ -584,7 +584,10 @@ class Explore
'persons' => $item->isPersons(),
'notif' => $item->isNotif(),
'sellChart' => $item->isSellChart(),
'topCommodities' => $item->isTopCommoditiesChart(),
];
if ($result['topCommodities'] === null)
$result['topCommodities'] = true;
if ($result['banks'] === null)
$result['banks'] = true;
if ($result['buys'] === null)

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,6 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/hesabixCore/vendor/autoload_runtime.php';
header("Access-Control-Allow-Credentials: true");
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};