diff --git a/src/main/java/zw/qantra/tm/domain/controllers/UptimeController.java b/src/main/java/zw/qantra/tm/domain/controllers/UptimeController.java new file mode 100644 index 0000000..7f1e0ab --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/controllers/UptimeController.java @@ -0,0 +1,30 @@ +package zw.qantra.tm.domain.controllers; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import zw.qantra.tm.domain.models.Uptime; +import zw.qantra.tm.domain.services.UptimeService; + +import java.util.UUID; + +@RestController +@RequestMapping("/public/uptimes") +@RequiredArgsConstructor +public class UptimeController { + + private final UptimeService uptimeService; + + @GetMapping + public ResponseEntity getAllUptimes(Pageable pageable) { + return ResponseEntity.ok(uptimeService.getAllUptimes(pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getUptime(@PathVariable UUID id) { + return uptimeService.getUptime(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/controllers/UserController.java b/src/main/java/zw/qantra/tm/domain/controllers/UserController.java new file mode 100644 index 0000000..a3fea2a --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/controllers/UserController.java @@ -0,0 +1,79 @@ +package zw.qantra.tm.domain.controllers; + +import lombok.RequiredArgsConstructor; +import net.kaczmarzyk.spring.data.jpa.domain.Equal; +import net.kaczmarzyk.spring.data.jpa.domain.EqualIgnoreCase; +import net.kaczmarzyk.spring.data.jpa.domain.LikeIgnoreCase; +import net.kaczmarzyk.spring.data.jpa.web.annotation.And; +import net.kaczmarzyk.spring.data.jpa.web.annotation.Join; +import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.services.UserService; + +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping + public ResponseEntity findAllUsers( + @Join(path = "workspaces", alias = "w") + @And({ + @Spec(path = "username", defaultVal = "null", spec = LikeIgnoreCase.class), + @Spec(path = "email", spec = EqualIgnoreCase.class), + @Spec(path = "firstName", spec = LikeIgnoreCase.class), + @Spec(path = "lastName", spec = LikeIgnoreCase.class), + @Spec(path = "phone", spec = EqualIgnoreCase.class), + @Spec(path = "w.id", params = "workspaceId", spec = Equal.class) + }) Specification spec, Pageable pageable) { + return ResponseEntity.ok(userService.findAllUsers(spec, pageable)); + } + + @GetMapping("/workspace") + public ResponseEntity searchAllUsers( + @Join(path = "workspaces", alias = "w") + @And({ + @Spec(path = "username", spec = LikeIgnoreCase.class), + @Spec(path = "email", spec = EqualIgnoreCase.class), + @Spec(path = "firstName", spec = LikeIgnoreCase.class), + @Spec(path = "lastName", spec = LikeIgnoreCase.class), + @Spec(path = "phone", spec = EqualIgnoreCase.class), + @Spec(path = "w.id", params = "workspaceId", defaultVal = "null", spec = Equal.class) + }) Specification spec, Pageable pageable) { + return ResponseEntity.ok(userService.findAllUsers(spec, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity getUser(@PathVariable UUID id) { + return userService.getUser(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PutMapping("/{id}") + public ResponseEntity updateUser(@PathVariable UUID id, @RequestBody User user) { + try { + User updated = userService.updateUser(id, user); + return ResponseEntity.ok(updated); + } catch (RuntimeException e) { + return ResponseEntity.notFound().build(); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable UUID id) { + if (userService.deleteUser(id)) { + return ResponseEntity.ok(Map.of("message", "User deleted successfully")); + } + return ResponseEntity.notFound().build(); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java b/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java index 4103e3a..4e000a2 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java @@ -59,14 +59,6 @@ public class WorkspaceController { return ResponseEntity.notFound().build(); } - @DeleteMapping("/{id}") - public ResponseEntity deleteWorkspace(@PathVariable UUID id) { - if (workspaceService.deleteWorkspace(id)) { - return ResponseEntity.ok(Map.of("message", "Workspace deleted successfully")); - } - return ResponseEntity.notFound().build(); - } - @PostMapping("/{workspaceId}/users/{userId}") public ResponseEntity associateUser(@PathVariable UUID workspaceId, @PathVariable UUID userId) { return workspaceService.associateUser(workspaceId, userId) diff --git a/src/main/java/zw/qantra/tm/domain/models/Uptime.java b/src/main/java/zw/qantra/tm/domain/models/Uptime.java new file mode 100644 index 0000000..71efea9 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/models/Uptime.java @@ -0,0 +1,24 @@ +package zw.qantra.tm.domain.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.SQLRestriction; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Entity +@Data +@EqualsAndHashCode(callSuper = true) +@SQLRestriction("deleted = false") +public class Uptime extends BaseEntity { + private String name; + private boolean status; +} diff --git a/src/main/java/zw/qantra/tm/domain/models/User.java b/src/main/java/zw/qantra/tm/domain/models/User.java index 2ff46b9..aee4fae 100644 --- a/src/main/java/zw/qantra/tm/domain/models/User.java +++ b/src/main/java/zw/qantra/tm/domain/models/User.java @@ -25,7 +25,8 @@ public class User extends BaseEntity implements UserDetails { @Column(unique = true, nullable = false) private String username; - + + @JsonIgnore @Column(nullable = false) private String password; diff --git a/src/main/java/zw/qantra/tm/domain/repositories/UptimeRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/UptimeRepository.java new file mode 100644 index 0000000..8de4c1a --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/repositories/UptimeRepository.java @@ -0,0 +1,12 @@ +package zw.qantra.tm.domain.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; +import zw.qantra.tm.domain.models.Uptime; + +import java.util.UUID; + +@Repository +public interface UptimeRepository extends JpaRepository, JpaSpecificationExecutor { +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java index 58f1a31..ba2c30e 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/UserRepository.java @@ -1,6 +1,7 @@ package zw.qantra.tm.domain.repositories; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import zw.qantra.tm.domain.models.User; @@ -8,7 +9,7 @@ import java.util.Optional; import java.util.UUID; @Repository -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { Optional findById(UUID uid); Optional findByUsername(String username); Optional findByEmail(String email); diff --git a/src/main/java/zw/qantra/tm/domain/services/UptimeService.java b/src/main/java/zw/qantra/tm/domain/services/UptimeService.java new file mode 100644 index 0000000..0a441bd --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/UptimeService.java @@ -0,0 +1,48 @@ +package zw.qantra.tm.domain.services; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.models.Uptime; +import zw.qantra.tm.domain.repositories.UptimeRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Getter +@Service +@RequiredArgsConstructor +public class UptimeService { + private final UptimeRepository uptimeRepository; + + public Page getAllUptimes(Pageable pageable) { + return uptimeRepository.findAll(pageable); + } + + public Optional getUptime(UUID id) { + return uptimeRepository.findById(id); + } + + public Uptime createUptime(Uptime uptime) { + return uptimeRepository.save(uptime); + } + + public Uptime updateUptime(UUID id, Uptime uptime) { + if (uptimeRepository.existsById(id)) { + uptime.setId(id); + return uptimeRepository.save(uptime); + } + return null; + } + + public boolean deleteUptime(UUID id) { + if (uptimeRepository.existsById(id)) { + uptimeRepository.deleteById(id); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/UserService.java b/src/main/java/zw/qantra/tm/domain/services/UserService.java index ee94972..032e489 100644 --- a/src/main/java/zw/qantra/tm/domain/services/UserService.java +++ b/src/main/java/zw/qantra/tm/domain/services/UserService.java @@ -2,18 +2,27 @@ package zw.qantra.tm.domain.services; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import zw.qantra.tm.domain.dtos.AuthResponse; import zw.qantra.tm.domain.dtos.LoginRequest; import zw.qantra.tm.domain.dtos.RegisterRequest; import zw.qantra.tm.domain.models.User; +import zw.qantra.tm.domain.models.Workspace; import zw.qantra.tm.domain.repositories.UserRepository; import zw.qantra.tm.utils.JwtUtils; +import java.util.Optional; +import java.util.UUID; + @Service @RequiredArgsConstructor public class UserService implements UserDetailsService { @@ -23,6 +32,7 @@ public class UserService implements UserDetailsService { private final PasswordEncoder passwordEncoder; private final JwtUtils jwtUtils; private final TransactionService transactionService; + private final WorkspaceService workspaceService; public User fetchUserByUsername(String username) { return userRepository.findByUsername(username) @@ -83,4 +93,52 @@ public class UserService implements UserDetailsService { return new AuthResponse(token, "Bearer", user.getUsername(), user.getEmail(), user.getPhone(), user.getFirstName(), user.getLastName(), user.getId()); } + + public Page findAllUsers(Specification spec, Pageable pageable) { + return userRepository.findAll(spec, pageable); + } + + public Optional getUser(UUID id) { + return userRepository.findById(id); + } + + @Transactional + public User createUser(User user) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setDeleted(false); + return userRepository.save(user); + } + + @Transactional + public User updateUser(UUID id, User userDetails) { + return userRepository.findById(id) + .map(existingUser -> { + existingUser.setFirstName(userDetails.getFirstName()); + existingUser.setLastName(userDetails.getLastName()); + existingUser.setEmail(userDetails.getEmail()); + existingUser.setPhone(userDetails.getPhone()); + existingUser.setPhotoUrl(userDetails.getPhotoUrl()); + existingUser.setWorkflowId(userDetails.getWorkflowId()); + existingUser.setEnabled(userDetails.isEnabled()); + existingUser.setAccountNonExpired(userDetails.isAccountNonExpired()); + existingUser.setAccountNonLocked(userDetails.isAccountNonLocked()); + existingUser.setCredentialsNonExpired(userDetails.isCredentialsNonExpired()); + if (userDetails.getPassword() != null && !userDetails.getPassword().isEmpty()) { + existingUser.setPassword(passwordEncoder.encode(userDetails.getPassword())); + } + return userRepository.save(existingUser); + }) + .orElseThrow(() -> new RuntimeException("User not found with id: " + id)); + } + + @Transactional + public boolean deleteUser(UUID id) { + return userRepository.findById(id) + .map(user -> { + user.setDeleted(true); + userRepository.save(user); + return true; + }) + .orElse(false); + } } diff --git a/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java b/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java index a82c6e0..7cc64ab 100644 --- a/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java +++ b/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java @@ -10,6 +10,8 @@ import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.models.Workspace; import zw.qantra.tm.domain.repositories.UserRepository; import zw.qantra.tm.domain.repositories.WorkspaceRepository; +import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.utils.SecurityUtils; import java.util.List; import java.util.Optional; @@ -69,6 +71,15 @@ public class WorkspaceService { @Transactional public Optional disassociateUser(UUID workspaceId, UUID userId) { + // Prevent a user from disassociating themselves from their own workspace + String currentUsername = SecurityUtils.getCurrentUsername(); + if (currentUsername != null) { + User currentUser = userRepository.findByUsername(currentUsername).orElse(null); + if (currentUser != null && currentUser.getId().equals(userId)) { + throw new ApiException("You cannot remove yourself from the workspace"); + } + } + Workspace workspace = workspaceRepository.findById(workspaceId).orElse(null); User user = userRepository.findById(userId).orElse(null); if (workspace == null || user == null) { diff --git a/src/main/resources/liquibase-diff-changeLog.xml b/src/main/resources/liquibase-diff-changeLog.xml index 1d53b37..23a0a47 100644 --- a/src/main/resources/liquibase-diff-changeLog.xml +++ b/src/main/resources/liquibase-diff-changeLog.xml @@ -1,41 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Convert party_type column from integer ordinal to string enum values to match @Enumerated(EnumType.STRING) in Transaction.java