diff --git a/.env b/.env index 42ad71d..f2ba6d2 100644 --- a/.env +++ b/.env @@ -24,9 +24,9 @@ APP_SECRET= # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml # # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" -# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" +DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" -DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### ###> symfony/messenger ### @@ -39,3 +39,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### MAILER_DSN=null://null ###< symfony/mailer ### + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' +###< nelmio/cors-bundle ### diff --git a/.env.local.php b/.env.local.php index c8c314e..d91628d 100644 --- a/.env.local.php +++ b/.env.local.php @@ -6,7 +6,8 @@ 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', + 'DATABASE_URL' => 'mysql://root:136431@127.0.0.1:3306/hsx?serverVersion=8.0.32&charset=utf8mb4', 'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0', 'MAILER_DSN' => 'null://null', + 'CORS_ALLOW_ORIGIN' => '^https?://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?$', ); diff --git a/README.md b/README.md index 172be4f..62df869 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ │ │ ├── views/ # صفحات اصلی │ │ ├── router/ # Vue Router │ │ ├── store/ # Vuex store -│ │ └── assets/ # فایل‌های استاتیک +│ │ ├── assets/ # فایل‌های استاتیک +│ │ │ └── styles/ # فایل‌های CSS +│ │ └── i18n/ # بین‌المللی‌سازی │ ├── public/ │ ├── package.json │ └── webpack.config.js @@ -87,10 +89,33 @@ npm run build # Build production ## 🎨 UI/UX Features - **RTL Support**: پشتیبانی کامل از راست به چپ -- **Persian Font**: فونت Vazirmatn +- **Persian Font**: فونت Vazir و Tahoma - **Material Design**: کامپوننت‌های زیبا - **Responsive**: سازگار با همه دستگاه‌ها - **Dark/Light Theme**: تم‌های مختلف +- **Multilingual**: پشتیبانی از فارسی و انگلیسی + +## 🌍 پشتیبانی از RTL (راست‌چین) + +### ویژگی‌های RTL +- **تغییر خودکار جهت**: صفحه به صورت خودکار راست‌چین/چپ‌چین می‌شود +- **فونت‌های مناسب**: Vazir برای فارسی، Roboto برای انگلیسی +- **کامپوننت‌های Vuetify**: تمام کامپوننت‌ها از RTL پشتیبانی می‌کنند +- **Responsive RTL**: سازگار با تمام اندازه‌های صفحه + +### فایل‌های RTL +- `src/assets/styles/rtl-ltr.css` - استایل‌های پایه RTL/LTR +- `src/assets/styles/vuetify-rtl.css` - پشتیبانی RTL برای Vuetify +- `src/assets/styles/components-rtl.css` - RTL برای کامپوننت‌های خاص +- `src/components/RTLTest.vue` - کامپوننت تست RTL + +### نحوه استفاده +```javascript +// تغییر زبان و جهت +import { changeLocale } from './i18n' +changeLocale('fa') // فارسی - راست‌چین +changeLocale('en') // انگلیسی - چپ‌چین +``` ## 📊 کامپوننت‌های Vuetify @@ -129,7 +154,8 @@ npm run build - Build process کاملاً مستقل است - Symfony به عنوان API backend عمل می‌کند - Vue Router برای client-side routing استفاده می‌شود -- فونت فارسی Vazirmatn برای RTL استفاده شده +- فونت فارسی Vazir برای RTL استفاده شده +- پشتیبانی کامل از RTL در تمام کامپوننت‌ها ## 🐛 عیب‌یابی @@ -137,14 +163,22 @@ npm run build 1. **Node modules**: `rm -rf node_modules && npm install` 2. **Build errors**: بررسی webpack.config.js 3. **Routing issues**: بررسی .htaccess +4. **RTL issues**: بررسی فایل‌های CSS و i18n ### Logs - Symfony: `core/var/log/` - Webpack: `frontend/` console +## 📚 مستندات اضافی + +- `frontend/RTL_SETUP.md` - راهنمای کامل RTL +- `frontend/MULTILINGUAL_SETUP.md` - راهنمای چندزبانه +- `frontend/THEME_SETUP.md` - راهنمای تم‌ها + ## 📞 پشتیبانی برای سوالات و مشکلات: - بررسی documentation - بررسی console errors - بررسی network tab +- تست کامپوننت RTLTest diff --git a/core/composer.json b/core/composer.json index bde38e4..a9d3e16 100644 --- a/core/composer.json +++ b/core/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.15", "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.5", + "nelmio/cors-bundle": "^2.5", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.3", "symfony/apache-pack": "*", diff --git a/core/composer.lock b/core/composer.lock index 2db0a93..b683e48 100644 --- a/core/composer.lock +++ b/core/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0c02e701f0667561c6de51c5995abb1f", + "content-hash": "42015cabfb93805b806ccf473df84f01", "packages": [ { "name": "doctrine/collections", @@ -1300,6 +1300,68 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "nelmio/cors-bundle", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0" + }, + "time": "2024-06-24T21:25:28+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", diff --git a/core/config/bundles.php b/core/config/bundles.php index 3325715..57b43cb 100644 --- a/core/config/bundles.php +++ b/core/config/bundles.php @@ -12,4 +12,5 @@ return [ Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], ]; diff --git a/core/config/packages/nelmio_cors.yaml b/core/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..92c5b67 --- /dev/null +++ b/core/config/packages/nelmio_cors.yaml @@ -0,0 +1,13 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_headers: ['Content-Type', 'Authorization'] + expose_headers: ['Link'] + max_age: 3600 + paths: + '^/api/': + allow_origin: ['*'] + allow_headers: ['*'] + allow_methods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS'] diff --git a/core/config/packages/translation.yaml b/core/config/packages/translation.yaml index 490bfc2..43fe1da 100644 --- a/core/config/packages/translation.yaml +++ b/core/config/packages/translation.yaml @@ -1,5 +1,11 @@ framework: - default_locale: en + default_locale: fa translator: default_path: '%kernel.project_dir%/translations' + fallbacks: + - fa providers: + app: + dsn: 'app://translations' + set_content_language_from_locale: true + set_locale_from_accept_language: true diff --git a/core/config/packages/twig.yaml b/core/config/packages/twig.yaml index 3f795d9..a9a3559 100644 --- a/core/config/packages/twig.yaml +++ b/core/config/packages/twig.yaml @@ -1,6 +1,5 @@ twig: file_name_pattern: '*.twig' - when@test: twig: strict_variables: true diff --git a/core/config/services.yaml b/core/config/services.yaml index 6bbad87..2cfa29e 100644 --- a/core/config/services.yaml +++ b/core/config/services.yaml @@ -16,5 +16,22 @@ services: App\: resource: '../src/' + # Locale Listener + App\EventListener\LocaleListener: + arguments: + $requestStack: '@request_stack' + $localeSwitcher: '@translation.locale_switcher' + tags: + - { name: kernel.event_listener, event: kernel.request, priority: 17 } + + # Event Listeners + App\EventListener\Auth\LoginAuditListener: + tags: + - { name: kernel.event_listener, event: App\Event\Auth\UserLoginEvent, method: onEvent, priority: 100 } + + App\EventListener\Auth\UserActivityListener: + tags: + - { name: kernel.event_listener, event: App\Event\Auth\UserLoginEvent, method: onEvent, priority: 200 } + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/core/migrations/Version20241201000001.php b/core/migrations/Version20241201000001.php new file mode 100644 index 0000000..7e35798 --- /dev/null +++ b/core/migrations/Version20241201000001.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE `user` ADD last_login_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE `user` ADD login_count INT NOT NULL DEFAULT 0'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `user` DROP last_login_at'); + $this->addSql('ALTER TABLE `user` DROP login_count'); + } +} diff --git a/core/src/Controller/Api/AuthController.php b/core/src/Controller/Api/AuthController.php new file mode 100644 index 0000000..1ecf51e --- /dev/null +++ b/core/src/Controller/Api/AuthController.php @@ -0,0 +1,282 @@ +getContent(), true); + + if (!$data || !isset($data['email']) || !isset($data['password'])) { + return $this->json([ + 'success' => false, + 'message' => 'ایمیل و رمز عبور الزامی است' + ], Response::HTTP_BAD_REQUEST); + } + + $user = $this->userRepository->findByEmail($data['email']); + + if (!$user || !$this->userPasswordHasher->isPasswordValid($user, $data['password'])) { + return $this->json([ + 'success' => false, + 'message' => 'ایمیل یا رمز عبور اشتباه است' + ], Response::HTTP_UNAUTHORIZED); + } + + try { + // ایجاد رویداد ورود کاربر + $loginEvent = new UserLoginEvent( + $user, + $request->getClientIp() ?? 'unknown', + $request->headers->get('User-Agent') ?? 'unknown' + ); + + // اجرای عملیات با رویدادها + $this->eventTransactionManager->executeWithEvents( + function () { + // عملیات اصلی (فعلاً خالی) + return true; + }, + [$loginEvent] + ); + + return $this->json([ + 'success' => true, + 'message' => 'ورود موفقیت‌آمیز', + 'user' => [ + 'id' => $user->getId(), + 'email' => $user->getEmail(), + 'fullName' => $user->getFullName(), + 'mobile' => $user->getMobile(), + 'roles' => $user->getRoles(), + 'lastLoginAt' => $user->getLastLoginAt()?->format('Y-m-d H:i:s'), + 'loginCount' => $user->getLoginCount() + ] + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در ورود: ' . $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + #[Route('/register', name: 'register', methods: ['POST'])] + public function register(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + if (!$data || !isset($data['email']) || !isset($data['password']) || !isset($data['fullName']) || !isset($data['mobile'])) { + return $this->json([ + 'success' => false, + 'message' => 'تمام فیلدها الزامی است' + ], Response::HTTP_BAD_REQUEST); + } + + // بررسی تکراری نبودن ایمیل و موبایل + $existingUser = $this->userRepository->findByEmail($data['email']); + if ($existingUser) { + return $this->json([ + 'success' => false, + 'message' => 'این ایمیل قبلاً ثبت شده است' + ], Response::HTTP_CONFLICT); + } + + $existingUser = $this->userRepository->findByMobile($data['mobile']); + if ($existingUser) { + return $this->json([ + 'success' => false, + 'message' => 'این شماره موبایل قبلاً ثبت شده است' + ], Response::HTTP_CONFLICT); + } + + $user = new User(); + $user->setEmail($data['email']); + $user->setMobile($data['mobile']); + $user->setFullName($data['fullName']); + $user->setPassword($this->userPasswordHasher->hashPassword($user, $data['password'])); + $user->setRoles(['ROLE_USER']); + $user->setIsVerified(true); + + // اعتبارسنجی entity + $errors = $this->validator->validate($user); + if (count($errors) > 0) { + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = $error->getMessage(); + } + return $this->json([ + 'success' => false, + 'message' => 'خطا در اعتبارسنجی داده‌ها', + 'errors' => $errorMessages + ], Response::HTTP_BAD_REQUEST); + } + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $this->json([ + 'success' => true, + 'message' => 'ثبت‌نام با موفقیت انجام شد', + 'user' => [ + 'id' => $user->getId(), + 'email' => $user->getEmail(), + 'fullName' => $user->getFullName(), + 'mobile' => $user->getMobile() + ] + ], Response::HTTP_CREATED); + } + + #[Route('/forgot-password', name: 'forgot_password', methods: ['POST'])] + public function forgotPassword(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + if (!$data || !isset($data['email'])) { + return $this->json([ + 'success' => false, + 'message' => 'ایمیل الزامی است' + ], Response::HTTP_BAD_REQUEST); + } + + $user = $this->userRepository->findByEmail($data['email']); + + if (!$user) { + // برای امنیت، همیشه پیام موفقیت نمایش می‌دهیم + return $this->json([ + 'success' => true, + 'message' => 'اگر ایمیل در سیستم موجود باشد، لینک بازنشانی ارسال خواهد شد' + ]); + } + + try { + $resetToken = $this->tokenGenerator->generateToken(); + $user->setResetToken($resetToken); + $user->setResetTokenExpiresAt(new \DateTimeImmutable('+1 hour')); + + $this->entityManager->flush(); + + // ارسال ایمیل (در حالت واقعی باید template مناسب ایجاد شود) + $email = (new TemplatedEmail()) + ->from('noreply@hesabix.ir') + ->to($user->getEmail()) + ->subject('درخواست بازنشانی رمز عبور') + ->htmlTemplate('emails/reset_password.html.twig') + ->context([ + 'resetToken' => $resetToken, + 'user' => $user, + ]) + ; + + $this->mailer->send($email); + + return $this->json([ + 'success' => true, + 'message' => 'لینک بازنشانی رمز عبور به ایمیل شما ارسال شد' + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در ارسال ایمیل بازنشانی' + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + #[Route('/reset-password', name: 'reset_password', methods: ['POST'])] + public function resetPassword(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + if (!$data || !isset($data['token']) || !isset($data['password'])) { + return $this->json([ + 'success' => false, + 'message' => 'توکن و رمز عبور جدید الزامی است' + ], Response::HTTP_BAD_REQUEST); + } + + $user = $this->userRepository->findByResetToken($data['token']); + + if (!$user || $user->isResetTokenExpired()) { + return $this->json([ + 'success' => false, + 'message' => 'توکن بازنشانی نامعتبر یا منقضی شده است' + ], Response::HTTP_BAD_REQUEST); + } + + $user->setPassword($this->userPasswordHasher->hashPassword($user, $data['password'])); + $user->setResetToken(null); + $user->setResetTokenExpiresAt(null); + + $this->entityManager->flush(); + + return $this->json([ + 'success' => true, + 'message' => 'رمز عبور شما با موفقیت تغییر یافت' + ]); + } + + #[Route('/logout', name: 'logout', methods: ['POST'])] + public function logout(): JsonResponse + { + // در اینجا می‌توانید JWT token را invalid کنید یا session را پاک کنید + return $this->json([ + 'success' => true, + 'message' => 'خروج موفقیت‌آمیز' + ]); + } + + #[Route('/me', name: 'me', methods: ['GET'])] + #[IsGranted('ROLE_USER')] + public function me(): JsonResponse + { + $user = $this->getUser(); + + return $this->json([ + 'success' => true, + 'user' => [ + 'id' => $user->getId(), + 'email' => $user->getEmail(), + 'fullName' => $user->getFullName(), + 'mobile' => $user->getMobile(), + 'roles' => $user->getRoles(), + 'isVerified' => $user->isVerified(), + 'createdAt' => $user->getCreatedAt()->format('Y-m-d H:i:s') + ] + ]); + } +} diff --git a/core/src/Controller/Api/LocaleApiController.php b/core/src/Controller/Api/LocaleApiController.php new file mode 100644 index 0000000..f4aae1c --- /dev/null +++ b/core/src/Controller/Api/LocaleApiController.php @@ -0,0 +1,48 @@ +get('_locale', 'fa'); + + return $this->json([ + 'locale' => $locale, + 'direction' => $locale === 'fa' ? 'rtl' : 'ltr', + 'language' => $locale === 'fa' ? 'فارسی' : 'English' + ]); + } + + #[Route('/change/{locale}', name: 'api_locale_change', methods: ['POST'], requirements: ['locale' => 'fa|en'])] + public function changeLocale( + string $locale, + Request $request, + SessionInterface $session, + LocaleSwitcher $localeSwitcher + ): JsonResponse { + // Set locale in session + $session->set('_locale', $locale); + + // Set locale for current request + $localeSwitcher->setLocale($locale); + + return $this->json([ + 'success' => true, + 'locale' => $locale, + 'direction' => $locale === 'fa' ? 'rtl' : 'ltr', + 'language' => $locale === 'fa' ? 'فارسی' : 'English', + 'message' => 'زبان با موفقیت تغییر کرد' + ]); + } +} diff --git a/core/src/Controller/Api/TestEventController.php b/core/src/Controller/Api/TestEventController.php new file mode 100644 index 0000000..367da9e --- /dev/null +++ b/core/src/Controller/Api/TestEventController.php @@ -0,0 +1,64 @@ +setEmail('test@example.com'); + $user->setFullName('کاربر تست'); + $user->setMobile('09123456789'); + $user->setPassword('password'); + $user->setRoles(['ROLE_USER']); + + // ایجاد رویداد ورود + $loginEvent = new UserLoginEvent( + $user, + '127.0.0.1', + 'Test Browser' + ); + + // اجرای عملیات با رویدادها + $result = $this->eventTransactionManager->executeWithEvents( + function () { + // عملیات اصلی (فعلاً خالی) + return 'عملیات اصلی با موفقیت انجام شد'; + }, + [$loginEvent] + ); + + return $this->json([ + 'success' => true, + 'message' => 'سیستم رویدادها با موفقیت کار کرد', + 'result' => $result, + 'eventData' => $loginEvent->getData(), + 'eventSuccess' => $loginEvent->isSuccessful(), + 'eventErrors' => $loginEvent->getErrors() + ]); + + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'message' => 'خطا در تست سیستم رویدادها: ' . $e->getMessage() + ], 500); + } + } +} diff --git a/core/src/Controller/Auth/RegistrationController.php b/core/src/Controller/Auth/RegistrationController.php deleted file mode 100644 index f39f09b..0000000 --- a/core/src/Controller/Auth/RegistrationController.php +++ /dev/null @@ -1,78 +0,0 @@ -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('auth/registration/register.html.twig', [ - 'registrationForm' => $form->createView(), - ]); - } - - $existingUser = $userRepository->findByMobile($user->getMobile()); - if ($existingUser) { - $this->addFlash('error', 'این شماره موبایل قبلاً ثبت شده است'); - return $this->render('auth/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('auth/registration/register.html.twig', [ - 'registrationForm' => $form->createView(), - ]); - } -} diff --git a/core/src/Controller/Auth/ResetPasswordController.php b/core/src/Controller/Auth/ResetPasswordController.php deleted file mode 100644 index cfa8214..0000000 --- a/core/src/Controller/Auth/ResetPasswordController.php +++ /dev/null @@ -1,128 +0,0 @@ -createForm(ResetPasswordRequestFormType::class); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - return $this->processSendingPasswordResetEmail( - $form->get('email')->getData() - ); - } - - return $this->render('auth/reset_password/request.html.twig', [ - 'requestForm' => $form->createView(), - ]); - } - - #[Route('/check-email', name: 'app_check_email')] - public function checkEmail(): Response - { - // این صفحه فقط برای نمایش پیام استفاده می‌شود - return $this->render('auth/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('auth/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'); - } -} diff --git a/core/src/Controller/Auth/SecurityController.php b/core/src/Controller/Auth/SecurityController.php deleted file mode 100644 index a311cb1..0000000 --- a/core/src/Controller/Auth/SecurityController.php +++ /dev/null @@ -1,36 +0,0 @@ -getUser()) { - return $this->redirectToRoute('app_home'); - } - - $error = $authenticationUtils->getLastAuthenticationError(); - $lastUsername = $authenticationUtils->getLastUsername(); - - return $this->render('auth/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 مدیریت شود'); - } -} diff --git a/core/src/Controller/LocaleController.php b/core/src/Controller/LocaleController.php new file mode 100644 index 0000000..dcd8620 --- /dev/null +++ b/core/src/Controller/LocaleController.php @@ -0,0 +1,46 @@ + 'fa|en'])] + public function switchLocale( + string $locale, + Request $request, + SessionInterface $session, + LocaleSwitcher $localeSwitcher + ): Response { + // Set locale in session + $session->set('_locale', $locale); + + // Set locale for current request + $localeSwitcher->setLocale($locale); + + // Redirect back to previous page or home + $referer = $request->headers->get('referer'); + if ($referer) { + return $this->redirect($referer); + } + + return $this->redirectToRoute('app_home'); + } + + #[Route('/api/locale', name: 'api_locale_get', methods: ['GET'])] + public function getCurrentLocale(SessionInterface $session): Response + { + $locale = $session->get('_locale', 'fa'); + + return $this->json([ + 'locale' => $locale, + 'direction' => $locale === 'fa' ? 'rtl' : 'ltr' + ]); + } +} diff --git a/core/src/Controller/UIController.php b/core/src/Controller/UIController.php index c99d481..3a9d900 100644 --- a/core/src/Controller/UIController.php +++ b/core/src/Controller/UIController.php @@ -10,39 +10,22 @@ 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 + public function app_ui_home(string $route = ''): Response { - // Extract the main page from route - $page = $this->extractPageFromRoute($route); return $this->render('ui/app.html.twig', [ - 'page' => $page, + 'route' => $route + ]); + } + + #[Route('/auth', name: 'app_auth_home')] + #[Route('/auth/{route}', name: 'app_auth_route', requirements: ['route' => '.+'])] + public function app_auth_home(string $route = ''): Response + { + + return $this->render('ui/auth.html.twig', [ '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'; - } } diff --git a/core/src/Entity/User.php b/core/src/Entity/User.php index 4b87c4d..b54b2a0 100644 --- a/core/src/Entity/User.php +++ b/core/src/Entity/User.php @@ -64,6 +64,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $resetTokenExpiresAt = null; + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $lastLoginAt = null; + + #[ORM\Column] + private int $loginCount = 0; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -205,4 +211,26 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface } return $this->resetTokenExpiresAt < new \DateTimeImmutable(); } + + public function getLastLoginAt(): ?\DateTimeImmutable + { + return $this->lastLoginAt; + } + + public function setLastLoginAt(?\DateTimeImmutable $lastLoginAt): static + { + $this->lastLoginAt = $lastLoginAt; + return $this; + } + + public function getLoginCount(): int + { + return $this->loginCount; + } + + public function setLoginCount(int $loginCount): static + { + $this->loginCount = $loginCount; + return $this; + } } diff --git a/core/src/Event/Auth/UserLoginEvent.php b/core/src/Event/Auth/UserLoginEvent.php new file mode 100644 index 0000000..c13e29b --- /dev/null +++ b/core/src/Event/Auth/UserLoginEvent.php @@ -0,0 +1,38 @@ +setData([ + 'userId' => $user->getId(), + 'email' => $user->getEmail(), + 'ipAddress' => $ipAddress, + 'userAgent' => $userAgent, + 'timestamp' => new \DateTimeImmutable() + ]); + } + + public function getUser(): User + { + return $this->user; + } + + public function getIpAddress(): string + { + return $this->ipAddress; + } + + public function getUserAgent(): string + { + return $this->userAgent; + } +} diff --git a/core/src/Event/BaseEvent.php b/core/src/Event/BaseEvent.php new file mode 100644 index 0000000..e5241e2 --- /dev/null +++ b/core/src/Event/BaseEvent.php @@ -0,0 +1,64 @@ +success = false; + $this->errors[] = $error; + } + + public function markAsSuccessful(): void + { + $this->success = true; + } + + public function isSuccessful(): bool + { + return $this->success; + } + + public function canRollback(): bool + { + return $this->canRollback; + } + + public function setCanRollback(bool $canRollback): void + { + $this->canRollback = $canRollback; + } + + public function getErrors(): array + { + return $this->errors; + } + + public function hasErrors(): bool + { + return count($this->errors) > 0; + } + + public function setData(array $data): void + { + $this->data = $data; + } + + public function getData(): array + { + return $this->data; + } + + public function addData(string $key, mixed $value): void + { + $this->data[$key] = $value; + } +} diff --git a/core/src/EventListener/Auth/LoginAuditListener.php b/core/src/EventListener/Auth/LoginAuditListener.php new file mode 100644 index 0000000..01de89c --- /dev/null +++ b/core/src/EventListener/Auth/LoginAuditListener.php @@ -0,0 +1,55 @@ +getUser(); + $data = $event->getData(); + + $this->logger->info('User login attempt', [ + 'userId' => $user->getId(), + 'email' => $user->getEmail(), + 'ipAddress' => $data['ipAddress'], + 'userAgent' => $data['userAgent'], + 'timestamp' => $data['timestamp']->format('Y-m-d H:i:s') + ]); + + $event->markAsSuccessful(); + } catch (\Exception $e) { + $event->markAsFailed('خطا در ثبت لاگ ورود: ' . $e->getMessage()); + } + } + + public function rollback(BaseEvent $event): void + { + if (!$event instanceof UserLoginEvent) { + return; + } + + // برای لاگ نیازی به rollback نیست + $this->logger->warning('Login audit rollback - no action needed'); + } + + public function getPriority(): int + { + return 100; // اولویت بالا + } +} diff --git a/core/src/EventListener/Auth/UserActivityListener.php b/core/src/EventListener/Auth/UserActivityListener.php new file mode 100644 index 0000000..938ad64 --- /dev/null +++ b/core/src/EventListener/Auth/UserActivityListener.php @@ -0,0 +1,71 @@ +getUser(); + + // ذخیره زمان ورود قبلی برای rollback + $this->previousLastLoginAt = $user->getLastLoginAt(); + + // به‌روزرسانی زمان آخرین ورود + $user->setLastLoginAt(new \DateTimeImmutable()); + $user->setLoginCount($user->getLoginCount() + 1); + + $this->entityManager->flush(); + + $event->markAsSuccessful(); + } catch (\Exception $e) { + $event->markAsFailed('خطا در به‌روزرسانی فعالیت کاربر: ' . $e->getMessage()); + } + } + + public function rollback(BaseEvent $event): void + { + if (!$event instanceof UserLoginEvent) { + return; + } + + try { + $user = $event->getUser(); + + // بازگرداندن مقادیر قبلی + if ($this->previousLastLoginAt) { + $user->setLastLoginAt($this->previousLastLoginAt); + } + $user->setLoginCount($user->getLoginCount() - 1); + + $this->entityManager->flush(); + } catch (\Exception $e) { + // در صورت خطا در rollback، لاگ می‌کنیم + // این یک وضعیت بحرانی است + } + } + + public function getPriority(): int + { + return 200; // اولویت متوسط + } +} diff --git a/core/src/EventListener/LocaleListener.php b/core/src/EventListener/LocaleListener.php new file mode 100644 index 0000000..9b86d14 --- /dev/null +++ b/core/src/EventListener/LocaleListener.php @@ -0,0 +1,40 @@ +getRequest(); + + // Skip for API routes + if (str_starts_with($request->getPathInfo(), '/api/')) { + return; + } + + // Get session from request + $session = $request->getSession(); + if (!$session) { + return; + } + + // Get locale from session or default to 'fa' + $locale = $session->get('_locale', 'fa'); + + // Set locale for current request + $this->localeSwitcher->setLocale($locale); + + // Set locale in request attributes + $request->setLocale($locale); + } +} diff --git a/core/src/EventListener/RollbackableEventListenerInterface.php b/core/src/EventListener/RollbackableEventListenerInterface.php new file mode 100644 index 0000000..68754c7 --- /dev/null +++ b/core/src/EventListener/RollbackableEventListenerInterface.php @@ -0,0 +1,23 @@ +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([]); - } -} diff --git a/core/src/Form/Auth/RegistrationFormType.php b/core/src/Form/Auth/RegistrationFormType.php deleted file mode 100644 index 22bcc42..0000000 --- a/core/src/Form/Auth/RegistrationFormType.php +++ /dev/null @@ -1,106 +0,0 @@ -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, - ]); - } -} diff --git a/core/src/Form/Auth/ResetPasswordRequestFormType.php b/core/src/Form/Auth/ResetPasswordRequestFormType.php deleted file mode 100644 index 97a3049..0000000 --- a/core/src/Form/Auth/ResetPasswordRequestFormType.php +++ /dev/null @@ -1,35 +0,0 @@ -add('email', EmailType::class, [ - 'label' => 'ایمیل', - 'attr' => [ - 'class' => 'form-control', - 'placeholder' => 'ایمیل خود را وارد کنید' - ], - 'constraints' => [ - new NotBlank([ - 'message' => 'لطفاً ایمیل خود را وارد کنید', - ]), - ], - ]) - ; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([]); - } -} diff --git a/core/src/Service/EventTransactionManager.php b/core/src/Service/EventTransactionManager.php new file mode 100644 index 0000000..35c6235 --- /dev/null +++ b/core/src/Service/EventTransactionManager.php @@ -0,0 +1,123 @@ +entityManager->beginTransaction(); + + try { + // اجرای عملیات اصلی + $result = $operation(); + + // انتشار رویدادها + $this->dispatchEvents($events); + + // بررسی موفقیت همه رویدادها + if ($this->areAllEventsSuccessful($events)) { + $this->entityManager->commit(); + $this->logger->info('Transaction committed successfully'); + return $result; + } else { + // rollback در صورت شکست رویدادها + $this->rollbackEvents($events); + $this->entityManager->rollback(); + $this->logger->warning('Transaction rolled back due to event failures'); + throw new \RuntimeException('عملیات به دلیل شکست در رویدادها لغو شد'); + } + + } catch (\Exception $e) { + $this->entityManager->rollback(); + $this->rollbackEvents($events); + $this->logger->error('Transaction rolled back due to exception: ' . $e->getMessage()); + throw $e; + } + } + + /** + * انتشار رویدادها + */ + private function dispatchEvents(array $events): void + { + foreach ($events as $event) { + $this->eventDispatcher->dispatch($event); + } + } + + /** + * بررسی موفقیت همه رویدادها + */ + private function areAllEventsSuccessful(array $events): bool + { + foreach ($events as $event) { + if (!$event->isSuccessful()) { + $this->logger->warning('Event failed: ' . get_class($event), [ + 'errors' => $event->getErrors() + ]); + return false; + } + } + return true; + } + + /** + * Rollback رویدادها + */ + private function rollbackEvents(array $events): void + { + foreach ($events as $event) { + if ($event->canRollback()) { + $this->rollbackEvent($event); + } + } + } + + /** + * Rollback یک رویداد خاص + */ + private function rollbackEvent(BaseEvent $event): void + { + $listeners = $this->getEventListeners($event); + + foreach ($listeners as $listener) { + if ($listener instanceof RollbackableEventListenerInterface) { + try { + $listener->rollback($event); + } catch (\Exception $e) { + $this->logger->error('Error during event rollback: ' . $e->getMessage(), [ + 'event' => get_class($event), + 'listener' => get_class($listener) + ]); + } + } + } + } + + /** + * دریافت Listener های یک رویداد + */ + private function getEventListeners(BaseEvent $event): array + { + // این متد باید بر اساس Event Dispatcher پیاده‌سازی شود + // فعلاً یک پیاده‌سازی ساده + return []; + } +} diff --git a/core/symfony.lock b/core/symfony.lock index 9c6149e..b23c310 100644 --- a/core/symfony.lock +++ b/core/symfony.lock @@ -35,6 +35,18 @@ "migrations/.gitignore" ] }, + "nelmio/cors-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, "phpunit/phpunit": { "version": "12.3", "recipe": { diff --git a/core/templates/auth/base.html.twig b/core/templates/auth/base.html.twig deleted file mode 100644 index b1515a5..0000000 --- a/core/templates/auth/base.html.twig +++ /dev/null @@ -1,514 +0,0 @@ - - - - - - {% block title %}سیستم احراز هویت - حسابی‌کس{% endblock %} - - - - - - - - - - - - {% block stylesheets %}{% endblock %} - - -
-
- -
-

- - {% block auth_title %}احراز هویت{% endblock %} -

-

{% block auth_subtitle %}به سیستم حسابی‌کس خوش آمدید{% endblock %}

-
- - -
- - {% for message in app.flashes('success') %} - - {% endfor %} - - {% for message in app.flashes('error') %} - - {% endfor %} - - {% for message in app.flashes('info') %} - - {% endfor %} - - {% for message in app.flashes('warning') %} - - {% endfor %} - - - {% block auth_body %}{% endblock %} -
- - - -
-
- - - - - - - - {% block javascripts %}{% endblock %} - - diff --git a/core/templates/auth/registration/register.html.twig b/core/templates/auth/registration/register.html.twig deleted file mode 100644 index bcd05a9..0000000 --- a/core/templates/auth/registration/register.html.twig +++ /dev/null @@ -1,109 +0,0 @@ -{% extends 'auth/base.html.twig' %} - -{% block title %}ثبت‌نام - سیستم حسابداری{% endblock %} - -{% block auth_title %}ثبت‌نام در سیستم{% endblock %} - -{% block auth_subtitle %}حساب کاربری جدید ایجاد کنید{% endblock %} - -{% block auth_body %} -
-
- - {{ form_widget(registrationForm.fullName, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'نام و نام خانوادگی خود را وارد کنید', - 'id': 'registration_form_fullName' - } - }) }} - {{ form_errors(registrationForm.fullName) }} -
- -
- - {{ form_widget(registrationForm.email, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'example@email.com', - 'id': 'registration_form_email' - } - }) }} - {{ form_errors(registrationForm.email) }} -
- -
- - {{ form_widget(registrationForm.mobile, { - 'attr': { - 'class': 'form-control', - 'placeholder': '09123456789', - 'id': 'registration_form_mobile' - } - }) }} - {{ form_errors(registrationForm.mobile) }} -
- -
- - {{ form_widget(registrationForm.plainPassword.first, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'رمز عبور خود را وارد کنید', - 'id': 'registration_form_plainPassword_first' - } - }) }} - {{ form_errors(registrationForm.plainPassword.first) }} -
- -
- - {{ form_widget(registrationForm.plainPassword.second, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'رمز عبور را دوباره وارد کنید', - 'id': 'registration_form_plainPassword_second' - } - }) }} - {{ form_errors(registrationForm.plainPassword.second) }} -
- -
- -
-
- -
- قبلاً ثبت‌نام کرده‌اید؟ -
- -
- - وارد شوید - -
-{% endblock %} - -{% block auth_footer %} - -{% endblock %} diff --git a/core/templates/auth/reset_password/check_email.html.twig b/core/templates/auth/reset_password/check_email.html.twig deleted file mode 100644 index 1db450e..0000000 --- a/core/templates/auth/reset_password/check_email.html.twig +++ /dev/null @@ -1,61 +0,0 @@ -{% extends 'auth/base.html.twig' %} - -{% block title %}بررسی ایمیل - سیستم حسابداری{% endblock %} - -{% block auth_title %}بررسی ایمیل{% endblock %} - -{% block auth_subtitle %}لینک بازنشانی رمز عبور ارسال شد{% endblock %} - -{% block auth_body %} -
-
- -
- -
-
- ایمیل ارسال شد! -
-

- اگر ایمیلی با آدرس وارد شده در سیستم ثبت شده باشد، - لینک بازنشانی رمز عبور برای شما ارسال شده است. -

-
- -
-
- مهم! -
- -
-
- -
- مراحل بعدی -
- -
- - بازگشت به صفحه ورود - - - تلاش مجدد - -
-{% endblock %} - -{% block auth_footer %} - -{% endblock %} diff --git a/core/templates/auth/reset_password/request.html.twig b/core/templates/auth/reset_password/request.html.twig deleted file mode 100644 index 1f9d178..0000000 --- a/core/templates/auth/reset_password/request.html.twig +++ /dev/null @@ -1,64 +0,0 @@ -{% extends 'auth/base.html.twig' %} - -{% block title %}فراموشی رمز عبور - سیستم حسابداری{% endblock %} - -{% block auth_title %}فراموشی رمز عبور{% endblock %} - -{% block auth_subtitle %}ایمیل خود را وارد کنید تا لینک بازنشانی ارسال شود{% endblock %} - -{% block auth_body %} -
- -

- نگران نباشید! ما لینک بازنشانی رمز عبور را برای شما ارسال خواهیم کرد. -

-
- -
-
- - {{ form_widget(requestForm.email, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'ایمیل خود را وارد کنید', - 'id': 'request_form_email' - } - }) }} - {{ form_errors(requestForm.email) }} -
- -
- -
-
- -
- یا -
- -
- - بازگشت به صفحه ورود - -
-{% endblock %} - -{% block auth_footer %} - -{% endblock %} diff --git a/core/templates/auth/reset_password/reset.html.twig b/core/templates/auth/reset_password/reset.html.twig deleted file mode 100644 index a80edc0..0000000 --- a/core/templates/auth/reset_password/reset.html.twig +++ /dev/null @@ -1,90 +0,0 @@ -{% extends 'auth/base.html.twig' %} - -{% block title %}تغییر رمز عبور - سیستم حسابداری{% endblock %} - -{% block auth_title %}تغییر رمز عبور{% endblock %} - -{% block auth_subtitle %}رمز عبور جدید خود را وارد کنید{% endblock %} - -{% block auth_body %} -
- -

- رمز عبور جدید خود را انتخاب کنید. این رمز عبور جایگزین رمز عبور قبلی خواهد شد. -

-
- -
-
- - {{ form_widget(resetForm.plainPassword.first, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'رمز عبور جدید را وارد کنید', - 'id': 'change_password_form_plainPassword_first' - } - }) }} - {{ form_errors(resetForm.plainPassword.first) }} -
- -
- - {{ form_widget(resetForm.plainPassword.second, { - 'attr': { - 'class': 'form-control', - 'placeholder': 'رمز عبور جدید را دوباره وارد کنید', - 'id': 'change_password_form_plainPassword_second' - } - }) }} - {{ form_errors(resetForm.plainPassword.second) }} -
- -
- -
-
- -
- نکات امنیتی -
- -
-
- رمز عبور قوی: -
- -
- -
- یا -
- -
- - بازگشت به صفحه ورود - -
-{% endblock %} - -{% block auth_footer %} - -{% endblock %} diff --git a/core/templates/auth/security/login.html.twig b/core/templates/auth/security/login.html.twig deleted file mode 100644 index 30a812e..0000000 --- a/core/templates/auth/security/login.html.twig +++ /dev/null @@ -1,103 +0,0 @@ -{% extends 'auth/base.html.twig' %} - -{% block title %}ورود - سیستم حسابداری{% endblock %} - -{% block auth_title %}ورود به سیستم{% endblock %} - -{% block auth_subtitle %}برای دسترسی به سیستم حسابداری وارد شوید{% endblock %} - -{% block auth_body %} - {% if error %} -
- - {{ error.messageKey|trans(error.messageData, 'security') }} -
- {% endif %} - -
- {% if csrf_token('authenticate') %} - - {% endif %} - -
- - -
- لطفاً یک ایمیل معتبر وارد کنید -
-
- -
- - -
- لطفاً رمز عبور خود را وارد کنید -
-
- -
-
- - -
-
- -
- -
-
- -
- یا -
- -
- - فراموشی رمز عبور - -
- -
- حساب کاربری ندارید؟ -
- -
- - ثبت‌نام کنید - -
-{% endblock %} - -{% block auth_footer %} - -{% endblock %} diff --git a/core/templates/base.html.twig b/core/templates/base.html.twig index 0abe3d6..05a85e5 100644 --- a/core/templates/base.html.twig +++ b/core/templates/base.html.twig @@ -1,17 +1,17 @@ - + - {% block title %}سیستم حسابداری{% endblock %} - + {% block title %}{{ 'common.brand_name'|trans }}{% endblock %} + {% block stylesheets %} - {{ encore_entry_link_tags('app') }} + {{ encore_entry_link_tags('main') }} {% endblock %} {% block javascripts %} - {{ encore_entry_script_tags('app') }} + {{ encore_entry_script_tags('main') }} {% endblock %} diff --git a/core/templates/auth/reset_password/email.html.twig b/core/templates/emails/reset_password.html.twig similarity index 66% rename from core/templates/auth/reset_password/email.html.twig rename to core/templates/emails/reset_password.html.twig index c74608b..6fbd650 100644 --- a/core/templates/auth/reset_password/email.html.twig +++ b/core/templates/emails/reset_password.html.twig @@ -1,35 +1,36 @@ - + بازنشانی رمز عبور diff --git a/frontend/src/components/ThemeSwitcher.vue b/frontend/src/components/ThemeSwitcher.vue new file mode 100644 index 0000000..bc9a409 --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js new file mode 100644 index 0000000..9518ccd --- /dev/null +++ b/frontend/src/i18n/index.js @@ -0,0 +1,47 @@ +import { createI18n } from 'vue-i18n' +import fa from '../locales/fa' +import en from '../locales/en' + +const i18n = createI18n({ + legacy: false, + locale: 'fa', + fallbackLocale: 'fa', + messages: { + fa, + en + }, + allowComposition: true, + useScope: 'global' +}) + +export const changeLocale = (locale) => { + if (['fa', 'en'].includes(locale)) { + i18n.global.locale.value = locale + localStorage.setItem('locale', locale) + + const isRTL = locale === 'fa' + + // Set document direction and language + document.documentElement.dir = isRTL ? 'rtl' : 'ltr' + document.documentElement.lang = locale + document.body.style.direction = isRTL ? 'rtl' : 'ltr' + + // Apply font family + if (isRTL) { + document.body.style.fontFamily = "'Vazir', 'Tahoma', sans-serif" + } else { + document.body.style.fontFamily = "'Roboto', 'Arial', sans-serif" + } + + // Force Vuetify components to update + setTimeout(() => { + window.dispatchEvent(new Event('resize')) + }, 100) + } +} + +// Initialize +const savedLocale = localStorage.getItem('locale') || 'fa' +changeLocale(savedLocale) + +export default i18n diff --git a/frontend/src/locales/en.js b/frontend/src/locales/en.js new file mode 100644 index 0000000..5e8f43a --- /dev/null +++ b/frontend/src/locales/en.js @@ -0,0 +1,74 @@ +export default { + // Authentication + auth: { + welcome: 'Welcome', + subtitle: 'Sign in to your account to continue', + email: 'Email', + password: 'Password', + rememberMe: 'Remember me', + forgotPassword: 'Forgot password', + login: 'Login', + register: 'Register', + noAccount: 'Do not have an account', + registerHere: 'Sign up here', + or: 'or', + emailRequired: 'Email is required', + emailInvalid: 'Invalid email format', + passwordRequired: 'Password is required', + passwordMinLength: 'Password must be at least 6 characters', + placeholder: { + email: 'example at email dot com', + password: 'Enter your password' + } + }, + + // Common + common: { + brandName: 'Hesabix', + loading: 'Loading', + error: 'Error', + success: 'Success', + cancel: 'Cancel', + save: 'Save', + edit: 'Edit', + delete: 'Delete', + search: 'Search', + filter: 'Filter', + sort: 'Sort' + }, + + // Navigation + nav: { + dashboard: 'Dashboard', + accounts: 'Accounts', + transactions: 'Transactions', + reports: 'Reports', + settings: 'Settings', + profile: 'Profile', + logout: 'Logout' + }, + + // Dashboard + dashboard: { + title: 'Dashboard', + totalBalance: 'Total Balance', + monthlyIncome: 'Monthly Income', + monthlyExpense: 'Monthly Expense', + recentTransactions: 'Recent Transactions', + quickActions: 'Quick Actions' + }, + + // Language + language: { + fa: 'Farsi', + en: 'English', + changeLanguage: 'Change Language' + }, + + // Theme + theme: { + light: 'Light', + dark: 'Dark', + system: 'System' + } +} diff --git a/frontend/src/locales/fa.js b/frontend/src/locales/fa.js new file mode 100644 index 0000000..d1609fb --- /dev/null +++ b/frontend/src/locales/fa.js @@ -0,0 +1,74 @@ +export default { + // Authentication + auth: { + welcome: 'خوش آمدید', + subtitle: 'برای ادامه وارد حساب کاربری خود شوید', + email: 'ایمیل', + password: 'رمز عبور', + rememberMe: 'مرا به خاطر بسپار', + forgotPassword: 'فراموشی رمز عبور', + login: 'ورود', + register: 'ثبت نام', + noAccount: 'حساب کاربری ندارید؟', + registerHere: 'اینجا ثبت نام کنید', + or: 'یا', + emailRequired: 'ایمیل الزامی است', + emailInvalid: 'ایمیل نامعتبر است', + passwordRequired: 'رمز عبور الزامی است', + passwordMinLength: 'رمز عبور باید حداقل 6 کاراکتر باشد', + placeholder: { + email: 'example at email dot com', + password: 'رمز عبور خود را وارد کنید' + } + }, + + // Common + common: { + brandName: 'حسابی‌کس', + loading: 'در حال بارگذاری', + error: 'خطا', + success: 'موفقیت', + cancel: 'انصراف', + save: 'ذخیره', + edit: 'ویرایش', + delete: 'حذف', + search: 'جستجو', + filter: 'فیلتر', + sort: 'مرتب‌سازی' + }, + + // Navigation + nav: { + dashboard: 'داشبورد', + accounts: 'حساب‌ها', + transactions: 'تراکنش‌ها', + reports: 'گزارش‌ها', + settings: 'تنظیمات', + profile: 'پروفایل', + logout: 'خروج' + }, + + // Dashboard + dashboard: { + title: 'داشبورد', + totalBalance: 'موجودی کل', + monthlyIncome: 'درآمد ماهانه', + monthlyExpense: 'هزینه ماهانه', + recentTransactions: 'تراکنش‌های اخیر', + quickActions: 'عملیات سریع' + }, + + // Language + language: { + fa: 'فارسی', + en: 'انگلیسی', + changeLanguage: 'تغییر زبان' + }, + + // Theme + theme: { + light: 'روشن', + dark: 'تاریک', + system: 'سیستم' + } +} diff --git a/frontend/src/main.js b/frontend/src/main.js index d3d6309..9169512 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,48 +1,110 @@ import { createApp } from 'vue' import router from './router' +import i18n from './i18n' +import vuetify from './plugins/vuetify' +import './assets/styles/theme.css' +import './assets/styles/rtl-ltr.css' +import './assets/styles/vuetify-rtl.css' +import './assets/styles/components-rtl.css' -// 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: ` - - - - - - - - ` -}) - -// 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}`) +// Theme detection and management +function detectSystemTheme() { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark' + } + return 'light' } -app.use(router) -app.use(vuetify) +function initializeTheme() { + // Check for saved theme preference or default to light + const savedTheme = localStorage.getItem('vuetify-theme') + let currentTheme = savedTheme || 'light' + + if (currentTheme === 'system') { + currentTheme = detectSystemTheme() + } + + // Apply theme - Vuetify 3 syntax + vuetify.theme.global.name = currentTheme + + // Apply CSS classes for better theme support + if (currentTheme === 'dark') { + document.body.classList.add('v-theme--dark') + document.body.classList.remove('v-theme--light') + document.documentElement.setAttribute('data-theme', 'dark') + } else { + document.body.classList.add('v-theme--light') + document.body.classList.remove('v-theme--dark') + document.documentElement.setAttribute('data-theme', 'light') + } + + // Listen for system theme changes + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (localStorage.getItem('vuetify-theme') === 'system') { + const newTheme = e.matches ? 'dark' : 'light' + vuetify.theme.global.name = newTheme + + // Update CSS classes + if (newTheme === 'dark') { + document.body.classList.add('v-theme--dark') + document.body.classList.remove('v-theme--light') + document.documentElement.setAttribute('data-theme', 'dark') + } else { + document.body.classList.add('v-theme--light') + document.body.classList.remove('v-theme--dark') + document.documentElement.setAttribute('data-theme', 'light') + } + } + }) + } + + return currentTheme +} -app.mount('#app') +// Common function to create and configure app +function createAndMountApp(mountElement, initialRoute, defaultRoute) { + const app = createApp({ + template: ` + + + + + + + + ` + }) + + // Set initial route if provided + if (initialRoute && initialRoute !== '') { + router.push(`${defaultRoute}/${initialRoute}`) + } else { + router.push(defaultRoute) + } + + app.use(i18n) + app.use(router) + app.use(vuetify) + + // Initialize theme after mounting + app.mount(mountElement) + initializeTheme() +} + +// Check which section we're in +const appElement = document.getElementById('app') +const authElement = document.getElementById('auth') + +if (appElement) { + // App section + const initialRoute = appElement.dataset.route || '' + createAndMountApp('#app', initialRoute, '/ui') +} else if (authElement) { + // Auth section + const initialRoute = authElement.dataset.route || '' + createAndMountApp('#auth', initialRoute, '/auth') +} else { + // Fallback - create main app + createAndMountApp('#app', '', '/ui') +} diff --git a/frontend/src/plugins/vuetify.js b/frontend/src/plugins/vuetify.js new file mode 100644 index 0000000..7a93878 --- /dev/null +++ b/frontend/src/plugins/vuetify.js @@ -0,0 +1,63 @@ +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' + +// Theme configuration for Vuetify 3 +const vuetify = createVuetify({ + components, + directives, + theme: { + defaultTheme: 'system', + themes: { + light: { + dark: false, + colors: { + primary: '#1976d2', + secondary: '#424242', + accent: '#82b1ff', + error: '#ff5252', + info: '#2196f3', + success: '#4caf50', + warning: '#ff9800', + surface: '#ffffff', + background: '#fafafa', + 'on-surface': '#000000', + 'on-background': '#000000', + } + }, + dark: { + dark: true, + colors: { + primary: '#90caf9', + secondary: '#bdbdbd', + accent: '#82b1ff', + error: '#ff5252', + info: '#2196f3', + success: '#4caf50', + warning: '#ff9800', + surface: '#121212', + background: '#000000', + 'on-surface': '#ffffff', + 'on-background': '#ffffff', + } + } + } + }, + defaults: { + VCard: { + rounded: 'lg', + elevation: 2 + }, + VTextField: { + variant: 'outlined', + density: 'comfortable' + }, + VBtn: { + rounded: 'lg' + } + } +}) + +export default vuetify diff --git a/frontend/src/router/app.js b/frontend/src/router/app.js new file mode 100644 index 0000000..3940dab --- /dev/null +++ b/frontend/src/router/app.js @@ -0,0 +1,75 @@ +import { createRouter, createMemoryHistory } from 'vue-router' + +// Placeholder component for app pages +const AppPlaceholderComponent = { + template: ` + + + + + {{ $route.name }} + +

این صفحه در حال توسعه است.

+

Route: {{ $route.path }}

+

Page: {{ $route.meta.page }}

+
+
+
+
+
+ ` +} + +const appRoutes = [ + { + path: '/', + name: 'Dashboard', + component: AppPlaceholderComponent, + meta: { page: 'dashboard' } + }, + { + path: '/dashboard', + name: 'DashboardAlt', + component: AppPlaceholderComponent, + meta: { page: 'dashboard' } + }, + { + path: '/accounts', + name: 'Accounts', + component: AppPlaceholderComponent, + meta: { page: 'accounts' } + }, + { + path: '/transactions', + name: 'Transactions', + component: AppPlaceholderComponent, + meta: { page: 'transactions' } + }, + { + path: '/reports', + name: 'Reports', + component: AppPlaceholderComponent, + meta: { page: 'reports' } + }, + { + path: '/settings', + name: 'Settings', + component: AppPlaceholderComponent, + meta: { page: 'settings' } + }, + { + path: '/users', + name: 'Users', + component: AppPlaceholderComponent, + meta: { page: 'users' } + }, + { + path: '/profile', + name: 'Profile', + component: AppPlaceholderComponent, + meta: { page: 'profile' } + } +] + +// Export routes array instead of router instance +export default appRoutes diff --git a/frontend/src/router/auth.js b/frontend/src/router/auth.js new file mode 100644 index 0000000..5f98cee --- /dev/null +++ b/frontend/src/router/auth.js @@ -0,0 +1,68 @@ +import { createRouter, createMemoryHistory } from 'vue-router' +import LoginPage from '../views/auth/login.vue' +import RegisterPage from '../views/auth/register.vue' +import ForgotPasswordPage from '../views/auth/forgot-password.vue' + +// Placeholder component for other auth pages +const AuthPlaceholderComponent = { + template: ` + + + + + + {{ $route.name }} + + +

صفحه احراز هویت در حال توسعه است.

+

Route: {{ $route.path }}

+

Page: {{ $route.meta.page }}

+
+
+
+
+
+ ` +} + +const authRoutes = [ + { + path: '/', + name: 'Login', + component: LoginPage, + meta: { page: 'login' } + }, + { + path: '/login', + name: 'LoginAlt', + component: LoginPage, + meta: { page: 'login' } + }, + { + path: '/register', + name: 'Register', + component: RegisterPage, + meta: { page: 'register' } + }, + { + path: '/forgot-password', + name: 'ForgotPassword', + component: ForgotPasswordPage, + meta: { page: 'forgot-password' } + }, + { + path: '/reset-password', + name: 'ResetPassword', + component: AuthPlaceholderComponent, + meta: { page: 'reset-password' } + }, + { + path: '/verify-email', + name: 'VerifyEmail', + component: AuthPlaceholderComponent, + meta: { page: 'verify-email' } + } +] + +// Export routes array instead of router instance +export default authRoutes diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index f027df1..44c3462 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,76 +1,34 @@ import { createRouter, createWebHistory } from 'vue-router' +import appRoutes from './app' +import authRoutes from './auth' -// Placeholder component for now -const PlaceholderComponent = { - template: ` - - - - - {{ $route.name }} - -

این صفحه در حال توسعه است.

-

Route: {{ $route.path }}

-

Page: {{ $route.meta.page }}

-
-
-
-
-
- ` -} - -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 +// Main router that handles both app and auth sections +const mainRouter = createRouter({ + history: createWebHistory(), + routes: [ + // App routes with /ui prefix + ...appRoutes.map(route => ({ + ...route, + path: `/ui${route.path === '/' ? '' : route.path}` + })), + // Auth routes with /auth prefix + ...authRoutes.map(route => ({ + ...route, + path: `/auth${route.path === '/' ? '' : route.path}` + })), + // Default redirect + { + path: '/', + redirect: '/ui' + } + ] }) -// 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) - } +// Navigation guard for main router +mainRouter.beforeEach((to, from, next) => { + console.log('Main router navigating to:', to.path) + console.log('Route matched:', to.matched) next() }) -export default router +export default mainRouter diff --git a/frontend/src/views/auth/forgot-password.vue b/frontend/src/views/auth/forgot-password.vue new file mode 100644 index 0000000..222a5c8 --- /dev/null +++ b/frontend/src/views/auth/forgot-password.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/views/auth/login.vue b/frontend/src/views/auth/login.vue new file mode 100644 index 0000000..213ed0f --- /dev/null +++ b/frontend/src/views/auth/login.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/frontend/src/views/auth/register.vue b/frontend/src/views/auth/register.vue new file mode 100644 index 0000000..95e0db0 --- /dev/null +++ b/frontend/src/views/auth/register.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 61b1593..b95ba1b 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -1,15 +1,24 @@ const Encore = require('@symfony/webpack-encore'); +const webpack = require('webpack'); Encore .setOutputPath('../public_html/build/') .setPublicPath('/build/') - .addEntry('app', './src/main.js') + .addEntry('main', './src/main.js') .enableVueLoader() .enableSassLoader() .enableVersioning() .disableSingleRuntimeChunk() .splitEntryChunks() .enableSourceMaps(!Encore.isProduction()) - .enableBuildNotifications(); + .enableBuildNotifications() + .addStyleEntry('rtl-ltr', './src/assets/styles/rtl-ltr.css') + .addStyleEntry('vuetify-rtl', './src/assets/styles/vuetify-rtl.css') + .addStyleEntry('components-rtl', './src/assets/styles/components-rtl.css') + .addPlugin(new webpack.DefinePlugin({ + __VUE_OPTIONS_API__: JSON.stringify(true), + __VUE_PROD_DEVTOOLS__: JSON.stringify(false), + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(false) + })); module.exports = Encore.getWebpackConfig(); diff --git a/public_html/.htaccess b/public_html/.htaccess index 2f17e92..cc7d049 100644 --- a/public_html/.htaccess +++ b/public_html/.htaccess @@ -25,6 +25,14 @@ DirectoryIndex index.php RewriteCond %{REQUEST_URI} ^/ui RewriteRule ^ %{ENV:BASE}/index.php [L] + # Handle Auth routes - let Symfony handle /auth/* routes + RewriteCond %{REQUEST_URI} ^/auth + RewriteRule ^ %{ENV:BASE}/index.php [L] + + # Handle API routes - let Symfony handle /api/* routes + RewriteCond %{REQUEST_URI} ^/api + RewriteRule ^ %{ENV:BASE}/index.php [L] + # If the requested filename exists, simply serve it RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ %{ENV:BASE}/index.php [L] diff --git a/public_html/favicon.ico b/public_html/favicon.ico new file mode 100644 index 0000000..ab60ead Binary files /dev/null and b/public_html/favicon.ico differ diff --git a/public_html/img/avatar.jpg b/public_html/img/avatar.jpg new file mode 100644 index 0000000..82ff3ca Binary files /dev/null and b/public_html/img/avatar.jpg differ diff --git a/public_html/img/avatar.png b/public_html/img/avatar.png new file mode 100644 index 0000000..c3fe2cc Binary files /dev/null and b/public_html/img/avatar.png differ diff --git a/public_html/img/logo-blue.png b/public_html/img/logo-blue.png new file mode 100644 index 0000000..b628a03 Binary files /dev/null and b/public_html/img/logo-blue.png differ diff --git a/public_html/img/logo-light.png b/public_html/img/logo-light.png new file mode 100644 index 0000000..0d38d08 Binary files /dev/null and b/public_html/img/logo-light.png differ diff --git a/public_html/img/logo.png b/public_html/img/logo.png new file mode 100644 index 0000000..acbb09c Binary files /dev/null and b/public_html/img/logo.png differ diff --git a/public_html/img/logo32.png b/public_html/img/logo32.png new file mode 100644 index 0000000..d33117f Binary files /dev/null and b/public_html/img/logo32.png differ diff --git a/public_html/robots.txt b/public_html/robots.txt new file mode 100644 index 0000000..4bbcf47 --- /dev/null +++ b/public_html/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: +Disallow: /cgi-bin/