Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FSADT1-1631): updating details api for legacy clients #1356

Merged
merged 28 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7a31863
feat(FSADT1-1631): removing unused code
paulushcgcj Dec 9, 2024
d277516
chore: updating legacy api
paulushcgcj Dec 9, 2024
d8cbb49
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 9, 2024
c452c61
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 17, 2024
0bc8600
feat(FSADT1-1631): added context propagator for roles
paulushcgcj Dec 17, 2024
8045989
chore: small update on the DTO
paulushcgcj Dec 17, 2024
b146cf2
chore: added the role mdc constant
paulushcgcj Dec 17, 2024
bd23321
feat(FSADT1-1631): updated principal util roles to set
paulushcgcj Dec 17, 2024
eb7ba7c
feat(FSADT1-1631): added client details obfuscator
paulushcgcj Dec 17, 2024
318db86
feat(FSADT1-1631): added location and contact load
paulushcgcj Dec 17, 2024
200e0f4
test(FSADT1-1631): added tests
paulushcgcj Dec 17, 2024
0c52c1b
chore: removed print
paulushcgcj Dec 17, 2024
bdd9bc2
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 17, 2024
3bf5232
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 18, 2024
baf3c7b
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 18, 2024
77265e3
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 18, 2024
fadce8f
Doing code reviews
mamartinezmejia Dec 18, 2024
656533e
Doing code reviews
mamartinezmejia Dec 18, 2024
983bf40
chore: fixing discrepancies
paulushcgcj Dec 18, 2024
6e2cf93
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
0c50de1
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
5da9dbe
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
abf26b2
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
9952ec3
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
2705586
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
6a6ae7b
chore: fixing discrepancies
paulushcgcj Dec 18, 2024
bbbf47e
chore: fixing discrepancies
paulushcgcj Dec 18, 2024
43ee1d6
test: adding test
paulushcgcj Dec 19, 2024
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
48 changes: 39 additions & 9 deletions backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,18 @@ public final class ApplicationConstant {
dc.district_code as district,
dc.district_code || ' - ' || dc.description as district_desc
FROM nrfc.submission s
left join nrfc.submission_status_code ssc on ssc.submission_status_code = s.submission_status_code\s
left join nrfc.submission_type_code stc on stc.submission_type_code = s.submission_type_code
left join nrfc.submission_detail sd on sd.submission_id = s.submission_id\s
left join nrfc.business_type_code btc on btc.business_type_code = sd.business_type_code\s
left join nrfc.district_code dc on dc.district_code = sd.district_code\s
left join nrfc.client_type_code ctc on ctc.client_type_code = sd.client_type_code\s
left join nrfc.submission_status_code ssc
on ssc.submission_status_code = s.submission_status_code
left join nrfc.submission_type_code stc
on stc.submission_type_code = s.submission_type_code
left join nrfc.submission_detail sd
on sd.submission_id = s.submission_id
left join nrfc.business_type_code btc
on btc.business_type_code = sd.business_type_code
left join nrfc.district_code dc
on dc.district_code = sd.district_code
left join nrfc.client_type_code ctc
on ctc.client_type_code = sd.client_type_code
where s.submission_id = :submissionId""";

public static final String SUBMISSION_CONTACTS_QUERY = """
Expand All @@ -71,7 +77,15 @@ public final class ApplicationConstant {
sc.last_name,
sc.business_phone_number,
sc.email_address,
(select STRING_AGG(sl.location_name,', ') as locations from nrfc.submission_location sl left join nrfc.submission_location_contact_xref slcx on slcx.submission_location_id = sl.submission_location_id left join nrfc.submission_contact sc on sc.submission_contact_id = slcx.submission_contact_id where sl.submission_id = :submissionId) as locations,
(
select STRING_AGG(sl.location_name,', ') as locations
from nrfc.submission_location sl
left join nrfc.submission_location_contact_xref slcx
on slcx.submission_location_id = sl.submission_location_id
left join nrfc.submission_contact sc
on sc.submission_contact_id = slcx.submission_contact_id
where sl.submission_id = :submissionId
) as locations,
sc.idp_user_id
FROM nrfc.submission_contact sc
left join nrfc.contact_type_code ctc on ctc.contact_type_code = sc.contact_type_code
Expand All @@ -90,7 +104,9 @@ public final class ApplicationConstant {
sl.location_name
FROM nrfc.submission_location sl
left join nrfc.country_code cc on cc.country_code = sl.country_code
left join nrfc.province_code pc on (pc.province_code = sl.province_code and pc.country_code = cc.country_code)
left join nrfc.province_code pc on (
pc.province_code = sl.province_code and pc.country_code = cc.country_code
)
where sl.submission_id = :submissionId
order by sl.submission_location_id""";

Expand All @@ -108,9 +124,23 @@ left join nrfc.province_code pc on (pc.province_code = sl.province_code and pc.c
public static final String ROLE_ADMIN = "CLIENT_ADMIN";
public static final String ROLE_SUSPEND = "CLIENT_SUSPEND";

public static final String OPENDATA_FILTER = "<Filter><Or><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike></Or></Filter>";
public static final String OPENDATA_FILTER = "<Filter><Or><PropertyIsLike wildCard=\"*\" "
+ "singleChar=\".\" escape=\"!\">"
+ "<PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "<PropertyIsLike wildCard=\"*\" singleChar=\".\" "
+ "escape=\"!\"><PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "<PropertyIsLike wildCard=\"*\" singleChar=\".\" "
+ "escape=\"!\"><PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "<PropertyIsLike wildCard=\"*\" singleChar=\".\" "
+ "escape=\"!\"><PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "</Or></Filter>";

public static final String MDC_USERID = "X-USER";

public static final String MDC_USERROLES = "X-Role";
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ca.bc.gov.app.configuration;

import ca.bc.gov.app.converters.ForestClientDetailsSerializerModifier;
import ca.bc.gov.app.dto.ValidationError;
import ca.bc.gov.app.dto.bcregistry.BcRegistryAddressDto;
import ca.bc.gov.app.dto.bcregistry.BcRegistryAlternateNameDto;
Expand Down Expand Up @@ -54,6 +55,8 @@
import ca.bc.gov.app.health.HealthExchangeFilterFunction;
import ca.bc.gov.app.health.ManualHealthIndicator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.beans.factory.annotation.Qualifier;
Expand Down Expand Up @@ -245,6 +248,15 @@ public WebClient openDataSacBandApi(
.baseUrl(configuration.getOpenData().getSacBandUrl()).build();
}

/**
* Configures and provides a WebClient for accessing the Open Data SAC Tribe API. This WebClient
* is pre-configured with the base URL for the SAC Tribe API, as specified in the provided
* {@link ForestClientConfiguration}.
*
* @param configuration The configuration containing the SAC Tribe API URL and other settings.
* @param webClientBuilder A builder for creating WebClient instances.
* @return A WebClient instance configured for the Open Data SAC Tribe API.
*/
@Bean
public WebClient openDataSacTribeApi(
ForestClientConfiguration configuration,
Expand Down Expand Up @@ -272,6 +284,16 @@ public WebClient openDataBcMapsBandApi(
return webClientBuilder.baseUrl(configuration.getOpenData().getOpenMapsBandUrl()).build();
}

/**
* Configures and provides a WebClient for accessing the Open Data BC Maps Tribe API. This
* WebClient is pre-configured with the base URL for the BC Maps Tribe API, as specified in the
* provided {@link ForestClientConfiguration}.
*
* @param configuration The configuration containing the BC Maps Tribe API URL and other
* settings.
* @param webClientBuilder A builder for creating WebClient instances.
* @return A WebClient instance configured for the Open Data BC Maps Tribe API.
*/
@Bean
public WebClient openDataBcMapsTribeApi(
ForestClientConfiguration configuration,
Expand Down Expand Up @@ -303,9 +325,36 @@ public WebClient processorApi(
return webClientBuilder.baseUrl(configuration.getProcessor().getUrl()).build();
}

/**
* Configures and provides an ObjectMapper bean. This ObjectMapper is built using the provided
* Jackson2ObjectMapperBuilder and is configured with the JavaTimeModule and a custom
* ForestClientDetailsSerializerModifier module.
*
* @param builder The Jackson2ObjectMapperBuilder used to build the ObjectMapper.
* @return A configured ObjectMapper instance.
*/
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.build();

ObjectMapper mapper = builder.build();
mapper.registerModule(new JavaTimeModule());
mapper.registerModule(forestClientDetailsDtoModule());

return mapper;
}

/**
* Creates and configures a SimpleModule for customizing the serialization of ForestClientDetails.
* This module registers a custom serializer modifier, ForestClientDetailsSerializerModifier.
*
* @return A configured SimpleModule instance with the custom serializer modifier.
*/
SimpleModule forestClientDetailsDtoModule() {
SimpleModule module = new SimpleModule();

// Register the serializer modifier
module.setSerializerModifier(new ForestClientDetailsSerializerModifier());
return module;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class ClientController {
* provider) is extracted from the token to authorize the request.
*
* @param clientNumber the incorporation number of the client whose details are being requested
* @param principal the JWT authentication token containing user and business information
* @param principal the JWT authentication token containing user and business information
* @return a {@link Mono} emitting the {@link ClientDetailsDto} containing the client's details
*/
@GetMapping("/{clientNumber}")
Expand All @@ -64,44 +64,25 @@ public Mono<ClientDetailsDto> getClientDetailsByIncorporationNumber(
JwtPrincipalUtil.getProvider(principal)
);
}

/**
* Handles HTTP GET requests to retrieve client details based on the provided client number.
*
* <p>This method fetches the details of a client from the {@code ClientService} using the
* specified {@code clientNumber}. The caller's JWT authentication token is used to extract
* user-related information such as groups and user ID.</p>
*
* @param clientNumber the unique identifier of the client whose details are to be retrieved.
* @param principal the {@link JwtAuthenticationToken} containing the authenticated user's
* information, including their roles and groups.
* @return a {@link Mono} emitting the {@link ForestClientDetailsDto} containing the requested
* client details, or an error if the client cannot be found or accessed.
*/

@GetMapping("/details/{clientNumber}")
public Mono<ForestClientDetailsDto> getClientDetailsByClientNumber(
@PathVariable String clientNumber,
JwtAuthenticationToken principal
) {
log.info("Requesting client details for client number {} from the client service. {}",
clientNumber,
JwtPrincipalUtil.getUserId(principal)
);
return clientService.getClientDetailsByClientNumber(
clientNumber,
JwtPrincipalUtil.getGroups(principal));
@PathVariable String clientNumber) {
log.info("Requesting client details for client number {}", clientNumber);
return clientService.getClientDetailsByClientNumber(clientNumber);
}

/**
* Performs a full-text search for clients based on the provided keyword, with pagination support.
* Performs a full-text search for clients based on the provided keyword, with pagination
* support.
*
* <p>This endpoint allows searching for clients by a keyword. The results are paginated, and the
* <p>This endpoint allows searching for clients by a keyword. The results are paginated, and the
* total count of matching records is included in the response headers.
*
* @param page the page number to retrieve (default is 0)
* @param size the number of records per page (default is 10)
* @param keyword the keyword to search for (default is an empty string, which returns all
* records)
* @param page the page number to retrieve (default is 0)
* @param size the number of records per page (default is 10)
* @param keyword the keyword to search for (default is an empty string, which returns all
* records)
* @param serverResponse the HTTP response to include the total count of records in the headers
* @return a {@link Flux} emitting {@link ClientListDto} objects containing the search results
*/
Expand All @@ -128,15 +109,15 @@ public Flux<ClientListDto> fullSearch(
})
.map(Pair::getFirst)
.doFinally(signalType ->
serverResponse
.getHeaders()
.putIfAbsent(
ApplicationConstant.X_TOTAL_COUNT,
List.of("0")
)
serverResponse
.getHeaders()
.putIfAbsent(
ApplicationConstant.X_TOTAL_COUNT,
List.of("0")
)
);
}

/**
* Retrieve a Flux of ClientLookUpDto objects by searching for clients with a specific name.
*
Expand All @@ -154,18 +135,18 @@ public Flux<ClientLookUpDto> findByClientName(@PathVariable String name) {
/**
* Finds a client based on their registration number.
*
* <p>This endpoint retrieves client information by searching for a registration number.
* <p>This endpoint retrieves client information by searching for a registration number.
* If no client is found, an error is returned.
*
* @param registrationNumber the registration number of the client to look up
* @return a {@link Mono} emitting the {@link ClientLookUpDto} if found, or an error
* if no data exists
* @return a {@link Mono} emitting the {@link ClientLookUpDto} if found, or an error if no data
* exists
*/
@GetMapping(value = "/incorporation/{registrationNumber}")
public Mono<ClientLookUpDto> findByRegistrationNumber(
@PathVariable String registrationNumber) {
log.info("Requesting a client with registration number {} from the client service.",
registrationNumber);
registrationNumber);
return clientService
.findByClientNameOrIncorporation(registrationNumber)
.next()
Expand All @@ -175,10 +156,10 @@ public Mono<ClientLookUpDto> findByRegistrationNumber(
/**
* Searches for an individual client by user ID and last name.
*
* <p>This endpoint fetches an individual client using their user ID and last name.
* <p>This endpoint fetches an individual client using their user ID and last name.
* The request is validated against existing records in the system.
*
* @param userId the unique identifier of the individual to search for
* @param userId the unique identifier of the individual to search for
* @param lastName the last name of the individual to search for
* @return a {@link Mono} indicating completion, or an error if the individual is not found
*/
Expand All @@ -187,9 +168,9 @@ public Mono<Void> findByIndividual(
@PathVariable String userId,
@RequestParam String lastName
) {
log.info("Receiving request to search individual with id {} and last name {}",
userId,
lastName);
log.info("Receiving request to search individual with id {} and last name {}",
userId,
lastName);
return clientService.findByIndividual(userId, lastName);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ca.bc.gov.app.controller.filters;

import io.micrometer.context.ContextRegistry;
import java.util.function.Function;
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

/**
* This class is a web filter that extracts a value from the security context and sets it in the MDC
* (Mapped Diagnostic Context) for logging and tracing purposes. The value is then propagated down
* the filter chain and set in the reactive context to ensure it is available for logging and
* tracing in both reactive and non-reactive parts of the application.
*/
public abstract class ContextPropagatorWebFilter implements WebFilter {

protected abstract String getContextKey();

protected abstract Function<Authentication, String> getContextValueExtractor();

/**
* Filters each incoming request to extract from the security context a {@link String} value using
* {@link #getContextValueExtractor()} and set it in the MDC. The value is then propagated down
* the filter chain and set in the reactive context to ensure it is available for logging and
* tracing in both reactive and non-reactive parts of the application.
*
* @param exchange The current server web exchange that contains information about the request and
* response.
* @param chain The web filter chain that allows the filter to pass on the request to the next
* entity in the chain.
* @return A {@link Mono} of {@link Void} that indicates when request handling is complete.
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// This is to be able to tackle non-reactive context
contextLoad();

return
// Here we are getting the user id from the security context
ReactiveSecurityContextHolder
.getContext()
.map(SecurityContext::getAuthentication)
.map(getContextValueExtractor())
// Then we set it to the MDC
.doOnNext(userId -> MDC.put(getContextKey(), userId))
// Then we chain the filter, passing the context down
.flatMap(userId -> chain
.filter(exchange)
.contextWrite(Context.of(getContextKey(), userId))
// While we are at it, we also set the context for the reactive part
.doOnNext(v -> contextLoad())
);
}

/**
* Initializes and registers a thread-local context for the current thread. This method configures
* the {@link ContextRegistry} to handle the value within the MDC (Mapped Diagnostic Context),
* allowing for the propagation of the value across different parts of the application that run on
* the same thread. It specifically sets up accessors for getting, setting, and removing the value
* from the MDC. This setup is crucial for maintaining state across the reactive and non-reactive
* parts of the application.
*/
private void contextLoad() {
ContextRegistry
.getInstance()
.registerThreadLocalAccessor(
getContextKey(),
() -> MDC.get(getContextKey()),
userId -> MDC.put(getContextKey(), userId),
() -> MDC.remove(getContextKey())
);
}

}
Loading
Loading