start working on auth part
This commit is contained in:
parent
02ea1b0a26
commit
29b49d397d
12
.env.local.php
Normal file
12
.env.local.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// This file was generated by running "composer dump-env dev"
|
||||||
|
|
||||||
|
return array (
|
||||||
|
'APP_ENV' => 'dev',
|
||||||
|
'SYMFONY_DOTENV_PATH' => './../.env',
|
||||||
|
'APP_SECRET' => '6c6ccf94990dea080eeba986bf7e23af',
|
||||||
|
'DATABASE_URL' => 'postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8',
|
||||||
|
'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0',
|
||||||
|
'MAILER_DSN' => 'null://null',
|
||||||
|
);
|
||||||
68
.gitignore
vendored
68
.gitignore
vendored
|
|
@ -1,20 +1,54 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
# Build files
|
||||||
/.env.local
|
public_html/build/
|
||||||
/.env.local.php
|
core/var/cache/
|
||||||
/.env.*.local
|
core/var/log/
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
frontend/node_modules/
|
||||||
/public_html/bundles/
|
# Environment files
|
||||||
/var/
|
.env
|
||||||
/vendor/
|
.env.local
|
||||||
###< symfony/framework-bundle ###
|
.env.prod
|
||||||
|
|
||||||
###> phpunit/phpunit ###
|
# IDE files
|
||||||
/phpunit.xml
|
.vscode/
|
||||||
/.phpunit.cache/
|
.idea/
|
||||||
###< phpunit/phpunit ###
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
###> symfony/asset-mapper ###
|
# OS files
|
||||||
/public/assets/
|
.DS_Store
|
||||||
/assets/vendor/
|
Thumbs.db
|
||||||
###< symfony/asset-mapper ###
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
var/
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
|
||||||
150
README.md
Normal file
150
README.md
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# سیستم حسابداری - Vue.js + Vuetify + Symfony
|
||||||
|
|
||||||
|
این پروژه یک سیستم حسابداری کامل است که از Vue.js 3، Vuetify 3 و Symfony 7 استفاده میکند.
|
||||||
|
|
||||||
|
## 🏗️ ساختار پروژه
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/v2.hesabix.ir/
|
||||||
|
├── core/ # Symfony backend
|
||||||
|
├── frontend/ # Vue.js + Vuetify frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # کامپوننتهای Vue
|
||||||
|
│ │ ├── views/ # صفحات اصلی
|
||||||
|
│ │ ├── router/ # Vue Router
|
||||||
|
│ │ ├── store/ # Vuex store
|
||||||
|
│ │ └── assets/ # فایلهای استاتیک
|
||||||
|
│ ├── public/
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── webpack.config.js
|
||||||
|
├── public_html/ # فایلهای build شده
|
||||||
|
└── vendor/ # PHP dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 نصب و راهاندازی
|
||||||
|
|
||||||
|
### پیشنیازها
|
||||||
|
- PHP 8.2+
|
||||||
|
- Node.js 18+
|
||||||
|
- Composer
|
||||||
|
- npm
|
||||||
|
|
||||||
|
### Backend (Symfony)
|
||||||
|
```bash
|
||||||
|
cd core
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (Vue.js)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 دستورات توسعه
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev # Build development
|
||||||
|
npm run watch # Build + watch changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build # Build production
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 ویژگیها
|
||||||
|
|
||||||
|
### کامپوننتهای اصلی
|
||||||
|
- **Dashboard**: نمایش خلاصه مالی
|
||||||
|
- **Accounts**: مدیریت حسابها
|
||||||
|
- **Transactions**: مدیریت تراکنشها
|
||||||
|
- **Reports**: گزارشات مالی
|
||||||
|
- **Settings**: تنظیمات سیستم
|
||||||
|
|
||||||
|
### تکنولوژیها
|
||||||
|
- **Vue 3**: Composition API
|
||||||
|
- **Vuetify 3**: Material Design components
|
||||||
|
- **Vue Router 4**: Client-side routing
|
||||||
|
- **Vuex 4**: State management
|
||||||
|
- **Webpack Encore**: Asset compilation
|
||||||
|
|
||||||
|
## 🌐 روتینگ
|
||||||
|
|
||||||
|
### Vue Router (Frontend)
|
||||||
|
- `/` - Dashboard
|
||||||
|
- `/accounts` - مدیریت حسابها
|
||||||
|
- `/transactions` - مدیریت تراکنشها
|
||||||
|
- `/reports` - گزارشات
|
||||||
|
- `/settings` - تنظیمات
|
||||||
|
|
||||||
|
### Symfony Routes (Backend)
|
||||||
|
- `/api/*` - API endpoints
|
||||||
|
|
||||||
|
## 🎨 UI/UX Features
|
||||||
|
|
||||||
|
- **RTL Support**: پشتیبانی کامل از راست به چپ
|
||||||
|
- **Persian Font**: فونت Vazirmatn
|
||||||
|
- **Material Design**: کامپوننتهای زیبا
|
||||||
|
- **Responsive**: سازگار با همه دستگاهها
|
||||||
|
- **Dark/Light Theme**: تمهای مختلف
|
||||||
|
|
||||||
|
## 📊 کامپوننتهای Vuetify
|
||||||
|
|
||||||
|
- **Data Tables**: نمایش دادهها
|
||||||
|
- **Cards**: کارتهای اطلاعاتی
|
||||||
|
- **Forms**: فرمهای ورودی
|
||||||
|
- **Navigation**: منوهای کاربری
|
||||||
|
- **Charts**: نمودارها (آینده)
|
||||||
|
- **Dialogs**: پنجرههای تعاملی
|
||||||
|
|
||||||
|
## 🔄 Build Process
|
||||||
|
|
||||||
|
1. **Development**: `npm run dev`
|
||||||
|
2. **Watch Mode**: `npm run watch`
|
||||||
|
3. **Production**: `npm run build`
|
||||||
|
|
||||||
|
فایلهای build شده در `public_html/build/` قرار میگیرند.
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### Development Server
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 نکات مهم
|
||||||
|
|
||||||
|
- فایلهای frontend در پوشه جداگانه قرار دارند
|
||||||
|
- Build process کاملاً مستقل است
|
||||||
|
- Symfony به عنوان API backend عمل میکند
|
||||||
|
- Vue Router برای client-side routing استفاده میشود
|
||||||
|
- فونت فارسی Vazirmatn برای RTL استفاده شده
|
||||||
|
|
||||||
|
## 🐛 عیبیابی
|
||||||
|
|
||||||
|
### مشکلات رایج
|
||||||
|
1. **Node modules**: `rm -rf node_modules && npm install`
|
||||||
|
2. **Build errors**: بررسی webpack.config.js
|
||||||
|
3. **Routing issues**: بررسی .htaccess
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
- Symfony: `core/var/log/`
|
||||||
|
- Webpack: `frontend/` console
|
||||||
|
|
||||||
|
## 📞 پشتیبانی
|
||||||
|
|
||||||
|
برای سوالات و مشکلات:
|
||||||
|
- بررسی documentation
|
||||||
|
- بررسی console errors
|
||||||
|
- بررسی network tab
|
||||||
11
core/.gitignore
vendored
11
core/.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
|
|
@ -14,7 +13,9 @@
|
||||||
/.phpunit.cache/
|
/.phpunit.cache/
|
||||||
###< phpunit/phpunit ###
|
###< phpunit/phpunit ###
|
||||||
|
|
||||||
###> symfony/asset-mapper ###
|
###> symfony/webpack-encore-bundle ###
|
||||||
/public/assets/
|
/node_modules/
|
||||||
/assets/vendor/
|
/../public_html/build/
|
||||||
###< symfony/asset-mapper ###
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
###< symfony/webpack-encore-bundle ###
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import './bootstrap.js';
|
|
||||||
/*
|
|
||||||
* Welcome to your app's main JavaScript file!
|
|
||||||
*
|
|
||||||
* This file will be included onto the page via the importmap() Twig function,
|
|
||||||
* which should already be in your base.html.twig.
|
|
||||||
*/
|
|
||||||
import './styles/app.css';
|
|
||||||
|
|
||||||
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
|
|
||||||
5
core/assets/bootstrap.js
vendored
5
core/assets/bootstrap.js
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
import { startStimulusApp } from '@symfony/stimulus-bundle';
|
|
||||||
|
|
||||||
const app = startStimulusApp();
|
|
||||||
// register any custom, 3rd party controllers here
|
|
||||||
// app.register('some_controller_name', SomeImportedController);
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"controllers": {
|
|
||||||
"@symfony/ux-turbo": {
|
|
||||||
"turbo-core": {
|
|
||||||
"enabled": true,
|
|
||||||
"fetch": "eager"
|
|
||||||
},
|
|
||||||
"mercure-turbo-stream": {
|
|
||||||
"enabled": false,
|
|
||||||
"fetch": "eager"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrypoints": []
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
|
||||||
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
|
||||||
|
|
||||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
|
||||||
document.addEventListener('submit', function (event) {
|
|
||||||
generateCsrfToken(event.target);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
|
||||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
|
||||||
document.addEventListener('turbo:submit-start', function (event) {
|
|
||||||
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
|
|
||||||
Object.keys(h).map(function (k) {
|
|
||||||
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
|
||||||
document.addEventListener('turbo:submit-end', function (event) {
|
|
||||||
removeCsrfToken(event.detail.formSubmission.formElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
export function generateCsrfToken (formElement) {
|
|
||||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
|
||||||
|
|
||||||
if (!csrfField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
|
||||||
let csrfToken = csrfField.value;
|
|
||||||
|
|
||||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
|
||||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
|
||||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
|
||||||
}
|
|
||||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
|
|
||||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
|
||||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
|
||||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateCsrfHeaders (formElement) {
|
|
||||||
const headers = {};
|
|
||||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
|
||||||
|
|
||||||
if (!csrfField) {
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
|
||||||
|
|
||||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
|
||||||
headers[csrfCookie] = csrfField.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeCsrfToken (formElement) {
|
|
||||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
|
||||||
|
|
||||||
if (!csrfField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
|
||||||
|
|
||||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
|
||||||
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
|
||||||
|
|
||||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stimulusFetch: 'lazy' */
|
|
||||||
export default 'csrf-protection-controller';
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { Controller } from '@hotwired/stimulus';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is an example Stimulus controller!
|
|
||||||
*
|
|
||||||
* Any element with a data-controller="hello" attribute will cause
|
|
||||||
* this controller to be executed. The name "hello" comes from the filename:
|
|
||||||
* hello_controller.js -> "hello"
|
|
||||||
*
|
|
||||||
* Delete this file or adapt it for your use!
|
|
||||||
*/
|
|
||||||
export default class extends Controller {
|
|
||||||
connect() {
|
|
||||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: skyblue;
|
|
||||||
}
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
"doctrine/orm": "^3.5",
|
"doctrine/orm": "^3.5",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
"symfony/apache-pack": "*",
|
||||||
"symfony/asset": "7.3.*",
|
"symfony/asset": "7.3.*",
|
||||||
"symfony/asset-mapper": "7.3.*",
|
|
||||||
"symfony/console": "7.3.*",
|
"symfony/console": "7.3.*",
|
||||||
"symfony/doctrine-messenger": "7.3.*",
|
"symfony/doctrine-messenger": "7.3.*",
|
||||||
"symfony/dotenv": "7.3.*",
|
"symfony/dotenv": "7.3.*",
|
||||||
|
|
@ -34,13 +34,12 @@
|
||||||
"symfony/runtime": "7.3.*",
|
"symfony/runtime": "7.3.*",
|
||||||
"symfony/security-bundle": "7.3.*",
|
"symfony/security-bundle": "7.3.*",
|
||||||
"symfony/serializer": "7.3.*",
|
"symfony/serializer": "7.3.*",
|
||||||
"symfony/stimulus-bundle": "^2.30",
|
|
||||||
"symfony/string": "7.3.*",
|
"symfony/string": "7.3.*",
|
||||||
"symfony/translation": "7.3.*",
|
"symfony/translation": "7.3.*",
|
||||||
"symfony/twig-bundle": "7.3.*",
|
"symfony/twig-bundle": "7.3.*",
|
||||||
"symfony/ux-turbo": "^2.30",
|
|
||||||
"symfony/validator": "7.3.*",
|
"symfony/validator": "7.3.*",
|
||||||
"symfony/web-link": "7.3.*",
|
"symfony/web-link": "7.3.*",
|
||||||
|
"symfony/webpack-encore-bundle": "^2.3",
|
||||||
"symfony/yaml": "7.3.*",
|
"symfony/yaml": "7.3.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
|
|
@ -79,8 +78,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"auto-scripts": {
|
"auto-scripts": {
|
||||||
"cache:clear": "symfony-cmd",
|
"cache:clear": "symfony-cmd",
|
||||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
"importmap:install": "symfony-cmd"
|
|
||||||
},
|
},
|
||||||
"post-install-cmd": [
|
"post-install-cmd": [
|
||||||
"@auto-scripts"
|
"@auto-scripts"
|
||||||
|
|
@ -94,7 +92,7 @@
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"symfony": {
|
"symfony": {
|
||||||
"allow-contrib": false,
|
"allow-contrib": true,
|
||||||
"require": "7.3.*",
|
"require": "7.3.*",
|
||||||
"docker": true
|
"docker": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
4680
core/composer.lock
generated
4680
core/composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,10 +7,9 @@ return [
|
||||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
|
||||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
|
||||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
framework:
|
|
||||||
asset_mapper:
|
|
||||||
# The paths to make available to the asset mapper.
|
|
||||||
paths:
|
|
||||||
- assets/
|
|
||||||
missing_import_mode: strict
|
|
||||||
|
|
||||||
when@prod:
|
|
||||||
framework:
|
|
||||||
asset_mapper:
|
|
||||||
missing_import_mode: warn
|
|
||||||
|
|
@ -2,28 +2,32 @@ security:
|
||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
users_in_memory: { memory: null }
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: email
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
security: false
|
security: false
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: users_in_memory
|
provider: app_user_provider
|
||||||
|
form_login:
|
||||||
|
login_path: app_login
|
||||||
|
check_path: app_login
|
||||||
|
enable_csrf: true
|
||||||
|
default_target_path: app_home
|
||||||
|
logout:
|
||||||
|
path: app_logout
|
||||||
|
target: app_home
|
||||||
|
|
||||||
# activate different ways to authenticate
|
|
||||||
# https://symfony.com/doc/current/security.html#the-firewall
|
|
||||||
|
|
||||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
|
||||||
# switch_user: true
|
|
||||||
|
|
||||||
# Easy way to control access for large sections of your site
|
|
||||||
# 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: ^/profile, roles: ROLE_USER }
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# Enable stateless CSRF protection for forms and logins/logouts
|
|
||||||
framework:
|
|
||||||
csrf_protection:
|
|
||||||
check_header: true
|
|
||||||
45
core/config/packages/webpack_encore.yaml
Normal file
45
core/config/packages/webpack_encore.yaml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
webpack_encore:
|
||||||
|
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||||
|
output_path: '%kernel.project_dir%/../public_html/build'
|
||||||
|
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||||
|
# output_path: false
|
||||||
|
|
||||||
|
# Set attributes that will be rendered on all script and link tags
|
||||||
|
script_attributes:
|
||||||
|
defer: true
|
||||||
|
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||||
|
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||||
|
# 'data-turbo-track': reload
|
||||||
|
# link_attributes:
|
||||||
|
# Uncomment if using Turbo Drive
|
||||||
|
# 'data-turbo-track': reload
|
||||||
|
|
||||||
|
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||||
|
# crossorigin: 'anonymous'
|
||||||
|
|
||||||
|
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
|
||||||
|
# preload: true
|
||||||
|
|
||||||
|
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
|
||||||
|
# strict_mode: false
|
||||||
|
|
||||||
|
# If you have multiple builds:
|
||||||
|
# builds:
|
||||||
|
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||||
|
|
||||||
|
# pass the build name as the 3rd argument to the Twig functions
|
||||||
|
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||||
|
|
||||||
|
framework:
|
||||||
|
assets:
|
||||||
|
json_manifest_path: '%kernel.project_dir%/../public_html/build/manifest.json'
|
||||||
|
|
||||||
|
#when@prod:
|
||||||
|
# webpack_encore:
|
||||||
|
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||||
|
# # Available in version 1.2
|
||||||
|
# cache: true
|
||||||
|
|
||||||
|
#when@test:
|
||||||
|
# webpack_encore:
|
||||||
|
# strict_mode: false
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the importmap for this application.
|
|
||||||
*
|
|
||||||
* - "path" is a path inside the asset mapper system. Use the
|
|
||||||
* "debug:asset-map" command to see the full list of paths.
|
|
||||||
*
|
|
||||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
|
||||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
|
||||||
*
|
|
||||||
* The "importmap:require" command can be used to add new entries to this file.
|
|
||||||
*/
|
|
||||||
return [
|
|
||||||
'app' => [
|
|
||||||
'path' => './assets/app.js',
|
|
||||||
'entrypoint' => true,
|
|
||||||
],
|
|
||||||
'@hotwired/stimulus' => [
|
|
||||||
'version' => '3.2.2',
|
|
||||||
],
|
|
||||||
'@symfony/stimulus-bundle' => [
|
|
||||||
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
|
|
||||||
],
|
|
||||||
'@hotwired/turbo' => [
|
|
||||||
'version' => '7.3.0',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
44
core/migrations/Version20241201000000.php
Normal file
44
core/migrations/Version20241201000000.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?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 Version20241201000000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create user table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE `user` (
|
||||||
|
id INT AUTO_INCREMENT NOT NULL,
|
||||||
|
email VARCHAR(180) NOT NULL,
|
||||||
|
mobile VARCHAR(15) NOT NULL,
|
||||||
|
full_name VARCHAR(100) NOT NULL,
|
||||||
|
roles JSON NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
is_verified TINYINT(1) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
|
||||||
|
updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
|
||||||
|
reset_token VARCHAR(255) DEFAULT NULL,
|
||||||
|
reset_token_expires_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
|
||||||
|
UNIQUE INDEX UNIQ_8D93D649E7927C74 (email),
|
||||||
|
UNIQUE INDEX UNIQ_8D93D6493C7323CB (mobile),
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE `user`');
|
||||||
|
}
|
||||||
|
}
|
||||||
20
core/package.json
Normal file
20
core/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.0",
|
||||||
|
"@babel/preset-env": "^7.16.0",
|
||||||
|
"@symfony/webpack-encore": "^5.0.0",
|
||||||
|
"bootstrap": "^5.3.0",
|
||||||
|
"core-js": "^3.38.0",
|
||||||
|
"regenerator-runtime": "^0.13.9",
|
||||||
|
"webpack": "^5.74.0",
|
||||||
|
"webpack-cli": "^5.1.0"
|
||||||
|
},
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev-server": "encore dev-server",
|
||||||
|
"dev": "encore dev",
|
||||||
|
"watch": "encore dev --watch",
|
||||||
|
"build": "encore production --progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
core/src/Controller/HomeController.php
Normal file
16
core/src/Controller/HomeController.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class HomeController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/', name: 'app_home')]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return $this->render('home/index.html.twig');
|
||||||
|
}
|
||||||
|
}
|
||||||
78
core/src/Controller/RegistrationController.php
Normal file
78
core/src/Controller/RegistrationController.php
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Form\RegistrationFormType;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
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\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
|
||||||
|
|
||||||
|
class RegistrationController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/register', name: 'app_register')]
|
||||||
|
public function register(
|
||||||
|
Request $request,
|
||||||
|
UserPasswordHasherInterface $userPasswordHasher,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
UserRepository $userRepository
|
||||||
|
): Response
|
||||||
|
{
|
||||||
|
// اگر کاربر قبلاً وارد شده باشد، به صفحه اصلی هدایت شود
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$form = $this->createForm(RegistrationFormType::class, $user);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
// بررسی تکراری نبودن ایمیل و موبایل
|
||||||
|
$existingUser = $userRepository->findByEmail($user->getEmail());
|
||||||
|
if ($existingUser) {
|
||||||
|
$this->addFlash('error', 'این ایمیل قبلاً ثبت شده است');
|
||||||
|
return $this->render('registration/register.html.twig', [
|
||||||
|
'registrationForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingUser = $userRepository->findByMobile($user->getMobile());
|
||||||
|
if ($existingUser) {
|
||||||
|
$this->addFlash('error', 'این شماره موبایل قبلاً ثبت شده است');
|
||||||
|
return $this->render('registration/register.html.twig', [
|
||||||
|
'registrationForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// رمزگذاری رمز عبور
|
||||||
|
$user->setPassword(
|
||||||
|
$userPasswordHasher->hashPassword(
|
||||||
|
$user,
|
||||||
|
$form->get('plainPassword')->getData()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// تنظیم نقش کاربر
|
||||||
|
$user->setRoles(['ROLE_USER']);
|
||||||
|
$user->setIsVerified(true); // در حالت واقعی باید تایید ایمیل انجام شود
|
||||||
|
|
||||||
|
$entityManager->persist($user);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'ثبتنام با موفقیت انجام شد. حالا میتوانید وارد شوید.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('registration/register.html.twig', [
|
||||||
|
'registrationForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
core/src/Controller/ResetPasswordController.php
Normal file
128
core/src/Controller/ResetPasswordController.php
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Form\ChangePasswordFormType;
|
||||||
|
use App\Form\ResetPasswordRequestFormType;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||||
|
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
#[Route('/reset-password')]
|
||||||
|
class ResetPasswordController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
private TokenGeneratorInterface $tokenGenerator,
|
||||||
|
private MailerInterface $mailer
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', name: 'app_forgot_password_request')]
|
||||||
|
public function request(Request $request): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(ResetPasswordRequestFormType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
return $this->processSendingPasswordResetEmail(
|
||||||
|
$form->get('email')->getData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('reset_password/request.html.twig', [
|
||||||
|
'requestForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/check-email', name: 'app_check_email')]
|
||||||
|
public function checkEmail(): Response
|
||||||
|
{
|
||||||
|
// این صفحه فقط برای نمایش پیام استفاده میشود
|
||||||
|
return $this->render('reset_password/check_email.html.twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/reset/{token}', name: 'app_reset_password')]
|
||||||
|
public function reset(string $token, Request $request, UserPasswordHasherInterface $userPasswordHasher): Response
|
||||||
|
{
|
||||||
|
$user = $this->userRepository->findByResetToken($token);
|
||||||
|
|
||||||
|
if (null === $user || $user->isResetTokenExpired()) {
|
||||||
|
$this->addFlash('error', 'توکن بازنشانی نامعتبر یا منقضی شده است');
|
||||||
|
return $this->redirectToRoute('app_forgot_password_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(ChangePasswordFormType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$user->setPassword(
|
||||||
|
$userPasswordHasher->hashPassword(
|
||||||
|
$user,
|
||||||
|
$form->get('plainPassword')->getData()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$user->setResetToken(null);
|
||||||
|
$user->setResetTokenExpiresAt(null);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'رمز عبور شما با موفقیت تغییر یافت');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('reset_password/reset.html.twig', [
|
||||||
|
'resetForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processSendingPasswordResetEmail(string $emailFormData): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $this->userRepository->findByEmail($emailFormData);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
// برای امنیت، همیشه پیام موفقیت نمایش میدهیم
|
||||||
|
return $this->redirectToRoute('app_check_email');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resetToken = $this->tokenGenerator->generateToken();
|
||||||
|
$user->setResetToken($resetToken);
|
||||||
|
$user->setResetTokenExpiresAt(new \DateTimeImmutable('+1 hour'));
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->addFlash('error', 'خطا در ایجاد توکن بازنشانی');
|
||||||
|
return $this->redirectToRoute('app_forgot_password_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = (new TemplatedEmail())
|
||||||
|
->from('noreply@hesabix.ir')
|
||||||
|
->to($user->getEmail())
|
||||||
|
->subject('درخواست بازنشانی رمز عبور')
|
||||||
|
->htmlTemplate('reset_password/email.html.twig')
|
||||||
|
->context([
|
||||||
|
'resetToken' => $resetToken,
|
||||||
|
'user' => $user,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_check_email');
|
||||||
|
}
|
||||||
|
}
|
||||||
36
core/src/Controller/SecurityController.php
Normal file
36
core/src/Controller/SecurityController.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
|
||||||
|
class SecurityController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route(path: '/login', name: 'app_login')]
|
||||||
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
|
{
|
||||||
|
// اگر کاربر قبلاً وارد شده باشد، به صفحه اصلی هدایت شود
|
||||||
|
if ($this->getUser()) {
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
|
$lastUsername = $authenticationUtils->getLastUsername();
|
||||||
|
|
||||||
|
return $this->render('security/login.html.twig', [
|
||||||
|
'last_username' => $lastUsername,
|
||||||
|
'error' => $error,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/logout', name: 'app_logout')]
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
// این متد توسط Symfony Security Bundle مدیریت میشود
|
||||||
|
// کد اینجا اجرا نمیشود
|
||||||
|
throw new \LogicException('این متد باید توسط Symfony Security Bundle مدیریت شود');
|
||||||
|
}
|
||||||
|
}
|
||||||
48
core/src/Controller/UIController.php
Normal file
48
core/src/Controller/UIController.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class UIController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/ui', name: 'app_ui_home')]
|
||||||
|
#[Route('/ui/{route}', name: 'app_ui_route', requirements: ['route' => '.+'])]
|
||||||
|
public function index(string $route = ''): Response
|
||||||
|
{
|
||||||
|
// Extract the main page from route
|
||||||
|
$page = $this->extractPageFromRoute($route);
|
||||||
|
|
||||||
|
return $this->render('ui/app.html.twig', [
|
||||||
|
'page' => $page,
|
||||||
|
'route' => $route
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractPageFromRoute(string $route): string
|
||||||
|
{
|
||||||
|
// Remove leading slash and get first segment
|
||||||
|
$route = ltrim($route, '/');
|
||||||
|
|
||||||
|
if (empty($route)) {
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by slash and get first part
|
||||||
|
$segments = explode('/', $route);
|
||||||
|
$mainPage = $segments[0];
|
||||||
|
|
||||||
|
// Map route to page
|
||||||
|
$pageMap = [
|
||||||
|
'dashboard' => 'dashboard',
|
||||||
|
'accounts' => 'accounts',
|
||||||
|
'transactions' => 'transactions',
|
||||||
|
'reports' => 'reports',
|
||||||
|
'settings' => 'settings'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $pageMap[$mainPage] ?? 'dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
208
core/src/Entity/User.php
Normal file
208
core/src/Entity/User.php
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
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;
|
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||||
|
#[ORM\Table(name: '`user`')]
|
||||||
|
#[UniqueEntity(fields: ['email'], message: 'این ایمیل قبلاً ثبت شده است')]
|
||||||
|
#[UniqueEntity(fields: ['mobile'], message: 'این شماره موبایل قبلاً ثبت شده است')]
|
||||||
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 180, unique: true)]
|
||||||
|
#[Assert\NotBlank(message: 'ایمیل الزامی است')]
|
||||||
|
#[Assert\Email(message: 'فرمت ایمیل صحیح نیست')]
|
||||||
|
private ?string $email = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 15, unique: true)]
|
||||||
|
#[Assert\NotBlank(message: 'شماره موبایل الزامی است')]
|
||||||
|
#[Assert\Regex(
|
||||||
|
pattern: '/^09[0-9]{9}$/',
|
||||||
|
message: 'فرمت شماره موبایل صحیح نیست (مثال: 09123456789)'
|
||||||
|
)]
|
||||||
|
private ?string $mobile = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 100)]
|
||||||
|
#[Assert\NotBlank(message: 'نام الزامی است')]
|
||||||
|
#[Assert\Length(
|
||||||
|
min: 2,
|
||||||
|
max: 100,
|
||||||
|
minMessage: 'نام باید حداقل {{ limit }} کاراکتر باشد',
|
||||||
|
maxMessage: 'نام نمیتواند بیشتر از {{ limit }} کاراکتر باشد'
|
||||||
|
)]
|
||||||
|
private ?string $fullName = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private array $roles = [];
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?string $password = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?bool $isVerified = false;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?\DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?\DateTimeImmutable $updatedAt = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?string $resetToken = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?\DateTimeImmutable $resetTokenExpiresAt = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
|
$this->roles = ['ROLE_USER'];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getMobile(): ?string
|
||||||
|
{
|
||||||
|
return $this->mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMobile(string $mobile): static
|
||||||
|
{
|
||||||
|
$this->mobile = $mobile;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFullName(): ?string
|
||||||
|
{
|
||||||
|
return $this->fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFullName(string $fullName): static
|
||||||
|
{
|
||||||
|
$this->fullName = $fullName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return (string) $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoles(): array
|
||||||
|
{
|
||||||
|
$roles = $this->roles;
|
||||||
|
$roles[] = 'ROLE_USER';
|
||||||
|
|
||||||
|
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 isVerified(): bool
|
||||||
|
{
|
||||||
|
return $this->isVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsVerified(bool $isVerified): static
|
||||||
|
{
|
||||||
|
$this->isVerified = $isVerified;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedAt(\DateTimeImmutable $createdAt): static
|
||||||
|
{
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResetToken(): ?string
|
||||||
|
{
|
||||||
|
return $this->resetToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResetToken(?string $resetToken): static
|
||||||
|
{
|
||||||
|
$this->resetToken = $resetToken;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResetTokenExpiresAt(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->resetTokenExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResetTokenExpiresAt(?\DateTimeImmutable $resetTokenExpiresAt): static
|
||||||
|
{
|
||||||
|
$this->resetTokenExpiresAt = $resetTokenExpiresAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isResetTokenExpired(): bool
|
||||||
|
{
|
||||||
|
if (!$this->resetTokenExpiresAt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $this->resetTokenExpiresAt < new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
core/src/Form/ChangePasswordFormType.php
Normal file
54
core/src/Form/ChangePasswordFormType.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?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\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
class ChangePasswordFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('plainPassword', RepeatedType::class, [
|
||||||
|
'type' => PasswordType::class,
|
||||||
|
'mapped' => false,
|
||||||
|
'first_options' => [
|
||||||
|
'label' => 'رمز عبور جدید',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'رمز عبور جدید را وارد کنید'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'second_options' => [
|
||||||
|
'label' => 'تکرار رمز عبور جدید',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'رمز عبور جدید را دوباره وارد کنید'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'invalid_message' => 'رمزهای عبور یکسان نیستند',
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'رمز عبور جدید الزامی است',
|
||||||
|
]),
|
||||||
|
new Length([
|
||||||
|
'min' => 6,
|
||||||
|
'minMessage' => 'رمز عبور باید حداقل {{ limit }} کاراکتر باشد',
|
||||||
|
'max' => 4096,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
core/src/Form/RegistrationFormType.php
Normal file
106
core/src/Form/RegistrationFormType.php
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
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\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\Regex;
|
||||||
|
|
||||||
|
class RegistrationFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('fullName', TextType::class, [
|
||||||
|
'label' => 'نام و نام خانوادگی',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'نام و نام خانوادگی خود را وارد کنید'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'نام و نام خانوادگی الزامی است',
|
||||||
|
]),
|
||||||
|
new Length([
|
||||||
|
'min' => 2,
|
||||||
|
'max' => 100,
|
||||||
|
'minMessage' => 'نام باید حداقل {{ limit }} کاراکتر باشد',
|
||||||
|
'maxMessage' => 'نام نمیتواند بیشتر از {{ limit }} کاراکتر باشد',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('email', EmailType::class, [
|
||||||
|
'label' => 'ایمیل',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'example@email.com'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'ایمیل الزامی است',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('mobile', TextType::class, [
|
||||||
|
'label' => 'شماره موبایل',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => '09123456789'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'شماره موبایل الزامی است',
|
||||||
|
]),
|
||||||
|
new Regex([
|
||||||
|
'pattern' => '/^09[0-9]{9}$/',
|
||||||
|
'message' => 'فرمت شماره موبایل صحیح نیست (مثال: 09123456789)',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('plainPassword', RepeatedType::class, [
|
||||||
|
'type' => PasswordType::class,
|
||||||
|
'mapped' => false,
|
||||||
|
'first_options' => [
|
||||||
|
'label' => 'رمز عبور',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'رمز عبور خود را وارد کنید'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'second_options' => [
|
||||||
|
'label' => 'تکرار رمز عبور',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'رمز عبور را دوباره وارد کنید'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'invalid_message' => 'رمزهای عبور یکسان نیستند',
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'رمز عبور الزامی است',
|
||||||
|
]),
|
||||||
|
new Length([
|
||||||
|
'min' => 6,
|
||||||
|
'minMessage' => 'رمز عبور باید حداقل {{ limit }} کاراکتر باشد',
|
||||||
|
'max' => 4096,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => User::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
core/src/Form/ResetPasswordRequestFormType.php
Normal file
35
core/src/Form/ResetPasswordRequestFormType.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
|
class ResetPasswordRequestFormType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('email', EmailType::class, [
|
||||||
|
'label' => 'ایمیل',
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'placeholder' => 'ایمیل خود را وارد کنید'
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank([
|
||||||
|
'message' => 'لطفاً ایمیل خود را وارد کنید',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
core/src/Repository/UserRepository.php
Normal file
82
core/src/Repository/UserRepository.php
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<User>
|
||||||
|
*
|
||||||
|
* @method User|null find($id, $lockMode = null, $lockVersion = null)
|
||||||
|
* @method User|null findOneBy(array $criteria, array $orderBy = null)
|
||||||
|
* @method User[] findAll()
|
||||||
|
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
|
*/
|
||||||
|
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(User $entity, bool $flush = false): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->persist($entity);
|
||||||
|
|
||||||
|
if ($flush) {
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(User $entity, bool $flush = false): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->remove($entity);
|
||||||
|
|
||||||
|
if ($flush) {
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to upgrade (rehash) the user's password automatically over time.
|
||||||
|
*/
|
||||||
|
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||||
|
{
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->setPassword($newHashedPassword);
|
||||||
|
$this->save($user, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByEmail(string $email): ?User
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['email' => $email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByMobile(string $mobile): ?User
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['mobile' => $mobile]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByResetToken(string $token): ?User
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['resetToken' => $token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findExpiredResetTokens(): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('u');
|
||||||
|
$qb->where('u.resetToken IS NOT NULL')
|
||||||
|
->andWhere('u.resetTokenExpiresAt < :now')
|
||||||
|
->setParameter('now', new \DateTimeImmutable());
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,19 +50,16 @@
|
||||||
"bin/phpunit"
|
"bin/phpunit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/asset-mapper": {
|
"symfony/apache-pack": {
|
||||||
"version": "7.3",
|
"version": "1.0",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
"repo": "github.com/symfony/recipes",
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"version": "6.4",
|
"version": "1.0",
|
||||||
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
|
"ref": "5d454ec6cc4c700ed3d963f3803e1d427d9669fb"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"assets/app.js",
|
".public_html/.htaccess"
|
||||||
"assets/styles/app.css",
|
|
||||||
"config/packages/asset_mapper.yaml",
|
|
||||||
"importmap.php"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/console": {
|
"symfony/console": {
|
||||||
|
|
@ -229,21 +226,6 @@
|
||||||
"config/routes/security.yaml"
|
"config/routes/security.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/stimulus-bundle": {
|
|
||||||
"version": "2.30",
|
|
||||||
"recipe": {
|
|
||||||
"repo": "github.com/symfony/recipes",
|
|
||||||
"branch": "main",
|
|
||||||
"version": "2.20",
|
|
||||||
"ref": "c30f782a8910ae9f12e7a1399db607d172d23da6"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"assets/bootstrap.js",
|
|
||||||
"assets/controllers.json",
|
|
||||||
"assets/controllers/csrf_protection_controller.js",
|
|
||||||
"assets/controllers/hello_controller.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"symfony/translation": {
|
"symfony/translation": {
|
||||||
"version": "7.3",
|
"version": "7.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
@ -270,18 +252,6 @@
|
||||||
"templates/base.html.twig"
|
"templates/base.html.twig"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symfony/ux-turbo": {
|
|
||||||
"version": "2.30",
|
|
||||||
"recipe": {
|
|
||||||
"repo": "github.com/symfony/recipes",
|
|
||||||
"branch": "main",
|
|
||||||
"version": "2.20",
|
|
||||||
"ref": "287f7c6eb6e9b65e422d34c00795b360a787380b"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"config/packages/ux_turbo.yaml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"symfony/validator": {
|
"symfony/validator": {
|
||||||
"version": "7.3",
|
"version": "7.3",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
@ -319,6 +289,22 @@
|
||||||
"config/packages/messenger.yaml"
|
"config/packages/messenger.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/webpack-encore-bundle": {
|
||||||
|
"version": "2.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.0",
|
||||||
|
"ref": "9ef5412a4a2a8415aca3a3f2b4edd3866aab9a19"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/app.js",
|
||||||
|
"assets/styles/app.css",
|
||||||
|
"config/packages/webpack_encore.yaml",
|
||||||
|
"package.json",
|
||||||
|
"webpack.config.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
"twig/extra-bundle": {
|
"twig/extra-bundle": {
|
||||||
"version": "v3.21.0"
|
"version": "v3.21.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
514
core/templates/auth/base.html.twig
Normal file
514
core/templates/auth/base.html.twig
Normal file
|
|
@ -0,0 +1,514 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}سیستم احراز هویت - حسابیکس{% endblock %}</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Tahoma', 'Arial', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
animation: slideUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 30px 30px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||||
|
animation: rotate 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-body {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(102, 126, 234, 0.25);
|
||||||
|
background-color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: rgba(220, 53, 69, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-valid {
|
||||||
|
border-color: #28a745;
|
||||||
|
background-color: rgba(40, 167, 69, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid-feedback {
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 15px 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 30px rgba(40, 167, 69, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 30px rgba(220, 53, 69, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-right: 4px solid;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #155724;
|
||||||
|
border-right-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #721c24;
|
||||||
|
border-right-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: rgba(23, 162, 184, 0.1);
|
||||||
|
color: #0c5460;
|
||||||
|
border-right-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: rgba(255, 193, 7, 0.1);
|
||||||
|
color: #856404;
|
||||||
|
border-right-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a:hover {
|
||||||
|
color: #5a6fd8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links a {
|
||||||
|
color: #6c757d;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links a:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider span {
|
||||||
|
background: white;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.auth-container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
padding: 30px 20px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-body {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 14px 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form validation enhancement */
|
||||||
|
.was-validated .form-control:valid {
|
||||||
|
border-color: #28a745;
|
||||||
|
background-color: rgba(40, 167, 69, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.was-validated .form-control:invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: rgba(220, 53, 69, 0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block stylesheets %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="auth-container">
|
||||||
|
<div class="auth-card">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="auth-header">
|
||||||
|
<h1>
|
||||||
|
<i class="bi bi-shield-lock me-3"></i>
|
||||||
|
{% block auth_title %}احراز هویت{% endblock %}
|
||||||
|
</h1>
|
||||||
|
<p>{% block auth_subtitle %}به سیستم حسابیکس خوش آمدید{% endblock %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="auth-body">
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% for message in app.flashes('success') %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check-circle me-2"></i>{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('error') %}
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('info') %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('warning') %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-circle me-2"></i>{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
{% block auth_body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="auth-footer">
|
||||||
|
{% block auth_footer %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('app_home') }}">
|
||||||
|
<i class="bi bi-house me-1"></i>بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JavaScript -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Auto-hide alerts after 8 seconds
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(function(alert) {
|
||||||
|
setTimeout(function() {
|
||||||
|
const bsAlert = new bootstrap.Alert(alert);
|
||||||
|
bsAlert.close();
|
||||||
|
}, 8000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced form validation
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
forms.forEach(function(form) {
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add loading state to submit buttons
|
||||||
|
const submitButtons = document.querySelectorAll('button[type="submit"]');
|
||||||
|
submitButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
if (this.form && this.form.checkValidity()) {
|
||||||
|
this.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>در حال پردازش...';
|
||||||
|
this.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced input focus effects
|
||||||
|
const inputs = document.querySelectorAll('.form-control');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
input.addEventListener('focus', function() {
|
||||||
|
this.parentElement.classList.add('focused');
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
this.parentElement.classList.remove('focused');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth animations for form elements
|
||||||
|
const formElements = document.querySelectorAll('.form-group');
|
||||||
|
formElements.forEach((element, index) => {
|
||||||
|
element.style.opacity = '0';
|
||||||
|
element.style.transform = 'translateY(20px)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.transition = 'all 0.5s ease';
|
||||||
|
element.style.opacity = '1';
|
||||||
|
element.style.transform = 'translateY(0)';
|
||||||
|
}, index * 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% block javascripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="fa" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}سیستم حسابداری{% endblock %}</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||||
|
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
|
{{ encore_entry_link_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
{{ encore_entry_script_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
23
core/templates/home/index.html.twig
Normal file
23
core/templates/home/index.html.twig
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}صفحه اصلی - سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1 class="display-4">به سیستم حسابداری حسابیکس خوش آمدید!</h1>
|
||||||
|
<p class="lead">این سیستم برای مدیریت مالی و حسابداری طراحی شده است.</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
{% if app.user %}
|
||||||
|
<p>شما با موفقیت وارد شدهاید. میتوانید از امکانات سیستم استفاده کنید.</p>
|
||||||
|
<a class="btn btn-primary btn-lg" href="#" role="button">شروع کار</a>
|
||||||
|
{% else %}
|
||||||
|
<p>برای استفاده از سیستم، لطفاً وارد شوید یا ثبتنام کنید.</p>
|
||||||
|
<a class="btn btn-primary btn-lg me-2" href="{{ path('app_login') }}" role="button">ورود</a>
|
||||||
|
<a class="btn btn-success btn-lg" href="{{ path('app_register') }}" role="button">ثبتنام</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
109
core/templates/registration/register.html.twig
Normal file
109
core/templates/registration/register.html.twig
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
{% extends 'auth/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}ثبتنام - سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_title %}ثبتنام در سیستم{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_subtitle %}حساب کاربری جدید ایجاد کنید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_body %}
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registration_form_fullName" class="form-label">
|
||||||
|
<i class="bi bi-person me-2"></i>نام و نام خانوادگی
|
||||||
|
</label>
|
||||||
|
{{ form_widget(registrationForm.fullName, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'نام و نام خانوادگی خود را وارد کنید',
|
||||||
|
'id': 'registration_form_fullName'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(registrationForm.fullName) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registration_form_email" class="form-label">
|
||||||
|
<i class="bi bi-envelope me-2"></i>ایمیل
|
||||||
|
</label>
|
||||||
|
{{ form_widget(registrationForm.email, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'example@email.com',
|
||||||
|
'id': 'registration_form_email'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(registrationForm.email) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registration_form_mobile" class="form-label">
|
||||||
|
<i class="bi bi-phone me-2"></i>شماره موبایل
|
||||||
|
</label>
|
||||||
|
{{ form_widget(registrationForm.mobile, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': '09123456789',
|
||||||
|
'id': 'registration_form_mobile'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(registrationForm.mobile) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registration_form_plainPassword_first" class="form-label">
|
||||||
|
<i class="bi bi-lock me-2"></i>رمز عبور
|
||||||
|
</label>
|
||||||
|
{{ form_widget(registrationForm.plainPassword.first, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'رمز عبور خود را وارد کنید',
|
||||||
|
'id': 'registration_form_plainPassword_first'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(registrationForm.plainPassword.first) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="registration_form_plainPassword_second" class="form-label">
|
||||||
|
<i class="bi bi-lock-fill me-2"></i>تکرار رمز عبور
|
||||||
|
</label>
|
||||||
|
{{ form_widget(registrationForm.plainPassword.second, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'رمز عبور را دوباره وارد کنید',
|
||||||
|
'id': 'registration_form_plainPassword_second'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(registrationForm.plainPassword.second) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>ثبتنام
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>قبلاً ثبتنام کردهاید؟</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ path('app_login') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>وارد شوید
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_footer %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('app_home') }}">
|
||||||
|
<i class="bi bi-house me-1"></i>بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<a href="{{ path('app_login') }}">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-1"></i>ورود
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
61
core/templates/reset_password/check_email.html.twig
Normal file
61
core/templates/reset_password/check_email.html.twig
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{% extends 'auth/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}بررسی ایمیل - سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_title %}بررسی ایمیل{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_subtitle %}لینک بازنشانی رمز عبور ارسال شد{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_body %}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="bi bi-envelope-check text-success" style="font-size: 4rem;"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h5 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>ایمیل ارسال شد!
|
||||||
|
</h5>
|
||||||
|
<p class="mb-0">
|
||||||
|
اگر ایمیلی با آدرس وارد شده در سیستم ثبت شده باشد،
|
||||||
|
لینک بازنشانی رمز عبور برای شما ارسال شده است.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-clock me-2"></i>مهم!
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0 text-start">
|
||||||
|
<li>لینک ارسال شده تا 1 ساعت معتبر است</li>
|
||||||
|
<li>پوشه اسپم خود را بررسی کنید</li>
|
||||||
|
<li>اگر ایمیل دریافت نکردید، دوباره تلاش کنید</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>مراحل بعدی</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ path('app_login') }}" class="btn btn-primary me-2">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>بازگشت به صفحه ورود
|
||||||
|
</a>
|
||||||
|
<a href="{{ path('app_forgot_password_request') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-clockwise me-2"></i>تلاش مجدد
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_footer %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('app_home') }}">
|
||||||
|
<i class="bi bi-house me-1"></i>بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<a href="{{ path('app_register') }}">
|
||||||
|
<i class="bi bi-person-plus me-1"></i>ثبتنام
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
100
core/templates/reset_password/email.html.twig
Normal file
100
core/templates/reset_password/email.html.twig
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<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;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
color: #007bff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>بازنشانی رمز عبور</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p>سلام {{ user.fullName }}،</p>
|
||||||
|
|
||||||
|
<p>شما درخواست بازنشانی رمز عبور کردهاید. برای ادامه، روی دکمه زیر کلیک کنید:</p>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ url('app_reset_password', {token: resetToken}) }}" class="button">
|
||||||
|
تغییر رمز عبور
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<strong>توجه:</strong> این لینک فقط تا 1 ساعت معتبر است. اگر درخواست شما نبوده، این ایمیل را نادیده بگیرید.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>اگر دکمه بالا کار نمیکند، میتوانید این لینک را در مرورگر خود کپی کنید:</p>
|
||||||
|
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px; border-radius: 5px;">
|
||||||
|
{{ url('app_reset_password', {token: resetToken}) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||||
|
<p>© {{ "now"|date("Y") }} سیستم حسابداری حسابیکس</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
core/templates/reset_password/request.html.twig
Normal file
64
core/templates/reset_password/request.html.twig
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% extends 'auth/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}فراموشی رمز عبور - سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_title %}فراموشی رمز عبور{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_subtitle %}ایمیل خود را وارد کنید تا لینک بازنشانی ارسال شود{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_body %}
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<i class="bi bi-question-circle text-primary" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">
|
||||||
|
نگران نباشید! ما لینک بازنشانی رمز عبور را برای شما ارسال خواهیم کرد.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="request_form_email" class="form-label">
|
||||||
|
<i class="bi bi-envelope me-2"></i>ایمیل
|
||||||
|
</label>
|
||||||
|
{{ form_widget(requestForm.email, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'ایمیل خود را وارد کنید',
|
||||||
|
'id': 'request_form_email'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(requestForm.email) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-send me-2"></i>ارسال لینک بازنشانی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>یا</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ path('app_login') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-right me-2"></i>بازگشت به صفحه ورود
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_footer %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('app_home') }}">
|
||||||
|
<i class="bi bi-house me-1"></i>بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<a href="{{ path('app_login') }}">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-1"></i>ورود
|
||||||
|
</a>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<a href="{{ path('app_register') }}">
|
||||||
|
<i class="bi bi-person-plus me-1"></i>ثبتنام
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
90
core/templates/reset_password/reset.html.twig
Normal file
90
core/templates/reset_password/reset.html.twig
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{% extends 'auth/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}تغییر رمز عبور - سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_title %}تغییر رمز عبور{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_subtitle %}رمز عبور جدید خود را وارد کنید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_body %}
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<i class="bi bi-key text-warning" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">
|
||||||
|
رمز عبور جدید خود را انتخاب کنید. این رمز عبور جایگزین رمز عبور قبلی خواهد شد.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="change_password_form_plainPassword_first" class="form-label">
|
||||||
|
<i class="bi bi-lock me-2"></i>رمز عبور جدید
|
||||||
|
</label>
|
||||||
|
{{ form_widget(resetForm.plainPassword.first, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'رمز عبور جدید را وارد کنید',
|
||||||
|
'id': 'change_password_form_plainPassword_first'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(resetForm.plainPassword.first) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="change_password_form_plainPassword_second" class="form-label">
|
||||||
|
<i class="bi bi-lock-fill me-2"></i>تکرار رمز عبور جدید
|
||||||
|
</label>
|
||||||
|
{{ form_widget(resetForm.plainPassword.second, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'رمز عبور جدید را دوباره وارد کنید',
|
||||||
|
'id': 'change_password_form_plainPassword_second'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{{ form_errors(resetForm.plainPassword.second) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
<i class="bi bi-check-circle me-2"></i>تغییر رمز عبور
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>نکات امنیتی</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-shield-check me-2"></i>رمز عبور قوی:
|
||||||
|
</h6>
|
||||||
|
<ul class="mb-0 text-start">
|
||||||
|
<li>حداقل 8 کاراکتر</li>
|
||||||
|
<li>ترکیبی از حروف بزرگ و کوچک</li>
|
||||||
|
<li>شامل اعداد و نمادهای خاص</li>
|
||||||
|
<li>عدم استفاده از اطلاعات شخصی</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>یا</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ path('app_login') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-right me-2"></i>بازگشت به صفحه ورود
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_footer %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('app_home') }}">
|
||||||
|
<i class="bi bi-house me-1"></i>بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<a href="{{ path('app_register') }}">
|
||||||
|
<i class="bi bi-person-plus me-1"></i>ثبتنام
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
103
core/templates/security/login.html.twig
Normal file
103
core/templates/security/login.html.twig
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
{% extends 'auth/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}ورود - سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_title %}ورود به سیستم{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_subtitle %}برای دسترسی به سیستم حسابداری وارد شوید{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_body %}
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
{% if csrf_token('authenticate') %}
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputEmail" class="form-label">
|
||||||
|
<i class="bi bi-envelope me-2"></i>ایمیل
|
||||||
|
</label>
|
||||||
|
<input type="email"
|
||||||
|
value="{{ last_username }}"
|
||||||
|
name="_username"
|
||||||
|
id="inputEmail"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="email"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
placeholder="ایمیل خود را وارد کنید">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
لطفاً یک ایمیل معتبر وارد کنید
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputPassword" class="form-label">
|
||||||
|
<i class="bi bi-lock me-2"></i>رمز عبور
|
||||||
|
</label>
|
||||||
|
<input type="password"
|
||||||
|
name="_password"
|
||||||
|
id="inputPassword"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
placeholder="رمز عبور خود را وارد کنید">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
لطفاً رمز عبور خود را وارد کنید
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="remember_me" name="_remember_me">
|
||||||
|
<label class="form-check-label" for="remember_me">
|
||||||
|
<i class="bi bi-clock me-1"></i>مرا به خاطر بسپار
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>ورود
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>یا</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ path('app_forgot_password_request') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-question-circle me-2"></i>فراموشی رمز عبور
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>حساب کاربری ندارید؟</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ path('app_register') }}" class="btn btn-success">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>ثبتنام کنید
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block auth_footer %}
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ path('app_home') }}">
|
||||||
|
<i class="bi bi-house me-1"></i>بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<a href="{{ path('app_register') }}">
|
||||||
|
<i class="bi bi-person-plus me-1"></i>ثبتنام
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
7
core/templates/ui/app.html.twig
Normal file
7
core/templates/ui/app.html.twig
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}سیستم حسابداری{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div id="app" data-page="{{ page }}" data-route="{{ route }}"></div>
|
||||||
|
{% endblock %}
|
||||||
76
core/webpack.config.js
Normal file
76
core/webpack.config.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
const Encore = require('@symfony/webpack-encore');
|
||||||
|
|
||||||
|
// Manually configure the runtime environment if not already configured yet by the "encore" command.
|
||||||
|
// It's useful when you use tools that rely on webpack.config.js file.
|
||||||
|
if (!Encore.isRuntimeEnvironmentConfigured()) {
|
||||||
|
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
Encore
|
||||||
|
// directory where compiled assets will be stored
|
||||||
|
.setOutputPath('public/build/')
|
||||||
|
// public path used by the web server to access the output path
|
||||||
|
.setPublicPath('/build')
|
||||||
|
// only needed for CDN's or subdirectory deploy
|
||||||
|
//.setManifestKeyPrefix('build/')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ENTRY CONFIG
|
||||||
|
*
|
||||||
|
* Each entry will result in one JavaScript file (e.g. app.js)
|
||||||
|
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
|
||||||
|
*/
|
||||||
|
.addEntry('app', './assets/app.js')
|
||||||
|
|
||||||
|
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
|
||||||
|
.splitEntryChunks()
|
||||||
|
|
||||||
|
// will require an extra script tag for runtime.js
|
||||||
|
// but, you probably want this, unless you're building a single-page app
|
||||||
|
.enableSingleRuntimeChunk()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FEATURE CONFIG
|
||||||
|
*
|
||||||
|
* Enable & configure other features below. For a full
|
||||||
|
* list of features, see:
|
||||||
|
* https://symfony.com/doc/current/frontend.html#adding-more-features
|
||||||
|
*/
|
||||||
|
.cleanupOutputBeforeBuild()
|
||||||
|
|
||||||
|
// Displays build status system notifications to the user
|
||||||
|
// .enableBuildNotifications()
|
||||||
|
|
||||||
|
.enableSourceMaps(!Encore.isProduction())
|
||||||
|
// enables hashed filenames (e.g. app.abc123.css)
|
||||||
|
.enableVersioning(Encore.isProduction())
|
||||||
|
|
||||||
|
// configure Babel
|
||||||
|
// .configureBabel((config) => {
|
||||||
|
// config.plugins.push('@babel/a-babel-plugin');
|
||||||
|
// })
|
||||||
|
|
||||||
|
// enables and configure @babel/preset-env polyfills
|
||||||
|
.configureBabelPresetEnv((config) => {
|
||||||
|
config.useBuiltIns = 'usage';
|
||||||
|
config.corejs = '3.38';
|
||||||
|
})
|
||||||
|
|
||||||
|
// enables Sass/SCSS support
|
||||||
|
//.enableSassLoader()
|
||||||
|
|
||||||
|
// uncomment if you use TypeScript
|
||||||
|
//.enableTypeScriptLoader()
|
||||||
|
|
||||||
|
// uncomment if you use React
|
||||||
|
//.enableReactPreset()
|
||||||
|
|
||||||
|
// uncomment to get integrity="..." attributes on your script & link tags
|
||||||
|
// requires WebpackEncoreBundle 1.4 or higher
|
||||||
|
//.enableIntegrityHashes(Encore.isProduction())
|
||||||
|
|
||||||
|
// uncomment if you're having problems with a jQuery plugin
|
||||||
|
//.autoProvidejQuery()
|
||||||
|
;
|
||||||
|
|
||||||
|
module.exports = Encore.getWebpackConfig();
|
||||||
7813
frontend/package-lock.json
generated
Normal file
7813
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
frontend/package.json
Normal file
35
frontend/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Vue.js + Vuetify frontend for accounting system",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "encore dev",
|
||||||
|
"watch": "encore dev --watch",
|
||||||
|
"build": "encore production --progress",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"vue",
|
||||||
|
"vuetify",
|
||||||
|
"accounting",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
|
"@symfony/webpack-encore": "^4.7.0",
|
||||||
|
"vue": "^3.4.15",
|
||||||
|
"vue-loader": "^17.4.2",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vuetify": "^3.5.8",
|
||||||
|
"vuex": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/compiler-sfc": "^3.4.15",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-loader": "^14.1.0",
|
||||||
|
"webpack-notifier": "^1.15.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
frontend/public/index.html
Normal file
12
frontend/public/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>سیستم حسابداری</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
frontend/src/main.js
Normal file
48
frontend/src/main.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
// Vuetify
|
||||||
|
import 'vuetify/styles'
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
template: `
|
||||||
|
<v-app>
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid>
|
||||||
|
<router-view></router-view>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get initial page and route from data attributes
|
||||||
|
const appElement = document.getElementById('app')
|
||||||
|
const initialPage = appElement ? appElement.dataset.page : 'dashboard'
|
||||||
|
const initialRoute = appElement ? appElement.dataset.route : ''
|
||||||
|
|
||||||
|
// Set initial route based on page and route
|
||||||
|
if (initialPage && initialPage !== 'dashboard') {
|
||||||
|
// Use router.push for programmatic navigation
|
||||||
|
router.push(`/${initialPage}`)
|
||||||
|
} else if (initialRoute && initialRoute !== 'dashboard') {
|
||||||
|
// Handle nested routes
|
||||||
|
router.push(`/${initialRoute}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(vuetify)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
76
frontend/src/router/index.js
Normal file
76
frontend/src/router/index.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
// Placeholder component for now
|
||||||
|
const PlaceholderComponent = {
|
||||||
|
template: `
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>{{ $route.name }}</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p>این صفحه در حال توسعه است.</p>
|
||||||
|
<p>Route: {{ $route.path }}</p>
|
||||||
|
<p>Page: {{ $route.meta.page }}</p>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
meta: { page: 'dashboard' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'DashboardAlt',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
meta: { page: 'dashboard' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/accounts',
|
||||||
|
name: 'Accounts',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
meta: { page: 'accounts' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions',
|
||||||
|
name: 'Transactions',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
meta: { page: 'transactions' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reports',
|
||||||
|
name: 'Reports',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
meta: { page: 'reports' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
meta: { page: 'settings' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory('/ui/'),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigation guard to handle page changes
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// Update current page in meta
|
||||||
|
if (to.meta.page) {
|
||||||
|
console.log('Navigating to:', to.meta.page)
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
15
frontend/webpack.config.js
Normal file
15
frontend/webpack.config.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const Encore = require('@symfony/webpack-encore');
|
||||||
|
|
||||||
|
Encore
|
||||||
|
.setOutputPath('../public_html/build/')
|
||||||
|
.setPublicPath('/build/')
|
||||||
|
.addEntry('app', './src/main.js')
|
||||||
|
.enableVueLoader()
|
||||||
|
.enableSassLoader()
|
||||||
|
.enableVersioning()
|
||||||
|
.disableSingleRuntimeChunk()
|
||||||
|
.splitEntryChunks()
|
||||||
|
.enableSourceMaps(!Encore.isProduction())
|
||||||
|
.enableBuildNotifications();
|
||||||
|
|
||||||
|
module.exports = Encore.getWebpackConfig();
|
||||||
37
public_html/.htaccess
Normal file
37
public_html/.htaccess
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Use the front controller as index file
|
||||||
|
DirectoryIndex index.php
|
||||||
|
|
||||||
|
# Disabling MultiViews prevents unwanted negotiation
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Determine the RewriteBase automatically and set it as environment variable
|
||||||
|
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||||
|
RewriteRule .* - [E=BASE:%1]
|
||||||
|
|
||||||
|
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||||
|
RewriteCond %{HTTP:Authorization} .+
|
||||||
|
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||||
|
|
||||||
|
# Redirect to URI without front controller to prevent duplicate content
|
||||||
|
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||||
|
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=308,L]
|
||||||
|
|
||||||
|
# Handle UI routes - let Symfony handle /ui/* routes
|
||||||
|
RewriteCond %{REQUEST_URI} ^/ui
|
||||||
|
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||||
|
|
||||||
|
# If the requested filename exists, simply serve it
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule !mod_rewrite.c>
|
||||||
|
<IfModule mod_alias.c>
|
||||||
|
RedirectMatch 307 ^/$ /index.php/
|
||||||
|
</IfModule>
|
||||||
|
</IfModule>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use App\Kernel;
|
use App\Kernel;
|
||||||
|
|
||||||
require_once dirname(__DIR__,2).'/vendor/autoload_runtime.php';
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
return function (array $context) {
|
return function (array $context) {
|
||||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue