feat: Implement SMS sending functionality with KavehNegar and Rangineh providers

- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
hamed
2026-06-09 22:00:34 +03:30
commit de1a78a235
222 changed files with 36388 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
];
+4
View File
@@ -0,0 +1,4 @@
framework:
cache:
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_URL)%'
+5
View File
@@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
+45
View File
@@ -0,0 +1,45 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src'
prefix: 'App'
alias: App
controller_resolver:
auto_mapping: false
when@test:
doctrine:
dbal:
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system
+6
View File
@@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false
+15
View File
@@ -0,0 +1,15 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file
@@ -0,0 +1,5 @@
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600
+22
View File
@@ -0,0 +1,22 @@
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 5000
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
sync: 'sync://'
routing:
'App\Shared\Message\SendSmsMessage': async
when@test:
framework:
messenger:
transports:
async: 'in-memory://'
+19
View File
@@ -0,0 +1,19 @@
nelmio_api_doc:
documentation:
info:
title: ClinicPro API
description: مستندات API سیستم کلینیک‌پرو
version: 1.0.0
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
areas:
path_patterns:
- ^/api
- ^/oauth
- ^/health
+16
View File
@@ -0,0 +1,16 @@
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'Content-Disposition']
expose_headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset']
max_age: 3600
allow_credentials: false
paths:
'^/api/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
'^/oauth/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
'^/health':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
+3
View File
@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true
+13
View File
@@ -0,0 +1,13 @@
framework:
rate_limiter:
# OTP send-code: max 5 requests per hour per IP (prevents SMS flood)
send_code:
policy: 'sliding_window'
limit: 5
interval: '60 minutes'
# Login: max 10 attempts per minute per IP (brute force protection)
login:
policy: 'fixed_window'
limit: 10
interval: '1 minute'
+10
View File
@@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:
router:
strict_requirements: null
+79
View File
@@ -0,0 +1,79 @@
security:
password_hashers:
App\Auth\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Auth\Entity\User
property: mobileNumber
firewalls:
dev:
pattern: ^/(_profiler|_wdt|assets|build)/
security: false
health:
pattern: ^/health$
security: false
public_endpoints:
pattern: ^/(api/v1/user/(send-code|verify-code|register)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/comments/|api/v1/rate/|api/v1/blogs$)
stateless: true
security: false
payment_callback:
pattern: ^/api/v1/(payment|subscription-payment)/callback/
stateless: true
security: false
api:
pattern: ^/(api|oauth)/
stateless: true
provider: app_user_provider
custom_authenticators:
- App\Auth\Security\PasswordAuthenticator
jwt: ~
access_control:
- { path: ^/health$, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/send-code, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/verify-code, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/register, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/login, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/appointment-slots, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/comments/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/rate/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/blogs$, roles: PUBLIC_ACCESS }
- path: '^/api/v1/blog/[^/]+$'
methods: [GET]
roles: PUBLIC_ACCESS
- { path: ^/oauth/token$, roles: PUBLIC_ACCESS }
- { path: ^/oauth/token/refresh$, roles: PUBLIC_ACCESS }
- { path: ^/session/token, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/payment/callback/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/subscription-payment/callback/, roles: PUBLIC_ACCESS }
- { path: ^/api/doc, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/categorys/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/doctors$, roles: PUBLIC_ACCESS }
- path: '^/api/v1/doctor/[^/]+$'
methods: [GET]
roles: PUBLIC_ACCESS
- { path: ^/api/v1/clinic/doctor-list/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/clinic-pro/doctor-addresses/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/clinics$, roles: PUBLIC_ACCESS }
- path: '^/api/v1/clinic/[^/]+$'
methods: [GET]
roles: PUBLIC_ACCESS
- { path: ^/api/v1/user/\d+$, methods: [DELETE], roles: ROLE_ADMIN }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/oauth/userinfo, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/oauth/logout, roles: IS_AUTHENTICATED_FULLY }
when@test:
security:
password_hashers:
App\Auth\Entity\User:
algorithm: auto
cost: 4
+11
View File
@@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false
+5
View File
@@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}
+1587
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers:
resource: routing.controllers
+4
View File
@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error
+11
View File
@@ -0,0 +1,11 @@
app.swagger_ui:
path: /api/doc
methods: GET
defaults:
_controller: nelmio_api_doc.controller.swagger_ui
app.swagger_json:
path: /api/doc.json
methods: GET
defaults:
_controller: nelmio_api_doc.controller.swagger
+3
View File
@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service
+75
View File
@@ -0,0 +1,75 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: {}
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# 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/'
App\Doctor\Controller\DoctorController:
arguments:
$projectDir: '%kernel.project_dir%'
App\Clinic\Controller\ClinicController:
arguments:
$projectDir: '%kernel.project_dir%'
App\Auth\Service\OtpService:
arguments:
$otpTtl: '%env(int:OTP_TTL)%'
$appEnv: '%kernel.environment%'
App\Auth\Service\TokenService:
arguments:
$refreshTokenTtl: '%env(int:REFRESH_TOKEN_TTL)%'
App\Auth\Security\PasswordAuthenticator:
arguments:
$refreshTokenTtl: '%env(int:REFRESH_TOKEN_TTL)%'
$loginLimiter: '@limiter.login'
App\Auth\Controller\AuthController:
arguments:
$sendCodeLimiter: '@limiter.send_code'
App\Payment\Gateway\MellatGateway:
arguments:
$terminalId: '%env(MELLAT_TERMINAL_ID)%'
$username: '%env(MELLAT_USERNAME)%'
$password: '%env(MELLAT_PASSWORD)%'
App\Payment\Gateway\SepGateway:
arguments:
$terminalId: '%env(SEP_TERMINAL_ID)%'
App\Payment\Controller\PaymentController:
arguments:
$appBaseUrl: '%env(APP_BASE_URL)%'
$allowedFrontendHosts: '%env(ALLOWED_FRONTEND_HOSTS)%'
App\Sms\Provider\KavehNegarProvider:
arguments:
$apiKey: '%env(KAVENEGAR_API_KEY)%'
$sender: '%env(KAVENEGAR_SENDER)%'
App\Sms\Provider\RanginehProvider:
arguments:
$apiKey: '%env(RANGINEH_API_KEY)%'
$sender: '%env(RANGINEH_SENDER)%'
App\Blog\Controller\BlogController:
arguments:
$projectDir: '%kernel.project_dir%'