diff --git a/Documents/JournalDeBord.md b/Documents/JournalDeBord.md index 2d45fe139d0ef0b93eb503b2f106c8c08d0566a0..41579a32da8701c44b3d2e6a6542bbe859bbd6de 100644 --- a/Documents/JournalDeBord.md +++ b/Documents/JournalDeBord.md @@ -9,4 +9,5 @@ | 07.11.2024 - 10.11.2024 | Recherche approfondie sur BDNS | - | - | - | | | 14.11.2024 | Envoie email j+s pour plus d'info sur API | - | - | - | 17.11.2024 | Début de base de données | - | - | - -| 21.11.2024 | Génération du projet et initialisation | - | - | - \ No newline at end of file +| 21.11.2024 | Génération du projet et initialisation | - | - | - +| 23-24.11.2024 | Création d'utilisateur | - | - | - \ No newline at end of file diff --git a/VolleyHub/backend/.gitattributes:Zone.Identifier b/VolleyHub/backend/.gitattributes:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/.gitattributes:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/.gitignore:Zone.Identifier b/VolleyHub/backend/.gitignore:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/.gitignore:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/HELP.md:Zone.Identifier b/VolleyHub/backend/HELP.md:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/HELP.md:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/mvnw.cmd:Zone.Identifier b/VolleyHub/backend/mvnw.cmd:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/mvnw.cmd:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/mvnw:Zone.Identifier b/VolleyHub/backend/mvnw:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/mvnw:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/pom.xml b/VolleyHub/backend/pom.xml index 2d07b6434b14e10ae3a5a24983e61a6e71706fe5..659f1379502768dff9b423710bec956d9a43b9ac 100644 --- a/VolleyHub/backend/pom.xml +++ b/VolleyHub/backend/pom.xml @@ -1,86 +1,115 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - <parent> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-parent</artifactId> - <version>3.3.5</version> - <relativePath/> <!-- lookup parent from repository --> - </parent> - <groupId>com.hepia</groupId> - <artifactId>volleyhub</artifactId> - <version>0.0.1-SNAPSHOT</version> - <name>volleyhub</name> - <description>Backend for volleyhub</description> - <url/> - <licenses> - <license/> - </licenses> - <developers> - <developer/> - </developers> - <scm> - <connection/> - <developerConnection/> - <tag/> - <url/> - </scm> - <properties> - <java.version>17</java.version> - </properties> - <dependencies> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-data-jpa</artifactId> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-security</artifactId> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-web</artifactId> - </dependency> - - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-devtools</artifactId> - <scope>runtime</scope> - <optional>true</optional> - </dependency> - <dependency> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - <optional>true</optional> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-test</artifactId> - <scope>test</scope> - </dependency> - </dependencies> - - <build> - <plugins> - <plugin> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-maven-plugin</artifactId> - <configuration> - <excludes> - <exclude> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - </exclude> - </excludes> - </configuration> - </plugin> - </plugins> - </build> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>3.3.5</version> + <relativePath/> <!-- lookup parent from repository --> + </parent> + <groupId>com.hepia</groupId> + <artifactId>volleyhub</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>volleyhub</name> + <description>Backend for volleyhub</description> + <url/> + <licenses> + <license/> + </licenses> + <developers> + <developer/> + </developers> + <scm> + <connection/> + <developerConnection/> + <tag/> + <url/> + </scm> + <properties> + <java.version>17</java.version> + </properties> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mail</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.12.3</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.12.3</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.12.3</version> + </dependency> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <version>2.3.0</version> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <excludes> + <exclude> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> </project> diff --git a/VolleyHub/backend/pom.xml:Zone.Identifier b/VolleyHub/backend/pom.xml:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/pom.xml:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java index 1ee71768ec5dc033d80caec3c6634f245d540a86..eea21d3315e41e087554329de51ad949dd85b0c9 100644 --- a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java @@ -1,13 +1,29 @@ package com.hepia.volleyhub; +import com.hepia.volleyhub.model.entity.Role; +import com.hepia.volleyhub.repository.RoleRepository; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync +@EnableJpaAuditing public class VolleyhubApplication { - public static void main(String[] args) { - SpringApplication.run(VolleyhubApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(VolleyhubApplication.class, args); + } + @Bean + public CommandLineRunner runner(RoleRepository roleRepository) { + return args -> { + if (roleRepository.findByName("USER").isEmpty()) { + roleRepository.save(Role.builder().name("USER").build()); + } + }; + } } diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java:Zone.Identifier b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/VolleyhubApplication.java:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/BeansConfig.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/BeansConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8db95cfc6f9f0002aa0811ca2ced7e272f15cbe8 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/BeansConfig.java @@ -0,0 +1,69 @@ +package com.hepia.volleyhub.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; +import java.util.Collections; + +import static org.springframework.http.HttpHeaders.*; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpMethod.*; + +@Configuration +@RequiredArgsConstructor +public class BeansConfig { + private final UserDetailsService userDetailsService; + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CorsFilter corsFilter() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + final CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowCredentials(true); + corsConfiguration.setAllowedOrigins(Collections.singletonList("http://localhost:4200")); + corsConfiguration.setAllowedHeaders(Arrays.asList( + ORIGIN, + CONTENT_TYPE, + ACCEPT, + AUTHORIZATION + )); + corsConfiguration.setAllowedMethods(Arrays.asList( + GET.name(), + POST.name(), + PUT.name(), + PATCH.name(), + DELETE.name() + )); + source.registerCorsConfiguration("/**", corsConfiguration); + return new CorsFilter(source); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/OpenApiConfig.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..034c2499cbf4148f1a04f3fbd9ec06b61707995e --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/OpenApiConfig.java @@ -0,0 +1,48 @@ +package com.hepia.volleyhub.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.servers.Server; + +@OpenAPIDefinition( + info = @Info( + contact = @Contact( + name = "Thibault Capt", + email = "thibault.capt@etu.hesge.ch", + url = "https://gitedu.hesge.ch/niklaus.eggenber/capt-24-25" + ), + description = "Documentation for the VolleyHub API", + title = "OpenAPI VolleyHub specification", + version = "1.0.0", + license = @License( + name = "HES-SO/HEPIA", + url = "https://www.hesge.ch/hepia" + ), + termsOfService = "https://www.hesge.ch/hepia/mentions-legales" + ), + servers = @Server( + url = "http://localhost:8088/api/v1", + description = "Local environment" + ), + security = { + @SecurityRequirement( + name = "bearerAuth" + ) + } +) +@SecurityScheme( + name = "bearerAuth", + description = "JWT auth token", + scheme = "bearer", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + in = SecuritySchemeIn.HEADER +) +public class OpenApiConfig { +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/SecurityConfig.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/SecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..17db386137128864c619b7c1c9b9c435089a54fa --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/config/SecurityConfig.java @@ -0,0 +1,57 @@ +package com.hepia.volleyhub.config; + + +import com.hepia.volleyhub.filter.JwtAuthFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfig { + private final JwtAuthFilter jwtAuthFilter; + private final AuthenticationProvider authenticationProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // With new Spring Security 5.4, we can use the following code to disable CSRF + http + .cors(withDefaults()) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(req -> + req.requestMatchers( + "/auth/**", + "/v2/api-docs", + "/v3/api-docs", + "/v3/api-docs/**", + "/swagger-resources", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/security", + "/swagger-ui/**", + "/webjars/**", + "/swagger-ui.html" + ).permitAll() + .anyRequest() + .authenticated() + ) + .sessionManagement(session -> + session.sessionCreationPolicy(STATELESS) + ) + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } +} \ No newline at end of file diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/controller/AuthenticationController.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/controller/AuthenticationController.java new file mode 100644 index 0000000000000000000000000000000000000000..16b935f3d40a8eed347d5f98503457dadd39182f --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/controller/AuthenticationController.java @@ -0,0 +1,44 @@ +package com.hepia.volleyhub.controller; + +import com.hepia.volleyhub.model.dto.AuthenticationRequest; +import com.hepia.volleyhub.model.dto.AuthenticationResponse; +import com.hepia.volleyhub.model.dto.RegistrationRequest; +import com.hepia.volleyhub.service.AuthenticationService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("auth") +@RequiredArgsConstructor +@Tag(name = "Authentication") +public class AuthenticationController { + private final AuthenticationService authenticationService; + + @PostMapping("/register") + @ResponseStatus(HttpStatus.ACCEPTED) + public ResponseEntity<?> register( + @RequestBody @Valid RegistrationRequest request + ) throws MessagingException { + authenticationService.register(request); + return ResponseEntity.accepted().build(); + } + + @PostMapping("/authenticate") + public ResponseEntity<AuthenticationResponse> authenticate( + @RequestBody @Valid AuthenticationRequest request + ) { + return ResponseEntity.ok(authenticationService.authenticate(request)); + } + + @GetMapping("/activate-account") + public void confirmAccount( + @RequestParam("token") String token + ) throws MessagingException { + authenticationService.activateAccount(token); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/filter/JwtAuthFilter.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/filter/JwtAuthFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..899cf805ed947185ba565abd9c04223ee553334d --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/filter/JwtAuthFilter.java @@ -0,0 +1,71 @@ +package com.hepia.volleyhub.filter; + + +import com.hepia.volleyhub.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Service; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +@Service +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + // Every request will pass through this filter + if (request.getServletPath().contains("/api/v1/auth")) { + // If the request is for the authentication endpoint, we don't need to check for the token + filterChain.doFilter(request, response); + return; + } + + final String authorizationHeader = request.getHeader(AUTHORIZATION); + final String jwt; + final String email; + if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { + // If the request is for the authentication endpoint, we don't need to check for the token + filterChain.doFilter(request, response); + return; + } + + jwt = authorizationHeader.substring(7); // "bearer " is 7 characters long + email = jwtService.extractUsername(jwt); + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + if (jwtService.isTokenValid(jwt, userDetails)) { + // If the token is valid, we authenticate the user + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + // We set the details of the authentication + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // We set the authentication in the context + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + // We continue with the filter chain + filterChain.doFilter(request, response); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/handler/GlobalExceptionHandler.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..477f5771d6db86a3fc1a2cfb3dfd725c1afb4131 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/handler/GlobalExceptionHandler.java @@ -0,0 +1,99 @@ +package com.hepia.volleyhub.handler; + + +import com.hepia.volleyhub.model.dto.ExceptionResponse; +import jakarta.mail.MessagingException; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashSet; +import java.util.Set; + +import static com.hepia.volleyhub.model.enums.BusinessErrorCode.*; +import static org.springframework.http.HttpStatus.*; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(LockedException.class) + public ResponseEntity<ExceptionResponse> handleException(LockedException e) { + return ResponseEntity + .status(UNAUTHORIZED) + .body( + ExceptionResponse.builder() + .businessErrorCode(ACCOUNT_LOCKED.getCode()) + .businessExceptionDescription(ACCOUNT_LOCKED.getDescription()) + .error(e.getMessage()) + .build() + ); + } + + @ExceptionHandler(DisabledException.class) + public ResponseEntity<ExceptionResponse> handleException(DisabledException e) { + return ResponseEntity + .status(UNAUTHORIZED) + .body( + ExceptionResponse.builder() + .businessErrorCode(ACCOUNT_DISABLED.getCode()) + .businessExceptionDescription(ACCOUNT_DISABLED.getDescription()) + .error(e.getMessage()) + .build() + ); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity<ExceptionResponse> handleException(BadCredentialsException e) { + return ResponseEntity + .status(UNAUTHORIZED) + .body( + ExceptionResponse.builder() + .businessErrorCode(BAD_CREDENTIALS.getCode()) + .businessExceptionDescription(BAD_CREDENTIALS.getDescription()) + .error(BAD_CREDENTIALS.getDescription()) + .build() + ); + } + + @ExceptionHandler(MessagingException.class) + public ResponseEntity<ExceptionResponse> handleException(MessagingException e) { + return ResponseEntity + .status(INTERNAL_SERVER_ERROR) + .body( + ExceptionResponse.builder() + .error(e.getMessage()) + .build() + ); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity<ExceptionResponse> handleException(MethodArgumentNotValidException e) { + Set<String> errors = new HashSet<>(); + e.getBindingResult().getAllErrors().forEach(error -> errors.add(error.getDefaultMessage())); + return ResponseEntity + .status(BAD_REQUEST) + .body( + ExceptionResponse.builder() + .validationErrors(errors) + .build() + ); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity<ExceptionResponse> handleException(Exception e) { + // Log the exception + // TODO: Replace with a logger + e.printStackTrace(); + return ResponseEntity + .status(INTERNAL_SERVER_ERROR) + .body( + ExceptionResponse.builder() + .businessExceptionDescription("Internal server error, please contact the administrator") + .error(e.getMessage()) + .build() + ); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/AuthenticationRequest.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/AuthenticationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..8837f24af5a65128be1d3eb1242cad91c2299f28 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/AuthenticationRequest.java @@ -0,0 +1,21 @@ +package com.hepia.volleyhub.model.dto; + +import jakarta.validation.constraints.*; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class AuthenticationRequest { + @Email(message = "Email is not valid") + @NotEmpty(message = "Email is required") + @NotBlank(message = "Email is required") + private String email; + + @NotEmpty(message = "Password is required") + @NotNull(message = "Password is required") + @Size(min = 8, message = "Password must be at least 8 characters long") + private String password; +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/AuthenticationResponse.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/AuthenticationResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..47f45662f1b42bb391438ef69719a924692a71ca --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/AuthenticationResponse.java @@ -0,0 +1,13 @@ +package com.hepia.volleyhub.model.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class AuthenticationResponse { + private String token; +} + diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/ExceptionResponse.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/ExceptionResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..51c26d86d89c945bf5bdbdf21c08be2393bfca4e --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/ExceptionResponse.java @@ -0,0 +1,21 @@ +package com.hepia.volleyhub.model.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +import java.util.Map; +import java.util.Set; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ExceptionResponse { + private int businessErrorCode; + private String businessExceptionDescription; + private String error; + private Set<String> validationErrors; + private Map<String, String> errors; +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/RegistrationRequest.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/RegistrationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..cc737ce9ef074a795688a077ad2df0242852cc95 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/dto/RegistrationRequest.java @@ -0,0 +1,21 @@ +package com.hepia.volleyhub.model.dto; + +import jakarta.validation.constraints.*; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class RegistrationRequest { + @Email(message = "Email is not valid") + @NotEmpty(message = "Email is required") + @NotBlank(message = "Email is required") + private String email; + + @NotEmpty(message = "Password is required") + @NotNull(message = "Password is required") + @Size(min = 8, message = "Password must be at least 8 characters long") + private String password; +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/Role.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..71b4da3ed65c9bc7d6d18937fdca5a896c906d5b --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/Role.java @@ -0,0 +1,38 @@ +package com.hepia.volleyhub.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@EntityListeners(AuditingEntityListener.class) +public class Role { + @Id + @GeneratedValue + private long id; + @Column(unique = true) + private String name; + + @ManyToMany(mappedBy = "roles") + @JsonIgnore // Prevent infinite recursion + private List<User> users; + + @CreatedDate + @Column(updatable = false, nullable = false) + private LocalDate createdAt; + + @LastModifiedDate + @Column(insertable = false) + private LocalDate lastModifiedAt; +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/Token.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/Token.java new file mode 100644 index 0000000000000000000000000000000000000000..544b92ce943a9e90d063ce80169758adbc194dd3 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/Token.java @@ -0,0 +1,27 @@ +package com.hepia.volleyhub.model.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class Token { + @Id + @GeneratedValue + private long id; + private String token; + private LocalDateTime createdAt; + private LocalDateTime expiresAt; + private LocalDateTime validatedAt; + + @ManyToOne + @JoinColumn(name = "userId", nullable = false) + private User user; + +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/User.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/User.java new file mode 100644 index 0000000000000000000000000000000000000000..1d28ceed192b77aae00c9fb68be9a9f4df003f24 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/entity/User.java @@ -0,0 +1,89 @@ +package com.hepia.volleyhub.model.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.security.Principal; +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "_user") +@EntityListeners(AuditingEntityListener.class) +public class User implements UserDetails, Principal { + @Id + @GeneratedValue + private long id; + @Column(unique = true) + private String email; + private String password; + private boolean accountLocked; + private boolean enabled; + + @ManyToMany(fetch = FetchType.EAGER) + private List<Role> roles; + + @CreatedDate + @Column(updatable = false, nullable = false) + private LocalDate createdAt; + + @LastModifiedDate + @Column(insertable = false) + private LocalDate lastModifiedAt; + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return roles + .stream() + .map(r -> new SimpleGrantedAuthority(r.getName())) + .collect(Collectors.toList()); + } + + @Override + public String getName() { + return email; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public boolean isAccountNonExpired() { + return UserDetails.super.isAccountNonExpired(); // true + } + + @Override + public boolean isAccountNonLocked() { + return !accountLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return UserDetails.super.isCredentialsNonExpired(); // true + } + + @Override + public boolean isEnabled() { + return enabled; + } +} \ No newline at end of file diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/enums/BusinessErrorCode.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/enums/BusinessErrorCode.java new file mode 100644 index 0000000000000000000000000000000000000000..4586603edf7fe4191680632631e4c8bc3577207b --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/enums/BusinessErrorCode.java @@ -0,0 +1,23 @@ +package com.hepia.volleyhub.model.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.NOT_IMPLEMENTED; + +@Getter +@AllArgsConstructor +public enum BusinessErrorCode { + NO_CODE(0, NOT_IMPLEMENTED, "No code"), + INCORRECT_CURRENT_PASSWORD(300, HttpStatus.BAD_REQUEST, "Current password is incorrect"), + NEW_PASSWORD_DOES_NOT_MATCH(301, HttpStatus.BAD_REQUEST, "New password does not match"), + ACCOUNT_LOCKED(302, HttpStatus.LOCKED, "User account is locked"), + ACCOUNT_DISABLED(303, HttpStatus.LOCKED, "User account is disabled"), + BAD_CREDENTIALS(304, HttpStatus.LOCKED, "email and / or password are incorrect"), + ; + private final int code; + private final HttpStatus httpStatus; + private final String description; +} + diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/enums/EmailTemplateName.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/enums/EmailTemplateName.java new file mode 100644 index 0000000000000000000000000000000000000000..781ad6602ad71e19cbadcf86dc0f580145fe3c93 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/model/enums/EmailTemplateName.java @@ -0,0 +1,13 @@ +package com.hepia.volleyhub.model.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EmailTemplateName { + ACTIVATION("activate_account"); + + private final String name; +} + diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/RoleRepository.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/RoleRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..4dedb93fc806b3fea0a0915a5bee48af21433e35 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/RoleRepository.java @@ -0,0 +1,10 @@ +package com.hepia.volleyhub.repository; + +import com.hepia.volleyhub.model.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RoleRepository extends JpaRepository<Role, Long> { + Optional<Role> findByName(String name); +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/TokenRepository.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/TokenRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..e5638fd32c9894cb326af48b5f18da683fdf8977 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/TokenRepository.java @@ -0,0 +1,10 @@ +package com.hepia.volleyhub.repository; + +import com.hepia.volleyhub.model.entity.Token; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TokenRepository extends JpaRepository<Token, Long> { + Optional<Token> findByToken(String token); +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/UserRepository.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..925855094dbabf43b0a248afb82b064f4c234109 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.hepia.volleyhub.repository; + +import com.hepia.volleyhub.model.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository<User, Long> { + Optional<User> findByEmail(String email); +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/AuthenticationService.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/AuthenticationService.java new file mode 100644 index 0000000000000000000000000000000000000000..3ceff2ca0c0a915ec15cfe3ee6904fd567e05669 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/AuthenticationService.java @@ -0,0 +1,131 @@ +package com.hepia.volleyhub.service; + +import com.hepia.volleyhub.model.dto.AuthenticationRequest; +import com.hepia.volleyhub.model.dto.AuthenticationResponse; +import com.hepia.volleyhub.model.dto.RegistrationRequest; +import com.hepia.volleyhub.model.entity.Token; +import com.hepia.volleyhub.model.entity.User; +import com.hepia.volleyhub.repository.RoleRepository; +import com.hepia.volleyhub.repository.TokenRepository; +import com.hepia.volleyhub.repository.UserRepository; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; + +import static com.hepia.volleyhub.model.enums.EmailTemplateName.ACTIVATION; + + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; + private final TokenRepository tokenRepository; + private final EmailService emailService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + + @Value("${application.mailing.frontend.activation-url}") + String activationUrl; + + public void register(RegistrationRequest request) throws MessagingException { + var userRole = roleRepository.findByName("USER") + // TODO: better exception handling + .orElseThrow(() -> new IllegalStateException("Error: Role user is not found.")); + var user = User.builder() + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) + .accountLocked(false) + .enabled(false) + .roles(List.of(userRole)) + .build(); + + userRepository.save(user); + sendValidationEmail(user); + } + + private void sendValidationEmail(User user) throws MessagingException { + String newToken = generateAndSaveActivationToken(user); + // Send email with token + emailService.sendEmail( + user.getEmail(), + user.getEmail(), + ACTIVATION, + activationUrl, + newToken, + "Activate your account" + ); + + } + + private String generateAndSaveActivationToken(User user) { + // Generate a token + String generatedToken = generateActivationCode(6); + var token = Token + .builder() + .token(generatedToken) + .createdAt(LocalDateTime.now()) + .expiresAt(LocalDateTime.now().plusMinutes(15)) + .user(user) + .build(); + tokenRepository.save(token); + return generatedToken; + } + + private String generateActivationCode(int length) { + String characters = "0123456789"; + StringBuilder code = new StringBuilder(); + SecureRandom random = new SecureRandom(); + for (int i = 0; i < length; i++) { + // Generate a random index + int randomId = random.nextInt(characters.length()); + // Append the character at the random index + code.append(characters.charAt(randomId)); + } + + return code.toString(); + } + + public AuthenticationResponse authenticate(AuthenticationRequest request) { + var auth = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword() + ) + ); + var claims = new HashMap<String, Object>(); + var user = ((User) auth.getPrincipal()); + var jwtToken = jwtService.generateToken(claims, user); + return AuthenticationResponse.builder() + .token(jwtToken) + .build(); + } + + public void activateAccount(String token) throws MessagingException { + Token savedToken = tokenRepository.findByToken(token) + // TODO: Exception need to be defined + .orElseThrow(() -> new RuntimeException("Error: Invalid token")); + if (LocalDateTime.now().isAfter(savedToken.getExpiresAt())) { + sendValidationEmail(savedToken.getUser()); + throw new RuntimeException("Activation token expired. A new token has been sent to your email."); + } + User user = userRepository.findById(savedToken.getUser().getId()) + .orElseThrow(() -> new UsernameNotFoundException("Error: User not found")); + user.setEnabled(true); + userRepository.save(user); + savedToken.setValidatedAt(LocalDateTime.now()); + tokenRepository.save(savedToken); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/EmailService.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/EmailService.java new file mode 100644 index 0000000000000000000000000000000000000000..1583147227298e50dddf7cfe0943a0acf90b2584 --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/EmailService.java @@ -0,0 +1,66 @@ +package com.hepia.volleyhub.service; + + +import com.hepia.volleyhub.model.enums.EmailTemplateName; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +import java.util.HashMap; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.mail.javamail.MimeMessageHelper.MULTIPART_MODE_MIXED; + +@Service +@RequiredArgsConstructor +public class EmailService { + private final JavaMailSender mailSender; + private final SpringTemplateEngine templateEngine; + + @Async + public void sendEmail( + String to, + String username, + EmailTemplateName template, + String confirmationUrl, + String activationCode, + String subject + ) throws MessagingException { + String templateName; + if (template == null) { + templateName = "default"; + } else { + templateName = template.getName(); + } + + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper( + mimeMessage, + MULTIPART_MODE_MIXED, + UTF_8.name() + ); + + Map<String, Object> properties = new HashMap<>(); + properties.put("username", username); + properties.put("confirmationUrl", confirmationUrl); + properties.put("activationCode", activationCode); + + Context context = new Context(); + context.setVariables(properties); + + helper.setFrom("contact@volleyhub.com"); + helper.setTo(to); + helper.setSubject(subject); + + String templateHTML = templateEngine.process(templateName, context); + helper.setText(templateHTML, true); + mailSender.send(mimeMessage); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/JwtService.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/JwtService.java new file mode 100644 index 0000000000000000000000000000000000000000..b618cdcb0358dc1f184d2c2c631d04416fee536b --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/JwtService.java @@ -0,0 +1,79 @@ +package com.hepia.volleyhub.service; + + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.function.Function; + +@Service +public class JwtService { + @Value("${application.security.jwt.expiration}") + private long jwtExpiration; + @Value("${application.security.jwt.secret}") + private CharSequence secretKey; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts + .parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String generateToken(HashMap<String, Object> extraClaims, UserDetails userDetails) { + return buildToken(extraClaims, userDetails, jwtExpiration); + } + + private String buildToken(HashMap<String, Object> extraClaims, UserDetails userDetails, long jwtExpiration) { + var authorities = userDetails.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .toList(); + return Jwts.builder() + .claims(extraClaims) + .subject(userDetails.getUsername()) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .claim("authorities", authorities) + .signWith(getSignInKey()) + .compact(); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + return extractUsername(token).equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private SecretKey getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/UserDetailsServiceImpl.java b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2840486be18a3c4b62eb1fde103ea0812e997dfc --- /dev/null +++ b/VolleyHub/backend/src/main/java/com/hepia/volleyhub/service/UserDetailsServiceImpl.java @@ -0,0 +1,23 @@ +package com.hepia.volleyhub.service; + +import com.hepia.volleyhub.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User Not Found")); + } +} diff --git a/VolleyHub/backend/src/main/resources/application-dev.yml b/VolleyHub/backend/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..b7e8b92074c4e04e89ea0dbba14fe490dffcdd28 --- /dev/null +++ b/VolleyHub/backend/src/main/resources/application-dev.yml @@ -0,0 +1,41 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/db_volleyhub + username: username # TODO: Change username + password: password # TODO: Change password + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: update + show-sql: false + properties: + hibernate: + format_sql: true + database: postgresql + database-platform: org.hibernate.dialect.PostgreSQLDialect + mail: # Mail-dev + host: localhost + port: 1025 + username: mail # TODO: Change username + password: password # TODO: Change password + properties: + mail: + smtp: + trust: "*" + auth: true + starttls: # security layer + enable: true + connectiontimeout: 5000 + timeout: 3000 + writetimeout: 5000 + +application: + security: + jwt: + secret: 12569932e2f248ce091c43c099a36ca858c2ec703f171560ddea9afe0197ec8c + expiration: 86400000 # 1 day + mailing: + frontend: + activation-url: http://localhost:4200/activate-account +server: + port: 8088 \ No newline at end of file diff --git a/VolleyHub/backend/src/main/resources/application.properties b/VolleyHub/backend/src/main/resources/application.properties deleted file mode 100644 index 3c11d9e9451027b65e31d2cc4af3f7bb5cf153b6..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=volleyhub diff --git a/VolleyHub/backend/src/main/resources/application.properties:Zone.Identifier b/VolleyHub/backend/src/main/resources/application.properties:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/src/main/resources/application.properties:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/backend/src/main/resources/application.yml b/VolleyHub/backend/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..20243438fc9b9b5d64b309542b427e32f1175436 --- /dev/null +++ b/VolleyHub/backend/src/main/resources/application.yml @@ -0,0 +1,12 @@ +spring: + profiles: + active: dev + servlet: + multipart: + max-file-size: 50MB +springdoc: + default-produces-media-type: application/json + +server: + servlet: + context-path: /api/v1/ \ No newline at end of file diff --git a/VolleyHub/backend/src/main/resources/templates/activate_account.html b/VolleyHub/backend/src/main/resources/templates/activate_account.html new file mode 100644 index 0000000000000000000000000000000000000000..ffe7ba2527b3dec8ceadcb9073957d157c284e62 --- /dev/null +++ b/VolleyHub/backend/src/main/resources/templates/activate_account.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Activate Account</title> + <style> + body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; + } + + .container { + max-width: 600px; + margin: 10px auto; + padding: 20px; + background-color: #fff; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + } + + .activation-code { + font-size: 1.5em; + font-weight: bold; + text-align: center; + margin-bottom: 20px; + } + + .activation-link { + display: block; + text-align: center; + margin-top: 20px; + } + + .activation-link a { + display: inline-block; + padding: 10px 20px; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; + } + </style> +</head> +<body> +<div class="container"> + <h1>Activate your account</h1> + <p class="greeting" th:text="'Hello ' + ${username} + ','"></p> + <p>Please use the following activation code to activate your account.</p> + <div class="activation-code"> + <span th:text="${activationCode}"></span> + </div> + <div class="activation-link"> + <a th:href="${confirmationUrl}" target="_blank">Activate my account</a> + </div> +</div> +</body> +</html> \ No newline at end of file diff --git a/VolleyHub/backend/src/test/java/com/hepia/volleyhub/VolleyhubApplicationTests.java:Zone.Identifier b/VolleyHub/backend/src/test/java/com/hepia/volleyhub/VolleyhubApplicationTests.java:Zone.Identifier deleted file mode 100644 index b646743bfaa9649edaf896e361737b00514e33c4..0000000000000000000000000000000000000000 --- a/VolleyHub/backend/src/test/java/com/hepia/volleyhub/VolleyhubApplicationTests.java:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=C:\Users\titic\Downloads\volleyhub.zip diff --git a/VolleyHub/docker-compose.yml b/VolleyHub/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..cfcdc8d9dfad48f48517babde917acec852a808f --- /dev/null +++ b/VolleyHub/docker-compose.yml @@ -0,0 +1,30 @@ +services: + postgres: + container_name: postgres-sql-vh + image: postgres + environment: + POSTGRES_USER: username + POSTGRES_PASSWORD: password + PGDATA: /var/lib/postgresql/data + POSTGRES_DB: db_volleyhub + volumes: + - postgres:/data/postgres + ports: + - 5432:5432 + networks: + - network_vh + restart: unless-stopped + mail-dev: + container_name: mail-dev-vh + image: maildev/maildev + ports: + - 1080:1080 # Webapp + - 1025:1025 # Dev + +networks: + network_vh: + driver: bridge + +volumes: + postgres: + driver: local \ No newline at end of file