diff --git a/.env b/.env index e6f8fef..8379da7 100644 --- a/.env +++ b/.env @@ -4,4 +4,13 @@ DATABASE_URL="mysql://user:changeme@mariadb:3306/ninecompta" MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 APP_NAME=Ninecompta -APP_NOREPLY=admin@noreply.fr \ No newline at end of file +APP_NOREPLY=admin@noreply.fr +MODE_AUTH=SQL + +CAS_HOST=auth.cadoles.com +CAS_PORT=443 +CAS_PATH=/cas +CAS_USERNAME=uid +CAS_MAIL=mail +CAS_LASTNAME=lastname +CAS_FIRSTNAME=firstname diff --git a/.vscode/settings.json b/.vscode/settings.json index 81bb8d7..a0f8043 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,8 @@ "php-cs-fixer.executablePath": "${workspaceFolder}/vendor/bin/php-cs-fixer", "php-cs-fixer.onsave": true, "php-cs-fixer.rules": "@Symfony", - "php-cs-fixer.config": ".php-cs-fixer.dist.php" + "php-cs-fixer.config": ".php-cs-fixer.dist.php", + "vscode-php-cs-fixer.toolPath": "/usr/local/bin/php-cs-fixer", + "vscode-php-cs-fixer.config": ".php-cs-fixer.dist.php", + "vscode-php-cs-fixer.rules": "" } \ No newline at end of file diff --git a/composer.json b/composer.json index f578d1b..3dad67d 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", + "apereo/phpcas": "^1.6", "doctrine/dbal": "^3", "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.3", @@ -14,6 +15,7 @@ "oneup/uploader-bundle": "^5.0", "phpdocumentor/reflection-docblock": "^5.4", "phpstan/phpdoc-parser": "^1.33", + "ramsey/uuid": "^4.7", "symfony/asset": "7.1.*", "symfony/console": "7.1.*", "symfony/doctrine-messenger": "7.1.*", diff --git a/composer.lock b/composer.lock index 3053a94..9ba5c09 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,139 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "26388d2182ab98b93691c53c2fa59dc5", + "content-hash": "89c167c235fc8b9bdd27e885ec95db37", "packages": [ + { + "name": "apereo/phpcas", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/apereo/phpCAS.git", + "reference": "c129708154852656aabb13d8606cd5b12dbbabac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apereo/phpCAS/zipball/c129708154852656aabb13d8606cd5b12dbbabac", + "reference": "c129708154852656aabb13d8606cd5b12dbbabac", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-dom": "*", + "php": ">=7.1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "monolog/monolog": "^1.0.0 || ^2.0.0", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": ">=7.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "files": [ + "source/CAS.php" + ], + "classmap": [ + "source/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Joachim Fritschi", + "email": "jfritschi@freenet.de", + "homepage": "https://github.com/jfritschi" + }, + { + "name": "Adam Franco", + "homepage": "https://github.com/adamfranco" + }, + { + "name": "Henry Pan", + "homepage": "https://github.com/phy25" + } + ], + "description": "Provides a simple API for authenticating users against a CAS server", + "homepage": "https://wiki.jasig.org/display/CASC/phpCAS", + "keywords": [ + "apereo", + "cas", + "jasig" + ], + "support": { + "issues": "https://github.com/apereo/phpCAS/issues", + "source": "https://github.com/apereo/phpCAS/tree/1.6.1" + }, + "time": "2023-02-19T19:52:35+00:00" + }, + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, { "name": "doctrine/cache", "version": "2.2.0", @@ -2022,6 +2153,187 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" + }, { "name": "symfony/asset", "version": "v7.1.6", diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml index 9bb41b1..e8c97c1 100644 --- a/config/packages/oneup_uploader.yaml +++ b/config/packages/oneup_uploader.yaml @@ -1,4 +1,6 @@ oneup_uploader: mappings: avatar: - frontend: dropzone \ No newline at end of file + frontend: dropzone + logo: + frontend: dropzone \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index bf1e77c..b445fef 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,21 +1,24 @@ security: - # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto" - # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: - # used to reload user from session & other features (e.g. switch_user) - app_user_provider: + sql_provider: entity: class: App\Entity\User property: username + + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + main: - lazy: true - provider: app_user_provider + pattern: ^/ + provider: sql_provider + custom_authenticators: + - App\Security\DynamicAuthenticator form_login: login_path: app_login check_path: app_login @@ -23,31 +26,15 @@ security: default_target_path: / logout: path: app_logout - # where to redirect after logout - # target: app_any_route - # 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: - { path: ^/login, roles: PUBLIC_ACCESS } - - { path: ^/register, roles: PUBLIC_ACCESS } - - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/, roles: ROLE_USER } when@test: security: password_hashers: - # By default, password hashers are resource intensive and take time. This is - # important to generate secure password hashes. In tests however, secure hashes - # are not important, waste resources and increase test times. The following - # reduces the work factor to the lowest possible values. Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: algorithm: auto cost: 4 # Lowest possible value for bcrypt diff --git a/config/services.yaml b/config/services.yaml index 7613ecd..17fff24 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,22 +1,23 @@ -# This file is the entry point to configure your own services. -# Files in the packages/ subdirectory configure your dependencies. - -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: appEnv: "%env(resolve:APP_ENV)%" appSecret: "%env(resolve:APP_SECRET)%" appName: "%env(resolve:APP_NAME)%" appNoreply: "%env(resolve:APP_NOREPLY)%" + modeAuth: "%env(resolve:MODE_AUTH)%" + casHost: "%env(resolve:CAS_HOST)%" + casPort: "%env(resolve:CAS_PORT)%" + casPath: "%env(resolve:CAS_PATH)%" + casUsername: "%env(resolve:CAS_USERNAME)%" + casMail: "%env(resolve:CAS_MAIL)%" + casLastname: "%env(resolve:CAS_LASTNAME)%" + casFirstname: "%env(resolve:CAS_FIRSTNAME)%" + services: - # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name App\: resource: "../src/" exclude: @@ -30,4 +31,8 @@ services: tags: - name: "doctrine.event_listener" event: "postPersist" - entity: 'App\Entity\Company' \ No newline at end of file + entity: 'App\Entity\Company' + + App\Security\DynamicAuthenticator: + arguments: + $modeAuth: '%env(MODE_AUTH)%' diff --git a/public/uploads/logo/676bdbf5587bb.jpg b/public/uploads/logo/676bdbf5587bb.jpg new file mode 100644 index 0000000..1e2f217 Binary files /dev/null and b/public/uploads/logo/676bdbf5587bb.jpg differ diff --git a/public/uploads/logo/676bddca6cc2e.jpg b/public/uploads/logo/676bddca6cc2e.jpg new file mode 100644 index 0000000..1e2f217 Binary files /dev/null and b/public/uploads/logo/676bddca6cc2e.jpg differ diff --git a/public/uploads/logo/676bde2e409a7.jpg b/public/uploads/logo/676bde2e409a7.jpg new file mode 100644 index 0000000..1e2f217 Binary files /dev/null and b/public/uploads/logo/676bde2e409a7.jpg differ diff --git a/public/uploads/logo/thumb_676bdbf5587bb.jpg b/public/uploads/logo/thumb_676bdbf5587bb.jpg new file mode 100644 index 0000000..4c275ca Binary files /dev/null and b/public/uploads/logo/thumb_676bdbf5587bb.jpg differ diff --git a/public/uploads/logo/thumb_676bddca6cc2e.jpg b/public/uploads/logo/thumb_676bddca6cc2e.jpg new file mode 100644 index 0000000..4c275ca Binary files /dev/null and b/public/uploads/logo/thumb_676bddca6cc2e.jpg differ diff --git a/public/uploads/logo/thumb_676bde2e409a7.jpg b/public/uploads/logo/thumb_676bde2e409a7.jpg new file mode 100644 index 0000000..4c275ca Binary files /dev/null and b/public/uploads/logo/thumb_676bde2e409a7.jpg differ diff --git a/src/Controller/AccountingController.php b/src/Controller/AccountingController.php index 6fa62a7..358d26b 100644 --- a/src/Controller/AccountingController.php +++ b/src/Controller/AccountingController.php @@ -21,7 +21,7 @@ class AccountingController extends AbstractController return $this->render('accounting/list.html.twig', [ 'usemenu' => true, - 'usesidebar' => true, + 'usesidebar' => false, 'title' => 'Plan Comptable', 'routesubmit' => 'app_user_accounting_submit', 'routeupdate' => 'app_user_accounting_update', @@ -47,7 +47,7 @@ class AccountingController extends AbstractController return $this->render('accounting/edit.html.twig', [ 'usemenu' => true, - 'usesidebar' => true, + 'usesidebar' => false, 'title' => 'Création Compagnie', 'routecancel' => 'app_user_accounting', 'routedelete' => 'app_user_accounting_delete', @@ -75,7 +75,7 @@ class AccountingController extends AbstractController return $this->render('accounting/edit.html.twig', [ 'usemenu' => true, - 'usesidebar' => true, + 'usesidebar' => false, 'title' => 'Modification Compagnie = '.$accounting->getTitle(), 'routecancel' => 'app_user_accounting', 'routedelete' => 'app_user_accounting_delete', diff --git a/src/Controller/CompanyController.php b/src/Controller/CompanyController.php index cab5395..b8a8171 100644 --- a/src/Controller/CompanyController.php +++ b/src/Controller/CompanyController.php @@ -66,7 +66,7 @@ class CompanyController extends AbstractController if ($form->isSubmitted() && $form->isValid()) { $em->flush(); - return $this->redirectToRoute('app_admin_user'); + return $this->redirectToRoute('app_admin_company'); } return $this->render('company/edit.html.twig', [ diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 1e90033..aacb0c4 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -26,7 +26,7 @@ class HomeController extends AbstractController #[Route('/admin', name: 'app_admin')] public function admin(): Response { - return $this->render('home/home.html.twig', [ + return $this->render('home/blank.html.twig', [ 'usemenu' => true, 'usesidebar' => true, ]); diff --git a/src/Controller/OperationController.php b/src/Controller/OperationController.php new file mode 100644 index 0000000..ba19806 --- /dev/null +++ b/src/Controller/OperationController.php @@ -0,0 +1,151 @@ +getSession()->get('company'); + $operations = $operationRepository->findBy(['company' => $company], ['date' => 'DESC']); + + return $this->render('operation/list.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Grand Livre', + 'routesubmit' => 'app_user_operation_submit', + 'routeupdate' => 'app_user_operation_update', + 'operations' => $operations, + ]); + } + + #[Route('/user/operation/submit', name: 'app_user_operation_submit')] + public function submit(Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $operation = new Operation(); + $operation->setCompany($company); + $operation->setDate(new \DateTime()); + + $form = $this->createForm(OperationType::class, $operation, ['mode' => 'submit', 'company' => $company]); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($operation); + $em->flush(); + + return $this->redirectToRoute('app_user_operation'); + } + + return $this->render('operation/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Création Opération', + 'routecancel' => 'app_user_operation', + 'routedelete' => 'app_user_operation_delete', + 'mode' => 'submit', + 'form' => $form, + ]); + } + + #[Route('/user/operation/submit/{id}/{direction}', name: 'app_user_operation_submitwithaccounting')] + public function submitwithaccounting(int $id, string $direction, Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $accounting = $em->getRepository(Accounting::class)->find($id); + if (!$accounting) { + return $this->redirectToRoute('app_home'); + } + if ('debit' != $direction && 'credit' != $direction) { + return $this->redirectToRoute('app_home'); + } + + $operation = new Operation(); + $operation->setCompany($company); + $operation->setDate(new \DateTime()); + if ('debit' == $direction) { + $operation->setDebit($accounting); + } else { + $operation->setCredit($accounting); + } + + $form = $this->createForm(OperationType::class, $operation, ['mode' => 'submit', 'company' => $company, 'direction' => $direction]); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($operation); + $em->flush(); + + return $this->redirectToRoute('app_home'); + } + + return $this->render('operation/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Création Opération', + 'routecancel' => 'app_user_operation', + 'routedelete' => 'app_user_operation_delete', + 'mode' => 'submit', + 'form' => $form, + ]); + } + + #[Route('/user/operation/update/{id}', name: 'app_user_operation_update')] + public function update(int $id, Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $operation = $em->getRepository(Operation::class)->find($id); + if (!$operation || $operation->getCompany() != $company) { + return $this->redirectToRoute('app_user_operation'); + } + + $form = $this->createForm(OperationType::class, $operation, ['mode' => 'update', 'company' => $company]); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->flush(); + + return $this->redirectToRoute('app_user_operation'); + } + + return $this->render('operation/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Modification Compagnie = '.$operation->getTitle(), + 'routecancel' => 'app_user_operation', + 'routedelete' => 'app_user_operation_delete', + 'mode' => 'update', + 'form' => $form, + ]); + } + + #[Route('/user/operation/delete/{id}', name: 'app_user_operation_delete')] + public function delete(int $id, Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $operation = $em->getRepository(Operation::class)->find($id); + if (!$operation || $operation->getCompany() != $company) { + return $this->redirectToRoute('app_user_operation'); + } + + // Tentative de suppression + try { + $em->remove($operation); + $em->flush(); + } catch (\Exception $e) { + $this->addflash('error', $e->getMessage()); + + return $this->redirectToRoute('app_user_operation_update', ['id' => $id]); + } + + return $this->redirectToRoute('app_user_operation'); + } +} diff --git a/src/Controller/YearController.php b/src/Controller/YearController.php new file mode 100644 index 0000000..c9b3f10 --- /dev/null +++ b/src/Controller/YearController.php @@ -0,0 +1,108 @@ +getSession()->get('company'); + $years = $yearRepository->findBy(['company' => $company], ['dateStart' => 'ASC']); + + return $this->render('year/list.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Exercices', + 'routesubmit' => 'app_user_year_submit', + 'routeupdate' => 'app_user_year_update', + 'years' => $years, + ]); + } + + #[Route('/user/year/submit', name: 'app_user_year_submit')] + public function submit(Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $year = new Year(); + $year->setCompany($company); + + $form = $this->createForm(YearType::class, $year, ['mode' => 'submit']); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($year); + $em->flush(); + + return $this->redirectToRoute('app_user_year'); + } + + return $this->render('year/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Création Exercice', + 'routecancel' => 'app_user_year', + 'routedelete' => 'app_user_year_delete', + 'mode' => 'submit', + 'form' => $form, + ]); + } + + #[Route('/user/year/update/{id}', name: 'app_user_year_update')] + public function update(int $id, Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $year = $em->getRepository(Year::class)->find($id); + if (!$year || $year->getCompany() != $company) { + return $this->redirectToRoute('app_user_year'); + } + + $form = $this->createForm(YearType::class, $year, ['mode' => 'update']); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->flush(); + + return $this->redirectToRoute('app_user_year'); + } + + return $this->render('year/edit.html.twig', [ + 'usemenu' => true, + 'usesidebar' => false, + 'title' => 'Modification Exercice = '.$year->getDateStart()->format('d/m/Y').' - '.$year->getDateEnd()->format('d/m/Y'), + 'routecancel' => 'app_user_year', + 'routedelete' => 'app_user_year_delete', + 'mode' => 'update', + 'form' => $form, + ]); + } + + #[Route('/user/year/delete/{id}', name: 'app_user_year_delete')] + public function delete(int $id, Request $request, EntityManagerInterface $em): Response + { + $company = $request->getSession()->get('company'); + $year = $em->getRepository(Year::class)->find($id); + if (!$year || $year->getCompany() != $company) { + return $this->redirectToRoute('app_user_year'); + } + + // Tentative de suppression + try { + $em->remove($year); + $em->flush(); + } catch (\Exception $e) { + $this->addflash('error', $e->getMessage()); + + return $this->redirectToRoute('app_user_year_update', ['id' => $id]); + } + + return $this->redirectToRoute('app_user_year'); + } +} diff --git a/src/Entity/Accounting.php b/src/Entity/Accounting.php index 87df314..a0427ad 100644 --- a/src/Entity/Accounting.php +++ b/src/Entity/Accounting.php @@ -200,4 +200,36 @@ class Accounting return $this; } + + public function getSoldeDate(?\DateTime $date): float + { + $solde = (float) 0; + + foreach ($this->credits as $credit) { + if (!$date || $credit->getDate() <= $date) { + if ('passif' == $this->category || 'produit' == $this->category) { + $solde += $credit->getMontant(); + } else { + $solde -= $credit->getMontant(); + } + } + } + + foreach ($this->debits as $debit) { + if (!$date || $debit->getDate() <= $date) { + if ('passif' == $this->category || 'produit' == $this->category) { + $solde -= $debit->getMontant(); + } else { + $solde += $debit->getMontant(); + } + } + } + + return $solde; + } + + public function getSolde(): float + { + return $this->getSoldeDate(null); + } } diff --git a/src/Entity/Company.php b/src/Entity/Company.php index c6b24db..6e64800 100644 --- a/src/Entity/Company.php +++ b/src/Entity/Company.php @@ -69,10 +69,24 @@ class Company #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'companys')] private Collection $users; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Year::class, mappedBy: 'company', orphanRemoval: true)] + private Collection $years; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Operation::class, mappedBy: 'company', orphanRemoval: true)] + private Collection $operations; + public function __construct() { $this->accountings = new ArrayCollection(); $this->users = new ArrayCollection(); + $this->years = new ArrayCollection(); + $this->operations = new ArrayCollection(); } public function getId(): ?int @@ -280,4 +294,64 @@ class Company return $this; } + + /** + * @return Collection + */ + public function getYears(): Collection + { + return $this->years; + } + + public function addYear(Year $year): static + { + if (!$this->years->contains($year)) { + $this->years->add($year); + $year->setCompany($this); + } + + return $this; + } + + public function removeYear(Year $year): static + { + if ($this->years->removeElement($year)) { + // set the owning side to null (unless already changed) + if ($year->getCompany() === $this) { + $year->setCompany(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getOperations(): Collection + { + return $this->operations; + } + + public function addOperation(Operation $operation): static + { + if (!$this->operations->contains($operation)) { + $this->operations->add($operation); + $operation->setCompany($this); + } + + return $this; + } + + public function removeOperation(Operation $operation): static + { + if ($this->operations->removeElement($operation)) { + // set the owning side to null (unless already changed) + if ($operation->getCompany() === $this) { + $operation->setCompany(null); + } + } + + return $this; + } } diff --git a/src/Entity/Operation.php b/src/Entity/Operation.php index a520891..ebff3eb 100644 --- a/src/Entity/Operation.php +++ b/src/Entity/Operation.php @@ -31,6 +31,10 @@ class Operation #[ORM\JoinColumn(nullable: false)] private ?Accounting $debit = null; + #[ORM\ManyToOne(inversedBy: 'operations')] + #[ORM\JoinColumn(nullable: false)] + private ?Company $company = null; + public function getId(): ?int { return $this->id; @@ -95,4 +99,16 @@ class Operation return $this; } + + public function getCompany(): ?Company + { + return $this->company; + } + + public function setCompany(?Company $company): static + { + $this->company = $company; + + return $this; + } } diff --git a/src/Entity/Year.php b/src/Entity/Year.php new file mode 100644 index 0000000..f09d4f9 --- /dev/null +++ b/src/Entity/Year.php @@ -0,0 +1,67 @@ +id; + } + + public function getDateStart(): ?\DateTimeInterface + { + return $this->dateStart; + } + + public function setDateStart(\DateTimeInterface $dateStart): static + { + $this->dateStart = $dateStart; + + return $this; + } + + public function getDateEnd(): ?\DateTimeInterface + { + return $this->dateEnd; + } + + public function setDateEnd(\DateTimeInterface $dateEnd): static + { + $this->dateEnd = $dateEnd; + + return $this; + } + + public function getCompany(): ?Company + { + return $this->company; + } + + public function setCompany(?Company $company): static + { + $this->company = $company; + + return $this; + } +} diff --git a/src/EventListener/LogoutListener.php b/src/EventListener/LogoutListener.php new file mode 100644 index 0000000..b8cc690 --- /dev/null +++ b/src/EventListener/LogoutListener.php @@ -0,0 +1,33 @@ +parameterBag = $parameterBag; + } + + #[AsEventListener(event: LogoutEvent::class)] + public function onLogoutEvent(LogoutEvent $event): void + { + if ('CAS' == $this->parameterBag->get('modeAuth')) { + // Initialiser phpCAS + $request = $event->getRequest(); + $host = $request->headers->get('X-Forwarded-Host') ?? $request->getHost().($request->getPort() ? ':'.$request->getPort() : ''); + $scheme = $request->headers->get('X-Forwarded-Proto') ?? $request->getScheme(); + $url = $scheme.'://'.$host; + \phpCAS::client(CAS_VERSION_2_0, $this->parameterBag->get('casHost'), (int) $this->parameterBag->get('casPort'), $this->parameterBag->get('casPath'), $url, false); + \phpCAS::setNoCasServerValidation(); + \phpCAS::logoutWithRedirectService($url); + + } + } +} diff --git a/src/EventListener/SessionListener.php b/src/EventListener/SessionListener.php index b68e4db..c2d5553 100644 --- a/src/EventListener/SessionListener.php +++ b/src/EventListener/SessionListener.php @@ -39,8 +39,10 @@ final class SessionListener $session->set('company', $user->getCompany()); $session->set('companys', $user->getCompanys()); - if (!$user->getCompany()) { - $event->setResponse(new RedirectResponse($this->router->generate('app_user_nocompany', []))); + $currentPath = $request->getPathInfo(); + $noCompanyPath = $this->router->generate('app_user_nocompany'); + if (!$user->getCompany() && $currentPath != $noCompanyPath) { + $event->setResponse(new RedirectResponse($noCompanyPath)); } } } diff --git a/src/Form/AccountingType.php b/src/Form/AccountingType.php index 7151318..9cbe949 100644 --- a/src/Form/AccountingType.php +++ b/src/Form/AccountingType.php @@ -3,7 +3,6 @@ namespace App\Form; use App\Entity\Accounting; -use App\Form\DataTransformer\ThreeDigitTransformer; use App\Form\Type\FontawsomeType; use App\Form\Type\ThreeDigitType; use Symfony\Component\Form\AbstractType; diff --git a/src/Form/OperationType.php b/src/Form/OperationType.php new file mode 100644 index 0000000..dfa5819 --- /dev/null +++ b/src/Form/OperationType.php @@ -0,0 +1,99 @@ +add('submit', SubmitType::class, [ + 'label' => 'Valider', + 'attr' => ['class' => 'btn btn-success no-print'], + ]) + + ->add('date', DateType::class, [ + 'label' => 'Date', + ]) + + ->add('title', TextType::class, [ + 'label' => 'Titre', + ]); + + if (!$options['direction'] || 'credit' == $options['direction']) { + $builder + ->add('debit', EntityType::class, [ + 'label' => 'Débit', + 'class' => Accounting::class, + 'choice_label' => 'title', + 'attr' => ['class' => 'select2'], + 'placeholder' => 'Selectionnez un compte', + 'query_builder' => function (EntityRepository $er) use ($options) { + $result = $er->createQueryBuilder('acc') + ->where('acc.company = :company') + ->setParameter('company', $options['company']->getId()) + ->orderBy('acc.num01', 'ASC') + ->orderBy('acc.num02', 'ASC'); + + if ($options['direction']) { + $result = $result->andWhere('acc.category=:category')->setParameter('category', 'charge'); + } + + return $result; + }, + ]); + } + + if (!$options['direction'] || 'debit' == $options['direction']) { + $builder + ->add('credit', EntityType::class, [ + 'label' => 'Crédit', + 'class' => Accounting::class, + 'choice_label' => 'title', + 'attr' => ['class' => 'select2'], + 'placeholder' => 'Selectionnez un compte', + 'query_builder' => function (EntityRepository $er) use ($options) { + $result = $er->createQueryBuilder('acc') + ->where('acc.company = :company') + ->setParameter('company', $options['company']->getId()) + ->orderBy('acc.num01', 'ASC') + ->orderBy('acc.num02', 'ASC'); + + if ($options['direction']) { + $result = $result->andWhere('acc.category=:category')->setParameter('category', 'produit'); + } + + return $result; + }, + ]); + } + + $builder + ->add('montant', NumberType::class, [ + 'label' => 'Montant', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Operation::class, + 'company' => Company::class, + 'mode' => 'mode', + 'direction' => null, + ]); + } +} diff --git a/src/Form/YearType.php b/src/Form/YearType.php new file mode 100644 index 0000000..b617544 --- /dev/null +++ b/src/Form/YearType.php @@ -0,0 +1,43 @@ +add('submit', SubmitType::class, [ + 'label' => 'Valider', + 'attr' => ['class' => 'btn btn-success no-print'], + ]) + + ->add('dateStart', DateType::class, [ + 'label' => 'Date Début', + ]) + + ->add('dateEnd', DateType::class, [ + 'label' => 'Date Fin', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Year::class, + 'mode' => 'submit', + ]); + } +} diff --git a/src/Repository/YearRepository.php b/src/Repository/YearRepository.php new file mode 100644 index 0000000..59619b4 --- /dev/null +++ b/src/Repository/YearRepository.php @@ -0,0 +1,43 @@ + + */ +class YearRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Year::class); + } + + // /** + // * @return Year[] Returns an array of Year objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('y') + // ->andWhere('y.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('y.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Year + // { + // return $this->createQueryBuilder('y') + // ->andWhere('y.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/CasUserProvider.php b/src/Security/CasUserProvider.php new file mode 100644 index 0000000..47e84d3 --- /dev/null +++ b/src/Security/CasUserProvider.php @@ -0,0 +1,87 @@ +userRepository = $userRepository; + $this->em = $em; + $this->parameterBag = $parameterBag; + } + + /** + * Charge un utilisateur par son identifiant CAS. + */ + public function loadUserByIdentifier(string $identifier): UserInterface + { + // Récupérer l'utilisateur depuis la base de données + $user = $this->userRepository->findOneBy(['username' => $identifier]); + + if (!$user) { + throw new UserNotFoundException(sprintf('User with username "%s" not found.', $identifier)); + } + + return $user; + } + + /** + * Charge un utilisateur par son identifiant CAS et autosubmit autoupdate en fonction des attributs CAS + */ + public function loadUserByIdentifierAndAttributes(string $identifier, array $attributes): UserInterface + { + // Charger l'utilisateur existant depuis la base de données + $user = $this->userRepository->findOneBy(['username' => $identifier]); + + if (!$user) { + // Créer un nouvel utilisateur avec les attributs CAS + $user = new User(); + $user->setUsername($identifier); + $user->setPassword(Uuid::uuid4()->toString()); + $user->setRoles(['ROLE_USER']); + + // Persister l'utilisateur en base si nécessaire + $this->em->persist($user); + } + + $user->setEmail($attributes[$this->parameterBag->get('casMail')] ?? null); + $this->em->flush(); + + return $user; + } + + /** + * Permet de recharger un utilisateur déjà authentifié (si nécessaire). + */ + public function refreshUser(UserInterface $user): UserInterface + { + if (!$user instanceof User) { + throw new \InvalidArgumentException(sprintf('Instances of "%s" are not supported.', get_class($user))); + } + + // Recharge l'utilisateur en base si nécessaire (ex. : rôles mis à jour) + return $this->userRepository->find($user->getId()); + } + + /** + * Indique si ce provider supporte un type d'utilisateur donné. + */ + public function supportsClass(string $class): bool + { + return User::class === $class; + } +} diff --git a/src/Security/DynamicAuthenticator.php b/src/Security/DynamicAuthenticator.php new file mode 100644 index 0000000..0e58ded --- /dev/null +++ b/src/Security/DynamicAuthenticator.php @@ -0,0 +1,123 @@ +modeAuth = $modeAuth; + $this->userRepository = $userRepository; + $this->casUserProvider = $casUserProvider; + $this->parameterBag = $parameterBag; + } + + public function supports(Request $request): ?bool + { + // Vérifie si l'utilisateur est déjà connecté + if ($request->getSession()->get('_security_main')) { + return false; // L'utilisateur est déjà authentifié + } + + // Exclure les routes de login et logout pour éviter les boucles + $currentPath = $request->getPathInfo(); + if (in_array($currentPath, ['/login', '/logout'])) { + return false; + } + + return true; + } + + public function authenticate(Request $request): Passport + { + switch ($this->modeAuth) { + case 'SQL': + return $this->authenticateWithSql($request); + + case 'CAS': + return $this->authenticateWithCas($request); + + default: + throw new \InvalidArgumentException('Invalid authentication method'); + } + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + throw $exception; + } + + private function authenticateWithSql(Request $request): Passport + { + $username = $request->request->get('_username', ''); + $password = $request->request->get('_password', ''); + + if (!$username || !$password) { + throw new AuthenticationException('Username and password are required.'); + } + + // Charger l'utilisateur via Doctrine + $user = $this->userRepository->findOneBy(['username' => $username]); + + if (!$user) { + throw new AuthenticationException('User not found.'); + } + + return new Passport( + new UserBadge($username, function ($userIdentifier) { + return $this->userRepository->findOneBy(['username' => $userIdentifier]); + }), + new PasswordCredentials($password) + ); + } + + private function authenticateWithCas(Request $request): Passport + { + // Récupérer l'hôte d'origine derrière le reverse proxy + $host = $request->headers->get('X-Forwarded-Host') ?? $request->getHost().($request->getPort() ? ':'.$request->getPort() : ''); + $scheme = $request->headers->get('X-Forwarded-Proto') ?? $request->getScheme(); + + // Construire l'URL + $url = $scheme.'://'.$host; + + // \phpCAS::setDebug('/tmp/logcas.log'); + \phpCAS::client(CAS_VERSION_2_0, $this->parameterBag->get('casHost'), (int) $this->parameterBag->get('casPort'), $this->parameterBag->get('casPath'), $url, false); + \phpCAS::setNoCasServerValidation(); + \phpCAS::forceAuthentication(); + + $username = \phpCAS::getUser(); + $attributes = \phpCAS::getAttributes(); + + if (!$username) { + throw new AuthenticationException('CAS authentication failed.'); + } + + $userBadge = new UserBadge($username, function ($userIdentifier) use ($attributes) { + return $this->casUserProvider->loadUserByIdentifierAndAttributes($userIdentifier, $attributes); + }); + + return new SelfValidatingPassport($userBadge); + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 48df598..543c464 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -40,32 +40,25 @@ {% if usemenu is defined and usemenu %}