diff --git a/.gitignore b/.gitignore
index d0b9d6a..f71f2f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@ drivers/
# Error screenshots generated by TestBench for failed integration tests
error-screenshots/
webpack.generated.js
+*.env
diff --git a/pom.xml b/pom.xml
index a9cae3e..30815f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,10 @@
commons-beanutils
1.9.4
+
+ com.fasterxml.jackson.core
+ jackson-core
+
org.mockito
mockito-core
@@ -240,6 +244,11 @@
freemarker
2.3.32
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
org.apache.maven.plugins
maven-surefire-plugin
diff --git a/src/main/java/com/primefactorsolutions/model/Employee.java b/src/main/java/com/primefactorsolutions/model/Employee.java
index e128132..d8a11b0 100644
--- a/src/main/java/com/primefactorsolutions/model/Employee.java
+++ b/src/main/java/com/primefactorsolutions/model/Employee.java
@@ -1,147 +1,144 @@
- package com.primefactorsolutions.model;
- import com.google.common.collect.Lists;
- import jakarta.persistence.*;
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.EqualsAndHashCode;
- import lombok.NoArgsConstructor;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.userdetails.UserDetails;
+package com.primefactorsolutions.model;
- import java.time.LocalDate;
- import java.util.Collection;
+import com.google.common.collect.Lists;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
- @Data
- @Entity
- @AllArgsConstructor
- @NoArgsConstructor
- @EqualsAndHashCode(callSuper = true)
- public class Employee extends BaseEntity implements UserDetails {
- private String username;
- private String firstName;
- private String lastName;
- private LocalDate birthday;
- private String birthCity;
- private String age;
+import java.time.LocalDate;
+import java.util.Collection;
- private String residenceAddress;
- private String localAddress;
- private String phoneNumber;
- private String personalEmail;
- private String position;
- @ManyToOne
- @JoinColumn(name = "team_id", nullable = false)
- private Team team;
- private String emergencyCName;
- private String emergencyCAddress;
- private String emergencyCPhone;
- private String emergencyCEmail;
- private String numberOfChildren;
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class Employee extends BaseEntity implements UserDetails {
+ private String username;
+ private String firstName;
+ private String lastName;
+ private LocalDate birthday;
+ private String birthCity;
+ private String age;
- private String ci;
- private String issuedIn;
+ private String residenceAddress;
+ private String localAddress;
+ private String phoneNumber;
+ private String personalEmail;
+ private String position;
+ @ManyToOne
+ @JoinColumn(name = "team_id", nullable = false)
+ private Team team;
+ private String emergencyCName;
+ private String emergencyCAddress;
+ private String emergencyCPhone;
+ private String emergencyCEmail;
+ private String numberOfChildren;
- private String pTitle1;
- private String pTitle2;
- private String pTitle3;
+ private String ci;
+ private String issuedIn;
- private String pStudy1;
- private String pStudy2;
- private String pStudy3;
+ private String pTitle1;
+ private String pTitle2;
+ private String pTitle3;
- private String certification1;
- private String certification2;
- private String certification3;
- private String certification4;
+ private String pStudy1;
+ private String pStudy2;
+ private String pStudy3;
- private String recognition;
- private String achievements;
+ private String certification1;
+ private String certification2;
+ private String certification3;
+ private String certification4;
- private String language;
- private String languageLevel;
+ private String recognition;
+ private String achievements;
- private String cod;
- private String leadManager;
- private String project;
+ private String language;
+ private String languageLevel;
- private LocalDate dateOfEntry;
- private LocalDate dateOfExit;
+ private String cod;
+ private String leadManager;
+ private String project;
- private String contractType;
- private String seniority;
- private String salary;
+ private LocalDate dateOfEntry;
+ private LocalDate dateOfExit;
- private String bankName;
- private String accountNumber;
+ private String contractType;
+ private String seniority;
+ private String salary;
- private String gpss;
- private String sss;
- private String beneficiaries;
+ private String bankName;
+ private String accountNumber;
- @Column(columnDefinition = "TEXT")
- private String profileImage;
- @Enumerated(EnumType.STRING)
- private Status status;
+ private String gpss;
+ private String sss;
+ private String beneficiaries;
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
- return Lists.newArrayList();
- }
+ @Column(columnDefinition = "TEXT")
+ private String profileImage;
+ @Enumerated(EnumType.STRING)
+ private Status status;
- @Override
- public String getPassword() {
- return null;
- }
-
- @Override
- public String getUsername() {
- return this.username;
- }
-
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
-
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
-
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
-
- @Override
- public boolean isEnabled() {
- return true;
- }
-
- public enum Status {
- ACTIVE,
- INACTIVE
- }
- @Enumerated(EnumType.STRING)
- private MaritalStatus maritalStatus;
- public enum MaritalStatus {
- SINGLE,
- MARRIED,
- WIDOWED,
- DIVORCED
- }
- @Enumerated(EnumType.STRING)
- private Gender gender;
- public enum Gender {
- MALE,
- FEMALE
- }
-
- public Status getStatus() {
-
- return status;
- }
- public void setStatus(final Status status) {
- this.status = status;
- }
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return Lists.newArrayList();
}
+
+ @Override
+ public String getPassword() {
+ return null;
+ }
+
+ @Override
+ public String getUsername() {
+ return this.username;
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ public enum Status {
+ ACTIVE,
+ INACTIVE
+ }
+
+ @Enumerated(EnumType.STRING)
+ private MaritalStatus maritalStatus;
+
+ public enum MaritalStatus {
+ SINGLE,
+ MARRIED,
+ WIDOWED,
+ DIVORCED
+ }
+
+ @Enumerated(EnumType.STRING)
+ private Gender gender;
+
+ public enum Gender {
+ MALE,
+ FEMALE
+ }
+}
diff --git a/src/main/java/com/primefactorsolutions/repositories/EmployeeRepository.java b/src/main/java/com/primefactorsolutions/repositories/EmployeeRepository.java
index e27a88b..3016f8e 100644
--- a/src/main/java/com/primefactorsolutions/repositories/EmployeeRepository.java
+++ b/src/main/java/com/primefactorsolutions/repositories/EmployeeRepository.java
@@ -8,4 +8,6 @@ import java.util.UUID;
public interface EmployeeRepository extends JpaRepository {
Optional findByUsername(String username);
+
+ Optional findByPersonalEmail(String personalEmail);
}
diff --git a/src/main/java/com/primefactorsolutions/service/AccountService.java b/src/main/java/com/primefactorsolutions/service/AccountService.java
new file mode 100644
index 0000000..8a4bf76
--- /dev/null
+++ b/src/main/java/com/primefactorsolutions/service/AccountService.java
@@ -0,0 +1,101 @@
+package com.primefactorsolutions.service;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTCreationException;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.primefactorsolutions.model.Employee;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class AccountService {
+ private final EmailService emailService;
+ private final EmployeeService employeeService;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final String secret;
+
+ public AccountService(final EmailService emailService, final EmployeeService employeeService,
+ @Value("${application.jwtSecret}") final String secret) {
+ this.emailService = emailService;
+ this.employeeService = employeeService;
+ this.secret = secret;
+ }
+
+ public void sendResetPasswordEmail(final String personalEmail) {
+ final Employee employee = employeeService.getEmployeeByPersonalEmail(personalEmail);
+
+ if (employee == null) {
+ log.warn("Could not find employee for email {}", personalEmail);
+ return;
+ }
+
+ final String link = createResetPasswordLink(employee.getUsername());
+ final String content = "Visit this link to reset your password: " + link;
+ emailService.sendEmail(personalEmail, "PFS - Reset Password", content);
+ }
+
+ public void resetPassword(final String username, final String newPassword, final String token) {
+ DecodedJWT decodedJWT;
+
+ try {
+ Algorithm algorithm = Algorithm.HMAC512(secret);
+ JWTVerifier verifier = JWT.require(algorithm)
+ .withIssuer("pfs")
+ .build();
+
+ decodedJWT = verifier.verify(token);
+ final Map payload = (Map) objectMapper.readValue(decodedJWT.getPayload(), Map.class);
+
+ if (Instant.parse((String) payload.get("expire")).isBefore(Instant.now())
+ || !username.equals(payload.get("username"))) {
+ log.warn("token invalid {} {} {}", username, payload.get("username"), payload.get("expire"));
+ return;
+ }
+ } catch (JWTVerificationException | JsonProcessingException e) {
+ return;
+ }
+
+ final Employee employee = employeeService.getDetachedEmployeeByUsername(username);
+
+ if (employee == null) {
+ log.warn("Could not find employee for username {}", username);
+ return;
+ }
+
+ if (StringUtils.isBlank(newPassword) || newPassword.length() < 8) {
+ throw new IllegalArgumentException("New password should be at least 8 chars long");
+ }
+
+ employeeService.updatePassword(employee, newPassword);
+ }
+
+ private String createResetPasswordLink(final String username) {
+ String token = "";
+
+ try {
+ Algorithm algorithm = Algorithm.HMAC512(secret);
+ token = JWT.create()
+ .withIssuer("pfs")
+ .withPayload(objectMapper.writeValueAsString(Map.of("username", username,
+ "expire", Instant.now().plus(1, ChronoUnit.HOURS).toString())))
+ .sign(algorithm);
+ } catch (JWTCreationException | JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+
+ return String.format("https://intra.primefactorsolutions.com/reset-password?username=%s&token=%s", username,
+ token);
+ }
+}
diff --git a/src/main/java/com/primefactorsolutions/service/EmailService.java b/src/main/java/com/primefactorsolutions/service/EmailService.java
new file mode 100644
index 0000000..8045887
--- /dev/null
+++ b/src/main/java/com/primefactorsolutions/service/EmailService.java
@@ -0,0 +1,32 @@
+package com.primefactorsolutions.service;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+@Slf4j
+public class EmailService {
+ public static final String NO_REPLY_PRIMEFACTORSOLUTIONS_COM = "no-reply@primefactorsolutions.com";
+ private final JavaMailSender emailSender;
+
+ public void sendEmail(final String email, final String title, final String messageContent) {
+ try {
+ final SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(NO_REPLY_PRIMEFACTORSOLUTIONS_COM);
+ message.setBcc(NO_REPLY_PRIMEFACTORSOLUTIONS_COM);
+ message.setTo(email);
+ message.setSubject(title);
+ message.setText(messageContent);
+
+ emailSender.send(message);
+ log.info("Sent email to {}", email);
+ } catch (Exception e) {
+ log.error("Error sending email to {}", email, e);
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/com/primefactorsolutions/service/EmployeeService.java b/src/main/java/com/primefactorsolutions/service/EmployeeService.java
index 4313877..d273ac4 100644
--- a/src/main/java/com/primefactorsolutions/service/EmployeeService.java
+++ b/src/main/java/com/primefactorsolutions/service/EmployeeService.java
@@ -18,12 +18,18 @@ import java.util.Collections;
@Service
@AllArgsConstructor
public class EmployeeService {
+ private static final String USERPASSWORD = "userpassword";
+ private static final String OBJECTCLASS = "objectclass";
+ private static final String ORGANIZATIONAL_PERSON = "organizationalPerson";
+ private static final String INET_ORG_PERSON = "inetOrgPerson";
+ private static final String TOP = "top";
+ private static final String PERSON = "person";
+ public static final String BASE_DN = "dc=primefactorsolutions,dc=com";
+
private final EmployeeRepository employeeRepository;
private final LdapTemplate ldapTemplate;
private final EntityManager entityManager;
- public static final String BASE_DN = "dc=primefactorsolutions,dc=com";
-
protected Name buildDn(final Employee employee) {
return LdapNameBuilder.newInstance(BASE_DN)
.add("ou", "users")
@@ -63,6 +69,10 @@ public class EmployeeService {
return employees.subList(start, end);
}
+ public Employee getEmployeeByPersonalEmail(final String email) {
+ return employeeRepository.findByPersonalEmail(email).orElse(null);
+ }
+
public Employee createOrUpdate(final Employee employee) {
if (employee.getId() == null) {
final Name dn = buildDn(employee);
@@ -74,28 +84,29 @@ public class EmployeeService {
}
public Employee getEmployee(final UUID id) {
- Optional employee = employeeRepository.findById(id);
+ final Optional employee = employeeRepository.findById(id);
+
return employee.orElse(null);
}
private Attributes buildAttributes(final Employee employee) {
final Attributes attrs = new BasicAttributes();
- final BasicAttribute ocattr = new BasicAttribute("objectclass");
- ocattr.add("top");
- ocattr.add("person");
- ocattr.add("organizationalPerson");
- ocattr.add("inetOrgPerson");
+ final BasicAttribute ocattr = new BasicAttribute(OBJECTCLASS);
+ ocattr.add(TOP);
+ ocattr.add(PERSON);
+ ocattr.add(ORGANIZATIONAL_PERSON);
+ ocattr.add(INET_ORG_PERSON);
attrs.put(ocattr);
attrs.put("cn", String.format("%s %s", employee.getFirstName(), employee.getLastName()));
attrs.put("sn", String.format("%s %s", employee.getFirstName(), employee.getLastName()));
attrs.put("uid", employee.getUsername());
- attrs.put("userpassword", String.format("%s%s", employee.getUsername(), 123));
+ attrs.put(USERPASSWORD, String.format("%s%s", employee.getUsername(), 123));
return attrs;
}
- public void updatePassword(final Employee employee) {
- final Attribute attr = new BasicAttribute("userpassword", employee.getUsername() + "123");
+ public void updatePassword(final Employee employee, final String newPassword) {
+ final Attribute attr = new BasicAttribute(USERPASSWORD, newPassword);
final ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
ldapTemplate.modifyAttributes(buildDn(employee), new ModificationItem[] {item});
diff --git a/src/main/java/com/primefactorsolutions/views/EmployeeView.java b/src/main/java/com/primefactorsolutions/views/EmployeeView.java
index bdf5a82..9296f55 100644
--- a/src/main/java/com/primefactorsolutions/views/EmployeeView.java
+++ b/src/main/java/com/primefactorsolutions/views/EmployeeView.java
@@ -1,6 +1,7 @@
package com.primefactorsolutions.views;
import com.primefactorsolutions.model.Employee;
+import com.primefactorsolutions.model.Team;
import com.primefactorsolutions.service.EmployeeService;
import com.primefactorsolutions.service.ReportService;
import com.vaadin.componentfactory.pdfviewer.PdfViewer;
@@ -22,6 +23,9 @@ import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
+import com.vaadin.flow.data.binder.Result;
+import com.vaadin.flow.data.binder.ValueContext;
+import com.vaadin.flow.data.converter.Converter;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.*;
import com.vaadin.flow.server.StreamResource;
@@ -143,6 +147,18 @@ public class EmployeeView extends BeanValidationForm implements HasUrl
configureComponents();
addClassName("main-layout");
+
+ getBinder().setConverter("team", new Converter