Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/environment/admin_ci.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ springdoc.api-docs.enabled=@env.SWAGGER_DOC_ENABLED@
springdoc.swagger-ui.enabled=@env.SWAGGER_DOC_ENABLED@

cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@

# Ni-kshay device PHI password encryption key (Base64, 32 bytes); MMU-API must use the same key
nikshay.device.aes-key=@env.NIKSHAY_DEVICE_AES_KEY@
3 changes: 3 additions & 0 deletions src/main/environment/admin_docker.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ common-url=${COMMON_API}

springdoc.api-docs.enabled=${SWAGGER_DOC_ENABLED}
springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED}

# Ni-kshay device PHI password encryption key (Base64, 32 bytes); MMU-API must use the same key
nikshay.device.aes-key=${NIKSHAY_DEVICE_AES_KEY:}
2 changes: 2 additions & 0 deletions src/main/environment/admin_example.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ logging.file.name=logs/admin-api.log
jwt.secret=my-32-character-ultra-secure-and-ultra-long-secret

cors.allowed-origins=http://localhost:*

nikshay.device.aes-key=${NIKSHAY_DEVICE_AES_KEY:}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.iemr.admin.controller.nikshay;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.iemr.admin.model.nikshay.NikshayActivateRequest;
import com.iemr.admin.model.nikshay.NikshayDesignatedPhiRequest;
import com.iemr.admin.service.nikshay.NikshayDesignatedPhiService;
import com.iemr.admin.utils.response.OutputResponse;

import io.swagger.v3.oas.annotations.Operation;

/**
* Designated PHI of each Ni-kshay village, used by the transfer out phase of
* the Ni-kshay job (Module 1 of the Ni-kshay integration). Responses keep null
* fields (e.g. designationHierarchyId) so every row has the same keys.
*/
@RestController
@RequestMapping(value = "/nikshay/designatedPhi")
public class NikshayDesignatedPhiController {
private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

private static final Gson GSON = new GsonBuilder().serializeNulls().create();

@Autowired

Check warning on line 34 in src/main/java/com/iemr/admin/controller/nikshay/NikshayDesignatedPhiController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Admin-API&issues=AaF3X86ALMlUSY6wRo8P&open=AaF3X86ALMlUSY6wRo8P&pullRequest=157
private NikshayDesignatedPhiService nikshayDesignatedPhiService;

@Operation(summary = "Save a designated PHI for a village (a village can have several); saving the same village, TBU and PHI again updates it")
@PostMapping(value = "/save", produces = "application/json", headers = "Authorization")
public String saveDesignatedPhi(@RequestBody NikshayDesignatedPhiRequest request) {
OutputResponse response = new OutputResponse();
try {
response.setResponse(GSON.toJson(nikshayDesignatedPhiService.saveDesignatedPhi(request)));
} catch (Exception e) {
logger.error("saveDesignatedPhi failed: " + e.getMessage(), e);
response.setError(e);
}
return response.toStringWithSerialization();
}

@Operation(summary = "Update a designated PHI mapping by id; only the fields sent (not null) are changed")
@PostMapping(value = "/update", produces = "application/json", headers = "Authorization")
public String updateDesignatedPhi(@RequestBody NikshayDesignatedPhiRequest request) {
OutputResponse response = new OutputResponse();
try {
response.setResponse(GSON.toJson(nikshayDesignatedPhiService.updateDesignatedPhi(request)));
} catch (Exception e) {
logger.error("updateDesignatedPhi failed: " + e.getMessage(), e);
response.setError(e);
}
return response.toStringWithSerialization();
}

@Operation(summary = "Activate / deactivate a designated PHI mapping")
@PostMapping(value = "/activate", produces = "application/json", headers = "Authorization")
public String setActive(@RequestBody NikshayActivateRequest request) {
OutputResponse response = new OutputResponse();
try {
response.setResponse(GSON.toJson(nikshayDesignatedPhiService.setActive(request)));
} catch (Exception e) {
logger.error("setActive designated PHI failed: " + e.getMessage(), e);
response.setError(e);
}
return response.toStringWithSerialization();
}

@Operation(summary = "Mark a designated PHI mapping inactive by id (body: id, updatedBy)")
@PostMapping(value = "/markInactive", produces = "application/json", headers = "Authorization")
public String markInactive(@RequestBody NikshayActivateRequest request) {
OutputResponse response = new OutputResponse();
try {
response.setResponse(GSON.toJson(nikshayDesignatedPhiService.markInactive(request)));
} catch (Exception e) {
logger.error("markInactive designated PHI failed: " + e.getMessage(), e);
response.setError(e);
}
return response.toStringWithSerialization();
}

@Operation(summary = "Get designated PHI by village, TBU or district. unmappedOnly=true lists villages "
+ "without an active designated PHI; activeOnly=false includes inactive rows")
@GetMapping(value = "/get", produces = "application/json", headers = "Authorization")
public String getDesignatedPhi(@RequestParam(value = "villageId", required = false) Integer villageId,
@RequestParam(value = "tbuId", required = false) Integer tbuId,
@RequestParam(value = "districtId", required = false) Integer districtId,
@RequestParam(value = "activeOnly", required = false, defaultValue = "true") Boolean activeOnly,
@RequestParam(value = "unmappedOnly", required = false, defaultValue = "false") Boolean unmappedOnly) {
OutputResponse response = new OutputResponse();
try {
response.setResponse(GSON.toJson(
nikshayDesignatedPhiService.getDesignatedPhi(villageId, tbuId, districtId, activeOnly, unmappedOnly)));
} catch (Exception e) {
logger.error("getDesignatedPhi failed: " + e.getMessage(), e);
response.setError(e);
}
return response.toStringWithSerialization();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.iemr.admin.controller.nikshay;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.iemr.admin.model.nikshay.NikshayActivateRequest;
import com.iemr.admin.model.nikshay.NikshayDeviceRequest;
import com.iemr.admin.model.nikshay.NikshayVillageEnrollRequest;
import com.iemr.admin.model.nikshay.NikshayVillageRemoveRequest;
import com.iemr.admin.service.nikshay.NikshayDeviceService;
import com.iemr.admin.utils.response.OutputResponse;

import io.swagger.v3.oas.annotations.Operation;

/**
* Ni-kshay camp device configuration (Module 1 of the Ni-kshay integration):
* devices with their PHI login, and the villages each device serves.
* Request bodies are not logged here because device requests carry passwords.
*/
@RestController
@RequestMapping(value = "/nikshay/device")
public class NikshayDeviceController {
private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

private static final Gson GSON = new GsonBuilder().serializeNulls().create();

@Autowired

Check warning on line 36 in src/main/java/com/iemr/admin/controller/nikshay/NikshayDeviceController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Admin-API&issues=AaF3X86BLMlUSY6wRo8R&open=AaF3X86BLMlUSY6wRo8R&pullRequest=157
private NikshayDeviceService nikshayDeviceService;

@Operation(summary = "Create a Ni-kshay camp device (device name NAAT / XRAY) with its PHI login; the password is stored encrypted")
@PostMapping(value = "/create", produces = "application/json", headers = "Authorization")
public String createDevice(@RequestBody NikshayDeviceRequest request) {
return respond("createDevice", () -> nikshayDeviceService.createDevice(request));
}

@Operation(summary = "Update a device; the password changes only when a new one is sent")
@PostMapping(value = "/update", produces = "application/json", headers = "Authorization")
public String updateDevice(@RequestBody NikshayDeviceRequest request) {
return respond("updateDevice", () -> nikshayDeviceService.updateDevice(request));
}

@Operation(summary = "Activate / deactivate a device; deactivating also removes it from its villages")
@PostMapping(value = "/activate", produces = "application/json", headers = "Authorization")
public String setDeviceActive(@RequestBody NikshayActivateRequest request) {
return respond("setDeviceActive", () -> nikshayDeviceService.setDeviceActive(request));
}

@Operation(summary = "List devices (without password), optionally by device name (NAAT / XRAY) and active flag")
@GetMapping(value = "/list", produces = "application/json", headers = "Authorization")
public String listDevices(@RequestParam(value = "deviceName", required = false) String deviceName,
@RequestParam(value = "isActive", required = false) Boolean isActive) {
return respond("listDevices", () -> nikshayDeviceService.listDevices(deviceName, isActive));
}

@Operation(summary = "Search devices by device ID (partial, case-insensitive; exact match first), "
+ "without password, each with its active villages")
@GetMapping(value = "/search", produces = "application/json", headers = "Authorization")
public String searchByDeviceId(@RequestParam("deviceId") String deviceId) {
return respond("searchByDeviceId", () -> nikshayDeviceService.searchByDeviceId(deviceId));
}

@Operation(summary = "Get one device (without password) with its active villages")
@GetMapping(value = "/get", produces = "application/json", headers = "Authorization")
public String getDevice(@RequestParam("id") Integer id) {
return respond("getDevice", () -> nikshayDeviceService.getDevice(id));
}

@Operation(summary = "Enroll a device for one or more Ni-kshay villages. A village can have one active NAAT and one active XRAY device")
@PostMapping(value = "/village/enroll", produces = "application/json", headers = "Authorization")
public String enrollVillages(@RequestBody NikshayVillageEnrollRequest request) {
return respond("enrollVillages", () -> nikshayDeviceService.enrollVillages(request));
}

@Operation(summary = "Remove a device from villages (marks the mappings inactive)")
@PostMapping(value = "/village/remove", produces = "application/json", headers = "Authorization")
public String removeVillages(@RequestBody NikshayVillageRemoveRequest request) {
return respond("removeVillages", () -> nikshayDeviceService.removeVillages(request));
}

@Operation(summary = "Villages served by a device")
@GetMapping(value = "/village/list", produces = "application/json", headers = "Authorization")
public String getVillagesOfDevice(@RequestParam("deviceDetailId") Integer deviceDetailId,
@RequestParam(value = "activeOnly", required = false, defaultValue = "true") Boolean activeOnly) {
return respond("getVillagesOfDevice", () -> nikshayDeviceService.getVillagesOfDevice(deviceDetailId, activeOnly));
}

@Operation(summary = "Devices and PHI details (without password) mapped to a Ni-kshay location")
@GetMapping(value = "/phiByLocation", produces = "application/json", headers = "Authorization")
public String getPhiByLocation(@RequestParam(value = "stateId", required = false) Integer stateId,
@RequestParam(value = "districtId", required = false) Integer districtId,
@RequestParam(value = "tbuId", required = false) Integer tbuId,
@RequestParam(value = "facilityId", required = false) Integer facilityId,
@RequestParam(value = "villageId", required = false) Integer villageId,
@RequestParam(value = "deviceName", required = false) String deviceName) {
return respond("getPhiByLocation", () -> nikshayDeviceService.getPhiByLocation(stateId, districtId, tbuId,
facilityId, villageId, deviceName));
}

private String respond(String action, ThrowingSupplier<Object> call) {
OutputResponse response = new OutputResponse();
try {
response.setResponse(GSON.toJson(call.get()));
} catch (Exception e) {
logger.error(action + " failed: " + e.getMessage(), e);
response.setError(e);
}
return response.toString();
}

@FunctionalInterface
interface ThrowingSupplier<T> {
T get() throws Exception;

Check warning on line 121 in src/main/java/com/iemr/admin/controller/nikshay/NikshayDeviceController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Admin-API&issues=AaF3X86BLMlUSY6wRo8Q&open=AaF3X86BLMlUSY6wRo8Q&pullRequest=157
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.iemr.admin.data.nikshay;

import java.sql.Timestamp;

import com.google.gson.annotations.Expose;
import com.iemr.admin.utils.mapper.OutputMapper;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import lombok.Data;

/**
* One TBMJA camp device (device_name NAAT or XRAY) and the Ni-kshay PHI login created for
* it. The Ni-kshay job logs in with the device of the beneficiary's village:
* the NAAT device for ID generation, NAAT results, contacts and transfer out,
* the XRAY device for X-ray results.
*
* The password is stored AES encrypted (see NikshayPasswordCipher) and has no
* {@link Expose}, so it is never part of toString() or any API response.
*/
@Data
@Entity
@Table(name = "m_nikshay_device_configuration")
public class NikshayDeviceConfiguration {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Expose
@Column(name = "id")
private Integer id;

@Expose
@Column(name = "device_id", unique = true)
private String deviceId;

@Expose
@Enumerated(EnumType.STRING)
@Column(name = "device_name")
private NikshayDeviceName deviceName;

@Expose
@Column(name = "phi_user_id")
private String phiUserId;

@Column(name = "password")
private String password;

@Expose
@Column(name = "is_active")
private Boolean isActive = true;

@Expose
@Column(name = "created_by")
private String createdBy;

@Expose
@Column(name = "created_at", insertable = false, updatable = false)
private Timestamp createdAt;

@Expose
@Column(name = "updated_by")
private String updatedBy;

@Expose
@Column(name = "updated_at", insertable = false, updatable = false)
private Timestamp updatedAt;

@Expose
@Column(name = "more_data", columnDefinition = "json")
private String moreData;

@Transient
private OutputMapper outputMapper = new OutputMapper();

@Override
public String toString() {
return outputMapper.gson().toJson(this);

Check warning on line 84 in src/main/java/com/iemr/admin/data/nikshay/NikshayDeviceConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this instance-reference to a static reference.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Admin-API&issues=AaF3X838LMlUSY6wRo7l&open=AaF3X838LMlUSY6wRo7l&pullRequest=157
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/iemr/admin/data/nikshay/NikshayDeviceName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.iemr.admin.data.nikshay;

/**
* Name of a TBMJA camp device, stored as a string in
* m_nikshay_device_configuration.device_name. It decides which activities the
* device's PHI login is used for: NAAT for ID generation, NAAT results,
* contacts and transfer out; XRAY for X-ray results.
*/
public enum NikshayDeviceName {
NAAT, XRAY;

/** Case-insensitive parse with a readable error for API input. */
public static NikshayDeviceName from(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Device name is required");
}
try {
return valueOf(value.trim().toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Device name must be NAAT or XRAY");
}
}
}
Loading
Loading