diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php new file mode 100644 index 0000000..e734c8c --- /dev/null +++ b/src/Controller/DefaultController.php @@ -0,0 +1,30 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + if (!$user->getCompany()) { + return $this->redirectToRoute('app_register_step', ['step' => RegistrationController::REGISTER_STEP_TWO]); + } + + return $this->render( + 'internal/dashboard.html.twig', + [ + 'user' => $user + ] + ); + } +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..63e099c --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,157 @@ + $this->createForm(RegistrationFormType::class), + self::REGISTER_STEP_TWO => $this->renderRegisterStepTwo(), + default => $this->redirectToRoute('app_register_step', ['step' => self::REGISTER_STEP_ONE]), + }; + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + return match(true) { + $step === self::REGISTER_STEP_ONE => $this->handleRegisterStepOne($form, $request), + $step === self::REGISTER_STEP_TWO => $this->handleRegisterStepTwo($form, $user), + default => $this->redirectToRoute('app_register_step', ['step' => self::REGISTER_STEP_ONE]), + }; + } + + return $this->render(sprintf('registration/register-step-%s.html.twig', $step), [ + 'form' => $form, + 'data' => $form->getData(), + 'admin' => $user + ]); + } + + private function handleRegisterStepOne(FormInterface $form, Request $request): Response + { + $user = new User(); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // @var string $plainPassword + $plainPassword = $form->get('plainPassword')->getData(); + + // encode the plain password + $user->setPassword( + $this->userPasswordHasher->hashPassword( + $user, + $plainPassword + ) + ); + + $user->setJob(JobType::ADMIN); + $user->setRateType(RateType::FIXED); + $user->setRate('0.00'); + $user->setRoles(['ROLE_ADMIN']); + $user->setLevel(CaseLevel::ADMIN); + + // save user + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $this->redirectToRoute('app_dashboard'); + } + + return $this->redirectToRoute('app_register_step', ['step' => self::REGISTER_STEP_ONE]); + } + + private function renderRegisterStepTwo(): FormInterface + { + $company = $this->requestStack->getSession()->get('register-form-step-two'); + + if (!$company instanceof CompanyDetailsDto) { + $company = new CompanyDetailsDto(); + } + + return $this->createForm(CompanyFormType::class, $company); + } + + private function handleRegisterStepTwo(FormInterface $form, User $owner): Response + { + $company = $this->companyFactory->create($form->getData(), $owner); + + $owner->setCompany($company); + $this->entityManager->persist($owner); + $this->entityManager->persist($company); + $this->entityManager->flush(); + + return $this->redirectToRoute('app_dashboard'); + } + + #[Route('/new-user', name: 'app_new_user')] + public function newUser(Request $request): Response + { + return $this->render('registration/new-user.html.twig'); + } + + #[Route('/add-user', name: 'app_add_user')] + public function addUser(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response + { + $user = new User(); + $user->setUsername('new-user'); + $user->setEmail('g6eK1@example.com'); + $user->setName('New User'); + $user->setPassword( + $this->userPasswordHasher->hashPassword( + $user, + 'password' + ) + ); + $user->setJob(JobType::ADMIN); + $user->setRateType(RateType::FIXED); + $user->setRate('0.00'); + + $entityManager->persist($user); + $entityManager->flush(); + + return $this->redirectToRoute('dashboard'); + } + + #[Route('/register', name: 'app_register')] + public function register(): Response + { + return $this->redirectToRoute('app_register_step', ['step' => self::REGISTER_STEP_ONE]); + } + +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..6f23fed --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,34 @@ +isGranted('IS_AUTHENTICATED_FULLY')) { + return $this->redirectToRoute('app_dashboard'); + } + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + #[Route(path: '/logout', name: 'app_logout')] + public function logout(): void + { + } +} diff --git a/src/DataTransferObject/CompanyDetailsDto.php b/src/DataTransferObject/CompanyDetailsDto.php new file mode 100644 index 0000000..6162154 --- /dev/null +++ b/src/DataTransferObject/CompanyDetailsDto.php @@ -0,0 +1,199 @@ +name; + } + + /** + * Set the value of name + * + * @return self + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the value of address + */ + public function getAddress() + { + return $this->address; + } + + /** + * Set the value of address + * + * @return self + */ + public function setAddress($address) + { + $this->address = $address; + + return $this; + } + + /** + * Get the value of email + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set the value of email + * + * @return self + */ + public function setEmail($email) + { + $this->email = $email; + + return $this; + } + + /** + * Get the value of phone + */ + public function getPhone() + { + return $this->phone; + } + + /** + * Set the value of phone + * + * @return self + */ + public function setPhone($phone) + { + $this->phone = $phone; + + return $this; + } + + /** + * Get the value of url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the value of url + * + * @return self + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Get the value of zip + */ + public function getZip() + { + return $this->zip; + } + + /** + * Set the value of zip + * + * @return self + */ + public function setZip($zip) + { + $this->zip = $zip; + + return $this; + } + + /** + * Get the value of city + */ + public function getCity() + { + return $this->city; + } + + /** + * Set the value of city + * + * @return self + */ + public function setCity($city) + { + $this->city = $city; + + return $this; + } + + /** + * Get the value of state + */ + public function getState() + { + return $this->state; + } + + /** + * Set the value of state + * + * @return self + */ + public function setState($state) + { + $this->state = $state; + + return $this; + } + + /** + * Get the value of owner + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Set the value of owner + * + * @return self + */ + public function setOwner($owner) + { + $this->owner = $owner; + + return $this; + } +} diff --git a/src/Entity/Company.php b/src/Entity/Company.php new file mode 100644 index 0000000..ffcf2c6 --- /dev/null +++ b/src/Entity/Company.php @@ -0,0 +1,202 @@ + + */ + #[ORM\OneToMany(targetEntity: User::class, mappedBy: 'company')] + private Collection $users; + + #[ORM\OneToOne(cascade: ['persist', 'remove'])] + #[ORM\JoinColumn(nullable: false)] + private ?User $owner = null; + + public function __construct() + { + $this->users = new ArrayCollection(); + } + + public function getId(): ?Uuid + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getAddress(): ?string + { + return $this->address; + } + + public function setAddress(string $address): static + { + $this->address = $address; + + return $this; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(string $city): static + { + $this->city = $city; + + return $this; + } + + public function getState(): ?string + { + return $this->state; + } + + public function setState(string $state): static + { + $this->state = $state; + + return $this; + } + + public function getZip(): ?string + { + return $this->zip; + } + + public function setZip(string $zip): static + { + $this->zip = $zip; + + return $this; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(string $phone): static + { + $this->phone = $phone; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(?string $url): static + { + $this->url = $url; + + return $this; + } + + /** + * @return Collection + */ + public function getUsers(): Collection + { + return $this->users; + } + + public function addUser(User $user): static + { + if (!$this->users->contains($user)) { + $this->users->add($user); + $user->setCompany($this); + } + + return $this; + } + + public function removeUser(User $user): static + { + if ($this->users->removeElement($user)) { + // set the owning side to null (unless already changed) + if ($user->getCompany() === $this) { + $user->setCompany(null); + } + } + + return $this; + } + + public function getOwner(): ?User + { + return $this->owner; + } + + public function setOwner(User $owner): static + { + $this->owner = $owner; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..9a72eae --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,267 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + + #[ORM\Column(length: 45)] + private ?string $name = null; + + #[ORM\Column(length: 45)] + private ?string $email = null; + + #[ORM\Column(length: 45)] + private ?JobType $job = null; + + #[ORM\Column(enumType: RateType::class)] + private ?RateType $rateType = null; + + #[ORM\Column(type: Types::DECIMAL, precision: 6, scale: 2)] + private ?string $rate = null; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: UserCase::class, mappedBy: 'userId')] + private Collection $userCases; + + #[ORM\ManyToOne(inversedBy: 'users')] + #[ORM\JoinColumn(nullable: true)] + private ?Company $company = null; + + #[ORM\Column(enumType: CaseLevel::class)] + private ?CaseLevel $level = null; + + public function __construct() + { + $this->userCases = new ArrayCollection(); + } + + public function getId(): ?Uuid + { + return $this->id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = $username; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->username; + } + + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getJob(): ?JobType + { + return $this->job; + } + + public function setJob(JobType $job): static + { + $this->job = $job; + + return $this; + } + + public function getRateType(): ?RateType + { + return $this->rateType; + } + + public function setRateType(RateType $rateType): static + { + $this->rateType = $rateType; + + return $this; + } + + public function getRate(): ?string + { + return $this->rate; + } + + public function setRate(string $rate): static + { + $this->rate = $rate; + + return $this; + } + + /** + * @return Collection + */ + public function getUserCases(): Collection + { + return $this->userCases; + } + + public function addUserCase(UserCase $userCase): static + { + if (!$this->userCases->contains($userCase)) { + $this->userCases->add($userCase); + $userCase->setUserId($this); + } + + return $this; + } + + public function removeUserCase(UserCase $userCase): static + { + if ($this->userCases->removeElement($userCase)) { + // set the owning side to null (unless already changed) + if ($userCase->getUserId() === $this) { + $userCase->setUserId(null); + } + } + + return $this; + } + + public function getCompany(): ?Company + { + return $this->company; + } + + public function setCompany(?Company $company): static + { + $this->company = $company; + + return $this; + } + + public function getLevel(): ?CaseLevel + { + return $this->level; + } + + public function setLevel(CaseLevel $level): static + { + $this->level = $level; + + return $this; + } +} diff --git a/src/Factory/CompanyFactory.php b/src/Factory/CompanyFactory.php new file mode 100644 index 0000000..62c2518 --- /dev/null +++ b/src/Factory/CompanyFactory.php @@ -0,0 +1,29 @@ +setName($companyDetails->getName()); + $company->setAddress($companyDetails->getAddress()); + $company->setCity($companyDetails->getCity()); + $company->setState($companyDetails->getState()); + $company->setZip($companyDetails->getZip()); + $company->setPhone($companyDetails->getPhone()); + $company->setEmail($companyDetails->getEmail()); + $company->setUrl($companyDetails->getUrl()); + + $company->setOwner($ownerDetails); + + return $company; + } +} diff --git a/src/Form/CompanyFormType.php b/src/Form/CompanyFormType.php new file mode 100644 index 0000000..b5f7576 --- /dev/null +++ b/src/Form/CompanyFormType.php @@ -0,0 +1,55 @@ +add('name', TextType::class, [ + 'required' => true, + 'attr' => [ + 'placeholder' => 'Company Name', + 'class' => 'form-control', + 'autofocus' => true, + 'autocomplete' => 'off' + ] + ]) + ->add('address', TextType::class, [ + 'required' => true + ]) + ->add('city', TextType::class, [ + 'required' => true + ]) + ->add('state', TextType::class, [ + 'required' => true + ]) + ->add('zip', TextType::class, [ + 'required' => true + ]) + ->add('phone', TextType::class, [ + 'required' => true + ]) + ->add('email', EmailType::class, [ + 'required' => true + ]) + ->add('url', UrlType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => CompanyDetailsDto::class, + ]); + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..e40bcb1 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,47 @@ +add('name') + ->add('username') + ->add('email') + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Repository/CompanyRepository.php b/src/Repository/CompanyRepository.php new file mode 100644 index 0000000..c0140b5 --- /dev/null +++ b/src/Repository/CompanyRepository.php @@ -0,0 +1,43 @@ + + */ +class CompanyRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Company::class); + } + + // /** + // * @return Company[] Returns an array of Company objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('c.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Company + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..4f2804e --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 3cda30f..6121b2a 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,17 +1,54 @@ - - - - {% block title %}Welcome!{% endblock %} - - {% block stylesheets %} - {% endblock %} + + + + + + + + {% block title %}CM Tracker + {% endblock %} + + + + + + + + + + + + {% block stylesheets %}{% endblock %} + - {% block javascripts %} - {% block importmap %}{{ importmap('app') }}{% endblock %} - {% endblock %} - - - {% block body %}{% endblock %} - - + {% block body %}{% endblock %} + + {% block javascripts %} + + + + + + + + + + + {% block importmap %} + {{ importmap('app') }} + {% endblock %} + {% endblock %} + + diff --git a/templates/internal/dashboard.html.twig b/templates/internal/dashboard.html.twig new file mode 100644 index 0000000..3b57b31 --- /dev/null +++ b/templates/internal/dashboard.html.twig @@ -0,0 +1,6 @@ +{% extends 'base.html.twig' %} + +{% block title %}Dashboard +{% endblock %} + +{% block body %}{% endblock %} diff --git a/templates/libs/footer.html.twig b/templates/libs/footer.html.twig new file mode 100644 index 0000000..83dd071 --- /dev/null +++ b/templates/libs/footer.html.twig @@ -0,0 +1,38 @@ +{% block footer %} +
+
+
+
+ +
+
+ +
+
+
+
+ +{% endblock %} diff --git a/templates/libs/nav.html.twig b/templates/libs/nav.html.twig new file mode 100644 index 0000000..3aa23a3 --- /dev/null +++ b/templates/libs/nav.html.twig @@ -0,0 +1,52 @@ +{% block nav %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/registration/register-step-admin.html.twig b/templates/registration/register-step-admin.html.twig new file mode 100644 index 0000000..2d9352c --- /dev/null +++ b/templates/registration/register-step-admin.html.twig @@ -0,0 +1,93 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register Admin +{% endblock %} + +{% block body %} +
+
+
+ {{ block("nav", "libs/nav.html.twig") }} +
+
+
+
+
+ +
+
+ +{% endblock %} diff --git a/templates/registration/register-step-company.html.twig b/templates/registration/register-step-company.html.twig new file mode 100644 index 0000000..36570b5 --- /dev/null +++ b/templates/registration/register-step-company.html.twig @@ -0,0 +1,78 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register Company +{% endblock %} + +{% block body %} +
+
+
+ {{ block("nav", "libs/nav.html.twig") }} +
+
+
+
+
+ +
+
+ +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..ba67d4a --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,135 @@ +{% extends 'base.html.twig' %} + +{% block title %}Sign in +{% endblock %} + +{% block body %} + {# +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ You are logged in as + {{ app.user.userIdentifier }}, + Logout +
+ {% endif %} + +

Please sign in

+ + + + + + + + {# + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + +
+ + +
+ + + +
+ #} + +
+
+ {{ block("nav", "libs/nav.html.twig") }} +
+
+
+
+ + +
+{% endblock %}