added uptime monitoring

This commit is contained in:
2026-06-24 00:30:07 +02:00
parent a84a1000a8
commit db1fe2672a
11 changed files with 266 additions and 46 deletions

View File

@@ -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());
}
}

View File

@@ -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<User> 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<User> 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();
}
}

View File

@@ -59,14 +59,6 @@ public class WorkspaceController {
return ResponseEntity.notFound().build(); 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}") @PostMapping("/{workspaceId}/users/{userId}")
public ResponseEntity associateUser(@PathVariable UUID workspaceId, @PathVariable UUID userId) { public ResponseEntity associateUser(@PathVariable UUID workspaceId, @PathVariable UUID userId) {
return workspaceService.associateUser(workspaceId, userId) return workspaceService.associateUser(workspaceId, userId)

View File

@@ -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;
}

View File

@@ -26,6 +26,7 @@ public class User extends BaseEntity implements UserDetails {
@Column(unique = true, nullable = false) @Column(unique = true, nullable = false)
private String username; private String username;
@JsonIgnore
@Column(nullable = false) @Column(nullable = false)
private String password; private String password;

View File

@@ -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<Uptime, UUID>, JpaSpecificationExecutor<Uptime> {
}

View File

@@ -1,6 +1,7 @@
package zw.qantra.tm.domain.repositories; package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.models.User;
@@ -8,7 +9,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
Optional<User> findById(UUID uid); Optional<User> findById(UUID uid);
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email); Optional<User> findByEmail(String email);

View File

@@ -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<Uptime> getAllUptimes(Pageable pageable) {
return uptimeRepository.findAll(pageable);
}
public Optional<Uptime> 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;
}
}

View File

@@ -2,18 +2,27 @@ package zw.qantra.tm.domain.services;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; 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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import zw.qantra.tm.domain.dtos.AuthResponse; import zw.qantra.tm.domain.dtos.AuthResponse;
import zw.qantra.tm.domain.dtos.LoginRequest; import zw.qantra.tm.domain.dtos.LoginRequest;
import zw.qantra.tm.domain.dtos.RegisterRequest; import zw.qantra.tm.domain.dtos.RegisterRequest;
import zw.qantra.tm.domain.models.User; 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.UserRepository;
import zw.qantra.tm.utils.JwtUtils; import zw.qantra.tm.utils.JwtUtils;
import java.util.Optional;
import java.util.UUID;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserService implements UserDetailsService { public class UserService implements UserDetailsService {
@@ -23,6 +32,7 @@ public class UserService implements UserDetailsService {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final JwtUtils jwtUtils; private final JwtUtils jwtUtils;
private final TransactionService transactionService; private final TransactionService transactionService;
private final WorkspaceService workspaceService;
public User fetchUserByUsername(String username) { public User fetchUserByUsername(String username) {
return userRepository.findByUsername(username) return userRepository.findByUsername(username)
@@ -83,4 +93,52 @@ public class UserService implements UserDetailsService {
return new AuthResponse(token, "Bearer", user.getUsername(), return new AuthResponse(token, "Bearer", user.getUsername(),
user.getEmail(), user.getPhone(), user.getFirstName(), user.getLastName(), user.getId()); user.getEmail(), user.getPhone(), user.getFirstName(), user.getLastName(), user.getId());
} }
public Page<User> findAllUsers(Specification<User> spec, Pageable pageable) {
return userRepository.findAll(spec, pageable);
}
public Optional<User> 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);
}
} }

View File

@@ -10,6 +10,8 @@ import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.models.Workspace; import zw.qantra.tm.domain.models.Workspace;
import zw.qantra.tm.domain.repositories.UserRepository; import zw.qantra.tm.domain.repositories.UserRepository;
import zw.qantra.tm.domain.repositories.WorkspaceRepository; 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.List;
import java.util.Optional; import java.util.Optional;
@@ -69,6 +71,15 @@ public class WorkspaceService {
@Transactional @Transactional
public Optional<Workspace> disassociateUser(UUID workspaceId, UUID userId) { public Optional<Workspace> 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); Workspace workspace = workspaceRepository.findById(workspaceId).orElse(null);
User user = userRepository.findById(userId).orElse(null); User user = userRepository.findById(userId).orElse(null);
if (workspace == null || user == null) { if (workspace == null || user == null) {

View File

@@ -1,41 +1,5 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?> <?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="khoza (generated)" id="1782161994502-9">
<createIndex indexName="IX_charge_exclude_productsPK" tableName="charge_exclude_products" unique="true">
<column name="charge_id"/>
<column name="charge_condition_id"/>
</createIndex>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-10">
<createIndex indexName="IX_charge_include_productsPK" tableName="charge_include_products" unique="true">
<column name="charge_id"/>
<column name="charge_condition_id"/>
</createIndex>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-11">
<createIndex indexName="IX_workspace_usersPK" tableName="workspace_users" unique="true">
<column name="workspace_id"/>
<column name="user_id"/>
</createIndex>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-1">
<dropPrimaryKey tableName="charge_exclude_products"/>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-2">
<addPrimaryKey columnNames="charge_id, charge_condition_id" constraintName="charge_exclude_productsPK" tableName="charge_exclude_products"/>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-3">
<dropPrimaryKey tableName="charge_include_products"/>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-4">
<addPrimaryKey columnNames="charge_id, charge_condition_id" constraintName="charge_include_productsPK" tableName="charge_include_products"/>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-5">
<dropPrimaryKey tableName="workspace_users"/>
</changeSet>
<changeSet author="khoza (generated)" id="1782161994502-6">
<addPrimaryKey columnNames="workspace_id, user_id" constraintName="workspace_usersPK" tableName="workspace_users"/>
</changeSet>
<changeSet author="khoza" id="convert-party-type-to-string"> <changeSet author="khoza" id="convert-party-type-to-string">
<comment>Convert party_type column from integer ordinal to string enum values to match @Enumerated(EnumType.STRING) in Transaction.java</comment> <comment>Convert party_type column from integer ordinal to string enum values to match @Enumerated(EnumType.STRING) in Transaction.java</comment>