diff --git a/Makefile b/Makefile index 43a071c..a4e7cdd 100644 --- a/Makefile +++ b/Makefile @@ -8,4 +8,7 @@ backend-shell: docker-compose exec backend /bin/bash frontend-shell: - docker-compose exec frontend /bin/bash \ No newline at end of file + docker-compose exec frontend /bin/bash + +database-mysql: + docker-compose exec database mysql --default-character-set=utf8 -uroot -proot logo \ No newline at end of file diff --git a/README.md b/README.md index e13277c..a9de07c 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ Squelette applicatif React/Symfony 4 pour la formation React personnalisée Logo - [Docker](https://docs.docker.com/install/) - [Docker Compose](https://docs.docker.com/compose/install/) -``` +```bash git clone https://forge.cadoles.com/wpetit/react-logo.git cd react-logo -docker-compose up +make up # ou docker-compose up --build ``` Une fois la procédure d'initialisation terminée, les différentes parties de l'application devraient être disponibles aux adresses suivantes: @@ -28,28 +28,32 @@ Une fois la procédure d'initialisation terminée, les différentes parties de l Une fois l'application lancée, exécuter: -``` -docker-compose exec backend /bin/bash +```bash +make backend-shell # ou docker-compose exec backend /bin/bash ``` #### Comment ouvrir un shell interactif dans le conteneur "frontend" ? Une fois l'application lancée, exécuter: -``` -docker-compose exec frontend /bin/bash +```bash +make frontend-shell # ou docker-compose exec frontend /bin/bash ``` #### Comment ouvrir une console MySQL ? Une fois l'application lancée, exécuter: -``` -docker-compose exec database mysql -uroot -proot +```bash +make database-mysql # ou docker-compose exec database mysql -uroot -proot logo ``` ### Comment réinitialiser l'environnement ? +```bash +make down # ou docker-compose down -v ``` -docker-compose down -v -``` + +## Cahier des charges + +- [Application de suivi des demandes clients](./misc/projects/ticketing_app.md) diff --git a/backend/.env b/backend/.env index 762e995..323147f 100644 --- a/backend/.env +++ b/backend/.env @@ -27,3 +27,7 @@ APP_SECRET=bc3856916a3206bf0b23356f46c5cf7d # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7 ###< doctrine/doctrine-bundle ### + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$ +###< nelmio/cors-bundle ### diff --git a/backend/composer.json b/backend/composer.json index d872882..5d6f65b 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -5,15 +5,19 @@ "php": "^7.1.3", "ext-ctype": "*", "ext-iconv": "*", + "nelmio/cors-bundle": "^2.0", + "sensio/framework-extra-bundle": "^5.5", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.3.1", "symfony/framework-bundle": "4.4.*", + "symfony/inflector": "4.4.*", "symfony/orm-pack": "^1.0", "symfony/security-bundle": "4.4.*", "symfony/yaml": "4.4.*" }, "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^3.3", "symfony/maker-bundle": "^1.14", "symfony/web-server-bundle": "4.4.*" }, diff --git a/backend/composer.lock b/backend/composer.lock index f4c0ac9..a311122 100644 --- a/backend/composer.lock +++ b/backend/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": "6421c1affbb7f7322a6499bde9164df1", + "content-hash": "b8a24c5421258bb04d461c7dd0498820", "packages": [ { "name": "doctrine/annotations", @@ -1369,6 +1369,63 @@ ], "time": "2020-01-07T22:58:31+00:00" }, + { + "name": "nelmio/cors-bundle", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "9683e6d30d000ef998919261329d825de7c53499" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/9683e6d30d000ef998919261329d825de7c53499", + "reference": "9683e6d30d000ef998919261329d825de7c53499", + "shasum": "" + }, + "require": { + "symfony/framework-bundle": "^4.3 || ^5.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "symfony/phpunit-bridge": "^4.3 || ^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.0.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" + ], + "time": "2019-11-15T08:54:08+00:00" + }, { "name": "ocramius/package-versions", "version": "1.5.1", @@ -1639,6 +1696,84 @@ ], "time": "2019-11-01T11:05:21+00:00" }, + { + "name": "sensio/framework-extra-bundle", + "version": "v5.5.3", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", + "reference": "98f0807137b13d0acfdf3c255a731516e97015de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/98f0807137b13d0acfdf3c255a731516e97015de", + "reference": "98f0807137b13d0acfdf3c255a731516e97015de", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "php": ">=7.1.3", + "symfony/config": "^4.3|^5.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/framework-bundle": "^4.3|^5.0", + "symfony/http-kernel": "^4.3|^5.0" + }, + "conflict": { + "doctrine/doctrine-cache-bundle": "<1.3.1" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.5", + "nyholm/psr7": "^1.1", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/dom-crawler": "^4.3|^5.0", + "symfony/expression-language": "^4.3|^5.0", + "symfony/finder": "^4.3|^5.0", + "symfony/monolog-bridge": "^4.0|^5.0", + "symfony/monolog-bundle": "^3.2", + "symfony/phpunit-bridge": "^4.3.5|^5.0", + "symfony/psr-http-message-bridge": "^1.1", + "symfony/security-bundle": "^4.3|^5.0", + "symfony/twig-bundle": "^4.3|^5.0", + "symfony/yaml": "^4.3|^5.0", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "suggest": { + "symfony/expression-language": "", + "symfony/psr-http-message-bridge": "To use the PSR-7 converters", + "symfony/security-bundle": "" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sensio\\Bundle\\FrameworkExtraBundle\\": "src/" + }, + "exclude-from-classmap": [ + "/tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "This bundle provides a way to configure your controllers with annotations", + "keywords": [ + "annotations", + "controllers" + ], + "time": "2019-12-27T08:57:19+00:00" + }, { "name": "symfony/cache", "version": "v4.4.4", @@ -4017,6 +4152,137 @@ } ], "packages-dev": [ + { + "name": "doctrine/data-fixtures", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "39e9777c9089351a468f780b01cffa3cb0a42907" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/39e9777c9089351a468f780b01cffa3cb0a42907", + "reference": "39e9777c9089351a468f780b01cffa3cb0a42907", + "shasum": "" + }, + "require": { + "doctrine/common": "^2.11", + "doctrine/persistence": "^1.3.3", + "php": "^7.2" + }, + "conflict": { + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^6.0", + "doctrine/dbal": "^2.5.4", + "doctrine/mongodb-odm": "^1.3.0", + "doctrine/orm": "^2.7.0", + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM with PHP 7", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "lib/Doctrine/Common/DataFixtures" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database" + ], + "time": "2020-01-17T11:11:28+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "8f07fcfdac7f3591f3c4bf13a50cbae05f65ed70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/8f07fcfdac7f3591f3c4bf13a50cbae05f65ed70", + "reference": "8f07fcfdac7f3591f3c4bf13a50cbae05f65ed70", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^1.3", + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.6.0", + "php": "^7.1", + "symfony/config": "^3.4|^4.3|^5.0", + "symfony/console": "^3.4|^4.3|^5.0", + "symfony/dependency-injection": "^3.4|^4.3|^5.0", + "symfony/doctrine-bridge": "^3.4|^4.1|^5.0", + "symfony/http-kernel": "^3.4|^4.3|^5.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.4", + "symfony/phpunit-bridge": "^4.1|^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "time": "2019-11-13T15:46:58+00:00" + }, { "name": "nikic/php-parser", "version": "v4.3.0", diff --git a/backend/config/bundles.php b/backend/config/bundles.php index 119166a..951eca4 100644 --- a/backend/config/bundles.php +++ b/backend/config/bundles.php @@ -7,4 +7,7 @@ return [ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], ]; diff --git a/backend/config/packages/nelmio_cors.yaml b/backend/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..c766508 --- /dev/null +++ b/backend/config/packages/nelmio_cors.yaml @@ -0,0 +1,10 @@ +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: + '^/': null diff --git a/backend/config/packages/security.yaml b/backend/config/packages/security.yaml index ce69ba7..a7ea7e3 100644 --- a/backend/config/packages/security.yaml +++ b/backend/config/packages/security.yaml @@ -1,13 +1,26 @@ security: + encoders: + App\Entity\User: + algorithm: auto + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - in_memory: { memory: null } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: - anonymous: lazy + anonymous: ~ + json_login: + check_path: /api/v1/login + logout: + path: /api/v1/logout + target: /api/v1 # activate different ways to authenticate # https://symfony.com/doc/current/security.html#firewalls-authentication @@ -18,5 +31,3 @@ security: # 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: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } diff --git a/backend/config/packages/sensio_framework_extra.yaml b/backend/config/packages/sensio_framework_extra.yaml new file mode 100644 index 0000000..1821ccc --- /dev/null +++ b/backend/config/packages/sensio_framework_extra.yaml @@ -0,0 +1,3 @@ +sensio_framework_extra: + router: + annotations: false diff --git a/backend/config/routes.yaml b/backend/config/routes.yaml index d1523d3..781019b 100644 --- a/backend/config/routes.yaml +++ b/backend/config/routes.yaml @@ -2,29 +2,9 @@ # path: / # controller: App\Controller\DefaultController::index -app_home: - path: / - controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction - defaults: - path: /api/v1 - permanent: true +login: + path: /api/v1/login + controller: App\Controller\SecurityController::login -app_api_login: - path: /api/v1/login - methods: ['POST'] - controller: App\Controller\ApiController::login - -app_api_logout: - path: /api/v1/logout - methods: ['POST'] - controller: App\Controller\ApiController::logout - -app_api_show_info: - path: /api/v1 - methods: ['GET'] - controller: App\Controller\ApiController::showVersionInfo - -app_api_list_users: - path: /api/v1/users - methods: ['GET'] - controller: App\Controller\ApiController::listUsers +logout: + path: /api/v1/logout \ No newline at end of file diff --git a/backend/src/Controller/ApiController.php b/backend/src/Controller/ApiController.php deleted file mode 100644 index a68c3c5..0000000 --- a/backend/src/Controller/ApiController.php +++ /dev/null @@ -1,33 +0,0 @@ - '1', - ]); - } - - public function listUsers() - { - return new JsonResponse([]); - } -} \ No newline at end of file diff --git a/backend/src/Controller/IndexController.php b/backend/src/Controller/IndexController.php new file mode 100644 index 0000000..279476c --- /dev/null +++ b/backend/src/Controller/IndexController.php @@ -0,0 +1,20 @@ + '1', + ]); + } + +} \ No newline at end of file diff --git a/backend/src/Controller/RequestController.php b/backend/src/Controller/RequestController.php new file mode 100644 index 0000000..23f3c5e --- /dev/null +++ b/backend/src/Controller/RequestController.php @@ -0,0 +1,18 @@ +getUser(); + + return new DataResponse([ + 'username' => $user->getUsername(), + 'roles' => $user->getRoles(), + ]); + } +} \ No newline at end of file diff --git a/backend/src/Controller/UserController.php b/backend/src/Controller/UserController.php new file mode 100644 index 0000000..67c8e7d --- /dev/null +++ b/backend/src/Controller/UserController.php @@ -0,0 +1,72 @@ +getUser(); + + return new DataResponse([ + 'username' => $user->getUsername(), + 'roles' => $user->getRoles(), + ]); + } + + /** + * @Route("/api/v1/users", name="api_v1_list_users", methods={"GET"}) + * @IsGranted("ROLE_DEVELOPER") + */ + public function listUsers() + { + /** @var array */ + $users = $this->getDoctrine() + ->getRepository(User::class) + ->findAll() + ; + + $results = []; + foreach($users as $u) { + $results[] = [ + 'id' => $u->getId(), + 'username' => $u->getUsername(), + ]; + } + + return new DataResponse([ + 'users' => $results, + ]); + } + + /** + * @Route("/api/v1/users/{userId}", name="api_v1_get_user", methods={"GET"}, requirements={"userId"="\d+"}) + * @IsGranted("ROLE_DEVELOPER") + */ + public function showUser($userId) + { + /** @var User */ + $user = $this->getDoctrine() + ->getRepository(User::class) + ->find($userId) + ; + + return new DataResponse([ + 'user' => [ + 'id' => $user->getId(), + 'username' => $user->getUsername(), + ] + ]); + } +} \ No newline at end of file diff --git a/backend/src/DataFixtures/AppFixtures.php b/backend/src/DataFixtures/AppFixtures.php new file mode 100644 index 0000000..fece475 --- /dev/null +++ b/backend/src/DataFixtures/AppFixtures.php @@ -0,0 +1,17 @@ +persist($product); + + $manager->flush(); + } +} diff --git a/backend/src/DataFixtures/RequestStatusFixtures.php b/backend/src/DataFixtures/RequestStatusFixtures.php new file mode 100644 index 0000000..1d7a867 --- /dev/null +++ b/backend/src/DataFixtures/RequestStatusFixtures.php @@ -0,0 +1,29 @@ +setLabel($statusLabel); + $manager->persist($status); + } + + $manager->flush(); + } +} diff --git a/backend/src/DataFixtures/UserFixtures.php b/backend/src/DataFixtures/UserFixtures.php new file mode 100644 index 0000000..0447c20 --- /dev/null +++ b/backend/src/DataFixtures/UserFixtures.php @@ -0,0 +1,55 @@ +passwordEncoder = $passwordEncoder; + } + + public function load(ObjectManager $manager) + { + // On créait l'utilisateur client1 et client2 + $client1 = new User(); + $client1->setUsername('client1'); + $client1->setPassword($this->passwordEncoder->encodePassword( + $client1, + 'client1' + )); + $client1->setRoles(['ROLE_CLIENT']); + $manager->persist($client1); + + $client2 = new User(); + $client2->setUsername('client2'); + $client2->setPassword($this->passwordEncoder->encodePassword( + $client2, + 'client2' + )); + $client2->setRoles(['ROLE_CLIENT']); + $manager->persist($client2); + + // On créait l'utilisateur dev1 + $dev1 = new User(); + $dev1->setUsername('dev1'); + $dev1->setPassword($this->passwordEncoder->encodePassword( + $dev1, + 'dev1' + )); + $dev1->setRoles(['ROLE_DEVELOPER']); + $manager->persist($dev1); + + $manager->flush(); + + } + + +} diff --git a/backend/src/Entity/Comment.php b/backend/src/Entity/Comment.php new file mode 100644 index 0000000..13cc96f --- /dev/null +++ b/backend/src/Entity/Comment.php @@ -0,0 +1,93 @@ +id; + } + + public function getRequest(): ?Request + { + return $this->request; + } + + public function setRequest(?Request $request): self + { + $this->request = $request; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getAuthor(): ?User + { + return $this->author; + } + + public function setAuthor(?User $author): self + { + $this->author = $author; + + return $this; + } + + public function getText(): ?string + { + return $this->text; + } + + public function setText(string $text): self + { + $this->text = $text; + + return $this; + } +} diff --git a/backend/src/Entity/Project.php b/backend/src/Entity/Project.php new file mode 100644 index 0000000..725c7b2 --- /dev/null +++ b/backend/src/Entity/Project.php @@ -0,0 +1,115 @@ +request = new ArrayCollection(); + $this->users = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection|Request[] + */ + public function getRequest(): Collection + { + return $this->request; + } + + public function addRequest(Request $request): self + { + if (!$this->request->contains($request)) { + $this->request[] = $request; + $request->setProject($this); + } + + return $this; + } + + public function removeRequest(Request $request): self + { + if ($this->request->contains($request)) { + $this->request->removeElement($request); + // set the owning side to null (unless already changed) + if ($request->getProject() === $this) { + $request->setProject(null); + } + } + + return $this; + } + + /** + * @return Collection|User[] + */ + public function getUsers(): Collection + { + return $this->users; + } + + public function addUser(User $user): self + { + if (!$this->users->contains($user)) { + $this->users[] = $user; + } + + return $this; + } + + public function removeUser(User $user): self + { + if ($this->users->contains($user)) { + $this->users->removeElement($user); + } + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/backend/src/Entity/Request.php b/backend/src/Entity/Request.php new file mode 100644 index 0000000..c107f21 --- /dev/null +++ b/backend/src/Entity/Request.php @@ -0,0 +1,154 @@ +comments = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getAuthor(): ?User + { + return $this->author; + } + + public function setAuthor(?User $author): self + { + $this->author = $author; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): self + { + $this->project = $project; + + return $this; + } + + /** + * @return Collection|Comment[] + */ + public function getComments(): Collection + { + return $this->comments; + } + + public function addComment(Comment $comment): self + { + if (!$this->comments->contains($comment)) { + $this->comments[] = $comment; + $comment->setRequest($this); + } + + return $this; + } + + public function removeComment(Comment $comment): self + { + if ($this->comments->contains($comment)) { + $this->comments->removeElement($comment); + // set the owning side to null (unless already changed) + if ($comment->getRequest() === $this) { + $comment->setRequest(null); + } + } + + return $this; + } + + public function getStatus(): ?RequestStatus + { + return $this->status; + } + + public function setStatus(?RequestStatus $status): self + { + $this->status = $status; + + return $this; + } +} diff --git a/backend/src/Entity/RequestStatus.php b/backend/src/Entity/RequestStatus.php new file mode 100644 index 0000000..803adc5 --- /dev/null +++ b/backend/src/Entity/RequestStatus.php @@ -0,0 +1,83 @@ +requests = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + /** + * @return Collection|Request[] + */ + public function getRequests(): Collection + { + return $this->requests; + } + + public function addRequest(Request $request): self + { + if (!$this->requests->contains($request)) { + $this->requests[] = $request; + $request->setStatus($this); + } + + return $this; + } + + public function removeRequest(Request $request): self + { + if ($this->requests->contains($request)) { + $this->requests->removeElement($request); + // set the owning side to null (unless already changed) + if ($request->getStatus() === $this) { + $request->setStatus(null); + } + } + + return $this; + } +} diff --git a/backend/src/Entity/User.php b/backend/src/Entity/User.php new file mode 100644 index 0000000..8ffbdc6 --- /dev/null +++ b/backend/src/Entity/User.php @@ -0,0 +1,222 @@ +requests = new ArrayCollection(); + $this->projects = new ArrayCollection(); + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUsername(): string + { + return (string) $this->username; + } + + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see UserInterface + */ + public function getPassword(): string + { + return (string) $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function getSalt() + { + // not needed when using the "bcrypt" algorithm in security.yaml + } + + /** + * @see UserInterface + */ + public function eraseCredentials() + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + /** + * @return Collection|Request[] + */ + public function getRequests(): Collection + { + return $this->requests; + } + + public function addRequest(Request $request): self + { + if (!$this->requests->contains($request)) { + $this->requests[] = $request; + $request->setAuthor($this); + } + + return $this; + } + + public function removeRequest(Request $request): self + { + if ($this->requests->contains($request)) { + $this->requests->removeElement($request); + // set the owning side to null (unless already changed) + if ($request->getAuthor() === $this) { + $request->setAuthor(null); + } + } + + return $this; + } + + /** + * @return Collection|Project[] + */ + public function getProjects(): Collection + { + return $this->projects; + } + + public function addProject(Project $project): self + { + if (!$this->projects->contains($project)) { + $this->projects[] = $project; + $project->addUser($this); + } + + return $this; + } + + public function removeProject(Project $project): self + { + if ($this->projects->contains($project)) { + $this->projects->removeElement($project); + $project->removeUser($this); + } + + return $this; + } + + /** + * @return Collection|Comment[] + */ + public function getComments(): Collection + { + return $this->comments; + } + + public function addComment(Comment $comment): self + { + if (!$this->comments->contains($comment)) { + $this->comments[] = $comment; + $comment->setAuthor($this); + } + + return $this; + } + + public function removeComment(Comment $comment): self + { + if ($this->comments->contains($comment)) { + $this->comments->removeElement($comment); + // set the owning side to null (unless already changed) + if ($comment->getAuthor() === $this) { + $comment->setAuthor(null); + } + } + + return $this; + } +} diff --git a/backend/src/Http/DataResponse.php b/backend/src/Http/DataResponse.php new file mode 100644 index 0000000..e52dd0d --- /dev/null +++ b/backend/src/Http/DataResponse.php @@ -0,0 +1,16 @@ + $data], + $status, + $headers, + $json, + ); + } +} \ No newline at end of file diff --git a/backend/src/Http/ErrorResponse.php b/backend/src/Http/ErrorResponse.php new file mode 100644 index 0000000..0d6a53c --- /dev/null +++ b/backend/src/Http/ErrorResponse.php @@ -0,0 +1,22 @@ + [ + 'code' => $code, + 'message' => $message, + 'data' => $data, + ], + ], + $status, + $headers, + $json, + ); + } +} \ No newline at end of file diff --git a/backend/src/Migrations/Version20200217203938.php b/backend/src/Migrations/Version20200217203938.php new file mode 100644 index 0000000..e179c33 --- /dev/null +++ b/backend/src/Migrations/Version20200217203938.php @@ -0,0 +1,57 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE request_status (id INT AUTO_INCREMENT NOT NULL, label VARCHAR(64) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE comment (id INT AUTO_INCREMENT NOT NULL, request_id INT NOT NULL, author_id INT NOT NULL, created_at DATETIME NOT NULL, text LONGTEXT NOT NULL, INDEX IDX_9474526C427EB8A5 (request_id), INDEX IDX_9474526CF675F31B (author_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE project (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE project_user (project_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_B4021E51166D1F9C (project_id), INDEX IDX_B4021E51A76ED395 (user_id), PRIMARY KEY(project_id, user_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE request (id INT AUTO_INCREMENT NOT NULL, author_id INT NOT NULL, project_id INT NOT NULL, title VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_3B978F9FF675F31B (author_id), INDEX IDX_3B978F9F166D1F9C (project_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C427EB8A5 FOREIGN KEY (request_id) REFERENCES request (id)'); + $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526CF675F31B FOREIGN KEY (author_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE project_user ADD CONSTRAINT FK_B4021E51166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE project_user ADD CONSTRAINT FK_B4021E51A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE request ADD CONSTRAINT FK_3B978F9FF675F31B FOREIGN KEY (author_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE request ADD CONSTRAINT FK_3B978F9F166D1F9C FOREIGN KEY (project_id) REFERENCES project (id)'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526CF675F31B'); + $this->addSql('ALTER TABLE project_user DROP FOREIGN KEY FK_B4021E51A76ED395'); + $this->addSql('ALTER TABLE request DROP FOREIGN KEY FK_3B978F9FF675F31B'); + $this->addSql('ALTER TABLE project_user DROP FOREIGN KEY FK_B4021E51166D1F9C'); + $this->addSql('ALTER TABLE request DROP FOREIGN KEY FK_3B978F9F166D1F9C'); + $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526C427EB8A5'); + $this->addSql('DROP TABLE user'); + $this->addSql('DROP TABLE request_status'); + $this->addSql('DROP TABLE comment'); + $this->addSql('DROP TABLE project'); + $this->addSql('DROP TABLE project_user'); + $this->addSql('DROP TABLE request'); + } +} diff --git a/backend/src/Migrations/Version20200217211954.php b/backend/src/Migrations/Version20200217211954.php new file mode 100644 index 0000000..1a72a3b --- /dev/null +++ b/backend/src/Migrations/Version20200217211954.php @@ -0,0 +1,39 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE request ADD status_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE request ADD CONSTRAINT FK_3B978F9F6BF700BD FOREIGN KEY (status_id) REFERENCES request_status (id)'); + $this->addSql('CREATE INDEX IDX_3B978F9F6BF700BD ON request (status_id)'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE request DROP FOREIGN KEY FK_3B978F9F6BF700BD'); + $this->addSql('DROP INDEX IDX_3B978F9F6BF700BD ON request'); + $this->addSql('ALTER TABLE request DROP status_id'); + } +} diff --git a/backend/src/Repository/CommentRepository.php b/backend/src/Repository/CommentRepository.php new file mode 100644 index 0000000..cddf55d --- /dev/null +++ b/backend/src/Repository/CommentRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('c.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Comment + { + return $this->createQueryBuilder('c') + ->andWhere('c.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/backend/src/Repository/ProjectRepository.php b/backend/src/Repository/ProjectRepository.php new file mode 100644 index 0000000..50da37f --- /dev/null +++ b/backend/src/Repository/ProjectRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('p.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Project + { + return $this->createQueryBuilder('p') + ->andWhere('p.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/backend/src/Repository/RequestRepository.php b/backend/src/Repository/RequestRepository.php new file mode 100644 index 0000000..6047434 --- /dev/null +++ b/backend/src/Repository/RequestRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('r') + ->andWhere('r.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('r.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?Request + { + return $this->createQueryBuilder('r') + ->andWhere('r.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/backend/src/Repository/RequestStatusRepository.php b/backend/src/Repository/RequestStatusRepository.php new file mode 100644 index 0000000..be45c5e --- /dev/null +++ b/backend/src/Repository/RequestStatusRepository.php @@ -0,0 +1,50 @@ +createQueryBuilder('r') + ->andWhere('r.exampleField = :val') + ->setParameter('val', $value) + ->orderBy('r.id', 'ASC') + ->setMaxResults(10) + ->getQuery() + ->getResult() + ; + } + */ + + /* + public function findOneBySomeField($value): ?RequestStatus + { + return $this->createQueryBuilder('r') + ->andWhere('r.exampleField = :val') + ->setParameter('val', $value) + ->getQuery() + ->getOneOrNullResult() + ; + } + */ +} diff --git a/backend/src/Repository/UserRepository.php b/backend/src/Repository/UserRepository.php new file mode 100644 index 0000000..b2bdfd6 --- /dev/null +++ b/backend/src/Repository/UserRepository.php @@ -0,0 +1,39 @@ +setPassword($newEncodedPassword); + $this->_em->persist($user); + $this->_em->flush(); + } + +} diff --git a/backend/symfony.lock b/backend/symfony.lock index d455b1c..aeacb52 100644 --- a/backend/symfony.lock +++ b/backend/symfony.lock @@ -20,6 +20,9 @@ "doctrine/common": { "version": "2.12.0" }, + "doctrine/data-fixtures": { + "version": "1.4.2" + }, "doctrine/dbal": { "version": "v2.10.1" }, @@ -38,6 +41,18 @@ "src/Repository/.gitignore" ] }, + "doctrine/doctrine-fixtures-bundle": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.0", + "ref": "fc52d86631a6dfd9fdf3381d0b7e3df2069e51b3" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, "doctrine/doctrine-migrations-bundle": { "version": "1.2", "recipe": { @@ -87,6 +102,18 @@ "laminas/laminas-zendframework-bridge": { "version": "1.0.1" }, + "nelmio/cors-bundle": { + "version": "1.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.5", + "ref": "6388de23860284db9acce0a7a5d9d13153bcb571" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, "nikic/php-parser": { "version": "v4.3.0" }, @@ -108,6 +135,18 @@ "psr/log": { "version": "1.1.2" }, + "sensio/framework-extra-bundle": { + "version": "5.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "5.2", + "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" + }, + "files": [ + "config/packages/sensio_framework_extra.yaml" + ] + }, "symfony/cache": { "version": "v4.4.4" }, diff --git a/docker-compose.yml b/docker-compose.yml index fafa672..56890ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,9 @@ services: MYSQL_PASSWORD: logo ports: - 3306:3306 + command: + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" volumes: - db_data:/var/lib/mysql diff --git a/frontend/src/actions/.gitkeep b/frontend/src/actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/actions/chat.js b/frontend/src/actions/chat.js deleted file mode 100644 index c2b5742..0000000 --- a/frontend/src/actions/chat.js +++ /dev/null @@ -1,20 +0,0 @@ -export const SEND_MESSAGE = 'SEND_MESSAGE' -export const SEND_MESSAGE_SUCCESS = 'SEND_MESSAGE_SUCCESS'; -export const SEND_MESSAGE_FAILURE = 'SEND_MESSAGE_FAILURE'; - -export function sendMessage (channel, text) { - return { type: SEND_MESSAGE, channel, text } -} - -export const FETCH_MESSAGES = 'FETCH_MESSAGES' -export const FETCH_MESSAGES_SUCCESS = 'FETCH_MESSAGES_SUCCESS'; -export const FETCH_MESSAGES_FAILURE = 'FETCH_MESSAGES_FAILURE'; - -export function fetchMessages (channel) { - return { type: FETCH_MESSAGES, channel } -} - -export const STREAM_EVENTS = 'STREAM_EVENTS' -export function streamEvents (channel) { - return { type: STREAM_EVENTS, channel } -} diff --git a/frontend/src/actions/login.js b/frontend/src/actions/login.js deleted file mode 100644 index 3718de5..0000000 --- a/frontend/src/actions/login.js +++ /dev/null @@ -1,7 +0,0 @@ -export const LOGIN = 'LOGIN' -export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; -export const LOGIN_FAILURE = 'LOGIN_FAILURE'; - -export function login (username, password) { - return { type: LOGIN, username, password } -} diff --git a/frontend/src/actions/products.js b/frontend/src/actions/products.js deleted file mode 100644 index c915260..0000000 --- a/frontend/src/actions/products.js +++ /dev/null @@ -1,11 +0,0 @@ -export const ADD_PRODUCT = 'ADD_PRODUCT' - -export function addProduct (name, price) { - return {type: ADD_PRODUCT, product: {name, price}} -} - -export const REMOVE_PRODUCT = 'REMOVE_PRODUCT' - -export function removeProduct (name) { - return {type: REMOVE_PRODUCT, productName: name} -} \ No newline at end of file diff --git a/frontend/src/app.js b/frontend/src/app.js index 4a3fd6c..886df5a 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1,12 +1,8 @@ - import { Component, Fragment } from 'react' import { hot } from 'react-hot-loader' import { HashRouter } from 'react-router-dom' // ou BrowserRouter import { Route, Switch, Redirect } from 'react-router' -import LoginPage from './pages/login'; -import ChatPage from './pages/chat'; - -require('bulma/css/bulma.min.css') +import HomePage from './pages/home'; class App extends Component { render () { @@ -14,10 +10,8 @@ class App extends Component { - - - - } /> + + } /> diff --git a/frontend/src/components/.gitkeep b/frontend/src/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/__snapshots__/counter.test.js.snap b/frontend/src/components/__snapshots__/counter.test.js.snap deleted file mode 100644 index 8e0e769..0000000 --- a/frontend/src/components/__snapshots__/counter.test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Counter snapshot 1`] = ` -
- Count: - - 0 - -   - - -
-`; diff --git a/frontend/src/components/clock.js b/frontend/src/components/clock.js deleted file mode 100644 index 71370d7..0000000 --- a/frontend/src/components/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' - -export default class Clock extends React.Component { - - constructor(props) { - super(props) - // Initialisation du "state" du composant - this.state = { - time: new Date(), - foo: "bar" - } - - this.tick = this.tick.bind(this); - // On appelle la méthode tick() du composant - // toutes les secondes - setInterval(this.tick, this.props.interval); - } - - // Méthode de rendu du composant - render() { - return ( -
Time: { this.state.time.toString() }
- ) - } - - // La méthode tick() met à jour le state du composant avec - // la date courante - tick() { - this.setState({ time: new Date() }); - } - -} diff --git a/frontend/src/components/counter.js b/frontend/src/components/counter.js deleted file mode 100644 index d7b2900..0000000 --- a/frontend/src/components/counter.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' - -export default class Counter extends React.Component { - constructor(props) { - super(props) - // Initialisation du "state" du composant - this.state = { - count: 0 - } - // On "lie" les méthodes de la classe à l'instance - this.increment = this.increment.bind(this) - this.decrement = this.decrement.bind(this) - } - // Méthode de rendu du composant - render() { - console.log(this.props.match); - return ( -
- Count: { this.state.count }  - - -
- ) - } - // La méthode increment() incrémente la valeur du compteur de 1 - increment() { - this.setState(prevState => ({ count: prevState.count+1 })) - } - // La méthode decrement() décrémente la valeur du compteur de 1 - decrement() { - this.setState(prevState => ({ count: prevState.count-1 })) - } -} \ No newline at end of file diff --git a/frontend/src/components/counter.test.js b/frontend/src/components/counter.test.js deleted file mode 100644 index d83869f..0000000 --- a/frontend/src/components/counter.test.js +++ /dev/null @@ -1,26 +0,0 @@ -/* globals test, expect */ -import React from 'react'; -import Counter from './counter' -import renderer from 'react-test-renderer' - -test('Counter snapshot', () => { - - const component = renderer.create() - - let tree = component.toJSON() - - // Vérifier que le composant n'a pas changé depuis le dernier - // snapshot. - // Voir https://facebook.github.io/jest/docs/en/snapshot-testing.html - // pour plus d'informations - expect(tree).toMatchSnapshot() - - // L'API expect() de Jest est disponible à l'adresse - // https://facebook.github.io/jest/docs/en/expect.html - - // Il est possible d'effectuer des vérifications plus avancées - // grâce au projet Enzyme (vérification du DOM, etc) - // Voir http://airbnb.io/enzyme/ et - // https://facebook.github.io/jest/docs/en/tutorial-react.html#dom-testing - -}) \ No newline at end of file diff --git a/frontend/src/components/myform.js b/frontend/src/components/myform.js deleted file mode 100644 index b5e5842..0000000 --- a/frontend/src/components/myform.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import { addProduct } from '../actions/products' - - -class MyForm extends React.Component { - - constructor(props) { - super(props); - this.state = {name: ''}; - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleChange(evt) { - this.setState({ name: evt.target.value }); - } - - handleSubmit(evt) { - console.log(`Votre nom est ${this.state.name}`); - evt.preventDefault(); - } - - componentDidMount() { - this.props.dispatch(addProduct('pomme', 10)); - } - - render() { - return ( -
- - -
- ); - } -} - -export default connect()(MyForm) diff --git a/frontend/src/index.html b/frontend/src/index.html index c8f67c0..ccc8d7d 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -2,7 +2,7 @@ - Insight + PleaseWait <% for (var css in htmlWebpackPlugin.files.css) { %> <% } %> diff --git a/frontend/src/index.js b/frontend/src/index.js index c9bddeb..3c6acab 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -2,6 +2,7 @@ import ReactDOM from 'react-dom' import App from './app' import { configureStore } from './store/store' import { Provider } from 'react-redux' +import 'bulma/css/bulma.min.css'; const store = configureStore() diff --git a/frontend/src/pages/chat.jsx b/frontend/src/pages/chat.jsx deleted file mode 100644 index 0a0470f..0000000 --- a/frontend/src/pages/chat.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react' -import Page from './page'; -import { connect } from 'react-redux'; -import { fetchMessages, sendMessage, streamEvents } from '../actions/chat'; - -export class ChatPage extends React.Component { - - constructor(props) { - super(props); - this.state = { - message: '' - } - this.handleSendMessage = this.handleSendMessage.bind(this); - this.handleKeyDown = this.handleKeyDown.bind(this); - } - - render() { - const channel = "default"; - const { chat } = this.props; - const messages = channel in chat.messagesByChannel ? chat.messagesByChannel[channel] : []; - const users = []; - return ( - -
-
-
-
-
-
- Chat -
    - { - messages.map(msg => { - return ( -
  • [{msg.Username}] {msg.Text}
  • - ) - }) - } -
-
-
- -
-
-
-
-
- Utilisateurs -
-
-
-
-
-
- ); - } - - componentDidMount() { - this.props.dispatch(fetchMessages("default")); - this.props.dispatch(streamEvents("default")); - } - - handleSendMessage(evt) { - this.setState({ message: evt.target.value }); - } - - handleKeyDown(evt) { - if (evt.keyCode !== 13) return; - this.props.dispatch(sendMessage("default", this.state.message)); - this.setState({message: ""}); - } - -} - -export default connect(state => { - return { - chat: state.chat - } -})(ChatPage) \ No newline at end of file diff --git a/frontend/src/pages/home.js b/frontend/src/pages/home.js new file mode 100644 index 0000000..4392a47 --- /dev/null +++ b/frontend/src/pages/home.js @@ -0,0 +1,15 @@ +import React from 'react' +import Page from './page'; + +export default class HomePage extends React.PureComponent { + render() { + return ( + +
+

Bienvenue sur PleaseWait !

+

Le gestionnaire de ticket simplifié.

+
+
+ ); + } +} \ No newline at end of file diff --git a/frontend/src/pages/login.jsx b/frontend/src/pages/login.jsx deleted file mode 100644 index f1e23e8..0000000 --- a/frontend/src/pages/login.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import Page from './page'; -import { connect } from 'react-redux'; -import { login } from '../actions/login'; - -export class LoginPage extends React.Component { - - constructor(props) { - super(props); - this.login = this.login.bind(this); - } - - render() { - return ( - -
-
-
-
- -
- -
-
-
- -
- -
-
- -
-
-
-
- ); - } - - componentDidUpdate() { - if (this.props.user.isLoggedIn) this.props.history.push("/chat"); - } - - login() { - this.props.dispatch(login("foo", "bar")) - } - - } - - export default connect(state => { - return { - user: state.user - } - })(LoginPage) \ No newline at end of file diff --git a/frontend/src/pages/page.js b/frontend/src/pages/page.js index bca88d5..fb32593 100644 --- a/frontend/src/pages/page.js +++ b/frontend/src/pages/page.js @@ -1,11 +1,13 @@ -import React from 'react' +import React, { useEffect } from 'react' -export default class Page extends React.PureComponent { - render() { - return ( -
- { this.props.children } -
- ); - } +export default function Page({ title, children }) { + useEffect(() => { + document.title = title ? `${title } - PleaseWait` : 'PleaseWait'; + }); + + return ( +
+ { children } +
+ ); } \ No newline at end of file diff --git a/frontend/src/reducers/.gitkeep b/frontend/src/reducers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/reducers/chat.js b/frontend/src/reducers/chat.js deleted file mode 100644 index 6b0c86a..0000000 --- a/frontend/src/reducers/chat.js +++ /dev/null @@ -1,35 +0,0 @@ -import { LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/login'; -import { FETCH_MESSAGES_SUCCESS } from '../actions/chat'; - -const defaultState = { - messagesByChannel: {}, -} - -export default function chatReducer(state = defaultState, action) { - switch (action.type) { - case FETCH_MESSAGES_SUCCESS: - return { - ...state, - messagesByChannel: { - ...state.messagesByChannel, - [action.channel]: [...action.data.Messages] - } - }; - case 'CHANNEL_EVENT': - switch(action.event) { - case "message": - return { - ...state, - messagesByChannel: { - ...state.messagesByChannel, - [action.data.Channel]: [ - ...state.messagesByChannel[action.data.Channel], - action.data.Message - ] - } - }; - } - return state; - } - return state; -} diff --git a/frontend/src/reducers/login.js b/frontend/src/reducers/login.js deleted file mode 100644 index bce2420..0000000 --- a/frontend/src/reducers/login.js +++ /dev/null @@ -1,16 +0,0 @@ -import { LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/login'; - -const defaultState = { - isLoggedIn: false, - username: null, -} - -export default function loginReducer(state = defaultState, action) { - switch (action.type) { - case LOGIN_SUCCESS: - return { ...state, isLoggedIn: true, username: action.data.Username }; - case LOGIN_FAILURE: - return { ...state, isLoggedIn: false, username: null }; - } - return state; -} diff --git a/frontend/src/reducers/root.js b/frontend/src/reducers/root.js deleted file mode 100644 index 01bb407..0000000 --- a/frontend/src/reducers/root.js +++ /dev/null @@ -1,31 +0,0 @@ -import { ADD_PRODUCT, REMOVE_PRODUCT } from '../actions/products'; - -export const rootReducer = (state, action) => { - - console.log(`Action: ${JSON.stringify(action)}`) - - switch (action.type) { - - case ADD_PRODUCT: - // L'action est de type ADD_PRODUCT - // On ajoute le produit dans la liste et - // on retourne un nouvel état modifié - return { - products: [...state.products, action.product] - } - - - case REMOVE_PRODUCT: - // L'action est de type REMOVE_PRODUCT - // On filtre la liste des produits et on - // retourne un nouvel état modifié - return { - products: state.products.filter(p => p.name !== action.productName) - } - } - - // Si l'action n'est pas gérée, on retourne l'état - // sans le modifier - return state - -} \ No newline at end of file diff --git a/frontend/src/sagas/chat.js b/frontend/src/sagas/chat.js deleted file mode 100644 index 272894c..0000000 --- a/frontend/src/sagas/chat.js +++ /dev/null @@ -1,98 +0,0 @@ -import { call, put, take } from 'redux-saga/effects'; -import { eventChannel } from 'redux-saga' -import { - SEND_MESSAGE_FAILURE, SEND_MESSAGE_SUCCESS, - FETCH_MESSAGES_FAILURE, FETCH_MESSAGES_SUCCESS -} from '../actions/chat'; - -export function* sendMessageSaga(action) { - - let result; - try { - result = yield call(sendMessage, action.channel, action.text); - } catch(err) { - yield put({ type: SEND_MESSAGE_FAILURE, err }); - } - - if ('Error' in result) { - yield put({type: SEND_MESSAGE_FAILURE, err: result.Error}); - return - } - - yield put({type: SEND_MESSAGE_SUCCESS, data: result.Data }); - -} - - -function sendMessage(channel, text) { - return fetch(`http://192.168.0.126:3000/channels/${channel}`, { - method: 'POST', - body: JSON.stringify({ - Text: text, - }), - mode: 'cors', - credentials: 'include' -}) -.then(res => res.json()) -} - -export function* fetchMessagesSaga(action) { - - let result; - try { - result = yield call(fetchMessages, action.channel); - } catch(err) { - yield put({ type: FETCH_MESSAGES_FAILURE, err }); - } - - if ('Error' in result) { - yield put({type: FETCH_MESSAGES_FAILURE, err: result.Error}); - return - } - - yield put({type: FETCH_MESSAGES_SUCCESS, channel: action.channel, data: result.Data }); - -} - - -function fetchMessages(channel) { - return fetch(`http://192.168.0.126:3000/channels/${channel}`, { - mode: 'cors', - credentials: 'include' - }) - .then(res => res.json()) -} - -function channelEvents(channel) { - return eventChannel(emitter => { - - const eventSource = new EventSource( - `http://192.168.0.126:3000/channels/${channel}/stream`, - { withCredentials: true } - ); - - const emit = evt => { - emitter({type: evt.type, data: evt.data}); - }; - - eventSource.addEventListener("joined", emit); - eventSource.addEventListener("left", emit); - eventSource.addEventListener("message", emit); - - return () => { - eventSource.removeEventListener("joined", emit) - eventSource.removeEventListener("left", emit) - eventSource.removeEventListener("message", emit) - eventSource.close() - } - } - ) -} - -export function* streamEventsSaga(action) { - const stream = yield call(channelEvents, action.channel) - while (true) { - let event = yield take(stream) - yield put({ type: 'CHANNEL_EVENT', event: event.type, data: JSON.parse(event.data) }); - } -} \ No newline at end of file diff --git a/frontend/src/sagas/login.js b/frontend/src/sagas/login.js deleted file mode 100644 index b96cc93..0000000 --- a/frontend/src/sagas/login.js +++ /dev/null @@ -1,34 +0,0 @@ -import { call, put } from 'redux-saga/effects'; -import { LOGIN_FAILURE, LOGIN_SUCCESS } from '../actions/login'; - -export default function* loginSaga(action) { - - let result; - try { - result = yield call(doLogin, action.username, action.password); - } catch(err) { - yield put({ type: LOGIN_FAILURE, err }); - } - - if ('Error' in result) { - yield put({type: LOGIN_FAILURE, err: result.Error}); - return - } - - yield put({type: LOGIN_SUCCESS, data: result.Data }); - -} - - -function doLogin(username, password) { - return fetch('http://192.168.0.126:3000/login', { - method: 'POST', - body: JSON.stringify({ - Username: username, - Password: password - }), - mode: 'cors', - credentials: 'include' -}) -.then(res => res.json()) -} diff --git a/frontend/src/sagas/root.js b/frontend/src/sagas/root.js index ee0ecd8..879634f 100644 --- a/frontend/src/sagas/root.js +++ b/frontend/src/sagas/root.js @@ -1,14 +1,7 @@ import { all, takeLatest } from 'redux-saga/effects'; -import loginSaga from './login'; -import { LOGIN } from '../actions/login'; -import { SEND_MESSAGE, FETCH_MESSAGES, STREAM_EVENTS } from '../actions/chat'; -import { sendMessageSaga, fetchMessagesSaga, streamEventsSaga } from './chat'; export default function* rootSaga() { yield all([ - takeLatest(LOGIN, loginSaga), - takeLatest(SEND_MESSAGE, sendMessageSaga), - takeLatest(FETCH_MESSAGES, fetchMessagesSaga), - takeLatest(STREAM_EVENTS, streamEventsSaga) + ]); } diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 27f65f0..521df0a 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -1,14 +1,11 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux' -import loginReducer from '../reducers/login' -import rootSaga from '../sagas/root' import createSagaMiddleware from 'redux-saga' -import chatReducer from '../reducers/chat'; +import rootSaga from '../sagas/root' const sagaMiddleware = createSagaMiddleware() const rootReducer = combineReducers({ - user: loginReducer, - chat: chatReducer, + // Ajouter vos reducers ici }); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js deleted file mode 100644 index 34f96f2..0000000 --- a/frontend/src/store/store.test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* globals test, expect, jest */ -import { addProduct, removeProduct } from '../actions/products' -import { configureStore } from './store' - -test('Ajout/suppression des produits', () => { - // On crée une instance de notre store - // avec le state par défaut - const store = configureStore() - - // On crée un "faux" subscriber - // pour vérifier que l'état du store - // a bien été modifié le nombre de fois voulu - const subscriber = jest.fn() - - // On attache notre faux subscriber - // au store - store.subscribe(subscriber) - - // On "dispatch" nos actions - store.dispatch(addProduct('pomme', 5)) - store.dispatch(addProduct('orange', 7)) - store.dispatch(addProduct('orange', 10)) - store.dispatch(removeProduct('pomme')) - - // On s'assure que notre subscriber a bien été - // appelé - expect(subscriber).toHaveBeenCalledTimes(4) - - const state = store.getState() - - // On s'assure que l'état du store correspond - // à ce qu'on attend - expect(state).toMatchObject({ - products: [ - {name: 'orange', price: 7} - ] - }) - -}) \ No newline at end of file diff --git a/misc/containers/backend/docker-entrypoint.sh b/misc/containers/backend/docker-entrypoint.sh index e9e3b2b..c7c7f80 100644 --- a/misc/containers/backend/docker-entrypoint.sh +++ b/misc/containers/backend/docker-entrypoint.sh @@ -12,6 +12,9 @@ if [ ! -e "$FIRST_RUN_FLAG_FILE" ]; then echo "Applying database migrations. Please wait..." bin/console doctrine:migrations:migrate --no-interaction + echo "Loading fixtures. Please wait..." + bin/console doctrine:fixtures:load --no-interaction + touch "$FIRST_RUN_FLAG_FILE" fi diff --git a/misc/projects/ticketing_app.http b/misc/projects/ticketing_app.http new file mode 100644 index 0000000..31169d5 --- /dev/null +++ b/misc/projects/ticketing_app.http @@ -0,0 +1,44 @@ +@baseURL = http://localhost:8001/api/v1 + +### + +GET {{baseURL}} + +### + +// Login as "client1" + +POST {{baseURL}}/login +Content-Type: application/json + +{ "username": "client1", "password": "client1" } + +### + +// Login as "dev1" + +POST {{baseURL}}/login +Content-Type: application/json + +{ "username": "dev1", "password": "dev1" } + +### + +// Logout + +GET {{baseURL}}/logout +Content-Type: application/json + +### + +// Get current user info + +GET {{baseURL}}/me +Content-Type: application/json + +### + +// List users + +GET {{baseURL}}/users +Content-Type: application/json \ No newline at end of file diff --git a/misc/projects/ticketing_app.md b/misc/projects/ticketing_app.md new file mode 100644 index 0000000..91c15f4 --- /dev/null +++ b/misc/projects/ticketing_app.md @@ -0,0 +1,71 @@ +# Application de suivi des demandes client + +## Objectif + +Implémenter une application de suivi des demandes client proposant les fonctionnalités suivantes: + +- Authentification par identifiant et mot de passe +- Autorisation par rôles: + - "client" + - "développeur" +- Interfaces pour le rôle "client": + - Une interface de visualisation des projets qui lui sont associés + - Une interface de création d'une nouvelle demande pour un projet donné + - Une interface de visualisation des demandes en cours/cloturée pour un projet donné + - Une interface de visualisation de détails d'une demande et des commentaires associés, avec possibilité d'ajouter un commentaire + - Une interface de création d'une nouvelle demande pour un projet donné +- Interfaces pour le rôle "développeur": + - Une interface de création d'un nouveau projet + - Une interface de création d'un nouveau compte client + - Une interface de visualisation des clients (listing) + - Une interface de visualisation des projets (listing) + - Une interface de visualisation des dernières demandes ouvertes (multi-projet) + - Une interface de visualisation de détails d'une demande et des commentaires associés, avec possibilité d'ajouter un commentaire et de changer l'état d'une demande + +## Modèle de données + +### User (`User`) + +#### Attributs + +- `id:int (unique)` Identifiant (clé primaire) de l'utilisateur +- `username:string (unique)` Nom de l'utilisateur +- `password:string` Mot de passe de l'utilisateur +- `projects:[]Project` Projets associés à l'utilisateur + +### Projet (`Project`) + +#### Attributs + +- `id:int (unique)` Identifiant (clé primaire) du projet +- `name:string` Nom du projet +- `users:[]User` Utilisateurs attachés au projet + +### Demande (`Request`) + +#### Attributs + +- `id:int (unique)` Identifiant (clé primaire) de la demande +- `title:string` Titre associé à la demande +- `author:User` Auteur de la demande (clé étrangère) +- `project:Project` Identifiant du projet associé (clé étrangère) +- `status:RequestStatus` Identifiant du statut courant de la demande +- `comments:[]Comment` Commentaire associé à la demande +- `createdAt:Date` Date de la création du commentaire + +### Statut de demande (`RequestStatus`) + +#### Attributs + +- `id:int (unique)` Identifiant (clé primaire) du statut de requête +- `label:string` Label associé au statut + +### Commentaire (`Comment`) + +#### Attributs + +- `id:int (unique)` Identifiant (clé primaire) du commentaires +- `request:Request` Identifiant de la demande associé au commentaire (clé étrangère) +- `text:string` Texte du commentaire +- `createdAt:Date` Date de la création du commentaire +- `author:User` Auteur du commentaire (clé étrangère) \ No newline at end of file diff --git a/misc/projects/ticketing_app.pdf b/misc/projects/ticketing_app.pdf new file mode 100644 index 0000000..a36feae Binary files /dev/null and b/misc/projects/ticketing_app.pdf differ