Skip to content
Open
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
6 changes: 6 additions & 0 deletions dossierfacile-api-tenant/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
<groupId>fr.dossierfacile</groupId>
<artifactId>dossierfacile-common-library</artifactId>
</dependency>
<dependency>
<groupId>fr.dossierfacile</groupId>
<artifactId>dossierfacile-common-test-library</artifactId>
<version>${revision}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import fr.dossierfacile.api.front.config.filter.ConnectionContextFilter;
import fr.dossierfacile.api.front.security.PartnerAuthorizationManager;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationManager;
Expand Down Expand Up @@ -31,6 +32,9 @@
@RequiredArgsConstructor
public class ResourceServerConfig {

@Value("{resource.server.config.csp}")
private String configCsp;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
Expand All @@ -41,7 +45,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
.xssProtection(withDefaults())
.cacheControl(withDefaults())
.httpStrictTransportSecurity(transport -> transport.maxAgeInSeconds(63072000).includeSubDomains(true))
.contentSecurityPolicy(csp -> csp.policyDirectives("frame-ancestors 'none'; frame-src 'none'; child-src 'none'; upgrade-insecure-requests; default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; img-src 'self' data:; font-src 'self'; connect-src *.dossierfacile.fr *.dossierfacile.fr:*; base-uri 'self'; form-action 'none'; media-src 'none'; worker-src 'none'; manifest-src 'none'; prefetch-src 'none';"))
.contentSecurityPolicy(csp -> csp.policyDirectives(configCsp))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
)
.formLogin(AbstractHttpConfigurer::disable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class SwaggerConfig {

@Bean
@Profile("!dev")
public GroupedOpenApi dfcOpenApi() {
return GroupedOpenApi.builder().displayName("API DFC").group("dfc").pathsToMatch("/dfc/**").build();
}

@Bean
@Profile("!dev")
public GroupedOpenApi partnerOpenApi() {
return GroupedOpenApi.builder().displayName("API Partner").group("api-partner").pathsToMatch("/api-partner/**").build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!dev")
@WebFilter("/api/register/account")
public class RateLimitingFilter extends AbstractRateLimitingFilter implements Filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!dev")
@WebFilter("/api/support/email")
public class RateLimitingSupportMailFilter extends AbstractRateLimitingFilter implements Filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import fr.dossierfacile.common.entity.Tenant;
import fr.dossierfacile.common.model.ApartmentSharingLinkModel;
import fr.dossierfacile.common.service.ApartmentSharingLinkService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.ResponseEntity;
Expand All @@ -22,27 +25,51 @@ public class ApartmentSharingLinkController {
private final ApartmentSharingLinkService apartmentSharingLinkService;
private final TenantService tenantService;

@ApiOperation(value = "Get apartment sharing links", notes = "Retrieves the list of apartment sharing links for the logged-in tenant.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Links retrieved successfully", response = LinksResponse.class),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope")
})
@GetMapping
public ResponseEntity<LinksResponse> getApartmentSharingLinks() {
ApartmentSharing apartmentSharing = authenticationFacade.getLoggedTenant().getApartmentSharing();
List<ApartmentSharingLinkModel> linksByMail = apartmentSharingLinkService.getLinksByMail(apartmentSharing);
return ResponseEntity.ok(new LinksResponse(linksByMail));
}

@ApiOperation(value = "Update apartment sharing link status", notes = "Updates the status of an apartment sharing link.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Status updated successfully"),
@ApiResponse(code = 400, message = "Invalid request"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope or email unverified"),
@ApiResponse(code = 404, message = "Link not found")
})
@PutMapping("/{id}")
public ResponseEntity<Void> updateApartmentSharingLinksStatus(@PathVariable Long id, @RequestParam boolean enabled) {
ApartmentSharing apartmentSharing = authenticationFacade.getLoggedTenant().getApartmentSharing();
apartmentSharingLinkService.updateStatus(id, enabled, apartmentSharing);
return ResponseEntity.ok().build();
}

@ApiOperation(value = "Resend apartment sharing link", notes = "Resends an apartment sharing link to the tenant.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Link resent successfully"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope or email unverified or apartment sharing not in tenant"),
@ApiResponse(code = 500, message = "link is disabled or call too soon or error when sending email")
})
@PostMapping("/{id}/resend")
public ResponseEntity<Void> resendApartmentSharingLink(@PathVariable Long id) {
Tenant tenant = authenticationFacade.getLoggedTenant();
tenantService.resendLink(id, tenant);
return ResponseEntity.ok().build();
}

@ApiOperation(value = "Delete apartment sharing link", notes = "Deletes an apartment sharing link.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Link deleted successfully"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope or email unverified"),
@ApiResponse(code = 500, message = "link is disabled or call too soon or error when sending email")
})
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteApartmentSharingLink(@PathVariable Long id) {
Tenant tenant = authenticationFacade.getLoggedTenant();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import fr.dossierfacile.common.entity.Property;
import fr.dossierfacile.common.entity.Tenant;
import fr.dossierfacile.common.service.interfaces.ProcessingCapacityService;
import io.swagger.annotations.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -42,26 +43,55 @@ public class TenantController {
private final UserService userService;

@GetMapping(value = "/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<TenantModel> profile(@RequestParam MultiValueMap<String, String> params) {
@ApiOperation(value = "Get tenant profile", notes = "Retrieves the profile of the logged-in tenant.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Profile retrieved successfully", response = TenantModel.class),
@ApiResponse(code = 401, message = "Unauthorized: JWT token missing or invalid"),
@ApiResponse(code = 403, message = "Forbidden: User not verified")
})
public ResponseEntity<TenantModel> profile(
@ApiParam(value = "UTM campaign parameters", example = "campaign=utm_campaign&source=utm_source&medium=utm_medium")
@RequestParam MultiValueMap<String, String> params
) {
Tenant tenant = authenticationFacade.getLoggedTenant(AcquisitionData.from(params));
tenantService.updateLastLoginDateAndResetWarnings(tenant);
return ok(tenantMapper.toTenantModel(tenant, null));
}


@GetMapping(value = "/property/{token}", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Get property and owner information", notes = "Retrieves information about a property and its owner based on the provided token.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Property information retrieved successfully", response = PropertyOModel.class),
@ApiResponse(code = 401, message = "Unauthorized: JWT token missing or invalid"),
@ApiResponse(code = 404, message = "Property not found")
})
public ResponseEntity<PropertyOModel> getInfoOfPropertyAndOwner(@PathVariable("token") String propertyToken) {
Property property = propertyService.getPropertyByToken(propertyToken);
return ok(propertyMapper.toPropertyModel(property));
}

@DeleteMapping("/deleteCoTenant/{id}")
@ApiOperation(value = "Delete a co-tenant", notes = "Deletes a co-tenant based on the provided ID.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Co-tenant deleted successfully"),
@ApiResponse(code = 403, message = "Forbidden: user not verified or co-tenant not found"),
@ApiResponse(code = 401, message = "Unauthorized: JWT token missing or invalid")
})
public ResponseEntity<Void> deleteCoTenant(@PathVariable Long id) {
Tenant tenant = authenticationFacade.getLoggedTenant();
return (userService.deleteCoTenant(tenant, id) ? ok() : status(HttpStatus.FORBIDDEN)).build();
}

@PostMapping(value = "/linkFranceConnect", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Link FranceConnect", notes = "Generates a link to FranceConnect based on the provided URL.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "FranceConnect link generated successfully"),
@ApiResponse(code = 400, message = "Bad request: URL is missing or invalid"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid")
})
public ResponseEntity<String> linkFranceConnect(@RequestBody UrlForm urlDTO) {
// Todo : Could be replaced with @Valid annotation
String currentUrl = urlDTO.getUrl();
if (currentUrl == null) {
return badRequest().build();
Expand All @@ -71,10 +101,19 @@ public ResponseEntity<String> linkFranceConnect(@RequestBody UrlForm urlDTO) {
}

@PostMapping(value = "/sendFileByMail", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Send File By Mail", notes = "Sends a file by email based on the provided email and share type.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "File sent successfully"),
@ApiResponse(code = 400, message = "Bad request: email is invalid or limit reached"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid"),
@ApiResponse(code = 500, message = "Internal error: mail cannot be sent")
})
public ResponseEntity<String> sendFileByMail(@RequestBody ShareFileByMailForm shareFileByMailForm) {
// Todo : Should add the @Valid annotation to match ShareFileByMailForm validations
Tenant tenant = authenticationFacade.getLoggedTenant();
try {
tenantService.sendFileByMail(tenant, shareFileByMailForm.getEmail(), shareFileByMailForm.getShareType());
// Todo : inside the method sendFileByMail, there is an Internal error thrown and this exception is not caught by the controller
} catch (Exception e) {
return badRequest().build();
}
Expand All @@ -83,12 +122,28 @@ public ResponseEntity<String> sendFileByMail(@RequestBody ShareFileByMailForm sh

@PreAuthorize("hasPermissionOnTenant(#tenantId)")
@GetMapping(value = "/{id}/expectedProcessingTime", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Get Expected Processing Time", notes = "Retrieves the expected processing time for a tenant based on the provided ID.")
@ApiResponses(value = {
@ApiResponse(
code = 200,
message = "Expected processing time retrieved successfully",
response = LocalDateTime.class,
examples = @Example(value = {@ExampleProperty(mediaType = "application/json", value = "null")})),
@ApiResponse(code = 401, message = "Unauthorized: JWT token missing or invalid"),
@ApiResponse(code = 403, message = "Forbidden: Access denied"),
})
public ResponseEntity<LocalDateTime> expectedProcessingTime(@PathVariable("id") Long tenantId) {
LocalDateTime expectedProcessingTime = processingCapacityService.getExpectedProcessingTime(tenantId);
return ok(expectedProcessingTime);
}

@GetMapping("/doNotArchive/{token}")
@GetMapping(value = "/doNotArchive/{token}", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Do Not Archive", notes = "Prevents the archiving of a tenant based on the provided token.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Token processed successfully"),
@ApiResponse(code = 401, message = "Unauthorized: JWT token missing or invalid"),
@ApiResponse(code = 404, message = "Token not found")
})
public ResponseEntity<Void> doNotArchive(@PathVariable String token) {
tenantService.doNotArchive(token);
return ok().build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
import fr.dossierfacile.api.front.service.interfaces.UserService;
import fr.dossierfacile.common.entity.Tenant;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
import org.springframework.web.bind.annotation.*;

import static org.springframework.http.ResponseEntity.ok;

Expand All @@ -30,38 +27,71 @@ public class UserController {
private final AuthenticationFacade authenticationFacade;

@PostMapping(value = "/forgotPassword", consumes = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Request a password reset", notes = "Sends a password reset email to the user.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = ""),
@ApiResponse(code = 400, message = "Invalid email format"),
@ApiResponse(code = 404, message = "User not found"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope")
})
public ResponseEntity<Void> forgotPassword(@Validated @RequestBody EmailResetForm email) {
userService.forgotPassword(email.getEmail());
return ok().build();
}

@PostMapping(value = "/createPassword", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation("Set a new Password to logged user")
@ApiOperation(value = "Set a new Password to logged user", notes = "Sets a new password for the logged-in user.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Password set successfully", response = TenantModel.class),
@ApiResponse(code = 400, message = "Invalid password format"),
@ApiResponse(code = 403, message = "Forbidden: User not verified or JWT token missing")
})
public ResponseEntity<TenantModel> createPassword(@Validated @RequestBody PasswordForm password) {
return ok(userService.createPassword(authenticationFacade.getLoggedTenant(), password.getPassword()));
}

@PostMapping(value = "/createPassword/{token}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Set a new Password using token", notes = "Sets a new password using a provided token.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Password set successfully", response = TenantModel.class),
@ApiResponse(code = 400, message = "Invalid token or password format"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope")
})
public ResponseEntity<TenantModel> createPassword(@PathVariable String token, @Validated @RequestBody PasswordForm password) {
return ok(userService.createPassword(token, password.getPassword()));
}


@DeleteMapping("/deleteAccount")
@ApiOperation(value = "Delete the current user account")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Ok"),
@ApiResponse(code = 403, message = "Forbidden: User not verified or JWT token missing")
})
public ResponseEntity<Void> deleteAccount() {
Tenant tenant = authenticationFacade.getLoggedTenant();
userService.deleteAccount(tenant);
return ok().build();
}

@DeleteMapping("/franceConnect")
@ApiOperation("Unlink account from FranceConnect")
@ApiOperation(value = "Unlink account from FranceConnect", notes = "Unlinks the user's account from FranceConnect.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Account unlinked successfully"),
@ApiResponse(code = 403, message = "Forbidden: User not verified or JWT token missing")
})
public ResponseEntity<Void> unlinkFranceConnect() {
Tenant tenant = authenticationFacade.getLoggedTenant();
userService.unlinkFranceConnect(tenant);
return ok().build();
}

@PostMapping("/logout")
@ApiOperation(value = "Logout the user", notes = "Logs out the user from the system.")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "User logged out successfully"),
@ApiResponse(code = 403, message = "Forbidden: JWT token missing or invalid scope")
})
public ResponseEntity<Void> logout() {
userService.logout(authenticationFacade.getKeycloakUserId());
return ok().build();
Expand Down
Loading