|
|
@ -3,7 +3,12 @@ framework:
|
||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session: true
|
session:
|
||||||
|
enabled: true
|
||||||
|
cookie_lifetime: 604800 # 1 week
|
||||||
|
cookie_secure: false
|
||||||
|
cookie_httponly: true
|
||||||
|
cookie_samesite: 'lax'
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,41 @@ security:
|
||||||
entity:
|
entity:
|
||||||
class: App\Entity\User
|
class: App\Entity\User
|
||||||
property: email
|
property: email
|
||||||
|
customer_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\Customer
|
||||||
|
property: email
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
security: false
|
security: false
|
||||||
|
admin:
|
||||||
|
pattern: ^/admin
|
||||||
|
lazy: true
|
||||||
|
provider: app_user_provider
|
||||||
|
form_login:
|
||||||
|
login_path: login
|
||||||
|
check_path: login
|
||||||
|
logout:
|
||||||
|
path: logout
|
||||||
|
customer:
|
||||||
|
pattern: ^/customer
|
||||||
|
lazy: true
|
||||||
|
provider: customer_provider
|
||||||
|
form_login:
|
||||||
|
login_path: customer_login
|
||||||
|
check_path: /customer/login_check
|
||||||
|
default_target_path: customer_dashboard
|
||||||
|
enable_csrf: true
|
||||||
|
remember_me: true
|
||||||
|
remember_me:
|
||||||
|
secret: '%kernel.secret%'
|
||||||
|
lifetime: 604800 # 1 week
|
||||||
|
path: /
|
||||||
|
always_remember_me: false
|
||||||
|
logout:
|
||||||
|
path: customer_logout
|
||||||
|
target: app_home
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
|
|
@ -33,6 +64,7 @@ security:
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN }
|
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
|
- { path: ^/customer/dashboard, roles: ROLE_CUSTOMER }
|
||||||
# - { path: ^/profile, roles: ROLE_USER }
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
framework:
|
framework:
|
||||||
default_locale: en
|
default_locale: fa
|
||||||
translator:
|
translator:
|
||||||
default_path: '%kernel.project_dir%/translations'
|
default_path: '%kernel.project_dir%/translations'
|
||||||
fallbacks:
|
fallbacks:
|
||||||
|
- fa
|
||||||
- en
|
- en
|
||||||
providers:
|
providers:
|
||||||
|
|
|
||||||
37
migrations/Version20250903164119.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250903164119 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE customer (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, phone VARCHAR(20) NOT NULL, is_active TINYINT(1) NOT NULL, email_verified_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, last_login_at DATETIME DEFAULT NULL, subscription_type VARCHAR(50) DEFAULT NULL, subscription_expires_at DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_CUSTOMER_EMAIL (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE TABLE password_reset_token (id INT AUTO_INCREMENT NOT NULL, customer_id INT NOT NULL, token VARCHAR(255) NOT NULL, expires_at DATETIME NOT NULL, used_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, INDEX IDX_6B7BA4B69395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE password_reset_token ADD CONSTRAINT FK_6B7BA4B69395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_5A8A6C8DF47645AE ON post (url)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE password_reset_token DROP FOREIGN KEY FK_6B7BA4B69395C3F3');
|
||||||
|
$this->addSql('DROP TABLE customer');
|
||||||
|
$this->addSql('DROP TABLE password_reset_token');
|
||||||
|
$this->addSql('DROP INDEX UNIQ_5A8A6C8DF47645AE ON post');
|
||||||
|
}
|
||||||
|
}
|
||||||
2413
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
||||||
"style-loader": "^4.0.0",
|
"style-loader": "^4.0.0",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^5.1.0",
|
"webpack-cli": "^5.1.0",
|
||||||
|
"webpack-dev-server": "^5.2.2",
|
||||||
"webpack-notifier": "^1.15.0"
|
"webpack-notifier": "^1.15.0"
|
||||||
},
|
},
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
body {
|
body {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: 'Vazir', 'Tahoma', sans-serif; /* فونت فارسی دلخواه */
|
font-family: 'Yekan Bakh FaNum';
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-wrapper {
|
.login-wrapper {
|
||||||
|
|
|
||||||
3
public/img/icons/arrow-left.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 287 B |
3
public/img/icons/calendar.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h6V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
4
public/img/icons/check-circle.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||||
|
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
4
public/img/icons/cogs.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
|
||||||
|
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.292-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.292c.415.764-.42 1.6-1.185 1.184l-.292-.159a1.873 1.873 0 0 0-2.692 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.693-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.292A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
4
public/img/icons/exclamation-circle.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||||
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 322 B |
3
public/img/icons/heart.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="m8 2.748-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
3
public/img/icons/home.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
4
public/img/icons/info-circle.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||||
|
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 449 B |
4
public/img/icons/key.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 8a4 4 0 0 1 7.465-2H14a.5.5 0 0 1 .354.146l1.5 1.5a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0L13 9.207l-.646.647a.5.5 0 0 1-.708 0L11 9.207l-.646.647a.5.5 0 0 1-.708 0L9 9.207l-.646.647A.5.5 0 0 1 8 10h-.535A4 4 0 0 1 0 8zm4-3a3 3 0 1 0 2.712 4.285A.5.5 0 0 1 7.163 9h.63l.853-.854a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.646-.647a.5.5 0 0 1 .708 0l.646.647.793-.793-1-1h-6.63a.5.5 0 0 1-.451-.285A3 3 0 0 0 4 5z"/>
|
||||||
|
<path d="M4 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 614 B |
3
public/img/icons/lock.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 319 B |
4
public/img/icons/sign-in.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 3.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM.5 1.5A.5.5 0 0 1 1 1h3a.5.5 0 0 1 0 1H1v12h3a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5v-13z"/>
|
||||||
|
<path d="M11.854 7.646a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H1.5a.5.5 0 0 1 0-1h8.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 419 B |
4
public/img/icons/sign-out.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||||
|
<path d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 484 B |
4
public/img/icons/user-plus.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
|
||||||
|
<path d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 495 B |
3
public/img/icons/user.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 367 B |
218
src/Controller/CustomerController.php
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\PasswordResetToken;
|
||||||
|
use App\Form\CustomerRegistrationFormType;
|
||||||
|
|
||||||
|
use App\Form\ForgotPasswordFormType;
|
||||||
|
use App\Form\ResetPasswordFormType;
|
||||||
|
use App\Repository\CustomerRepository;
|
||||||
|
use App\Repository\PasswordResetTokenRepository;
|
||||||
|
use App\Service\EmailService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private CustomerRepository $customerRepository,
|
||||||
|
private PasswordResetTokenRepository $tokenRepository,
|
||||||
|
private UserPasswordHasherInterface $passwordHasher,
|
||||||
|
private EmailService $emailService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/customer/login', name: 'customer_login')]
|
||||||
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
|
{
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('customer_dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
|
$lastUsername = $authenticationUtils->getLastUsername();
|
||||||
|
|
||||||
|
return $this->render('customer/login.html.twig', [
|
||||||
|
'error' => $error,
|
||||||
|
'last_username' => $lastUsername,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/register', name: 'customer_register')]
|
||||||
|
public function register(Request $request): Response
|
||||||
|
{
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('customer_dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = new Customer();
|
||||||
|
$form = $this->createForm(CustomerRegistrationFormType::class, $customer);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
// بررسی وجود ایمیل
|
||||||
|
$existingCustomer = $this->customerRepository->findByEmail($customer->getEmail());
|
||||||
|
if ($existingCustomer) {
|
||||||
|
$this->addFlash('error', 'این ایمیل قبلاً ثبت شده است.');
|
||||||
|
return $this->render('customer/register.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// هش کردن کلمه عبور
|
||||||
|
$plainPassword = $form->get('plainPassword')->getData();
|
||||||
|
$customer->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($customer, $plainPassword)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ذخیره مشتری
|
||||||
|
$this->entityManager->persist($customer);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
// ارسال ایمیل تایید
|
||||||
|
$this->emailService->sendVerificationEmail($customer);
|
||||||
|
|
||||||
|
$this->addFlash('success', 'ثبتنام با موفقیت انجام شد. لطفاً ایمیل خود را بررسی کنید.');
|
||||||
|
return $this->redirectToRoute('customer_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('customer/register.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/verify-email/{token}', name: 'customer_verify_email')]
|
||||||
|
public function verifyEmail(string $token): Response
|
||||||
|
{
|
||||||
|
// برای سادگی، از ایمیل به عنوان توکن استفاده میکنیم
|
||||||
|
// در آینده میتوان یک سیستم توکن جداگانه پیاده کرد
|
||||||
|
$customer = $this->customerRepository->findOneBy(['email' => $token]);
|
||||||
|
|
||||||
|
if (!$customer) {
|
||||||
|
$this->addFlash('error', 'لینک تایید نامعتبر است.');
|
||||||
|
return $this->redirectToRoute('customer_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($customer->getEmailVerifiedAt()) {
|
||||||
|
$this->addFlash('info', 'ایمیل شما قبلاً تایید شده است.');
|
||||||
|
return $this->redirectToRoute('customer_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer->setEmailVerifiedAt(new \DateTime());
|
||||||
|
$customer->setIsActive(true);
|
||||||
|
$customer->setUpdatedAt(new \DateTime());
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'ایمیل شما با موفقیت تایید شد. حالا میتوانید وارد شوید.');
|
||||||
|
return $this->redirectToRoute('customer_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/forgot-password', name: 'customer_forgot_password')]
|
||||||
|
public function forgotPassword(Request $request): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(ForgotPasswordFormType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$email = $form->get('email')->getData();
|
||||||
|
$customer = $this->customerRepository->findByEmail($email);
|
||||||
|
|
||||||
|
if ($customer) {
|
||||||
|
// ایجاد توکن بازیابی
|
||||||
|
$resetToken = new PasswordResetToken();
|
||||||
|
$resetToken->setCustomer($customer);
|
||||||
|
|
||||||
|
$this->entityManager->persist($resetToken);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
// ارسال ایمیل بازیابی
|
||||||
|
$this->emailService->sendPasswordResetEmail($customer, $resetToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// همیشه پیام موفقیت نشان میدهیم (امنیت)
|
||||||
|
$this->addFlash('success', 'اگر ایمیل شما در سیستم موجود باشد، لینک بازیابی کلمه عبور ارسال خواهد شد.');
|
||||||
|
return $this->redirectToRoute('customer_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('customer/forgot_password.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/reset-password/{token}', name: 'customer_reset_password')]
|
||||||
|
public function resetPassword(string $token, Request $request): Response
|
||||||
|
{
|
||||||
|
$resetToken = $this->tokenRepository->findValidByToken($token);
|
||||||
|
|
||||||
|
if (!$resetToken) {
|
||||||
|
$this->addFlash('error', 'لینک بازیابی نامعتبر یا منقضی شده است.');
|
||||||
|
return $this->redirectToRoute('customer_forgot_password');
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = $resetToken->getCustomer();
|
||||||
|
$form = $this->createForm(ResetPasswordFormType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$newPassword = $form->get('plainPassword')->getData();
|
||||||
|
|
||||||
|
// تغییر کلمه عبور
|
||||||
|
$customer->setPassword(
|
||||||
|
$this->passwordHasher->hashPassword($customer, $newPassword)
|
||||||
|
);
|
||||||
|
$customer->setUpdatedAt(new \DateTime());
|
||||||
|
|
||||||
|
// غیرفعال کردن توکن
|
||||||
|
$resetToken->setUsedAt(new \DateTime());
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'کلمه عبور شما با موفقیت تغییر کرد. حالا میتوانید وارد شوید.');
|
||||||
|
return $this->redirectToRoute('customer_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('customer/reset_password.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'token' => $token,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/dashboard', name: 'customer_dashboard')]
|
||||||
|
public function dashboard(): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_CUSTOMER');
|
||||||
|
|
||||||
|
$customer = $this->getUser();
|
||||||
|
|
||||||
|
// بهروزرسانی زمان آخرین ورود
|
||||||
|
if ($customer instanceof Customer) {
|
||||||
|
$customer->setLastLoginAt(new \DateTime());
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('customer/dashboard.html.twig', [
|
||||||
|
'customer' => $customer,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/login_check', name: 'customer_login_check')]
|
||||||
|
public function loginCheck(): void
|
||||||
|
{
|
||||||
|
throw new \LogicException('This method can be blank - it will be intercepted by the login key on your firewall.');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/customer/logout', name: 'customer_logout')]
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/Entity/Customer.php
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\CustomerRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: CustomerRepository::class)]
|
||||||
|
#[ORM\Table(name: 'customer')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'UNIQ_CUSTOMER_EMAIL', fields: ['email'])]
|
||||||
|
class Customer implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 180)]
|
||||||
|
#[Assert\NotBlank(message: 'ایمیل الزامی است')]
|
||||||
|
#[Assert\Email(message: 'فرمت ایمیل صحیح نیست')]
|
||||||
|
private ?string $email = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private array $roles = [];
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?string $password = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Assert\NotBlank(message: 'نام الزامی است')]
|
||||||
|
private ?string $name = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 20)]
|
||||||
|
#[Assert\NotBlank(message: 'شماره موبایل الزامی است')]
|
||||||
|
#[Assert\Regex(pattern: '/^09[0-9]{9}$/', message: 'فرمت شماره موبایل صحیح نیست')]
|
||||||
|
private ?string $phone = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean')]
|
||||||
|
private bool $isActive = false;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
|
private ?\DateTimeInterface $emailVerifiedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime')]
|
||||||
|
private ?\DateTimeInterface $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
|
private ?\DateTimeInterface $updatedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
|
private ?\DateTimeInterface $lastLoginAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 50, nullable: true)]
|
||||||
|
private ?string $subscriptionType = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
|
private ?\DateTimeInterface $subscriptionExpiresAt = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new \DateTime();
|
||||||
|
$this->roles = ['ROLE_CUSTOMER'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(string $email): static
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->email ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
$roles = $this->roles;
|
||||||
|
$roles[] = 'ROLE_CUSTOMER';
|
||||||
|
return array_unique($roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRoles(array $roles): static
|
||||||
|
{
|
||||||
|
$this->roles = $roles;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): ?string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(string $password): static
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function eraseCredentials(): void
|
||||||
|
{
|
||||||
|
// If you store any temporary, sensitive data on the user, clear it here
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPhone(): ?string
|
||||||
|
{
|
||||||
|
return $this->phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPhone(string $phone): static
|
||||||
|
{
|
||||||
|
$this->phone = $phone;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isActive(): bool
|
||||||
|
{
|
||||||
|
return $this->isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsActive(bool $isActive): static
|
||||||
|
{
|
||||||
|
$this->isActive = $isActive;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmailVerifiedAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->emailVerifiedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmailVerifiedAt(?\DateTimeInterface $emailVerifiedAt): static
|
||||||
|
{
|
||||||
|
$this->emailVerifiedAt = $emailVerifiedAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(\DateTimeInterface $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastLoginAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->lastLoginAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLastLoginAt(?\DateTimeInterface $lastLoginAt): static
|
||||||
|
{
|
||||||
|
$this->lastLoginAt = $lastLoginAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubscriptionType(): ?string
|
||||||
|
{
|
||||||
|
return $this->subscriptionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSubscriptionType(?string $subscriptionType): static
|
||||||
|
{
|
||||||
|
$this->subscriptionType = $subscriptionType;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubscriptionExpiresAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->subscriptionExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSubscriptionExpiresAt(?\DateTimeInterface $subscriptionExpiresAt): static
|
||||||
|
{
|
||||||
|
$this->subscriptionExpiresAt = $subscriptionExpiresAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/Entity/PasswordResetToken.php
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\PasswordResetTokenRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: PasswordResetTokenRepository::class)]
|
||||||
|
#[ORM\Table(name: 'password_reset_token')]
|
||||||
|
class PasswordResetToken
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?Customer $customer = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $token = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime')]
|
||||||
|
private ?\DateTimeInterface $expiresAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||||
|
private ?\DateTimeInterface $usedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime')]
|
||||||
|
private ?\DateTimeInterface $createdAt = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new \DateTime();
|
||||||
|
$this->token = bin2hex(random_bytes(32));
|
||||||
|
$this->expiresAt = new \DateTime('+1 hour');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomer(): ?Customer
|
||||||
|
{
|
||||||
|
return $this->customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomer(?Customer $customer): static
|
||||||
|
{
|
||||||
|
$this->customer = $customer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getToken(): ?string
|
||||||
|
{
|
||||||
|
return $this->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setToken(string $token): static
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setExpiresAt(\DateTimeInterface $expiresAt): static
|
||||||
|
{
|
||||||
|
$this->expiresAt = $expiresAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsedAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->usedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUsedAt(?\DateTimeInterface $usedAt): static
|
||||||
|
{
|
||||||
|
$this->usedAt = $usedAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(\DateTimeInterface $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(): bool
|
||||||
|
{
|
||||||
|
return $this->expiresAt < new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUsed(): bool
|
||||||
|
{
|
||||||
|
return $this->usedAt !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return !$this->isExpired() && !$this->isUsed();
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/Form/CustomerRegistrationFormType.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
class CustomerRegistrationFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('name', TextType::class, [
|
||||||
|
'label' => 'نام و نام خانوادگی',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'نام و نام خانوادگی خود را وارد کنید',
|
||||||
|
'dir' => 'rtl'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً نام و نام خانوادگی خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('email', EmailType::class, [
|
||||||
|
'label' => 'پست الکترونیکی',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'example@domain.com',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً آدرس ایمیل خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('phone', TelType::class, [
|
||||||
|
'label' => 'شماره موبایل',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => '09123456789',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً شماره موبایل خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('plainPassword', RepeatedType::class, [
|
||||||
|
'type' => PasswordType::class,
|
||||||
|
'first_options' => [
|
||||||
|
'label' => 'کلمه عبور',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'کلمه عبور خود را وارد کنید',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'second_options' => [
|
||||||
|
'label' => 'تکرار کلمه عبور',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'کلمه عبور را مجدداً وارد کنید',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'invalid_message' => 'کلمههای عبور یکسان نیستند',
|
||||||
|
'mapped' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً کلمه عبور خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
new Length([
|
||||||
|
'min' => 8,
|
||||||
|
'minMessage' => 'کلمه عبور باید حداقل {{ limit }} کاراکتر باشد',
|
||||||
|
'max' => 4096,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('agreeTerms', CheckboxType::class, [
|
||||||
|
'label' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new IsTrue([
|
||||||
|
'message' => 'لطفاً قوانین و مقررات را بپذیرید',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Customer::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Form/ForgotPasswordFormType.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
class ForgotPasswordFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('email', EmailType::class, [
|
||||||
|
'label' => 'پست الکترونیکی',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'ایمیل خود را وارد کنید',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً آدرس ایمیل خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('submit', SubmitType::class, [
|
||||||
|
'label' => '<i class="fas fa-paper-plane"></i> ارسال لینک بازیابی',
|
||||||
|
'label_html' => true,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'btn btn-primary w-100 mb-3'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/Form/ResetPasswordFormType.php
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
class ResetPasswordFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('plainPassword', RepeatedType::class, [
|
||||||
|
'type' => PasswordType::class,
|
||||||
|
'first_options' => [
|
||||||
|
'label' => 'کلمه عبور جدید',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'کلمه عبور جدید خود را وارد کنید',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'second_options' => [
|
||||||
|
'label' => 'تکرار کلمه عبور جدید',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'کلمه عبور جدید را مجدداً وارد کنید',
|
||||||
|
'dir' => 'ltr'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'invalid_message' => 'کلمههای عبور یکسان نیستند',
|
||||||
|
'mapped' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً کلمه عبور جدید خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
new Length([
|
||||||
|
'min' => 8,
|
||||||
|
'minMessage' => 'کلمه عبور باید حداقل {{ limit }} کاراکتر باشد',
|
||||||
|
'max' => 4096,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('submit', SubmitType::class, [
|
||||||
|
'label' => '<i class="fas fa-save"></i> تغییر کلمه عبور',
|
||||||
|
'label_html' => true,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'btn btn-primary w-100 mb-3'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/Repository/CustomerRepository.php
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Customer>
|
||||||
|
*/
|
||||||
|
class CustomerRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Customer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Customer $entity, bool $flush = false): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->persist($entity);
|
||||||
|
|
||||||
|
if ($flush) {
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(Customer $entity, bool $flush = false): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->remove($entity);
|
||||||
|
|
||||||
|
if ($flush) {
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByEmail(string $email): ?Customer
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['email' => $email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findActiveByEmail(string $email): ?Customer
|
||||||
|
{
|
||||||
|
return $this->findOneBy([
|
||||||
|
'email' => $email,
|
||||||
|
'isActive' => true
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Repository/PasswordResetTokenRepository.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\PasswordResetToken;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<PasswordResetToken>
|
||||||
|
*/
|
||||||
|
class PasswordResetTokenRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PasswordResetToken::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(PasswordResetToken $entity, bool $flush = false): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->persist($entity);
|
||||||
|
|
||||||
|
if ($flush) {
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(PasswordResetToken $entity, bool $flush = false): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->remove($entity);
|
||||||
|
|
||||||
|
if ($flush) {
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByToken(string $token): ?PasswordResetToken
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['token' => $token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findValidByToken(string $token): ?PasswordResetToken
|
||||||
|
{
|
||||||
|
$tokenEntity = $this->findByToken($token);
|
||||||
|
|
||||||
|
if ($tokenEntity && $tokenEntity->isValid()) {
|
||||||
|
return $tokenEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeExpiredTokens(): int
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('t');
|
||||||
|
$qb->delete()
|
||||||
|
->where('t.expiresAt < :now')
|
||||||
|
->setParameter('now', new \DateTime());
|
||||||
|
|
||||||
|
return $qb->getQuery()->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/Service/EmailService.php
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\PasswordResetToken;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
|
class EmailService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MailerInterface $mailer,
|
||||||
|
private UrlGeneratorInterface $urlGenerator
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function sendVerificationEmail(Customer $customer): void
|
||||||
|
{
|
||||||
|
// برای سادگی، از ایمیل به عنوان توکن استفاده میکنیم
|
||||||
|
$verificationUrl = $this->urlGenerator->generate(
|
||||||
|
'customer_verify_email',
|
||||||
|
['token' => $customer->getEmail()],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL
|
||||||
|
);
|
||||||
|
|
||||||
|
$email = (new Email())
|
||||||
|
->from('noreply@hesabix.ir')
|
||||||
|
->to($customer->getEmail())
|
||||||
|
->subject('تایید ایمیل - باشگاه مشتریان حسابیکس')
|
||||||
|
->html($this->getVerificationEmailTemplate($customer->getName(), $verificationUrl));
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendPasswordResetEmail(Customer $customer, PasswordResetToken $token): void
|
||||||
|
{
|
||||||
|
$resetUrl = $this->urlGenerator->generate(
|
||||||
|
'customer_reset_password',
|
||||||
|
['token' => $token->getToken()],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL
|
||||||
|
);
|
||||||
|
|
||||||
|
$email = (new Email())
|
||||||
|
->from('noreply@hesabix.ir')
|
||||||
|
->to($customer->getEmail())
|
||||||
|
->subject('بازیابی کلمه عبور - باشگاه مشتریان حسابیکس')
|
||||||
|
->html($this->getPasswordResetEmailTemplate($customer->getName(), $resetUrl));
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getVerificationEmailTemplate(string $name, string $verificationUrl): string
|
||||||
|
{
|
||||||
|
return "
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir='rtl' lang='fa'>
|
||||||
|
<head>
|
||||||
|
<meta charset='UTF-8'>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
|
<title>تایید ایمیل</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Tahoma', Arial, sans-serif; direction: rtl; text-align: right; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: #0d6efd; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
||||||
|
.content { background: #f8f9fa; padding: 30px; border-radius: 0 0 8px 8px; }
|
||||||
|
.button { display: inline-block; background: #0d6efd; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; margin: 20px 0; }
|
||||||
|
.footer { text-align: center; margin-top: 30px; color: #6c757d; font-size: 14px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='container'>
|
||||||
|
<div class='header'>
|
||||||
|
<h1>باشگاه مشتریان حسابیکس</h1>
|
||||||
|
</div>
|
||||||
|
<div class='content'>
|
||||||
|
<h2>سلام {$name} عزیز!</h2>
|
||||||
|
<p>از عضویت شما در باشگاه مشتریان حسابیکس خوشحالیم.</p>
|
||||||
|
<p>برای فعالسازی حساب کاربری خود، لطفاً روی دکمه زیر کلیک کنید:</p>
|
||||||
|
<div style='text-align: center;'>
|
||||||
|
<a href='{$verificationUrl}' class='button'>تایید ایمیل</a>
|
||||||
|
</div>
|
||||||
|
<p>اگر دکمه بالا کار نمیکند، میتوانید لینک زیر را در مرورگر خود کپی کنید:</p>
|
||||||
|
<p style='word-break: break-all; background: #e9ecef; padding: 10px; border-radius: 4px;'>{$verificationUrl}</p>
|
||||||
|
<p><strong>نکته:</strong> این لینک تا 24 ساعت معتبر است.</p>
|
||||||
|
</div>
|
||||||
|
<div class='footer'>
|
||||||
|
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||||
|
<p>© 2024 حسابیکس - نرمافزار حسابداری آنلاین</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPasswordResetEmailTemplate(string $name, string $resetUrl): string
|
||||||
|
{
|
||||||
|
return "
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir='rtl' lang='fa'>
|
||||||
|
<head>
|
||||||
|
<meta charset='UTF-8'>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
|
<title>بازیابی کلمه عبور</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Tahoma', Arial, sans-serif; direction: rtl; text-align: right; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: #dc3545; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
||||||
|
.content { background: #f8f9fa; padding: 30px; border-radius: 0 0 8px 8px; }
|
||||||
|
.button { display: inline-block; background: #dc3545; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; margin: 20px 0; }
|
||||||
|
.footer { text-align: center; margin-top: 30px; color: #6c757d; font-size: 14px; }
|
||||||
|
.warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 4px; margin: 20px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='container'>
|
||||||
|
<div class='header'>
|
||||||
|
<h1>بازیابی کلمه عبور</h1>
|
||||||
|
</div>
|
||||||
|
<div class='content'>
|
||||||
|
<h2>سلام {$name} عزیز!</h2>
|
||||||
|
<p>درخواست بازیابی کلمه عبور برای حساب کاربری شما در باشگاه مشتریان حسابیکس دریافت شد.</p>
|
||||||
|
<p>برای تغییر کلمه عبور، لطفاً روی دکمه زیر کلیک کنید:</p>
|
||||||
|
<div style='text-align: center;'>
|
||||||
|
<a href='{$resetUrl}' class='button'>تغییر کلمه عبور</a>
|
||||||
|
</div>
|
||||||
|
<p>اگر دکمه بالا کار نمیکند، میتوانید لینک زیر را در مرورگر خود کپی کنید:</p>
|
||||||
|
<p style='word-break: break-all; background: #e9ecef; padding: 10px; border-radius: 4px;'>{$resetUrl}</p>
|
||||||
|
<div class='warning'>
|
||||||
|
<strong>هشدار امنیتی:</strong> اگر شما این درخواست را ارسال نکردهاید، لطفاً این ایمیل را نادیده بگیرید. کلمه عبور شما تغییر نخواهد کرد.
|
||||||
|
</div>
|
||||||
|
<p><strong>نکته:</strong> این لینک تا 1 ساعت معتبر است.</p>
|
||||||
|
</div>
|
||||||
|
<div class='footer'>
|
||||||
|
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||||
|
<p>© 2024 حسابیکس - نرمافزار حسابداری آنلاین</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,47 @@ gtag('config', 'G-K1R1SYQY8E');
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{# 'app' must match the first argument to addEntry() in webpack.config.js #}
|
{# 'app' must match the first argument to addEntry() in webpack.config.js #}
|
||||||
{{ encore_entry_link_tags('app') }}
|
{{ encore_entry_link_tags('app') }}
|
||||||
|
<style>
|
||||||
|
/* آیکونهای SVG در نوار ناوبری */
|
||||||
|
.icon-svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg svg {
|
||||||
|
fill: currentColor;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* آیکونهای رنگی */
|
||||||
|
.icon-user svg { fill: #3498db; }
|
||||||
|
.icon-cogs svg { fill: #95a5a6; }
|
||||||
|
.icon-sign-out svg { fill: #e74c3c; }
|
||||||
|
|
||||||
|
/* بهبود نمایش منوی dropdown */
|
||||||
|
.dropdown-menu {
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 10px 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dropdown-item.text-danger:hover {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
|
|
@ -74,7 +115,33 @@ gtag('config', 'G-K1R1SYQY8E');
|
||||||
<a class="nav-link px-3 py-2 rounded-3 transition-all" href="{{path('app_page',{'url':'contact'})}}">تماس با ما</a>
|
<a class="nav-link px-3 py-2 rounded-3 transition-all" href="{{path('app_page',{'url':'contact'})}}">تماس با ما</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="d-flex">
|
<div class="d-flex gap-2">
|
||||||
|
{% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
|
||||||
|
{# کاربر وارد شده - نمایش منوی داشبورد #}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-primary rounded-4 px-3 py-2 fw-bold transition-all dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<img src="{{ asset('/img/icons/user.svg') }}" alt="کاربر" class="icon-svg icon-user me-2">{{ app.user.name }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ path('customer_dashboard') }}">
|
||||||
|
<img src="{{ asset('/img/icons/cogs.svg') }}" alt="داشبورد" class="icon-svg icon-cogs me-2">داشبورد
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item text-danger" href="{{ path('customer_logout') }}">
|
||||||
|
<img src="{{ asset('/img/icons/sign-out.svg') }}" alt="خروج" class="icon-svg icon-sign-out me-2">خروج
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{# کاربر وارد نشده - نمایش دکمه ورود #}
|
||||||
|
<a class="btn btn-outline-primary rounded-4 px-3 py-2 fw-bold transition-all" href="{{ path('customer_login') }}">
|
||||||
|
باشگاه مشتریان
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a target="_blank" class="btn btn-primary rounded-4 px-4 py-2 fw-bold transition-all" href="https://app.hesabix.ir">
|
<a target="_blank" class="btn btn-primary rounded-4 px-4 py-2 fw-bold transition-all" href="https://app.hesabix.ir">
|
||||||
ورود / عضویت
|
ورود / عضویت
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
282
templates/customer/base.html.twig
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}باشگاه مشتریان حسابیکس - {{ block('page_title') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.customer-auth-container {
|
||||||
|
min-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px 0;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-auth-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-auth-header {
|
||||||
|
background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-auth-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-auth-header p {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-auth-body {
|
||||||
|
padding: 40px 30px;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating label {
|
||||||
|
color: #6c757d;
|
||||||
|
right: 0.75rem;
|
||||||
|
left: auto;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #0d6efd;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* فیلدهای انگلیسی (ایمیل و کلمه عبور) */
|
||||||
|
.form-control[type="email"],
|
||||||
|
.form-control[type="password"] {
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control[type="email"] + label,
|
||||||
|
.form-control[type="password"] + label {
|
||||||
|
right: auto;
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(13, 110, 253, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
margin: 20px 0;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-label {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links a {
|
||||||
|
color: #0d6efd;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #d1edff;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hesabix-logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-home {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-home:hover {
|
||||||
|
color: white;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* بهبود نمایش آیکونهای SVG */
|
||||||
|
.icon-svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-svg-large {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تغییر رنگ آیکونهای SVG */
|
||||||
|
.icon-svg svg,
|
||||||
|
.icon-svg-large svg {
|
||||||
|
fill: currentColor;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* آیکونهای رنگی */
|
||||||
|
.icon-heart svg { fill: #e74c3c; }
|
||||||
|
.icon-user svg { fill: #3498db; }
|
||||||
|
.icon-calendar svg { fill: #f39c12; }
|
||||||
|
.icon-cogs svg { fill: #95a5a6; }
|
||||||
|
.icon-key svg { fill: #9b59b6; }
|
||||||
|
.icon-home svg { fill: #2ecc71; }
|
||||||
|
.icon-sign-out svg { fill: #e74c3c; }
|
||||||
|
.icon-exclamation svg { fill: #e74c3c; }
|
||||||
|
.icon-sign-in svg { fill: #27ae60; }
|
||||||
|
.icon-user-plus svg { fill: #3498db; }
|
||||||
|
.icon-check svg { fill: #27ae60; }
|
||||||
|
.icon-info svg { fill: #3498db; }
|
||||||
|
.icon-lock svg { fill: #95a5a6; }
|
||||||
|
.icon-arrow-left svg { fill: #7f8c8d; }
|
||||||
|
|
||||||
|
/* آیکونهای بزرگ */
|
||||||
|
.icon-svg-large.icon-heart svg { fill: #e74c3c; }
|
||||||
|
.icon-svg-large.icon-key svg { fill: #9b59b6; }
|
||||||
|
.icon-svg-large.icon-lock svg { fill: #95a5a6; }
|
||||||
|
|
||||||
|
/* بهبود نمایش متنهای فارسی */
|
||||||
|
.text-muted {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="customer-auth-container">
|
||||||
|
<div class="customer-auth-card">
|
||||||
|
<div class="customer-auth-header">
|
||||||
|
<img src="{{ asset('/favicon/favicon.svg') }}" alt="حسابیکس" class="hesabix-logo">
|
||||||
|
<h1>باشگاه مشتریان حسابیکس</h1>
|
||||||
|
<p>{{ block('page_subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="customer-auth-body">
|
||||||
|
{% for message in app.flashes('success') %}
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
<img src="{{ asset('/img/icons/check-circle.svg') }}" alt="موفقیت" class="icon-svg icon-check"> {{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('error') %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation"> {{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('info') %}
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<img src="{{ asset('/img/icons/info-circle.svg') }}" alt="اطلاعات" class="icon-svg icon-info"> {{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% block auth_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
313
templates/customer/dashboard.html.twig
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}داشبورد - باشگاه مشتریان حسابیکس{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<style>
|
||||||
|
/* تنظیمات کلی برای فارسی */
|
||||||
|
.customer-dashboard * {
|
||||||
|
font-family: 'Yekan Bakh FaNum', 'Tahoma', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-dashboard {
|
||||||
|
min-height: 80vh;
|
||||||
|
padding: 40px 0;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px 0;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header p {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card h3 {
|
||||||
|
color: #0d6efd;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #6c757d;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background: #d1edff;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
margin-top: 30px;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-customer {
|
||||||
|
background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 25px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-customer:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(13, 110, 253, 0.4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message {
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message h4 {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message p {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* بهبود نمایش آیکونهای SVG */
|
||||||
|
.icon-svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تغییر رنگ آیکونهای SVG */
|
||||||
|
.icon-svg svg {
|
||||||
|
fill: currentColor;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* رنگهای مختلف برای آیکونها */
|
||||||
|
.text-primary .icon-svg svg {
|
||||||
|
fill: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success .icon-svg svg {
|
||||||
|
fill: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger .icon-svg svg {
|
||||||
|
fill: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning .icon-svg svg {
|
||||||
|
fill: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-info .icon-svg svg {
|
||||||
|
fill: #0dcaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted .icon-svg svg {
|
||||||
|
fill: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white .icon-svg svg {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* بهبود نمایش متنهای فارسی */
|
||||||
|
.text-center {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-end {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="customer-dashboard">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h1>خوش آمدید، {{ customer.name }} عزیز!</h1>
|
||||||
|
<p class="mb-0">به باشگاه مشتریان حسابیکس خوش آمدید</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<img src="{{ asset('/favicon/favicon.svg') }}" alt="حسابیکس" width="80" height="80">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="welcome-message">
|
||||||
|
<h4><img src="{{ asset('/img/icons/heart.svg') }}" alt="قلب" class="icon-svg icon-heart"> از عضویت شما در باشگاه مشتریان حسابیکس سپاسگزاریم!</h4>
|
||||||
|
<p class="mb-0">در این بخش میتوانید اطلاعات حساب کاربری خود را مشاهده و مدیریت کنید.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3><img src="{{ asset('/img/icons/user.svg') }}" alt="کاربر" class="icon-svg icon-user"> اطلاعات شخصی</h3>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">نام و نام خانوادگی:</span>
|
||||||
|
<span class="info-value">{{ customer.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">پست الکترونیکی:</span>
|
||||||
|
<span class="info-value">{{ customer.email }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">شماره موبایل:</span>
|
||||||
|
<span class="info-value">{{ customer.phone }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">وضعیت حساب:</span>
|
||||||
|
<span class="status-badge {{ customer.isActive ? 'status-active' : 'status-inactive' }}">
|
||||||
|
{{ customer.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3><img src="{{ asset('/img/icons/calendar.svg') }}" alt="تقویم" class="icon-svg icon-calendar"> اطلاعات عضویت</h3>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">تاریخ عضویت:</span>
|
||||||
|
<span class="info-value">{{ customer.createdAt|date('Y/m/d') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">آخرین ورود:</span>
|
||||||
|
<span class="info-value">
|
||||||
|
{{ customer.lastLoginAt ? customer.lastLoginAt|date('Y/m/d H:i') : 'هنوز وارد نشده' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">تایید ایمیل:</span>
|
||||||
|
<span class="status-badge {{ customer.emailVerifiedAt ? 'status-active' : 'status-inactive' }}">
|
||||||
|
{{ customer.emailVerifiedAt ? 'تایید شده' : 'تایید نشده' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if customer.subscriptionType %}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">نوع اشتراک:</span>
|
||||||
|
<span class="info-value">{{ customer.subscriptionType }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3><img src="{{ asset('/img/icons/cogs.svg') }}" alt="تنظیمات" class="icon-svg icon-cogs"> عملیات</h3>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="{{ path('customer_forgot_password') }}" class="btn-customer">
|
||||||
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg icon-key"> بازیابی کلمه عبور
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ path('app_home') }}" class="btn-customer">
|
||||||
|
<img src="{{ asset('/img/icons/home.svg') }}" alt="خانه" class="icon-svg icon-home"> بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ path('customer_logout') }}" class="btn-customer" style="background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);">
|
||||||
|
<img src="{{ asset('/img/icons/sign-out.svg') }}" alt="خروج" class="icon-svg icon-sign-out"> خروج
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
33
templates/customer/forgot_password.html.twig
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'customer/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block page_title %}بازیابی کلمه عبور{% endblock %}
|
||||||
|
{% block page_subtitle %}کلمه عبور خود را بازیابی کنید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_content %}
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg-large icon-key text-primary">
|
||||||
|
<p class="text-muted">ایمیل خود را وارد کنید تا لینک بازیابی کلمه عبور برای شما ارسال شود.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.email, {'attr': {'class': 'form-control', 'placeholder': 'ایمیل خود را وارد کنید'}}) }}
|
||||||
|
{{ form_label(form.email) }}
|
||||||
|
{{ form_errors(form.email) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('customer_login') }}">
|
||||||
|
<img src="{{ asset('/img/icons/arrow-left.svg') }}" alt="بازگشت" class="icon-svg icon-arrow-left"> بازگشت به صفحه ورود
|
||||||
|
</a>
|
||||||
|
<br><br>
|
||||||
|
<p>حساب کاربری ندارید؟
|
||||||
|
<a href="{{ path('customer_register') }}">
|
||||||
|
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus"> عضویت در باشگاه
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
63
templates/customer/login.html.twig
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{% extends 'customer/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block page_title %}ورود{% endblock %}
|
||||||
|
{% block page_subtitle %}وارد حساب کاربری خود شوید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_content %}
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation"> {{ error.messageKey|trans(error.messageData, 'security', 'fa') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ path('customer_login_check') }}">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input type="email"
|
||||||
|
class="form-control"
|
||||||
|
id="inputEmail"
|
||||||
|
name="_username"
|
||||||
|
value="{{ last_username }}"
|
||||||
|
placeholder="ایمیل خود را وارد کنید"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
dir="ltr">
|
||||||
|
<label for="inputEmail">پست الکترونیکی</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="inputPassword"
|
||||||
|
name="_password"
|
||||||
|
placeholder="کلمه عبور خود را وارد کنید"
|
||||||
|
required
|
||||||
|
dir="ltr">
|
||||||
|
<label for="inputPassword">کلمه عبور</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="remember_me" name="_remember_me" checked>
|
||||||
|
<label class="form-check-label" for="remember_me">
|
||||||
|
مرا به یاد داشته باش
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100 mb-3" type="submit">
|
||||||
|
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in"> ورود
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('customer_forgot_password') }}">
|
||||||
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg icon-key"> فراموشی کلمه عبور
|
||||||
|
</a>
|
||||||
|
<br><br>
|
||||||
|
<p>حساب کاربری ندارید؟
|
||||||
|
<a href="{{ path('customer_register') }}">
|
||||||
|
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus"> عضویت در باشگاه
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
60
templates/customer/register.html.twig
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends 'customer/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block page_title %}عضویت{% endblock %}
|
||||||
|
{% block page_subtitle %}به باشگاه مشتریان حسابیکس بپیوندید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_content %}
|
||||||
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.name, {'attr': {'class': 'form-control', 'placeholder': 'نام و نام خانوادگی خود را وارد کنید'}}) }}
|
||||||
|
{{ form_label(form.name) }}
|
||||||
|
{{ form_errors(form.name) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.email, {'attr': {'class': 'form-control', 'placeholder': 'example@domain.com'}}) }}
|
||||||
|
{{ form_label(form.email) }}
|
||||||
|
{{ form_errors(form.email) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.phone, {'attr': {'class': 'form-control', 'placeholder': '09123456789'}}) }}
|
||||||
|
{{ form_label(form.phone) }}
|
||||||
|
{{ form_errors(form.phone) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.plainPassword.first, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور خود را وارد کنید'}}) }}
|
||||||
|
{{ form_label(form.plainPassword.first) }}
|
||||||
|
{{ form_errors(form.plainPassword.first) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.plainPassword.second, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور را مجدداً وارد کنید'}}) }}
|
||||||
|
{{ form_label(form.plainPassword.second) }}
|
||||||
|
{{ form_errors(form.plainPassword.second) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
{{ form_widget(form.agreeTerms, {'attr': {'class': 'form-check-input'}}) }}
|
||||||
|
<label class="form-check-label" for="{{ form.agreeTerms.vars.id }}">
|
||||||
|
<a href="{{ path('app_page', {'url': 'terms'}) }}" target="_blank">قوانین و مقررات</a> را میپذیرم
|
||||||
|
</label>
|
||||||
|
{{ form_errors(form.agreeTerms) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100 mb-3" type="submit">
|
||||||
|
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus"> عضویت در باشگاه
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<p>قبلاً عضو شدهاید؟
|
||||||
|
<a href="{{ path('customer_login') }}">
|
||||||
|
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in"> ورود
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
33
templates/customer/reset_password.html.twig
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'customer/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block page_title %}تغییر کلمه عبور{% endblock %}
|
||||||
|
{% block page_subtitle %}کلمه عبور جدید خود را وارد کنید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_content %}
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{{ asset('/img/icons/lock.svg') }}" alt="قفل" class="icon-svg-large icon-lock text-primary">
|
||||||
|
<p class="text-muted">کلمه عبور جدید خود را وارد کنید.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.plainPassword.first, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور جدید خود را وارد کنید'}}) }}
|
||||||
|
{{ form_label(form.plainPassword.first) }}
|
||||||
|
{{ form_errors(form.plainPassword.first) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
{{ form_widget(form.plainPassword.second, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور جدید را مجدداً وارد کنید'}}) }}
|
||||||
|
{{ form_label(form.plainPassword.second) }}
|
||||||
|
{{ form_errors(form.plainPassword.second) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('customer_login') }}">
|
||||||
|
<img src="{{ asset('/img/icons/arrow-left.svg') }}" alt="بازگشت" class="icon-svg icon-arrow-left"> بازگشت به صفحه ورود
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
154
translations/messages.fa.yaml
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
# پیامهای عمومی سیستم
|
||||||
|
'Welcome': 'خوش آمدید'
|
||||||
|
'Login': 'ورود'
|
||||||
|
'Register': 'عضویت'
|
||||||
|
'Logout': 'خروج'
|
||||||
|
'Dashboard': 'داشبورد'
|
||||||
|
'Profile': 'پروفایل'
|
||||||
|
'Settings': 'تنظیمات'
|
||||||
|
'Save': 'ذخیره'
|
||||||
|
'Cancel': 'لغو'
|
||||||
|
'Delete': 'حذف'
|
||||||
|
'Edit': 'ویرایش'
|
||||||
|
'Create': 'ایجاد'
|
||||||
|
'Update': 'بهروزرسانی'
|
||||||
|
'Search': 'جستجو'
|
||||||
|
'Filter': 'فیلتر'
|
||||||
|
'Sort': 'مرتبسازی'
|
||||||
|
'Export': 'خروجی'
|
||||||
|
'Import': 'ورودی'
|
||||||
|
'Download': 'دانلود'
|
||||||
|
'Upload': 'آپلود'
|
||||||
|
'Submit': 'ارسال'
|
||||||
|
'Reset': 'بازنشانی'
|
||||||
|
'Clear': 'پاک کردن'
|
||||||
|
'Close': 'بستن'
|
||||||
|
'Open': 'باز کردن'
|
||||||
|
'View': 'مشاهده'
|
||||||
|
'Details': 'جزئیات'
|
||||||
|
'Back': 'بازگشت'
|
||||||
|
'Next': 'بعدی'
|
||||||
|
'Previous': 'قبلی'
|
||||||
|
'First': 'اول'
|
||||||
|
'Last': 'آخر'
|
||||||
|
'Yes': 'بله'
|
||||||
|
'No': 'خیر'
|
||||||
|
'OK': 'تأیید'
|
||||||
|
'Error': 'خطا'
|
||||||
|
'Success': 'موفقیت'
|
||||||
|
'Warning': 'هشدار'
|
||||||
|
'Info': 'اطلاعات'
|
||||||
|
'Loading': 'در حال بارگذاری'
|
||||||
|
'Please wait': 'لطفاً صبر کنید'
|
||||||
|
'Processing': 'در حال پردازش'
|
||||||
|
'Complete': 'تکمیل شده'
|
||||||
|
'Incomplete': 'ناتمام'
|
||||||
|
'Active': 'فعال'
|
||||||
|
'Inactive': 'غیرفعال'
|
||||||
|
'Enabled': 'فعال'
|
||||||
|
'Disabled': 'غیرفعال'
|
||||||
|
'Public': 'عمومی'
|
||||||
|
'Private': 'خصوصی'
|
||||||
|
'Draft': 'پیشنویس'
|
||||||
|
'Published': 'منتشر شده'
|
||||||
|
'Pending': 'در انتظار'
|
||||||
|
'Approved': 'تأیید شده'
|
||||||
|
'Rejected': 'رد شده'
|
||||||
|
'Expired': 'منقضی شده'
|
||||||
|
'Valid': 'معتبر'
|
||||||
|
'Invalid': 'نامعتبر'
|
||||||
|
'Required': 'الزامی'
|
||||||
|
'Optional': 'اختیاری'
|
||||||
|
'Available': 'موجود'
|
||||||
|
'Unavailable': 'ناموجود'
|
||||||
|
'Online': 'آنلاین'
|
||||||
|
'Offline': 'آفلاین'
|
||||||
|
'Connected': 'متصل'
|
||||||
|
'Disconnected': 'قطع شده'
|
||||||
|
'New': 'جدید'
|
||||||
|
'Old': 'قدیمی'
|
||||||
|
'Recent': 'اخیر'
|
||||||
|
'Popular': 'محبوب'
|
||||||
|
'Featured': 'ویژه'
|
||||||
|
'Recommended': 'پیشنهادی'
|
||||||
|
'Best': 'بهترین'
|
||||||
|
'Top': 'برتر'
|
||||||
|
'Latest': 'جدیدترین'
|
||||||
|
'Updated': 'بهروزرسانی شده'
|
||||||
|
'Created': 'ایجاد شده'
|
||||||
|
'Modified': 'تغییر یافته'
|
||||||
|
'Deleted': 'حذف شده'
|
||||||
|
'Restored': 'بازیابی شده'
|
||||||
|
'Archived': 'بایگانی شده'
|
||||||
|
'Unarchived': 'خارج از بایگانی'
|
||||||
|
'Locked': 'قفل شده'
|
||||||
|
'Unlocked': 'باز شده'
|
||||||
|
'Hidden': 'مخفی'
|
||||||
|
'Visible': 'قابل مشاهده'
|
||||||
|
'Read': 'خوانده شده'
|
||||||
|
'Unread': 'خوانده نشده'
|
||||||
|
'Mark as read': 'علامتگذاری به عنوان خوانده شده'
|
||||||
|
'Mark as unread': 'علامتگذاری به عنوان خوانده نشده'
|
||||||
|
'Star': 'ستاره'
|
||||||
|
'Unstar': 'حذف ستاره'
|
||||||
|
'Favorite': 'مورد علاقه'
|
||||||
|
'Unfavorite': 'حذف از علاقهمندیها'
|
||||||
|
'Like': 'لایک'
|
||||||
|
'Unlike': 'حذف لایک'
|
||||||
|
'Share': 'اشتراکگذاری'
|
||||||
|
'Copy': 'کپی'
|
||||||
|
'Paste': 'چسباندن'
|
||||||
|
'Cut': 'برش'
|
||||||
|
'Undo': 'برگردان'
|
||||||
|
'Redo': 'تکرار'
|
||||||
|
'Refresh': 'تازهسازی'
|
||||||
|
'Reload': 'بارگذاری مجدد'
|
||||||
|
'Restart': 'راهاندازی مجدد'
|
||||||
|
'Stop': 'توقف'
|
||||||
|
'Start': 'شروع'
|
||||||
|
'Pause': 'مکث'
|
||||||
|
'Resume': 'ادامه'
|
||||||
|
'Play': 'پخش'
|
||||||
|
'Record': 'ضبط'
|
||||||
|
'Stop recording': 'توقف ضبط'
|
||||||
|
'Pause recording': 'مکث ضبط'
|
||||||
|
'Resume recording': 'ادامه ضبط'
|
||||||
|
'Delete recording': 'حذف ضبط'
|
||||||
|
'Download recording': 'دانلود ضبط'
|
||||||
|
'Upload recording': 'آپلود ضبط'
|
||||||
|
'Share recording': 'اشتراکگذاری ضبط'
|
||||||
|
'Copy link': 'کپی لینک'
|
||||||
|
'Copy URL': 'کپی آدرس'
|
||||||
|
'Copy text': 'کپی متن'
|
||||||
|
'Copy image': 'کپی تصویر'
|
||||||
|
'Copy file': 'کپی فایل'
|
||||||
|
'Copy folder': 'کپی پوشه'
|
||||||
|
'Move': 'انتقال'
|
||||||
|
'Rename': 'تغییر نام'
|
||||||
|
'Duplicate': 'تکثیر'
|
||||||
|
'Archive': 'بایگانی'
|
||||||
|
'Extract': 'استخراج'
|
||||||
|
'Compress': 'فشردهسازی'
|
||||||
|
'Decompress': 'باز کردن فشرده'
|
||||||
|
'Encrypt': 'رمزگذاری'
|
||||||
|
'Decrypt': 'رمزگشایی'
|
||||||
|
'Sign': 'امضا'
|
||||||
|
'Verify': 'تأیید'
|
||||||
|
'Authenticate': 'احراز هویت'
|
||||||
|
'Authorize': 'مجوزدهی'
|
||||||
|
'Login required': 'ورود الزامی است'
|
||||||
|
'Access denied': 'دسترسی رد شد'
|
||||||
|
'Permission denied': 'مجوز رد شد'
|
||||||
|
'Not found': 'یافت نشد'
|
||||||
|
'Not available': 'در دسترس نیست'
|
||||||
|
'Not supported': 'پشتیبانی نمیشود'
|
||||||
|
'Not implemented': 'پیادهسازی نشده'
|
||||||
|
'Not configured': 'پیکربندی نشده'
|
||||||
|
'Not initialized': 'مقداردهی نشده'
|
||||||
|
'Not ready': 'آماده نیست'
|
||||||
|
'Not connected': 'متصل نیست'
|
||||||
|
'Not authenticated': 'احراز هویت نشده'
|
||||||
|
'Not authorized': 'مجاز نیست'
|
||||||
|
'Not permitted': 'مجاز نیست'
|
||||||
|
'Not allowed': 'مجاز نیست'
|
||||||
|
'Not valid': 'معتبر نیست'
|
||||||
16
translations/security.fa.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# پیامهای خطای احراز هویت
|
||||||
|
'Invalid credentials.': 'اطلاعات ورود نامعتبر است.'
|
||||||
|
'Bad credentials.': 'اطلاعات ورود اشتباه است.'
|
||||||
|
'Username could not be found.': 'نام کاربری یافت نشد.'
|
||||||
|
'Invalid CSRF token.': 'توکن امنیتی نامعتبر است.'
|
||||||
|
'Account is disabled.': 'حساب کاربری غیرفعال است.'
|
||||||
|
'Account is locked.': 'حساب کاربری قفل شده است.'
|
||||||
|
'User account has expired.': 'حساب کاربری منقضی شده است.'
|
||||||
|
'User credentials have expired.': 'اعتبارات کاربر منقضی شده است.'
|
||||||
|
'Authentication request could not be processed due to a system problem.': 'درخواست احراز هویت به دلیل مشکل سیستم قابل پردازش نیست.'
|
||||||
|
'Authentication service temporarily unavailable.': 'سرویس احراز هویت موقتاً در دسترس نیست.'
|
||||||
|
'Too many failed login attempts, please try again later.': 'تعداد تلاشهای ناموفق زیاد است، لطفاً بعداً تلاش کنید.'
|
||||||
|
'Session has expired.': 'جلسه منقضی شده است.'
|
||||||
|
'Authentication required.': 'احراز هویت الزامی است.'
|
||||||
|
'Access denied.': 'دسترسی رد شد.'
|
||||||
|
'You are not authorized to access this resource.': 'شما مجاز به دسترسی به این منبع نیستید.'
|
||||||