diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index 5accd00..0000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,256 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- "lastFilter": {
- "state": "OPEN",
- "assignee": "BelisAliosmanova"
- }
-}
- {
- "selectedUrlAndAccountId": {
- "url": "https://github.com/BelisAliosmanova/LifeAI.git",
- "accountId": "a9e64eaa-e818-4106-8acd-7f56e3dadcf0"
- }
-}
-
-
-
-
-
-
-
- {
- "associatedIndex": 3
-}
-
-
-
-
-
-
- {
- "keyToString": {
- "Application.LifeAiApplication.executor": "Run",
- "Docker.server.pgvector: Compose Deployment.executor": "Run",
- "Docker.server: Compose Deployment.executor": "Run",
- "JUnit.LifeAiApplicationTests.executor": "Run",
- "Maven.LifeAI [clean,test,install].executor": "Run",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "git-widget-placeholder": "crawlers",
- "ignore.virus.scanning.warn.message": "true",
- "kotlin-language-version-configured": "true",
- "last_opened_file_path": "C:/Users/Lenovo/Desktop/LifeAI/server/src/main/java/com/lifeAI/LifeAI/config"
- }
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1724664209058
-
-
- 1724664209058
-
-
-
- 1724672896291
-
-
-
- 1724672896291
-
-
-
- 1724673324894
-
-
-
- 1724673324894
-
-
-
- 1724673828988
-
-
-
- 1724673828988
-
-
-
- 1724759216722
-
-
-
- 1724759216722
-
-
-
- 1724768352224
-
-
-
- 1724768352225
-
-
-
- 1724778564361
-
-
-
- 1724778564362
-
-
-
- 1724843191672
-
-
-
- 1724843191672
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/server/pom.xml b/server/pom.xml
index 5de7a39..6cad1c7 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -45,7 +45,122 @@
selenium-java
4.9.1
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ com.github.jsqlparser
+ jsqlparser
+ 5.3
+ compile
+
+
+ io.github.resilience4j
+ resilience4j-spring-boot2
+ 2.0.2
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.8
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework
+ spring-webflux
+
+
+
+ com.google.api-client
+ google-api-client
+ 2.6.0
+
+
+ org.json
+ json
+ 20231013
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+ 3.2.1
+
+
+ org.jetbrains
+ annotations
+ 16.0.1
+
+
+ io.springfox
+ springfox-boot-starter
+ 3.0.0
+
+
+ org.modelmapper
+ modelmapper
+ 3.2.0
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.1
+ provided
+
+
+ io.github.resilience4j
+ resilience4j-spring-boot2
+ 2.0.2
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
org.simplejavamail
simple-java-mail
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/ApplicationConfig.java b/server/src/main/java/com/lifeAI/LifeAI/config/ApplicationConfig.java
index ebd233f..e948f85 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/config/ApplicationConfig.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/ApplicationConfig.java
@@ -3,12 +3,26 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.lifeAI.LifeAI.exceptions.user.UserNotFoundException;
+import com.lifeAI.LifeAI.model.baseEntity.BaseEntity;
+import com.lifeAI.LifeAI.model.dto.common.BaseDTO;
+import com.lifeAI.LifeAI.respository.UserRepository;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
-import org.springframework.context.MessageSource;
+import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableAsync;
+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.client.RestTemplate;
/**
@@ -18,13 +32,36 @@
@Configuration
@RequiredArgsConstructor
@EnableAspectJAutoProxy
+@EnableAsync
public class ApplicationConfig {
-
- private final MessageSource messageSource;
+ private final UserRepository repository;
@Bean
public ModelMapper modelMapper() {
- return new ModelMapper();
+ ModelMapper modelMapper = new ModelMapper();
+
+ modelMapper
+ .getConfiguration()
+ .setPropertyCondition(context -> {
+ if (
+ !(context.getParent().getDestination() instanceof BaseEntity &&
+ context.getParent().getSource() instanceof BaseDTO)
+ ) {
+ return true;
+ }
+
+ String destinationProperty = context.getMapping().getLastDestinationProperty().getName();
+
+ return !("id".equals(destinationProperty) ||
+ "createdAt".equals(destinationProperty) ||
+ "updatedAt".equals(destinationProperty) ||
+ "deletedAt".equals(destinationProperty));
+ })
+ .setFieldMatchingEnabled(true)
+ .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
+ .setMatchingStrategy(MatchingStrategies.STANDARD);
+
+ return modelMapper;
}
@Bean
@@ -38,10 +75,43 @@ public ObjectMapper objectMapper() {
return objectMapper;
}
+ @Bean
+ public Validator validator() {
+ Validator validator;
+
+ try (var factory = Validation.buildDefaultValidatorFactory()) {
+ validator = factory.getValidator();
+ }
+
+ return validator;
+ }
+
+ @Bean
+ public UserDetailsService userDetailsService() {
+ return email -> repository.findByEmail(email)
+ .orElseThrow(UserNotFoundException::new);
+ }
+
+ @Bean
+ public AuthenticationProvider authenticationProvider() {
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userDetailsService());
+ authProvider.setPasswordEncoder(passwordEncoder());
+ return authProvider;
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
-
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/FrontendConfig.java b/server/src/main/java/com/lifeAI/LifeAI/config/FrontendConfig.java
index 27932a8..7e4dec9 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/config/FrontendConfig.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/FrontendConfig.java
@@ -16,6 +16,6 @@
public class FrontendConfig {
private String baseUrl;
private String loginUrl;
- private String finishRegisterUrl;
private String forgottenPasswordUrl;
+ private String oauth2RedirectUrl;
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/MailConfig.java b/server/src/main/java/com/lifeAI/LifeAI/config/MailConfig.java
new file mode 100644
index 0000000..cc72174
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/MailConfig.java
@@ -0,0 +1,39 @@
+package com.lifeAI.LifeAI.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+
+import java.util.Properties;
+
+/**
+ * Configuration class for setting up email properties and creating
+ * a JavaMailSender bean for sending emails.
+ */
+@Configuration
+public class MailConfig {
+
+ @Value("${spring.security.mail.username}")
+ private String email;
+
+ @Value("${spring.security.mail.password}")
+ private String password;
+
+ @Bean
+ public JavaMailSender javaMailSender() {
+ JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
+ mailSender.setHost("smtp.gmail.com");
+ mailSender.setPort(587);
+ mailSender.setUsername(email);
+ mailSender.setPassword(password);
+
+ Properties properties = new Properties();
+ properties.put("mail.smtp.auth", true);
+ properties.put("mail.smtp.starttls.enable", true);
+ mailSender.setJavaMailProperties(properties);
+
+ return mailSender;
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/SecurityConfiguration.java b/server/src/main/java/com/lifeAI/LifeAI/config/SecurityConfiguration.java
new file mode 100644
index 0000000..878e549
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/SecurityConfiguration.java
@@ -0,0 +1,58 @@
+package com.lifeAI.LifeAI.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.lifeAI.LifeAI.filters.JwtAuthenticationFilter;
+import com.lifeAI.LifeAI.handlers.JwtAuthenticationEntryPoint;
+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.Customizer;
+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.config.http.SessionCreationPolicy;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+
+/**
+ * Configuration class for setting up security configurations, including authentication,
+ * authorization, and OAuth2 integration.
+ */
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+@EnableMethodSecurity
+public class SecurityConfiguration {
+
+ private final JwtAuthenticationFilter jwtAuthFilter;
+ private final AuthenticationProvider authenticationProvider;
+ private final LogoutHandler logoutHandler;
+ private final ObjectMapper objectMapper;
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .cors(Customizer.withDefaults())
+ .csrf(AbstractHttpConfigurer::disable)
+ .exceptionHandling(customizer -> {
+ customizer.authenticationEntryPoint(new JwtAuthenticationEntryPoint(objectMapper));
+ })
+ .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests((authorize) -> authorize
+ .anyRequest().permitAll()
+ )
+ .authenticationProvider(authenticationProvider)
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
+ .logout(httpSecurityLogoutConfigurer -> {
+ httpSecurityLogoutConfigurer.logoutUrl("/api/v1/auth/logout");
+ httpSecurityLogoutConfigurer.addLogoutHandler(logoutHandler);
+ httpSecurityLogoutConfigurer.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext());
+ });
+
+ return http.build();
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/WebClientConfig.java b/server/src/main/java/com/lifeAI/LifeAI/config/WebClientConfig.java
new file mode 100644
index 0000000..cd79065
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/WebClientConfig.java
@@ -0,0 +1,18 @@
+package com.lifeAI.LifeAI.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+public class WebClientConfig {
+
+ @Value("${spring.security.oauth2.resourceserver.opaque-token.introspection-uri}")
+ private String introspectUri;
+
+ @Bean
+ public WebClient userInfoClient() {
+ return WebClient.builder().baseUrl(introspectUri).build();
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/rateLimiting/RateLimiterConfigProperties.java b/server/src/main/java/com/lifeAI/LifeAI/config/rateLimiting/RateLimiterConfigProperties.java
new file mode 100644
index 0000000..9941412
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/rateLimiting/RateLimiterConfigProperties.java
@@ -0,0 +1,24 @@
+package com.lifeAI.LifeAI.config.rateLimiting;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Map;
+
+@Configuration
+@ConfigurationProperties(prefix = "resilience4j.rate-limiter")
+@Getter
+@Setter
+public class RateLimiterConfigProperties {
+ private Map instances;
+
+ @Getter
+ @Setter
+ public static class RateLimiterInstanceConfig {
+ private int limitForPeriod;
+ private String limitRefreshPeriod;
+ private int timeoutDuration;
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/config/rateLimiting/Resilience4jConfig.java b/server/src/main/java/com/lifeAI/LifeAI/config/rateLimiting/Resilience4jConfig.java
new file mode 100644
index 0000000..54be0cd
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/config/rateLimiting/Resilience4jConfig.java
@@ -0,0 +1,39 @@
+package com.lifeAI.LifeAI.config.rateLimiting;
+
+import io.github.resilience4j.ratelimiter.RateLimiterConfig;
+import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
+import lombok.AllArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.util.Map;
+
+@Configuration
+@AllArgsConstructor
+public class Resilience4jConfig {
+ private final RateLimiterConfigProperties rateLimiterConfigProperties;
+
+ @Bean
+ public RateLimiterRegistry rateLimiterRegistry() {
+ RateLimiterRegistry registry = RateLimiterRegistry.ofDefaults();
+
+ Map instances =
+ rateLimiterConfigProperties.getInstances();
+
+ for (Map.Entry entry : instances.entrySet()) {
+ String limiterName = entry.getKey();
+ RateLimiterConfigProperties.RateLimiterInstanceConfig config = entry.getValue();
+
+ RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
+ .limitForPeriod(config.getLimitForPeriod())
+ .limitRefreshPeriod(Duration.parse("PT" + config.getLimitRefreshPeriod().toUpperCase()))
+ .timeoutDuration(Duration.ofMillis(config.getTimeoutDuration()))
+ .build();
+
+ registry.rateLimiter(limiterName, rateLimiterConfig);
+ }
+
+ return registry;
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/controllers/ArticleController.java b/server/src/main/java/com/lifeAI/LifeAI/controllers/ArticleController.java
new file mode 100644
index 0000000..bdaa08f
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/controllers/ArticleController.java
@@ -0,0 +1,46 @@
+package com.lifeAI.LifeAI.controllers;
+
+import com.lifeAI.LifeAI.model.Article;
+import com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO;
+import com.lifeAI.LifeAI.services.ArticleService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v1/articles")
+@RequiredArgsConstructor
+public class ArticleController {
+
+ private final ArticleService articleService;
+
+ @GetMapping
+ public ResponseEntity> getAll(
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "10") int size) {
+ return ResponseEntity.ok(articleService.getAllArticles(page, size));
+ }
+
+ @GetMapping("/subcategory/{subCategory}")
+ public ResponseEntity> getBySubCategory(
+ @PathVariable String subCategory,
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "10") int size) {
+ return ResponseEntity.ok(articleService.getArticlesBySubCategory(subCategory, page, size));
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getById(@PathVariable Long id) {
+ return ResponseEntity.ok(articleService.getArticleById(id));
+ }
+
+ @GetMapping("/random/no-subcategory")
+ public ResponseEntity> getRandomNoSubCategory() {
+ List randomArticles = articleService.getRandomArticlesWithNoSubCategory(3);
+ return ResponseEntity.ok(randomArticles);
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/controllers/AuthenticationController.java b/server/src/main/java/com/lifeAI/LifeAI/controllers/AuthenticationController.java
new file mode 100644
index 0000000..a43a6cd
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/controllers/AuthenticationController.java
@@ -0,0 +1,104 @@
+package com.lifeAI.LifeAI.controllers;
+
+import com.lifeAI.LifeAI.config.FrontendConfig;
+import com.lifeAI.LifeAI.filters.JwtAuthenticationFilter;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationRequest;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+import com.lifeAI.LifeAI.model.dto.auth.RegisterRequest;
+import com.lifeAI.LifeAI.services.AuthenticationService;
+import com.lifeAI.LifeAI.services.security.events.OnPasswordResetRequestEvent;
+import com.lifeAI.LifeAI.services.security.events.OnRegistrationCompleteEvent;
+import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.modelmapper.ModelMapper;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+
+/**
+ * Controller class for handling authentication-related operations.
+ * JWT (access and refresh token);
+ * OAuth2;
+ * Email confirmation;
+ * Forgotten password.
+ */
+@RestController
+@RequestMapping("/api/v1/auth")
+@RequiredArgsConstructor
+@Tag(name = "Authentication")
+public class AuthenticationController {
+
+ private final AuthenticationService authenticationService;
+ private final ApplicationEventPublisher eventPublisher;
+ private final FrontendConfig frontendConfig;
+ private final ModelMapper modelMapper;
+
+ @Value("${server.backend.baseUrl}")
+ private String appBaseUrl;
+
+ @PostMapping("/register")
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity register(@RequestBody RegisterRequest request) {
+ AuthenticationResponse authenticationResponse = authenticationService.register(request);
+
+ // Email verification eventually
+ User user = modelMapper.map(authenticationResponse.getUser(), User.class);
+ eventPublisher.publishEvent(new OnRegistrationCompleteEvent(user, appBaseUrl));
+
+ return ResponseEntity.ok(authenticationResponse);
+ }
+
+ //Endpoint for email confirmation during registration
+ @GetMapping("/registrationConfirm")
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity confirmRegistration(@RequestParam("token") String token, HttpServletResponse httpServletResponse) throws IOException {
+ authenticationService.confirmRegistration(token);
+ httpServletResponse.sendRedirect(frontendConfig.getLoginUrl());
+ return ResponseEntity.ok("User registration confirmed successfully!");
+ }
+
+ @PostMapping("/authenticate") // login
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity authenticate(@RequestBody AuthenticationRequest request, HttpServletResponse servletResponse) {
+ AuthenticationResponse authenticationResponse = authenticationService.authenticate(request);
+ return ResponseEntity.ok(authenticationResponse);
+ }
+
+ @GetMapping("/refresh-token/{refreshToken}")
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity refreshToken(@PathVariable String refreshToken) throws IOException {
+ AuthenticationResponse authenticationResponse = authenticationService.refreshToken(refreshToken);
+ return ResponseEntity.ok(authenticationResponse);
+ }
+
+ @GetMapping("/me") // Retrieves current user information.
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity getMe(HttpServletRequest request) {
+ String jwtToken = (String) request.getAttribute(JwtAuthenticationFilter.JWT_KEY);
+ AuthenticationResponse authenticationResponse = authenticationService.me(jwtToken);
+
+ return ResponseEntity.ok(authenticationResponse);
+ }
+
+ @PostMapping("/forgot-password") // Sends link to email so the user can change their password
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity forgotPassword(@RequestParam("email") String email) {
+ User user = authenticationService.forgotPassword(email);
+ eventPublisher.publishEvent(new OnPasswordResetRequestEvent(user, appBaseUrl));
+ return ResponseEntity.ok("Password reset link sent to your email!");
+ }
+
+ @PostMapping("/password-reset")
+ @RateLimiter(name = "sensitive_operations_rate_limiter")
+ public ResponseEntity resetPassword(@RequestParam("token") String token, @RequestParam("newPassword") String newPassword) {
+ authenticationService.resetPassword(token, newPassword);
+ return ResponseEntity.ok("Password reset successfully");
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/controllers/DailyProgressController.java b/server/src/main/java/com/lifeAI/LifeAI/controllers/DailyProgressController.java
new file mode 100644
index 0000000..5fb9e69
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/controllers/DailyProgressController.java
@@ -0,0 +1,49 @@
+package com.lifeAI.LifeAI.controllers;
+
+import com.lifeAI.LifeAI.model.DailyProgress;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.services.DailyProgressService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v1/progress")
+@RequiredArgsConstructor
+public class DailyProgressController {
+
+ private final DailyProgressService dailyProgressService;
+
+ @PostMapping("/complete")
+ public ResponseEntity completeChallenge(
+ @AuthenticationPrincipal User user,
+ @RequestBody Map request
+ ) {
+ Integer challengeNumber = request.get("challengeNumber");
+
+ if (challengeNumber == null || challengeNumber < 1 || challengeNumber > 5) {
+ return ResponseEntity.badRequest().build();
+ }
+
+ DailyProgress progress = dailyProgressService.completeChallenge(
+ user.getId(),
+ challengeNumber
+ );
+
+ return ResponseEntity.ok(progress);
+ }
+
+ @GetMapping("/today")
+ public ResponseEntity getTodaysProgress(
+ @AuthenticationPrincipal User user
+ ) {
+ return dailyProgressService.getTodaysProgress(user.getId())
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.ok(DailyProgress.builder()
+ .userId(user.getId())
+ .build()));
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/controllers/MoodController.java b/server/src/main/java/com/lifeAI/LifeAI/controllers/MoodController.java
new file mode 100644
index 0000000..32b611d
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/controllers/MoodController.java
@@ -0,0 +1,41 @@
+package com.lifeAI.LifeAI.controllers;
+
+import com.lifeAI.LifeAI.model.MoodEntry;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.services.MoodService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v1/mood")
+@RequiredArgsConstructor
+public class MoodController {
+
+ private final MoodService moodService;
+
+ @PostMapping
+ public ResponseEntity saveMood(
+ @AuthenticationPrincipal User user,
+ @RequestBody Map request
+ ) {
+ Integer moodLevel = request.get("moodLevel");
+
+ if (moodLevel == null || moodLevel < 1 || moodLevel > 5) {
+ return ResponseEntity.badRequest().build();
+ }
+
+ MoodEntry saved = moodService.saveMood(user.getId(), moodLevel);
+ return ResponseEntity.ok(saved);
+ }
+
+ @GetMapping("/today")
+ public ResponseEntity getTodaysMood(@AuthenticationPrincipal User user) {
+ return moodService.getTodaysMood(user.getId())
+ .map(ResponseEntity::ok)
+ .orElse(ResponseEntity.notFound().build());
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/controllers/UserController.java b/server/src/main/java/com/lifeAI/LifeAI/controllers/UserController.java
new file mode 100644
index 0000000..a288b5d
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/controllers/UserController.java
@@ -0,0 +1,36 @@
+package com.lifeAI.LifeAI.controllers;
+
+import com.lifeAI.LifeAI.model.dto.request.UserRequestDTO;
+import com.lifeAI.LifeAI.model.dto.request.UserResponseDTO;
+import com.lifeAI.LifeAI.model.dto.response.BulkUserResponseDTO;
+import com.lifeAI.LifeAI.model.dto.response.UserService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+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 java.util.List;
+
+@RestController
+@RequestMapping("/api/users")
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserService userService;
+
+ @PostMapping("/create")
+ public ResponseEntity createUser(@Valid @RequestBody UserRequestDTO request) {
+ UserResponseDTO response = userService.createUser(request);
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ }
+
+ @PostMapping("/bulk")
+ public ResponseEntity createUsers(@Valid @RequestBody List requests) {
+ BulkUserResponseDTO response = userService.createUsers(requests);
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/enums/ExceptionSeverity.java b/server/src/main/java/com/lifeAI/LifeAI/enums/ExceptionSeverity.java
new file mode 100644
index 0000000..8dd2313
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/enums/ExceptionSeverity.java
@@ -0,0 +1,6 @@
+package com.lifeAI.LifeAI.enums;
+
+public enum ExceptionSeverity {
+ CRITICAL,
+ INFORMATIONAL
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/enums/Permission.java b/server/src/main/java/com/lifeAI/LifeAI/enums/Permission.java
new file mode 100644
index 0000000..4be6b44
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/enums/Permission.java
@@ -0,0 +1,25 @@
+package com.lifeAI.LifeAI.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Enumeration representing permissions available in the application.
+ * Each permission has a corresponding string representation.
+ * This class is used to define and access different permissions required for authorization purposes.
+ */
+@Getter
+@RequiredArgsConstructor
+public enum Permission {
+ ADMIN_READ("admin:read"),
+ ADMIN_UPDATE("admin:update"),
+ ADMIN_CREATE("admin:create"),
+ ADMIN_DELETE("admin:delete"),
+ PATIENT_READ("user:read"),
+ PATIENT_UPDATE("user:update"),
+ PATIENT_CREATE("user:create"),
+ PATIENT_DELETE("user:delete");
+
+ private final String permission;
+}
+
diff --git a/server/src/main/java/com/lifeAI/LifeAI/enums/Provider.java b/server/src/main/java/com/lifeAI/LifeAI/enums/Provider.java
new file mode 100644
index 0000000..9aa1296
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/enums/Provider.java
@@ -0,0 +1,35 @@
+package com.lifeAI.LifeAI.enums;
+
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enumeration representing OAuth2 providers available in the application.
+ * This class is used to define and access different OAuth2 providers supported for authentication.
+ */
+@Getter
+public enum Provider {
+ LOCAL("local"),
+ GOOGLE("google");
+
+ private static final Map providerMap = new HashMap<>();
+
+ static {
+ for (Provider provider : values()) {
+ providerMap.put(provider.getRegistrationId(), provider);
+ }
+ }
+
+ private final String registrationId;
+
+ Provider(String registrationId) {
+ this.registrationId = registrationId;
+ }
+
+ public static Provider getProvider(String registrationId) {
+ return providerMap.getOrDefault(registrationId, Provider.LOCAL);
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/enums/Role.java b/server/src/main/java/com/lifeAI/LifeAI/enums/Role.java
new file mode 100644
index 0000000..f5763b8
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/enums/Role.java
@@ -0,0 +1,38 @@
+package com.lifeAI.LifeAI.enums;
+
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.lifeAI.LifeAI.enums.Permission.*;
+
+
+/**
+ * Enumeration representing different roles in the application.
+ * Each role has a set of associated permissions.
+ */
+@Getter
+@RequiredArgsConstructor
+public enum Role {
+ PATIENT(Set.of(PATIENT_READ, PATIENT_UPDATE, PATIENT_CREATE, PATIENT_DELETE)),
+ ADMIN(Set.of(ADMIN_READ, ADMIN_UPDATE, ADMIN_DELETE, ADMIN_CREATE,
+ PATIENT_READ, PATIENT_UPDATE, PATIENT_CREATE, PATIENT_DELETE)),
+ DOCTOR(Set.of(/* doctor permissions here */)),
+ ASSISTANT(Set.of(/* assistant permissions here */));
+
+ private final Set permissions;
+
+ public List getAuthorities() {
+ List authorities = permissions.stream()
+ .map(p -> new SimpleGrantedAuthority(p.getPermission()))
+ .collect(Collectors.toList());
+
+ authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name().toUpperCase()));
+ return authorities;
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/enums/TokenType.java b/server/src/main/java/com/lifeAI/LifeAI/enums/TokenType.java
new file mode 100644
index 0000000..e09a2e4
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/enums/TokenType.java
@@ -0,0 +1,6 @@
+package com.lifeAI.LifeAI.enums;
+
+public enum TokenType {
+ ACCESS,
+ REFRESH
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/AccessDeniedException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/AccessDeniedException.java
index 83c6a2f..bfd9ef2 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/AccessDeniedException.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/AccessDeniedException.java
@@ -1,7 +1,5 @@
package com.lifeAI.LifeAI.exceptions.common;
-import org.springframework.context.MessageSource;
-import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
/**
@@ -10,7 +8,7 @@
* Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
*/
public class AccessDeniedException extends ApiException {
- public AccessDeniedException(MessageSource messageSource) {
- super(messageSource.getMessage("access.denied", null, LocaleContextHolder.getLocale()), HttpStatus.FORBIDDEN);
+ public AccessDeniedException() {
+ super("Отказан достъп!", HttpStatus.FORBIDDEN);
}
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/BadRequestException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/BadRequestException.java
index b0e42c3..db3a2dc 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/BadRequestException.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/BadRequestException.java
@@ -1,7 +1,5 @@
package com.lifeAI.LifeAI.exceptions.common;
-import org.springframework.context.MessageSource;
-import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
/**
@@ -10,10 +8,6 @@
* Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
*/
public class BadRequestException extends ApiException {
- public BadRequestException(MessageSource messageSource) {
- super(messageSource.getMessage("bad.request", null, LocaleContextHolder.getLocale()), HttpStatus.BAD_REQUEST);
- }
-
public BadRequestException(String message) {
super(message, HttpStatus.BAD_REQUEST);
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/InternalServerErrorException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/InternalServerErrorException.java
index 912fb96..bacea7d 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/InternalServerErrorException.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/InternalServerErrorException.java
@@ -1,7 +1,7 @@
package com.lifeAI.LifeAI.exceptions.common;
-import org.springframework.context.MessageSource;
-import org.springframework.context.i18n.LocaleContextHolder;
+import lombok.Getter;
+import lombok.Setter;
import org.springframework.http.HttpStatus;
/**
@@ -9,12 +9,18 @@
* Extends ApiException and sets the appropriate message and HTTP status code.
* Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
*/
+@Getter
+@Setter
public class InternalServerErrorException extends ApiException {
- public InternalServerErrorException(MessageSource messageSource) {
- super(messageSource.getMessage("internal.server.error", null, LocaleContextHolder.getLocale()), HttpStatus.INTERNAL_SERVER_ERROR);
+ private RuntimeException innerRuntimeException;
+
+ public InternalServerErrorException(RuntimeException runtimeException) {
+ super("Вътрешна грешка на сървъра!", HttpStatus.INTERNAL_SERVER_ERROR);
+ setInnerRuntimeException(runtimeException);
}
public InternalServerErrorException(String message) {
super(message, HttpStatus.INTERNAL_SERVER_ERROR);
+ setInnerRuntimeException(new RuntimeException(message));
}
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/NoSuchElementException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/NoSuchElementException.java
index 32d6b81..9d980cf 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/NoSuchElementException.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/NoSuchElementException.java
@@ -1,7 +1,5 @@
package com.lifeAI.LifeAI.exceptions.common;
-import org.springframework.context.MessageSource;
-import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
/**
@@ -10,10 +8,6 @@
* Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
*/
public class NoSuchElementException extends ApiException {
- public NoSuchElementException(MessageSource messageSource) {
- super(messageSource.getMessage("no.such.element", null, LocaleContextHolder.getLocale()), HttpStatus.NOT_FOUND);
- }
-
public NoSuchElementException(String message) {
super(message, HttpStatus.NOT_FOUND);
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/TooManyRequestsException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/TooManyRequestsException.java
index 7852831..31c5900 100644
--- a/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/TooManyRequestsException.java
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/common/TooManyRequestsException.java
@@ -2,13 +2,8 @@
import org.springframework.http.HttpStatus;
-/**
- * Exception thrown to indicate that the client has sent too many requests in a given amount of time.
- * Related to @RateLimited()
- * Extends ApiException and sets the appropriate message and HTTP status code.
- */
public class TooManyRequestsException extends ApiException {
- public TooManyRequestsException(String message) {
- super(message, HttpStatus.TOO_MANY_REQUESTS);
+ public TooManyRequestsException() {
+ super("Твърде много заявки. Моля, опитайте отново по-късно.", HttpStatus.TOO_MANY_REQUESTS);
}
}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/email/EmailNotVerified.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/email/EmailNotVerified.java
new file mode 100644
index 0000000..d3d53aa
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/email/EmailNotVerified.java
@@ -0,0 +1,14 @@
+package com.lifeAI.LifeAI.exceptions.email;
+
+
+import com.lifeAI.LifeAI.exceptions.common.BadRequestException;
+
+/**
+ * Exception thrown to indicate that the user's email has not been verified.
+ * Extends BadRequestException and sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
+ */
+public class EmailNotVerified extends BadRequestException {
+ public EmailNotVerified() {
+ super("Не можете да влезете, имейлът, даден по време на регистрацията, не е потвърден!");
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/token/ExpiredTokenException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/token/ExpiredTokenException.java
new file mode 100644
index 0000000..9ae311c
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/token/ExpiredTokenException.java
@@ -0,0 +1,10 @@
+package com.lifeAI.LifeAI.exceptions.token;
+
+
+import com.lifeAI.LifeAI.exceptions.common.UnauthorizedException;
+
+public class ExpiredTokenException extends UnauthorizedException {
+ public ExpiredTokenException() {
+ super("Токенът е изтекъл!");
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/token/InvalidTokenException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/token/InvalidTokenException.java
new file mode 100644
index 0000000..38c215b
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/token/InvalidTokenException.java
@@ -0,0 +1,13 @@
+package com.lifeAI.LifeAI.exceptions.token;
+
+import com.lifeAI.LifeAI.exceptions.common.UnauthorizedException;
+
+/**
+ * Exception thrown when the provided authentication token is invalid or expired (so the request cannot be authorized).
+ * Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
+ */
+public class InvalidTokenException extends UnauthorizedException {
+ public InvalidTokenException() {
+ super("Невалиден токен!");
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserCreateException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserCreateException.java
new file mode 100644
index 0000000..13b39ad
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserCreateException.java
@@ -0,0 +1,21 @@
+package com.lifeAI.LifeAI.exceptions.user;
+
+import com.lifeAI.LifeAI.exceptions.common.BadRequestException;
+
+/**
+ * Exception thrown when there is an issue creating a user, either due to invalid data or duplicate user details.
+ * Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
+ */
+public class UserCreateException extends BadRequestException {
+
+ /**
+ * Constructs a UserCreateException with a message indicating either a duplicate email or invalid user data.
+ */
+ public UserCreateException(boolean isUnique) {
+ super(
+ isUnique
+ ? "Вече съществува потребител със същия имейл!"
+ : "Невалидни потребителски данни!"
+ );
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserLoginException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserLoginException.java
new file mode 100644
index 0000000..e6dad12
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserLoginException.java
@@ -0,0 +1,12 @@
+package com.lifeAI.LifeAI.exceptions.user;
+
+import com.lifeAI.LifeAI.exceptions.common.BadRequestException;
+
+/**
+ * Exception thrown when there is an issue with user login, such as invalid email or password.
+ */
+public class UserLoginException extends BadRequestException {
+ public UserLoginException() {
+ super("Невалиден имейл или парола!");
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserNotFoundException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserNotFoundException.java
new file mode 100644
index 0000000..9918529
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserNotFoundException.java
@@ -0,0 +1,14 @@
+package com.lifeAI.LifeAI.exceptions.user;
+
+
+import com.lifeAI.LifeAI.exceptions.common.NoSuchElementException;
+
+/**
+ * Exception indicating that the user is not found.
+ * Sets the appropriate message using MessageSource (the messages are in src/main/resources/messages).
+ */
+public class UserNotFoundException extends NoSuchElementException {
+ public UserNotFoundException() {
+ super("Потребителят не е намерен!");
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserValidationException.java b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserValidationException.java
new file mode 100644
index 0000000..b9cc83f
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/exceptions/user/UserValidationException.java
@@ -0,0 +1,12 @@
+package com.lifeAI.LifeAI.exceptions.user;
+
+import com.lifeAI.LifeAI.exceptions.common.ValidationException;
+import jakarta.validation.ConstraintViolation;
+
+import java.util.Set;
+
+public class UserValidationException extends ValidationException {
+ public UserValidationException(Set> validationErrors) {
+ super(validationErrors);
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/filters/JwtAuthenticationFilter.java b/server/src/main/java/com/lifeAI/LifeAI/filters/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..fc39aee
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/filters/JwtAuthenticationFilter.java
@@ -0,0 +1,134 @@
+package com.lifeAI.LifeAI.filters;
+
+import com.lifeAI.LifeAI.exceptions.user.UserNotFoundException;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.PublicUserDTO;
+import com.lifeAI.LifeAI.respository.TokenRepository;
+import com.lifeAI.LifeAI.services.JwtService;
+import com.lifeAI.LifeAI.services.UserService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.modelmapper.ModelMapper;
+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.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+
+/**
+ * Filter responsible for JWT-based authentication.
+ */
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ /**
+ * Key to retrieve user information from request attribute.
+ */
+ public static final String USER_KEY = "user";
+ public static final String JWT_KEY = "jwt";
+
+ private final JwtService jwtService;
+ private final UserService userService;
+ private final ModelMapper modelMapper;
+ private final TokenRepository tokenRepository;
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain
+ ) throws ServletException, IOException {
+ String path = request.getServletPath();
+
+ // Skip authentication for specific paths related to authentication process
+ if (path.contains("/api/v1/auth") || path.contains("/api/v1/oauth2")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ request.setAttribute(USER_KEY, null);
+ request.setAttribute(JWT_KEY, null);
+
+ final String authHeader = request.getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ final String jwt = authHeader.substring(7);
+ if (jwt.isEmpty()) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String userEmail;
+
+ try {
+ userEmail = jwtService.extractUsername(jwt);
+ } catch (Exception exception) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails;
+
+ try {
+ userDetails = userService.findByEmail(userEmail);
+ } catch (UserNotFoundException exception) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // Check if token is valid and not revoked or expired
+ boolean isTokenValid = tokenRepository.findByToken(jwt)
+ .map(t -> !t.isExpired() && !t.isRevoked())
+ .orElse(false);
+
+ if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
+
+ // Set user authentication in security context
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+ userDetails,
+ null,
+ userDetails.getAuthorities()
+ );
+
+ authToken.setDetails(
+ new WebAuthenticationDetailsSource().buildDetails(request)
+ );
+
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+
+ request.setAttribute(USER_KEY, JwtAuthenticationFilter.safeMapUser(userDetails));
+ request.setAttribute(JWT_KEY, jwt);
+ }
+
+ filterChain.doFilter(request, response);
+ }
+
+ public static PublicUserDTO safeMapUser(UserDetails userDetails) {
+ if (userDetails instanceof User user) {
+ return PublicUserDTO.builder()
+ .id(user.getId())
+ .firstName(user.getFirstName())
+ .lastName(user.getLastName())
+ .email(user.getEmail())
+ .role(user.getRole())
+ .build();
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/handlers/CustomExceptionHandler.java b/server/src/main/java/com/lifeAI/LifeAI/handlers/CustomExceptionHandler.java
new file mode 100644
index 0000000..815cae3
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/handlers/CustomExceptionHandler.java
@@ -0,0 +1,86 @@
+package com.lifeAI.LifeAI.handlers;
+
+import com.lifeAI.LifeAI.exceptions.common.*;
+import com.lifeAI.LifeAI.exceptions.user.UserLoginException;
+import com.lifeAI.LifeAI.model.dto.response.ExceptionResponse;
+import com.lifeAI.LifeAI.services.ExceptionService;
+import com.lifeAI.LifeAI.utils.ApiExceptionParser;
+import io.github.resilience4j.ratelimiter.RequestNotPermitted;
+import jakarta.validation.ConstraintViolationException;
+import lombok.AllArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.transaction.TransactionException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+/**
+ * Global exception handler for handling various types of exceptions and converting them into standardized API responses.
+ */
+@ControllerAdvice
+@AllArgsConstructor
+public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
+ private final ExceptionService exceptionService;
+
+ @ExceptionHandler(RuntimeException.class)
+ public ResponseEntity handleRuntimeExceptions(RuntimeException exception) {
+ return handleApiExceptions(new InternalServerErrorException(exception));
+ }
+
+ @ExceptionHandler(InternalAuthenticationServiceException.class)
+ public ResponseEntity handleInternalAuthServiceExceptions(InternalAuthenticationServiceException exception) {
+ Throwable cause = exception.getCause();
+
+ if (cause instanceof ApiException) {
+ return handleApiExceptions((ApiException) cause);
+ }
+
+ return handleRuntimeExceptions(exception);
+ }
+
+ @ExceptionHandler(BadCredentialsException.class)
+ public ResponseEntity handleBadCredentialsExceptions() {
+ return handleApiExceptions(new UserLoginException());
+ }
+
+ @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
+ public ResponseEntity handleAccessDeniedExceptions() {
+ return handleApiExceptions(new AccessDeniedException());
+ }
+
+ @ExceptionHandler(TransactionException.class)
+ public ResponseEntity handleTransactionExceptions(TransactionException exception) {
+ if (exception.getRootCause() instanceof ConstraintViolationException) {
+ return handleConstraintValidationExceptions((ConstraintViolationException) exception.getRootCause());
+ }
+
+ return handleRuntimeExceptions(exception);
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ public ResponseEntity handleConstraintValidationExceptions(ConstraintViolationException exception) {
+ return handleApiExceptions(new ValidationException(exception.getConstraintViolations()));
+ }
+
+ @ExceptionHandler(RequestNotPermitted.class)
+ public ResponseEntity handleRateLimitException() {
+ return handleApiExceptions(new TooManyRequestsException());
+ }
+
+ @ExceptionHandler(ApiException.class)
+ public ResponseEntity handleApiExceptions(ApiException exception) {
+ ExceptionResponse apiException = ApiExceptionParser.parseException(exception);
+
+ if (exception instanceof InternalServerErrorException internalException) {
+ exceptionService.log(internalException.getInnerRuntimeException(), internalException.getStatusCode());
+ } else {
+ exceptionService.log(exception);
+ }
+
+ return ResponseEntity
+ .status(apiException.getStatus())
+ .body(apiException);
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/handlers/JwtAuthenticationEntryPoint.java b/server/src/main/java/com/lifeAI/LifeAI/handlers/JwtAuthenticationEntryPoint.java
new file mode 100644
index 0000000..c83f567
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/handlers/JwtAuthenticationEntryPoint.java
@@ -0,0 +1,37 @@
+package com.lifeAI.LifeAI.handlers;
+
+import com.lifeAI.LifeAI.exceptions.common.AccessDeniedException;
+import com.lifeAI.LifeAI.utils.ObjectMapperHelper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ * JwtAuthenticationEntryPoint class is responsible for handling authentication errors
+ * that occur when attempting to access secured endpoints without proper authentication credentials.
+ * It sends a standardized error response back to the client with appropriate HTTP status code and error message.
+ */
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
+ AuthenticationException e) throws IOException {
+ // Serialize the error response using ObjectMapper and write it to the HttpServletResponse
+ ObjectMapperHelper
+ .writeExceptionToObjectMapper(
+ objectMapper,
+ new AccessDeniedException(),
+ httpServletResponse
+ );
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/handlers/LogoutHandler.java b/server/src/main/java/com/lifeAI/LifeAI/handlers/LogoutHandler.java
new file mode 100644
index 0000000..7ba2192
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/handlers/LogoutHandler.java
@@ -0,0 +1,50 @@
+package com.lifeAI.LifeAI.handlers;
+
+import com.lifeAI.LifeAI.exceptions.token.InvalidTokenException;
+import com.lifeAI.LifeAI.services.TokenService;
+import com.lifeAI.LifeAI.utils.ObjectMapperHelper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+
+
+/**
+ * LogoutHandler is responsible for handling user logout by invalidating the JWT token and removing associated cookies.
+ */
+@Service
+@RequiredArgsConstructor
+public class LogoutHandler implements org.springframework.security.web.authentication.logout.LogoutHandler {
+
+ private final TokenService tokenService;
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Performs user logout by invalidating the JWT token and removing associated cookies.
+ * If the token is invalid or missing, it sends a standardized error response back to the client.
+ */
+ @Override
+ public void logout(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ Authentication authentication
+ ) {
+ final String authHeader = request.getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ try {
+ ObjectMapperHelper.writeExceptionToObjectMapper(objectMapper, new InvalidTokenException(), response);
+ return;
+ } catch (IOException exception) {
+ return;
+ }
+ }
+
+ final String jwt = authHeader.substring(7);
+ tokenService.logoutToken(jwt);
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/Article.java b/server/src/main/java/com/lifeAI/LifeAI/model/Article.java
new file mode 100644
index 0000000..e91a3ab
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/Article.java
@@ -0,0 +1,58 @@
+package com.lifeAI.LifeAI.model;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import java.time.LocalDateTime;
+
+@Entity
+@Getter
+@Setter
+@ToString
+@RequiredArgsConstructor
+@Table(name = "articles")
+public class Article {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false)
+ private Long id;
+
+ @Column(name = "title", columnDefinition = "TEXT")
+ private String title;
+
+ @Column(name = "description", columnDefinition = "LONGTEXT")
+ private String description;
+
+ @Column(name = "thumbnail_picture")
+ private String thumbnailPicture;
+
+ @Column(name = "category_id")
+ private Integer categoryId;
+
+ @Column(name = "visibility")
+ private String visibility;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+
+ @Column(name = "deleted_at")
+ private LocalDateTime deletedAt;
+
+ @Column(name = "sub_category")
+ private String subCategory;
+
+ @Column(name = "calories")
+ private Integer calories;
+
+ @Column(name = "hours")
+ private Integer hours;
+
+ @Column(name = "minutes")
+ private Integer minutes;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/DailyProgress.java b/server/src/main/java/com/lifeAI/LifeAI/model/DailyProgress.java
new file mode 100644
index 0000000..663306a
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/DailyProgress.java
@@ -0,0 +1,68 @@
+package com.lifeAI.LifeAI.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+@Table(name = "daily_progress", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"user_id", "date"})
+})
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DailyProgress {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "user_id", nullable = false)
+ private Long userId;
+
+ @Column(nullable = false)
+ private LocalDate date;
+
+ @ElementCollection(fetch = FetchType.EAGER)
+ @CollectionTable(
+ name = "completed_challenges",
+ joinColumns = @JoinColumn(name = "progress_id")
+ )
+ @Column(name = "challenge_number")
+ private Set completedChallenges = new HashSet<>();
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at", nullable = false)
+ private LocalDateTime updatedAt;
+
+ @PrePersist
+ protected void onCreate() {
+ createdAt = LocalDateTime.now();
+ updatedAt = LocalDateTime.now();
+ if (date == null) {
+ date = LocalDate.now();
+ }
+ }
+
+ @PreUpdate
+ protected void onUpdate() {
+ updatedAt = LocalDateTime.now();
+ }
+
+ public void addCompletedChallenge(Integer challengeNumber) {
+ if (completedChallenges == null) {
+ completedChallenges = new HashSet<>();
+ }
+ completedChallenges.add(challengeNumber);
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/Exception.java b/server/src/main/java/com/lifeAI/LifeAI/model/Exception.java
new file mode 100644
index 0000000..23dd923
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/Exception.java
@@ -0,0 +1,58 @@
+package com.lifeAI.LifeAI.model;
+
+import com.lifeAI.LifeAI.enums.ExceptionSeverity;
+import com.lifeAI.LifeAI.exceptions.common.ApiException;
+import com.lifeAI.LifeAI.model.baseEntity.BaseEntity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import lombok.*;
+
+import java.util.Arrays;
+
+@Getter
+@Setter
+@ToString
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Exception extends BaseEntity {
+ private Integer statusCode;
+ private String exceptionType;
+ private String exceptionMessage;
+ private String stackTraceString;
+ private String methodName;
+ private String className;
+ private Integer lineNumber;
+ @Enumerated(EnumType.STRING)
+ private ExceptionSeverity severity;
+
+ public static Exception mapFromRuntimeException(RuntimeException runtimeException, int statusCode) {
+ String exceptionType = runtimeException.getClass().getName();
+ String exceptionMessage = runtimeException.getMessage();
+
+ StackTraceElement[] stackTrace = runtimeException.getStackTrace();
+ String stackTraceString = Arrays.toString(stackTrace);
+
+ String methodName = stackTrace[0].getMethodName();
+ String className = stackTrace[0].getClassName();
+ int lineNumber = stackTrace[0].getLineNumber();
+
+ return Exception.builder()
+ .statusCode(statusCode)
+ .exceptionType(exceptionType)
+ .exceptionMessage(exceptionMessage)
+ .stackTraceString(stackTraceString)
+ .methodName(methodName)
+ .className(className)
+ .lineNumber(lineNumber)
+ .severity(ExceptionSeverity.CRITICAL)
+ .build();
+ }
+
+ public static Exception mapFromApiException(ApiException apiException) {
+ Exception exception = mapFromRuntimeException(apiException, apiException.getStatusCode());
+ exception.setSeverity(ExceptionSeverity.INFORMATIONAL);
+
+ return exception;
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/MoodEntry.java b/server/src/main/java/com/lifeAI/LifeAI/model/MoodEntry.java
new file mode 100644
index 0000000..ab8d9ce
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/MoodEntry.java
@@ -0,0 +1,45 @@
+package com.lifeAI.LifeAI.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "mood_entries", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"user_id", "date"})
+})
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MoodEntry {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "user_id", nullable = false)
+ private Long userId;
+
+ @Column(name = "mood_level", nullable = false)
+ private Integer moodLevel; // 1-5
+
+ @Column(nullable = false)
+ private LocalDate date;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+
+ @PrePersist
+ protected void onCreate() {
+ createdAt = LocalDateTime.now();
+ if (date == null) {
+ date = LocalDate.now();
+ }
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/Token.java b/server/src/main/java/com/lifeAI/LifeAI/model/Token.java
new file mode 100644
index 0000000..1d3bf54
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/Token.java
@@ -0,0 +1,37 @@
+package com.lifeAI.LifeAI.model;
+
+import com.lifeAI.LifeAI.enums.TokenType;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Entity
+@Table(name = "tokens")
+public class Token {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ public Long id;
+
+ @Column(unique = true)
+ public String token;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "token_type", nullable = false)
+ public TokenType tokenType;
+
+ @Column(nullable = false)
+ public boolean revoked;
+
+ @Column(nullable = false)
+ public boolean expired;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id")
+ private User user;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/User.java b/server/src/main/java/com/lifeAI/LifeAI/model/User.java
new file mode 100644
index 0000000..f0c8f43
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/User.java
@@ -0,0 +1,98 @@
+package com.lifeAI.LifeAI.model;
+
+import com.lifeAI.LifeAI.enums.Role;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+@Entity
+@Table(name = "users")
+@Data
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class User implements UserDetails {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String uuid;
+
+ private String firstName;
+
+ private String lastName;
+
+ @Column(unique = true)
+ private String email;
+
+ private String password;
+ private Boolean encrypted;
+ private String role;
+ @Column(name = "enabled")
+ private boolean enabled;
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+
+ @Column(name = "email_verified_at")
+ private LocalDateTime emailVerifiedAt;
+
+ private LocalDateTime deletedAt;
+
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ if (this.role == null) return List.of();
+
+ try {
+ Role roleEnum = Role.valueOf(this.role.toUpperCase());
+ return roleEnum.getAuthorities();
+ } catch (IllegalArgumentException e) {
+ return List.of();
+ }
+ }
+
+ @Override
+ public String getUsername() {
+ return this.email;
+ }
+
+ // Role check helpers
+ public boolean isAdmin() {
+ return "admin".equalsIgnoreCase(role);
+ }
+
+ public boolean isDoctor() {
+ return "doctor".equalsIgnoreCase(role);
+ }
+
+// // Password update method example
+// public boolean updatePassword(String currentPassword, String newPassword, String confirmPassword) {
+// BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+// if (currentPassword != null && encoder.matches(currentPassword, this.password) && newPassword.equals(confirmPassword)) {
+// this.password = encoder.encode(newPassword);
+// return true;
+// }
+// return false;
+// }
+
+ public boolean isAssistant() {
+ return "assistant".equalsIgnoreCase(role);
+ }
+
+ public boolean isPatient() {
+ return "patient".equalsIgnoreCase(role);
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/VerificationToken.java b/server/src/main/java/com/lifeAI/LifeAI/model/VerificationToken.java
new file mode 100644
index 0000000..de02aa3
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/VerificationToken.java
@@ -0,0 +1,43 @@
+package com.lifeAI.LifeAI.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.sql.Date;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.Calendar;
+
+@Entity
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Table(name = "verification_tokens")
+public class VerificationToken {
+ private static final int EXPIRATION = 60 * 24;
+ public LocalDateTime createdAt;
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ @Column(unique = false)
+ private String token;
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "user_id", nullable = false)
+ private User user;
+ private Date expiryDate;
+
+ public VerificationToken(String token, User user) {
+ this.token = token;
+ this.user = user;
+ this.expiryDate = calculateExpiryDate();
+ }
+
+ private Date calculateExpiryDate() {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(new Timestamp(cal.getTime().getTime()));
+ cal.add(Calendar.MINUTE, VerificationToken.EXPIRATION);
+ return new Date(cal.getTime().getTime());
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/baseEntity/BaseEntity.java b/server/src/main/java/com/lifeAI/LifeAI/model/baseEntity/BaseEntity.java
new file mode 100644
index 0000000..138db44
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/baseEntity/BaseEntity.java
@@ -0,0 +1,51 @@
+package com.lifeAI.LifeAI.model.baseEntity;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Getter
+@Setter
+@ToString
+@RequiredArgsConstructor
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class BaseEntity {
+
+ @Id
+ private UUID id;
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at", nullable = false)
+ private LocalDateTime updatedAt;
+
+ @Column(name = "deleted_at", nullable = true)
+ private LocalDateTime deletedAt;
+
+ @PrePersist
+ protected void onCreate() {
+ if (this.id == null) {
+ this.id = UUID.randomUUID();
+ }
+ LocalDateTime now = LocalDateTime.now();
+ this.createdAt = now;
+ this.updatedAt = now;
+ this.deletedAt = null;
+ }
+
+ @PreUpdate
+ protected void onUpdate() {
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void delete() {
+ this.deletedAt = LocalDateTime.now();
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AccessTokenBodyDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AccessTokenBodyDTO.java
new file mode 100644
index 0000000..43abdf7
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AccessTokenBodyDTO.java
@@ -0,0 +1,12 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AccessTokenBodyDTO {
+ private String accessToken;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AdminUserDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AdminUserDTO.java
new file mode 100644
index 0000000..fa33ffc
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AdminUserDTO.java
@@ -0,0 +1,14 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class AdminUserDTO extends PublicUserDTO {
+ private String password;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AuthenticationRequest.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AuthenticationRequest.java
new file mode 100644
index 0000000..42d2ee5
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AuthenticationRequest.java
@@ -0,0 +1,15 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class AuthenticationRequest {
+ private String email;
+ private String password;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AuthenticationResponse.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AuthenticationResponse.java
new file mode 100644
index 0000000..ac245b7
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/AuthenticationResponse.java
@@ -0,0 +1,19 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuthenticationResponse implements Serializable {
+ private String accessToken;
+ private String refreshToken;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private PublicUserDTO user;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/OAuth2UserInfoDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/OAuth2UserInfoDTO.java
new file mode 100644
index 0000000..2f48cdf
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/OAuth2UserInfoDTO.java
@@ -0,0 +1,22 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import com.lifeAI.LifeAI.enums.Provider;
+import com.lifeAI.LifeAI.model.dto.common.BaseDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@NoArgsConstructor
+public class OAuth2UserInfoDTO extends BaseDTO {
+ private String sub;
+ private String name;
+ private String given_name;
+ private String family_name;
+ private String picture;
+ private String email;
+ private boolean email_verified;
+ private String locale;
+ private Provider provider = Provider.LOCAL;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/PublicUserDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/PublicUserDTO.java
new file mode 100644
index 0000000..1135705
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/PublicUserDTO.java
@@ -0,0 +1,16 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PublicUserDTO {
+ private Long id;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private String role;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/RefreshTokenBodyDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/RefreshTokenBodyDTO.java
new file mode 100644
index 0000000..3f810e5
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/RefreshTokenBodyDTO.java
@@ -0,0 +1,12 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RefreshTokenBodyDTO {
+ private String refreshToken;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/RegisterRequest.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/RegisterRequest.java
new file mode 100644
index 0000000..1146413
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/auth/RegisterRequest.java
@@ -0,0 +1,26 @@
+package com.lifeAI.LifeAI.model.dto.auth;
+
+import com.lifeAI.LifeAI.enums.Provider;
+import com.lifeAI.LifeAI.enums.Role;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class RegisterRequest {
+ private String email;
+ private String password;
+ private String firstName;
+ private String lastName;
+ private String nationalIdNumber;
+ private Long cityId;
+ private String gender;
+ private String phoneNumber;
+ private LocalDate birthDate;
+ private Role role = Role.PATIENT;
+ private Provider provider = Provider.LOCAL;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/common/ArticleCardDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/common/ArticleCardDTO.java
new file mode 100644
index 0000000..242924b
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/common/ArticleCardDTO.java
@@ -0,0 +1,19 @@
+package com.lifeAI.LifeAI.model.dto.common;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class ArticleCardDTO {
+ private Long id;
+ private String title;
+ private String description;
+ private String thumbnailPicture;
+ private String subCategory;
+ private Integer calories;
+ private Integer hours;
+ private Integer minutes;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/common/BaseDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/common/BaseDTO.java
new file mode 100644
index 0000000..6b4e47d
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/common/BaseDTO.java
@@ -0,0 +1,4 @@
+package com.lifeAI.LifeAI.model.dto.common;
+
+abstract public class BaseDTO {
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/request/UserRequestDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/request/UserRequestDTO.java
new file mode 100644
index 0000000..d3e8839
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/request/UserRequestDTO.java
@@ -0,0 +1,23 @@
+package com.lifeAI.LifeAI.model.dto.request;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class UserRequestDTO {
+
+ private String firstName;
+
+ private String lastName;
+
+ private String email;
+
+ private String password;
+
+ private String role;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/request/UserResponseDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/request/UserResponseDTO.java
new file mode 100644
index 0000000..33affc4
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/request/UserResponseDTO.java
@@ -0,0 +1,20 @@
+package com.lifeAI.LifeAI.model.dto.request;
+
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class UserResponseDTO {
+
+ private Long id;
+ private String uuid;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private String role;
+ private boolean enabled;
+ private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/BulkUserResponseDTO.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/BulkUserResponseDTO.java
new file mode 100644
index 0000000..3d72f67
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/BulkUserResponseDTO.java
@@ -0,0 +1,16 @@
+package com.lifeAI.LifeAI.model.dto.response;
+
+import com.lifeAI.LifeAI.model.dto.request.UserResponseDTO;
+import lombok.*;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class BulkUserResponseDTO {
+ private int successCount;
+ private int failureCount;
+ private List createdUsers;
+ private List errors;
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/ExceptionResponse.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/ExceptionResponse.java
new file mode 100644
index 0000000..a8346ce
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/ExceptionResponse.java
@@ -0,0 +1,20 @@
+package com.lifeAI.LifeAI.model.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ExceptionResponse {
+ private String message;
+ private LocalDateTime dateTime;
+ private HttpStatus status;
+ private Integer statusCode;
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/UserService.java b/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/UserService.java
new file mode 100644
index 0000000..fe379fe
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/model/dto/response/UserService.java
@@ -0,0 +1,74 @@
+package com.lifeAI.LifeAI.model.dto.response;
+
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.request.UserRequestDTO;
+import com.lifeAI.LifeAI.model.dto.request.UserResponseDTO;
+import com.lifeAI.LifeAI.respository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class UserService {
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ public BulkUserResponseDTO createUsers(List requests) {
+ List created = new ArrayList<>();
+ List failed = new ArrayList<>();
+
+ for (UserRequestDTO request : requests) {
+ try {
+ created.add(createUser(request));
+ } catch (Exception e) {
+ failed.add(request.getEmail() + ": " + e.getMessage());
+ }
+ }
+
+ return BulkUserResponseDTO.builder()
+ .successCount(created.size())
+ .failureCount(failed.size())
+ .createdUsers(created)
+ .errors(failed)
+ .build();
+ }
+
+ public UserResponseDTO createUser(UserRequestDTO request) {
+ if (userRepository.existsByEmail(request.getEmail())) {
+ throw new RuntimeException("Email already in use: " + request.getEmail());
+ }
+
+ User user = User.builder()
+ .uuid(UUID.randomUUID().toString())
+ .firstName(request.getFirstName())
+ .lastName(request.getLastName())
+ .email(request.getEmail())
+ .password(passwordEncoder.encode(request.getPassword()))
+ .role(request.getRole().toUpperCase())
+ .encrypted(true)
+ .enabled(true)
+ .createdAt(LocalDateTime.now())
+ .updatedAt(LocalDateTime.now())
+ .build();
+
+ User saved = userRepository.save(user);
+
+ return UserResponseDTO.builder()
+ .id(saved.getId())
+ .uuid(saved.getUuid())
+ .firstName(saved.getFirstName())
+ .lastName(saved.getLastName())
+ .email(saved.getEmail())
+ .role(saved.getRole())
+ .enabled(saved.isEnabled())
+ .createdAt(saved.getCreatedAt())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/respository/ArticleRepository.java b/server/src/main/java/com/lifeAI/LifeAI/respository/ArticleRepository.java
new file mode 100644
index 0000000..35168c2
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/respository/ArticleRepository.java
@@ -0,0 +1,46 @@
+package com.lifeAI.LifeAI.respository;
+
+import com.lifeAI.LifeAI.model.Article;
+import com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ArticleRepository extends JpaRepository {
+
+ @Query("SELECT new com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO(" +
+ "a.id, a.title, " +
+ "CASE WHEN LENGTH(a.description) > 150 THEN CONCAT(SUBSTRING(a.description, 1, 150), '...') " +
+ "ELSE a.description END, " +
+ "a.thumbnailPicture, a.subCategory, a.calories, a.hours, a.minutes) " +
+ "FROM Article a " +
+ "WHERE a.deletedAt IS NULL AND a.subCategory IS NOT NULL " +
+ "ORDER BY a.createdAt DESC")
+ Page findAllCards(Pageable pageable);
+
+ @Query("SELECT new com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO(" +
+ "a.id, a.title, " +
+ "CASE WHEN LENGTH(a.description) > 150 THEN CONCAT(SUBSTRING(a.description, 1, 150), '...') " +
+ "ELSE a.description END, " +
+ "a.thumbnailPicture, a.subCategory, a.calories, a.hours, a.minutes) " +
+ "FROM Article a " +
+ "WHERE a.subCategory = :subCategory AND a.deletedAt IS NULL " +
+ "ORDER BY a.createdAt DESC")
+ Page findCardsBySubCategory(@Param("subCategory") String subCategory, Pageable pageable);
+
+ @Query("SELECT new com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO(" +
+ "a.id, a.title, " +
+ "CASE WHEN LENGTH(a.description) > 150 THEN CONCAT(SUBSTRING(a.description, 1, 150), '...') " +
+ "ELSE a.description END, " +
+ "a.thumbnailPicture, a.subCategory, a.calories, a.hours, a.minutes) " +
+ "FROM Article a " +
+ "WHERE a.subCategory IS NULL AND a.deletedAt IS NULL " +
+ "ORDER BY function('RAND')")
+ List findRandomArticlesWithNoSubCategory(Pageable pageable);
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/respository/DailyProgressRepository.java b/server/src/main/java/com/lifeAI/LifeAI/respository/DailyProgressRepository.java
new file mode 100644
index 0000000..ee17b6c
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/respository/DailyProgressRepository.java
@@ -0,0 +1,14 @@
+package com.lifeAI.LifeAI.respository;
+
+import com.lifeAI.LifeAI.model.DailyProgress;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+@Repository
+public interface DailyProgressRepository extends JpaRepository {
+
+ Optional findByUserIdAndDate(Long userId, LocalDate date);
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/respository/MoodRepository.java b/server/src/main/java/com/lifeAI/LifeAI/respository/MoodRepository.java
new file mode 100644
index 0000000..2d43b64
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/respository/MoodRepository.java
@@ -0,0 +1,16 @@
+package com.lifeAI.LifeAI.respository;
+
+import com.lifeAI.LifeAI.model.MoodEntry;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+@Repository
+public interface MoodRepository extends JpaRepository {
+
+ Optional findByUserIdAndDate(Long userId, LocalDate date);
+
+ boolean existsByUserIdAndDate(Long userId, LocalDate date);
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/respository/TokenRepository.java b/server/src/main/java/com/lifeAI/LifeAI/respository/TokenRepository.java
new file mode 100644
index 0000000..fc90783
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/respository/TokenRepository.java
@@ -0,0 +1,14 @@
+package com.lifeAI.LifeAI.respository;
+
+import com.lifeAI.LifeAI.model.Token;
+import com.lifeAI.LifeAI.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface TokenRepository extends JpaRepository {
+ List findAllByUser(User user);
+
+ Optional findByToken(String token);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/respository/UserRepository.java b/server/src/main/java/com/lifeAI/LifeAI/respository/UserRepository.java
new file mode 100644
index 0000000..79b5a18
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/respository/UserRepository.java
@@ -0,0 +1,17 @@
+package com.lifeAI.LifeAI.respository;
+
+import com.lifeAI.LifeAI.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+
+ Optional findByEmail(String email);
+ boolean existsByEmail(String email);
+ List findByEnabledFalseAndCreatedAtBeforeAndDeletedAtIsNull(LocalDateTime thresholdDateTime);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/respository/VerificationTokenRepository.java b/server/src/main/java/com/lifeAI/LifeAI/respository/VerificationTokenRepository.java
new file mode 100644
index 0000000..086a048
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/respository/VerificationTokenRepository.java
@@ -0,0 +1,19 @@
+package com.lifeAI.LifeAI.respository;
+
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.VerificationToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface VerificationTokenRepository extends JpaRepository {
+
+ VerificationToken findByToken(String token);
+
+ VerificationToken findByUser(User user);
+
+ List findByUserAndCreatedAtBefore(User user, LocalDateTime thresholdDateTime);
+
+ void deleteAllByUser(User user);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/ArticleService.java b/server/src/main/java/com/lifeAI/LifeAI/services/ArticleService.java
new file mode 100644
index 0000000..cc4d808
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/ArticleService.java
@@ -0,0 +1,14 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.model.Article;
+import com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO;
+import org.springframework.data.domain.Page;
+
+import java.util.List;
+
+public interface ArticleService {
+ Page getAllArticles(int page, int size);
+ Page getArticlesBySubCategory(String subCategory, int page, int size);
+ Article getArticleById(Long id);
+ List getRandomArticlesWithNoSubCategory(int count);
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/AuthenticationService.java b/server/src/main/java/com/lifeAI/LifeAI/services/AuthenticationService.java
new file mode 100644
index 0000000..5285a16
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/AuthenticationService.java
@@ -0,0 +1,26 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationRequest;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+import com.lifeAI.LifeAI.model.dto.auth.RegisterRequest;
+
+import java.io.IOException;
+
+public interface AuthenticationService {
+ AuthenticationResponse register(RegisterRequest request);
+
+ AuthenticationResponse authenticate(AuthenticationRequest request);
+
+ AuthenticationResponse refreshToken(String refreshToken) throws IOException;
+
+ AuthenticationResponse me(
+ String jwtToken
+ );
+
+ void resetPassword(String token, String newPassword);
+
+ void confirmRegistration(String verificationToken);
+
+ User forgotPassword(String email);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/DailyProgressService.java b/server/src/main/java/com/lifeAI/LifeAI/services/DailyProgressService.java
new file mode 100644
index 0000000..0cf0537
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/DailyProgressService.java
@@ -0,0 +1,41 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.model.DailyProgress;
+import com.lifeAI.LifeAI.respository.DailyProgressRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class DailyProgressService {
+
+ private final DailyProgressRepository dailyProgressRepository;
+
+ @Transactional
+ public DailyProgress completeChallenge(Long userId, Integer challengeNumber) {
+ if (challengeNumber < 1 || challengeNumber > 5) {
+ throw new IllegalArgumentException("Challenge number must be between 1 and 5");
+ }
+
+ LocalDate today = LocalDate.now();
+
+ DailyProgress progress = dailyProgressRepository
+ .findByUserIdAndDate(userId, today)
+ .orElse(DailyProgress.builder()
+ .userId(userId)
+ .date(today)
+ .build());
+
+ progress.addCompletedChallenge(challengeNumber);
+
+ return dailyProgressRepository.save(progress);
+ }
+
+ public Optional getTodaysProgress(Long userId) {
+ return dailyProgressRepository.findByUserIdAndDate(userId, LocalDate.now());
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/ExceptionService.java b/server/src/main/java/com/lifeAI/LifeAI/services/ExceptionService.java
new file mode 100644
index 0000000..ff4e303
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/ExceptionService.java
@@ -0,0 +1,11 @@
+package com.lifeAI.LifeAI.services;
+
+
+import com.lifeAI.LifeAI.exceptions.common.ApiException;
+
+public interface ExceptionService {
+
+ void log(ApiException runtimeException);
+
+ void log(RuntimeException runtimeException, int statusCode);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/JwtService.java b/server/src/main/java/com/lifeAI/LifeAI/services/JwtService.java
new file mode 100644
index 0000000..dff8824
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/JwtService.java
@@ -0,0 +1,21 @@
+package com.lifeAI.LifeAI.services;
+
+import io.jsonwebtoken.Claims;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Map;
+import java.util.function.Function;
+
+public interface JwtService {
+ String extractUsername(String token);
+
+ T extractClaim(String token, Function claimsResolver);
+
+ String generateToken(UserDetails userDetails);
+
+ String generateToken(Map extraClaims, UserDetails userDetails);
+
+ String generateRefreshToken(UserDetails userDetails);
+
+ boolean isTokenValid(String token, UserDetails userDetails);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/MoodService.java b/server/src/main/java/com/lifeAI/LifeAI/services/MoodService.java
new file mode 100644
index 0000000..fe8a923
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/MoodService.java
@@ -0,0 +1,44 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.model.MoodEntry;
+import com.lifeAI.LifeAI.respository.MoodRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class MoodService {
+
+ private final MoodRepository moodRepository;
+
+ @Transactional
+ public MoodEntry saveMood(Long userId, Integer moodLevel) {
+ LocalDate today = LocalDate.now();
+
+ // Check if mood already exists for today
+ Optional existing = moodRepository.findByUserIdAndDate(userId, today);
+
+ if (existing.isPresent()) {
+ // Update existing mood
+ MoodEntry mood = existing.get();
+ mood.setMoodLevel(moodLevel);
+ return moodRepository.save(mood);
+ } else {
+ // Create new mood entry
+ MoodEntry mood = MoodEntry.builder()
+ .userId(userId)
+ .moodLevel(moodLevel)
+ .date(today)
+ .build();
+ return moodRepository.save(mood);
+ }
+ }
+
+ public Optional getTodaysMood(Long userId) {
+ return moodRepository.findByUserIdAndDate(userId, LocalDate.now());
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/OAuth2AuthenticationService.java b/server/src/main/java/com/lifeAI/LifeAI/services/OAuth2AuthenticationService.java
new file mode 100644
index 0000000..12e5e8d
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/OAuth2AuthenticationService.java
@@ -0,0 +1,10 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+
+public interface OAuth2AuthenticationService {
+
+ String getOAuthGoogleLoginUrl();
+
+ AuthenticationResponse processOAuthGoogleLogin(String code);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/TokenService.java b/server/src/main/java/com/lifeAI/LifeAI/services/TokenService.java
new file mode 100644
index 0000000..86b0d0d
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/TokenService.java
@@ -0,0 +1,28 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.enums.TokenType;
+import com.lifeAI.LifeAI.model.Token;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+
+import java.util.List;
+
+public interface TokenService {
+ Token findByToken(String jwt);
+
+ List findByUser(User user);
+
+ void saveToken(User user, String jwtToken, TokenType tokenType);
+
+ void revokeToken(Token token);
+
+ void revokeAllUserTokens(User user);
+
+ void logoutToken(String jwt);
+
+ AuthenticationResponse generateAuthResponse(User user);
+
+ void createVerificationToken(User user, String token);
+
+ void clearVerificationTokensByUser(User user);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/UserService.java b/server/src/main/java/com/lifeAI/LifeAI/services/UserService.java
new file mode 100644
index 0000000..16a2dc4
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/UserService.java
@@ -0,0 +1,28 @@
+package com.lifeAI.LifeAI.services;
+
+
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.AdminUserDTO;
+import com.lifeAI.LifeAI.model.dto.auth.OAuth2UserInfoDTO;
+import com.lifeAI.LifeAI.model.dto.auth.PublicUserDTO;
+import com.lifeAI.LifeAI.model.dto.auth.RegisterRequest;
+
+import java.util.List;
+
+public interface UserService {
+ User createUser(RegisterRequest request);
+
+ User findByEmail(String email);
+
+ List getAllUsers();
+
+ AdminUserDTO getByIdAdmin(Long id);
+
+ AdminUserDTO updateUser(Long id, AdminUserDTO userDTO, PublicUserDTO currentUser);
+
+ void deleteUserById(Long id, PublicUserDTO currentUser);
+
+ User processOAuthUser(OAuth2UserInfoDTO oAuth2User);
+
+ User findById(Long id);
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/UserServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/UserServiceImpl.java
new file mode 100644
index 0000000..cb110b5
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/UserServiceImpl.java
@@ -0,0 +1,176 @@
+package com.lifeAI.LifeAI.services;
+
+import com.lifeAI.LifeAI.enums.Provider;
+import com.lifeAI.LifeAI.enums.Role;
+import com.lifeAI.LifeAI.exceptions.common.AccessDeniedException;
+import com.lifeAI.LifeAI.exceptions.user.UserCreateException;
+import com.lifeAI.LifeAI.exceptions.user.UserNotFoundException;
+import com.lifeAI.LifeAI.exceptions.user.UserValidationException;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.AdminUserDTO;
+import com.lifeAI.LifeAI.model.dto.auth.OAuth2UserInfoDTO;
+import com.lifeAI.LifeAI.model.dto.auth.PublicUserDTO;
+import com.lifeAI.LifeAI.model.dto.auth.RegisterRequest;
+import com.lifeAI.LifeAI.respository.UserRepository;
+import jakarta.validation.ConstraintViolationException;
+import lombok.RequiredArgsConstructor;
+import org.modelmapper.ModelMapper;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@Service
+public class UserServiceImpl implements UserService {
+ private final PasswordEncoder passwordEncoder;
+ private final UserRepository userRepository;
+ private final ModelMapper modelMapper;
+
+ /**
+ * Creates a new user based on the provided registration request.
+ *
+ * @param request The registration request containing user details.
+ * @return The created user.
+ * @throws UserCreateException If there is an issue creating the user.
+ * @throws DataIntegrityViolationException If there is a data integrity violation while creating the user.
+ * @throws ConstraintViolationException If there is a constraint violation while creating the user.
+ */
+ @Override
+ public User createUser(RegisterRequest request) {
+ if (userRepository.findByEmail(request.getEmail()).isPresent()) {
+ throw new UserCreateException(true);
+ }
+
+ try {
+ User user = buildUser(request);
+ user.setRole("patient");
+
+ return userRepository.save(user);
+ } catch (DataIntegrityViolationException exception) {
+ throw new UserCreateException(true);
+ } catch (ConstraintViolationException exception) {
+ throw new UserValidationException(exception.getConstraintViolations());
+ }
+ }
+
+ @Override
+ public User findByEmail(String email) {
+ return userRepository.findByEmail(email)
+ .orElseThrow(UserNotFoundException::new);
+ }
+
+ @Override
+ public List getAllUsers() {
+ return userRepository
+ .findAll();
+ }
+
+ @Override
+ public AdminUserDTO updateUser(Long id, AdminUserDTO userDTO, PublicUserDTO currentUser) {
+ User userToUpdate = findById(id);
+
+ if (!(userToUpdate.getId().equals(currentUser.getId())) && !currentUser.getRole().equals(Role.ADMIN)) {
+ throw new AccessDeniedException();
+ }
+
+ if (currentUser.getRole().equals(Role.PATIENT)) {
+ userToUpdate.setFirstName(userDTO.getFirstName());
+ userToUpdate.setLastName(userDTO.getLastName());
+ } else if (currentUser.getRole().equals(Role.ADMIN)) {
+ // It is not null it is "" so don't change it
+ if (userDTO.getPassword() == "") {
+ userDTO.setPassword(userToUpdate.getPassword());
+ } else {
+ userDTO.setPassword(passwordEncoder.encode(userDTO.getPassword()));
+ }
+
+ modelMapper.map(userDTO, userToUpdate);
+ }
+
+ userToUpdate.setId(id);
+
+ User updatedUser = userRepository.save(userToUpdate);
+ return modelMapper.map(updatedUser, AdminUserDTO.class);
+ }
+
+
+ @Override
+ public void deleteUserById(Long id, PublicUserDTO currentUser) {
+ User user = findById(id);
+
+ if (user.getId().equals(currentUser.getId())) {
+ throw new AccessDeniedException();
+ }
+
+ if (user.getDeletedAt() == null) {
+ user.setDeletedAt(LocalDateTime.now());
+ } else {
+ user.setDeletedAt(null);
+ }
+
+ userRepository.save(user);
+ }
+
+ /**
+ * Processes the OAuth user obtained from the OAuth2 provider.
+ * If the user does not exist in the database, a new user is created based on the OAuth user details.
+ *
+ * @param oAuth2User The OAuth2 user obtained from the OAuth provider.
+ * @return The processed user.
+ */
+ @Override
+ public User processOAuthUser(OAuth2UserInfoDTO oAuth2User) {
+ User user = userRepository.findByEmail(oAuth2User.getEmail()).orElse(null);
+
+ if (user == null) {
+ RegisterRequest registerRequest = new RegisterRequest();
+
+ registerRequest.setEmail(oAuth2User.getEmail());
+ registerRequest.setProvider(oAuth2User.getProvider());
+
+ if (oAuth2User.getProvider().equals(Provider.GOOGLE)) {
+ registerRequest.setFirstName(oAuth2User.getGiven_name());
+ registerRequest.setLastName(oAuth2User.getFamily_name());
+ }
+
+ user = userRepository.save(buildUser(registerRequest));
+ }
+
+ return user;
+ }
+
+ @Override
+ public User findById(Long id) {
+ return userRepository.findById(id)
+ .orElseThrow(UserNotFoundException::new);
+ }
+
+ @Override
+ public AdminUserDTO getByIdAdmin(Long id) {
+ User user = userRepository.findById(id).orElseThrow(UserNotFoundException::new);
+ return modelMapper.map(user, AdminUserDTO.class);
+ }
+
+ private User buildUser(RegisterRequest request) {
+ User.UserBuilder userBuilder = User
+ .builder()
+ .uuid(UUID.randomUUID().toString())
+ .firstName(request.getFirstName())
+ .lastName(request.getLastName())
+ .email(request.getEmail())
+ .createdAt(LocalDateTime.now())
+ .updatedAt(LocalDateTime.now())
+ .encrypted(false) // public data
+ .role("patient");
+
+ if (request.getPassword() != null) {
+ userBuilder.password(passwordEncoder.encode(request.getPassword()));
+ }
+
+ return userBuilder.build();
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/impl/ArticleServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/impl/ArticleServiceImpl.java
new file mode 100644
index 0000000..7749be3
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/impl/ArticleServiceImpl.java
@@ -0,0 +1,44 @@
+package com.lifeAI.LifeAI.services.impl;
+
+import com.lifeAI.LifeAI.model.Article;
+import com.lifeAI.LifeAI.model.dto.common.ArticleCardDTO;
+import com.lifeAI.LifeAI.respository.ArticleRepository;
+import com.lifeAI.LifeAI.services.ArticleService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+
+@Service
+@RequiredArgsConstructor
+public class ArticleServiceImpl implements ArticleService {
+ private final ArticleRepository articleRepository;
+
+ @Override
+ public Page getAllArticles(int page, int size) {
+ Pageable pageable = PageRequest.of(page, size);
+ return articleRepository.findAllCards(pageable);
+ }
+
+ @Override
+ public Page getArticlesBySubCategory(String subCategory, int page, int size) {
+ Pageable pageable = PageRequest.of(page, size);
+ return articleRepository.findCardsBySubCategory(subCategory, pageable);
+ }
+
+ @Override
+ public Article getArticleById(Long id) {
+ return articleRepository.findById(id)
+ .orElseThrow(() -> new NoSuchElementException("Article with id " + id + " not found"));
+ }
+
+ @Override
+ public List getRandomArticlesWithNoSubCategory(int count) {
+ return articleRepository.findRandomArticlesWithNoSubCategory(Pageable.ofSize(count));
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/impl/ExceptionServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/impl/ExceptionServiceImpl.java
new file mode 100644
index 0000000..a811325
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/impl/ExceptionServiceImpl.java
@@ -0,0 +1,27 @@
+package com.lifeAI.LifeAI.services.impl;
+
+import com.lifeAI.LifeAI.exceptions.common.ApiException;
+import com.lifeAI.LifeAI.model.Exception;
+import com.lifeAI.LifeAI.services.ExceptionService;
+import lombok.AllArgsConstructor;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+@Service
+@Component
+@AllArgsConstructor
+public class ExceptionServiceImpl implements ExceptionService {
+
+ @Override
+ @Async
+ public void log(ApiException apiException) {
+ Exception exception = Exception.mapFromApiException(apiException);
+ }
+
+ @Override
+ @Async
+ public void log(RuntimeException runtimeException, int statusCode) {
+ Exception exception = Exception.mapFromRuntimeException(runtimeException, statusCode);
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/AuthenticationServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/AuthenticationServiceImpl.java
new file mode 100644
index 0000000..ce86114
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/AuthenticationServiceImpl.java
@@ -0,0 +1,272 @@
+package com.lifeAI.LifeAI.services.security;
+
+import com.lifeAI.LifeAI.config.rateLimiting.RateLimiterConfigProperties;
+import com.lifeAI.LifeAI.enums.TokenType;
+import com.lifeAI.LifeAI.exceptions.email.EmailNotVerified;
+import com.lifeAI.LifeAI.exceptions.token.ExpiredTokenException;
+import com.lifeAI.LifeAI.exceptions.token.InvalidTokenException;
+import com.lifeAI.LifeAI.exceptions.user.UserLoginException;
+import com.lifeAI.LifeAI.exceptions.user.UserNotFoundException;
+import com.lifeAI.LifeAI.model.Token;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.VerificationToken;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationRequest;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+import com.lifeAI.LifeAI.model.dto.auth.PublicUserDTO;
+import com.lifeAI.LifeAI.model.dto.auth.RegisterRequest;
+import com.lifeAI.LifeAI.respository.UserRepository;
+import com.lifeAI.LifeAI.respository.VerificationTokenRepository;
+import com.lifeAI.LifeAI.services.AuthenticationService;
+import com.lifeAI.LifeAI.services.JwtService;
+import com.lifeAI.LifeAI.services.TokenService;
+import com.lifeAI.LifeAI.services.UserService;
+import com.lifeAI.LifeAI.utils.PasswordEncryptionUtils;
+import lombok.RequiredArgsConstructor;
+import org.modelmapper.ModelMapper;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Calendar;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class AuthenticationServiceImpl implements AuthenticationService {
+ private final UserService userService;
+ private final TokenService tokenService;
+ private final JwtService jwtService;
+ private final AuthenticationManager authenticationManager;
+ private final ModelMapper modelMapper;
+ private final VerificationTokenRepository verificationTokenRepository;
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final RateLimiterConfigProperties rateLimiterConfigProperties;
+
+ /**
+ * Registers a new user based on the provided registration request.
+ */
+ @Override
+ public AuthenticationResponse register(RegisterRequest request) {
+ User user = userService.createUser(request);
+
+ userRepository.save(user);
+
+ return tokenService.generateAuthResponse(user);
+ }
+
+ // Login with correct email and password
+ @Override
+ public AuthenticationResponse authenticate(AuthenticationRequest request) {
+ User user;
+
+ try {
+ user = userService.findByEmail(request.getEmail());
+ } catch (UserNotFoundException userNotFoundException) {
+ throw new UserLoginException();
+ }
+
+ boolean passedFirstCheck = PasswordEncryptionUtils.validatePassword(request.getPassword(), user.getPassword());
+
+ if (passedFirstCheck) {
+ user.setPassword(passwordEncoder.encode(request.getPassword()));
+ userRepository.save(user);
+
+ if (!user.isEnabled()) {
+ throw new EmailNotVerified();
+ }
+ } else {
+ try {
+ authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(
+ request.getEmail(),
+ request.getPassword()
+ )
+ );
+ } catch (DisabledException exception) {
+ throw new EmailNotVerified();
+ } catch (AuthenticationException exception) {
+ throw new UserLoginException();
+ }
+ }
+
+ System.out.println(user.getEmail());
+ tokenService.revokeAllUserTokens(user);
+ return tokenService.generateAuthResponse(user);
+ }
+
+ /**
+ * Generates a new access token and updates the refresh token based on the provided refresh token.
+ * If the refresh token is missing or invalid, it throws an InvalidTokenException.
+ * If the refresh token is valid, it generates a new access token, revokes all existing user tokens,
+ * and updates the refresh token to the provided one.
+ */
+ @Override
+ public AuthenticationResponse refreshToken(String refreshToken) {
+ if (refreshToken == null || refreshToken.isEmpty()) {
+ throw new InvalidTokenException();
+ }
+
+ String userEmail;
+
+ try {
+ userEmail = jwtService.extractUsername(refreshToken);
+ } catch (JwtException exception) {
+ throw new InvalidTokenException();
+ }
+
+ if (userEmail == null) {
+ throw new InvalidTokenException();
+ }
+
+ // Make sure token is a refresh token not access token
+ Token token = tokenService.findByToken(refreshToken);
+ if (token != null && token.tokenType != TokenType.REFRESH) {
+ throw new InvalidTokenException();
+ }
+
+ User user = userService.findByEmail(userEmail);
+
+ if (!jwtService.isTokenValid(refreshToken, user)) {
+ tokenService.revokeToken(token);
+ throw new InvalidTokenException();
+ }
+
+ String accessToken = jwtService.generateToken(user);
+
+ tokenService.revokeAllUserTokens(user);
+ tokenService.saveToken(user, accessToken, TokenType.ACCESS);
+ tokenService.saveToken(user, refreshToken, TokenType.REFRESH);
+
+ return AuthenticationResponse
+ .builder()
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .build();
+ }
+
+ /**
+ * Retrieves user information based on the provided JWT token.
+ * If the token is invalid or missing, it throws an InvalidTokenException.
+ * If the token is valid, it retrieves the user's access and refresh tokens, updates the refresh token if necessary,
+ * and returns an authentication response containing the user's information and tokens.
+ */
+ @Override
+ public AuthenticationResponse me(String jwtToken) {
+ if (jwtToken == null || jwtToken.isEmpty()) {
+ throw new InvalidTokenException();
+ }
+
+ Token accessToken = tokenService.findByToken(jwtToken);
+
+ if (accessToken == null) {
+ throw new InvalidTokenException();
+ }
+
+ User user = accessToken.getUser();
+
+ boolean isTokenValid;
+
+ try {
+ isTokenValid = jwtService.isTokenValid(accessToken.getToken(), user);
+ } catch (JwtException jwtException) {
+ isTokenValid = false;
+ }
+
+ if (!isTokenValid) {
+ tokenService.revokeAllUserTokens(user);
+ throw new InvalidTokenException();
+ }
+
+ List tokens = tokenService.findByUser(user);
+ List refreshTokens = tokens.stream().filter(x -> x.getTokenType() == TokenType.REFRESH).toList();
+
+ if (refreshTokens.isEmpty()) {
+ throw new InvalidTokenException();
+ }
+
+ Token refreshToken = refreshTokens.get(0);
+
+ if (refreshToken == null) {
+ throw new InvalidTokenException();
+ }
+
+ String refreshTokenString;
+
+ if (!jwtService.isTokenValid(refreshToken.getToken(), user)) {
+ refreshTokenString = jwtService.generateRefreshToken(user);
+ tokenService.saveToken(user, refreshTokenString, TokenType.REFRESH);
+ } else {
+ refreshTokenString = refreshToken.getToken();
+ }
+
+ PublicUserDTO publicUser = modelMapper.map(accessToken.getUser(), PublicUserDTO.class);
+
+ return AuthenticationResponse
+ .builder()
+ .accessToken(accessToken.getToken())
+ .refreshToken(refreshTokenString)
+ .user(publicUser)
+ .build();
+ }
+
+ /**
+ * Resets the password for a user based on the provided token and new password.
+ */
+ public void resetPassword(String token, String newPassword) {
+ VerificationToken verificationToken = verificationTokenRepository.findByToken(token);
+ if (verificationToken == null) {
+ throw new InvalidTokenException();
+ }
+
+
+ User user = verificationToken.getUser();
+ if (user == null) {
+ throw new InvalidTokenException();
+ }
+
+ verificationToken.setCreatedAt(LocalDateTime.now());
+ user.setPassword(passwordEncoder.encode(newPassword));
+
+ userRepository.save(user);
+ verificationTokenRepository.delete(verificationToken);
+ }
+
+ @Override
+ public void confirmRegistration(String token) {
+ VerificationToken verificationToken = verificationTokenRepository.findByToken(token);
+ if (verificationToken == null) {
+ throw new ExpiredTokenException();
+ }
+
+ verificationToken.setCreatedAt(LocalDateTime.now());
+
+ Calendar cal = Calendar.getInstance();
+ if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
+ throw new ExpiredTokenException();
+ }
+
+ User user = verificationToken.getUser();
+ user.setEnabled(true);
+ user.setEmailVerifiedAt(LocalDateTime.now());
+
+ userRepository.save(user);
+ verificationTokenRepository.delete(verificationToken);
+ }
+
+ @Override
+ public User forgotPassword(String email) {
+ User user = userService.findByEmail(email);
+
+ if (!user.isEnabled()) {
+ throw new EmailNotVerified();
+ }
+
+ return user;
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/JwtServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/JwtServiceImpl.java
new file mode 100644
index 0000000..dbfce0b
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/JwtServiceImpl.java
@@ -0,0 +1,119 @@
+package com.lifeAI.LifeAI.services.security;
+
+import com.lifeAI.LifeAI.services.JwtService;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.security.Key;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Implementation of the JwtService interface responsible for JWT token generation, validation, and extraction.
+ */
+@Service
+public class JwtServiceImpl implements JwtService {
+
+ // Secret key used for signing JWT tokens
+ @Value("${spring.security.jwt.secret-key}")
+ private String secretKey;
+
+ // JWT token expiration time in milliseconds
+ @Value("${spring.security.jwt.expiration}")
+ private long jwtExpiration;
+
+ // JWT token expiration time in milliseconds
+ @Value("${spring.security.jwt.refresh-token.expiration}")
+ private long refreshExpiration;
+
+ public JwtServiceImpl() {
+ }
+
+ public JwtServiceImpl(String secretKey, long jwtExpiration, long refreshExpiration) {
+ this.secretKey = secretKey;
+ this.jwtExpiration = jwtExpiration;
+ this.refreshExpiration = refreshExpiration;
+ }
+
+ @Override
+ public String extractUsername(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ @Override
+ public T extractClaim(String token, Function claimsResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
+
+ @Override
+ public String generateToken(UserDetails userDetails) {
+ return generateToken(new HashMap<>(), userDetails);
+ }
+
+ @Override
+ public String generateToken(
+ Map extraClaims,
+ UserDetails userDetails
+ ) {
+ return buildToken(extraClaims, userDetails, jwtExpiration);
+ }
+
+ @Override
+ public String generateRefreshToken(
+ UserDetails userDetails
+ ) {
+ return buildToken(new HashMap<>(), userDetails, refreshExpiration);
+ }
+
+ @Override
+ public boolean isTokenValid(String token, UserDetails userDetails) {
+ final String username = extractUsername(token);
+ return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
+ }
+
+ public String buildToken(
+ Map extraClaims,
+ UserDetails userDetails,
+ long expiration
+ ) {
+ return Jwts
+ .builder()
+ .setClaims(extraClaims)
+ .setSubject(userDetails.getUsername())
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
+ .signWith(getSignInKey(), SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ private boolean isTokenExpired(String token) {
+ return extractExpiration(token).before(new Date());
+ }
+
+ private Date extractExpiration(String token) {
+ return extractClaim(token, Claims::getExpiration);
+ }
+
+ public Claims extractAllClaims(String token) {
+ return Jwts
+ .parserBuilder()
+ .setSigningKey(getSignInKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ private Key getSignInKey() {
+ byte[] keyBytes = Decoders.BASE64.decode(secretKey);
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/OAuth2AuthenticationServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/OAuth2AuthenticationServiceImpl.java
new file mode 100644
index 0000000..9964da0
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/OAuth2AuthenticationServiceImpl.java
@@ -0,0 +1,88 @@
+package com.lifeAI.LifeAI.services.security;
+
+import com.lifeAI.LifeAI.config.FrontendConfig;
+import com.lifeAI.LifeAI.enums.Provider;
+import com.lifeAI.LifeAI.exceptions.token.InvalidTokenException;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+import com.lifeAI.LifeAI.model.dto.auth.OAuth2UserInfoDTO;
+import com.lifeAI.LifeAI.services.OAuth2AuthenticationService;
+import com.lifeAI.LifeAI.services.TokenService;
+import com.lifeAI.LifeAI.services.UserService;
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Implementation of the OAuth2AuthenticationService interface responsible for processing OAuth2 user authentication.
+ * This services handles the post-login process, including token generation and cookie attachment.
+ */
+@Service
+@RequiredArgsConstructor
+public class OAuth2AuthenticationServiceImpl implements OAuth2AuthenticationService {
+ private final List SCOPES = List.of("email", "profile", "openid");
+ private final FrontendConfig frontendConfig;
+ private final WebClient userInfoClient;
+ private final UserService userService;
+ private final TokenService tokenService;
+
+
+ @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}")
+ private String clientId;
+ @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-secret}")
+ private String clientSecret;
+
+ @Override
+ public String getOAuthGoogleLoginUrl() {
+ return new GoogleAuthorizationCodeRequestUrl(clientId, frontendConfig.getOauth2RedirectUrl(), SCOPES).build();
+ }
+
+ @Override
+ public AuthenticationResponse processOAuthGoogleLogin(String code) {
+ // Authenticate user with Google
+ String token = authorizeWithGoogle(code);
+ OAuth2UserInfoDTO oAuth2UserInfoDTO = getUserInfoFromGoogleToken(token);
+ oAuth2UserInfoDTO.setProvider(Provider.GOOGLE);
+
+ // Process OAuth2 user and retrieve associated user entity
+ User user = userService.processOAuthUser(oAuth2UserInfoDTO);
+ tokenService.revokeAllUserTokens(user);
+
+ return tokenService.generateAuthResponse(user);
+ }
+
+ private OAuth2UserInfoDTO getUserInfoFromGoogleToken(String googleToken) {
+ return userInfoClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/oauth2/v3/userinfo")
+ .queryParam("access_token", googleToken)
+ .build())
+ .retrieve()
+ .bodyToMono(OAuth2UserInfoDTO.class)
+ .block();
+ }
+
+ private String authorizeWithGoogle(String code) {
+ try {
+ return new GoogleAuthorizationCodeTokenRequest(
+ new NetHttpTransport(),
+ new GsonFactory(),
+ clientId,
+ clientSecret,
+ code,
+ frontendConfig.getOauth2RedirectUrl())
+ .execute()
+ .getAccessToken();
+ } catch (IOException e) {
+ throw new InvalidTokenException();
+ }
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/PasswordResetListener.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/PasswordResetListener.java
new file mode 100644
index 0000000..d749fce
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/PasswordResetListener.java
@@ -0,0 +1,72 @@
+package com.lifeAI.LifeAI.services.security;
+
+import com.lifeAI.LifeAI.config.FrontendConfig;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.services.TokenService;
+import com.lifeAI.LifeAI.services.security.events.OnPasswordResetRequestEvent;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.context.ApplicationListener;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.util.UUID;
+
+/**
+ * Component responsible for listening to password reset request events and sending password reset emails.
+ */
+@Component
+@RequiredArgsConstructor
+public class PasswordResetListener implements ApplicationListener {
+
+ private final JavaMailSender mailSender;
+ private final FrontendConfig frontendConfig;
+ private final TokenService tokenService;
+
+ /**
+ * Sends a password reset email upon receiving the password reset request event.
+ */
+ @Override
+ @Async
+ public void onApplicationEvent(@NotNull OnPasswordResetRequestEvent event) {
+ sendPasswordResetEmail(event);
+ }
+
+ protected void sendPasswordResetEmail(OnPasswordResetRequestEvent event) {
+ User user = event.getUser();
+
+ // Generate a password reset token and create a verification token for the user
+ String token = generateResetToken(user);
+ tokenService.createVerificationToken(user, token);
+
+ String recipientAddress = user.getEmail();
+ String subject = "Заявка за нулиране на парола в LIFE";
+ String message = getEmailMessage(token, user);
+
+ SimpleMailMessage email = new SimpleMailMessage();
+ email.setTo(recipientAddress);
+ email.setSubject(subject);
+ email.setText(message);
+
+ mailSender.send(email);
+ }
+
+ @NotNull
+ private String getEmailMessage(String token, User user) {
+ String confirmationUrl = frontendConfig.getForgottenPasswordUrl() + "?token=" + token;
+
+ return "Здравейте, " + user.getUsername() + ",\n\n"
+ + "Получихме заявка за нулиране на паролата за вашия акаунт.\n"
+ + "Моля, кликнете върху следния линк, за да нулирате паролата си:\n"
+ + confirmationUrl + "\n\n"
+ + "Ако не сте изпращали такава заявка, моля игнорирайте този имейл.\n\n"
+ + "С най-добри пожелания,\n"
+ + "Екипът на LIFE!";
+ }
+
+ private String generateResetToken(User user) {
+ return UUID.randomUUID().toString();
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/RegistrationListener.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/RegistrationListener.java
new file mode 100644
index 0000000..6009a62
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/RegistrationListener.java
@@ -0,0 +1,61 @@
+package com.lifeAI.LifeAI.services.security;
+
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.services.TokenService;
+import com.lifeAI.LifeAI.services.security.events.OnRegistrationCompleteEvent;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.MessageSource;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.util.UUID;
+
+/**
+ * Component responsible for handling registration confirmation emails.
+ */
+@Component
+@RequiredArgsConstructor
+public class RegistrationListener implements ApplicationListener {
+ private final TokenService tokenService;
+ private final MessageSource messages;
+ private final JavaMailSender mailSender;
+
+ @Override
+ @Async
+ public void onApplicationEvent(@NotNull OnRegistrationCompleteEvent event) {
+ this.confirmRegistration(event);
+ }
+
+ protected void confirmRegistration(OnRegistrationCompleteEvent event) {
+ User user = event.getUser();
+ String token = UUID.randomUUID().toString();
+ tokenService.createVerificationToken(user, token);
+
+ String recipientAddress = user.getEmail();
+ String subject = "Потвърждение на регистрацията в LIFE";
+ String message = getEmailNote(event, token, user);
+
+ SimpleMailMessage email = new SimpleMailMessage();
+ email.setTo(recipientAddress);
+ email.setSubject(subject);
+ email.setText(message);
+
+ mailSender.send(email);
+ }
+
+ private static @NotNull String getEmailNote(OnRegistrationCompleteEvent event, String token, User user) {
+ String confirmationUrl = event.getAppUrl() + "auth/registrationConfirm?token=" + token;
+
+ return "Здравейте, " + user.getUsername() + "\n\n"
+ + "Благодарим Ви, че се регистрирахте в LIFE!\n\n"
+ + "За да завършите регистрацията си, моля кликнете върху следния линк, за да потвърдите вашия имейл:\n"
+ + confirmationUrl + "\n\n"
+ + "Ако не сте създали акаунт при нас, моля игнорирайте този имейл.\n\n"
+ + "С най-добри пожелания,\n"
+ + "Екипът на LIFE!";
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/TokenServiceImpl.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/TokenServiceImpl.java
new file mode 100644
index 0000000..872b2f3
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/TokenServiceImpl.java
@@ -0,0 +1,116 @@
+package com.lifeAI.LifeAI.services.security;
+
+import com.lifeAI.LifeAI.enums.TokenType;
+import com.lifeAI.LifeAI.model.Token;
+import com.lifeAI.LifeAI.model.User;
+import com.lifeAI.LifeAI.model.VerificationToken;
+import com.lifeAI.LifeAI.model.dto.auth.AuthenticationResponse;
+import com.lifeAI.LifeAI.model.dto.auth.PublicUserDTO;
+import com.lifeAI.LifeAI.respository.TokenRepository;
+import com.lifeAI.LifeAI.respository.VerificationTokenRepository;
+import com.lifeAI.LifeAI.services.JwtService;
+import com.lifeAI.LifeAI.services.TokenService;
+import lombok.RequiredArgsConstructor;
+import org.modelmapper.ModelMapper;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * Service implementation responsible for handling authentication tokens.
+ */
+@RequiredArgsConstructor
+@Service
+public class TokenServiceImpl implements TokenService {
+
+ private final TokenRepository tokenRepository;
+ private final VerificationTokenRepository verificationTokenRepository;
+ private final JwtService jwtService;
+ private final ModelMapper modelMapper;
+
+ @Override
+ public Token findByToken(String jwt) {
+ return tokenRepository.findByToken(jwt).orElse(null);
+ }
+
+ @Override
+ public List findByUser(User user) {
+ return tokenRepository.findAllByUser(user);
+ }
+
+ @Override
+ public void saveToken(User user, String jwtToken, TokenType tokenType) {
+ Token token = Token.builder()
+ .user(user)
+ .token(jwtToken)
+ .tokenType(tokenType)
+ .expired(false)
+ .revoked(false)
+ .build();
+
+ tokenRepository.save(token);
+ }
+
+ @Override
+ public void revokeToken(Token token) {
+ tokenRepository.delete(token);
+ }
+
+ @Override
+ public void revokeAllUserTokens(User user) {
+ tokenRepository.deleteAll(tokenRepository.findAllByUser(user));
+ }
+
+ @Override
+ @Transactional
+ public void logoutToken(String jwt) {
+ Token storedToken = tokenRepository.findByToken(jwt)
+ .orElse(null);
+
+ if (storedToken == null) {
+ return;
+ }
+
+ revokeAllUserTokens(storedToken.getUser());
+ SecurityContextHolder.clearContext();
+ }
+
+ @Override
+ public AuthenticationResponse generateAuthResponse(User user) {
+ String jwtToken = jwtService.generateToken(user);
+ String refreshToken = jwtService.generateRefreshToken(user);
+
+ saveToken(user, jwtToken, TokenType.ACCESS);
+ saveToken(user, refreshToken, TokenType.REFRESH);
+
+ PublicUserDTO dto = PublicUserDTO.builder()
+ .id(user.getId())
+ .firstName(user.getFirstName() != null ? user.getFirstName() : "")
+ .lastName(user.getLastName() != null ? user.getLastName() : "")
+ .email(user.getEmail() != null ? user.getEmail() : "")
+ .role(user.getRole() != null ? user.getRole() : "")
+ .build();
+
+ return AuthenticationResponse.builder()
+ .accessToken(jwtToken)
+ .refreshToken(refreshToken)
+ .user(dto)
+ .build();
+ }
+
+ @Override
+ @Transactional
+ public void createVerificationToken(User user, String token) {
+ clearVerificationTokensByUser(user);
+ VerificationToken myToken = new VerificationToken(token, user);
+ verificationTokenRepository.save(myToken);
+ }
+
+ @Override
+ public void clearVerificationTokensByUser(User user) {
+ verificationTokenRepository.deleteAllByUser(user);
+ }
+
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/events/OnPasswordResetRequestEvent.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/events/OnPasswordResetRequestEvent.java
new file mode 100644
index 0000000..52249c9
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/events/OnPasswordResetRequestEvent.java
@@ -0,0 +1,22 @@
+package com.lifeAI.LifeAI.services.security.events;
+
+import com.lifeAI.LifeAI.model.User;
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * OnPasswordResetRequestEvent represents an event raised when a password reset request is initiated.
+ * It carries information about the user requesting the password reset and the base URL of the application.
+ */
+@Getter
+public class OnPasswordResetRequestEvent extends ApplicationEvent {
+
+ private final User user;
+ private final String appBaseUrl;
+
+ public OnPasswordResetRequestEvent(User user, String appBaseUrl) {
+ super(user);
+ this.user = user;
+ this.appBaseUrl = appBaseUrl;
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/com/lifeAI/LifeAI/services/security/events/OnRegistrationCompleteEvent.java b/server/src/main/java/com/lifeAI/LifeAI/services/security/events/OnRegistrationCompleteEvent.java
new file mode 100644
index 0000000..8842fe0
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/services/security/events/OnRegistrationCompleteEvent.java
@@ -0,0 +1,30 @@
+package com.lifeAI.LifeAI.services.security.events;
+
+import com.lifeAI.LifeAI.model.User;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.context.ApplicationEvent;
+
+import java.util.Locale;
+
+/**
+ * OnRegistrationCompleteEvent represents an event raised when a user completes registration.
+ * It carries information about the registered user, the application URL, and the locale.
+ */
+@Getter
+public class OnRegistrationCompleteEvent extends ApplicationEvent {
+ @Setter
+ private String appUrl;
+ private Locale locale;
+ @Setter
+ @Getter
+ private User user;
+
+ public OnRegistrationCompleteEvent(
+ User user, String appUrl) {
+ super(user);
+
+ this.user = user;
+ this.appUrl = appUrl;
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/utils/ApiExceptionParser.java b/server/src/main/java/com/lifeAI/LifeAI/utils/ApiExceptionParser.java
new file mode 100644
index 0000000..7d1b73f
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/utils/ApiExceptionParser.java
@@ -0,0 +1,22 @@
+package com.lifeAI.LifeAI.utils;
+
+import com.lifeAI.LifeAI.exceptions.common.ApiException;
+import com.lifeAI.LifeAI.model.dto.response.ExceptionResponse;
+
+import java.time.LocalDateTime;
+
+/**
+ * ApiExceptionParser is a utility class responsible for parsing ApiException objects into ExceptionResponse objects.
+ * It provides a static method to perform this parsing operation.
+ */
+public class ApiExceptionParser {
+ public static ExceptionResponse parseException(ApiException exception) {
+ return ExceptionResponse
+ .builder()
+ .dateTime(LocalDateTime.now())
+ .message(exception.getMessage())
+ .status(exception.getStatus())
+ .statusCode(exception.getStatusCode())
+ .build();
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/utils/CookieHelper.java b/server/src/main/java/com/lifeAI/LifeAI/utils/CookieHelper.java
new file mode 100644
index 0000000..1a657f0
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/utils/CookieHelper.java
@@ -0,0 +1,29 @@
+package com.lifeAI.LifeAI.utils;
+
+import jakarta.servlet.http.Cookie;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+public class CookieHelper {
+
+ /**
+ * Reads the value of a cookie with the specified key from an array of cookies.
+ *
+ * @param key The key of the cookie to be read.
+ * @param cookies An array of Cookie objects.
+ * @return An Optional containing the value of the cookie if found, or Optional.empty() if not found.
+ */
+ public static Optional readCookie(String key, Cookie[] cookies) {
+ // If cookies array is null, return Optional.empty()
+ if (cookies == null) {
+ return Optional.empty();
+ }
+
+ // Stream through the array of cookies, filter by key, and map to the value
+ return Arrays.stream(cookies)
+ .filter(c -> key.equals(c.getName())) // Filter by key
+ .map(Cookie::getValue) // Map to value
+ .findAny(); // Find any matching cookie value
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/utils/ObjectMapperHelper.java b/server/src/main/java/com/lifeAI/LifeAI/utils/ObjectMapperHelper.java
new file mode 100644
index 0000000..e2bd804
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/utils/ObjectMapperHelper.java
@@ -0,0 +1,34 @@
+package com.lifeAI.LifeAI.utils;
+
+import com.lifeAI.LifeAI.exceptions.common.ApiException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.lifeAI.LifeAI.model.dto.response.ExceptionResponse;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+
+import java.io.IOException;
+
+/**
+ * ObjectMapperHelper provides utility methods for working with ObjectMapper and handling exceptions.
+ */
+public class ObjectMapperHelper {
+ public static void writeExceptionToObjectMapper(
+ ObjectMapper objectMapper,
+ ApiException exception,
+ HttpServletResponse httpServletResponse
+ ) throws IOException {
+ // Parse the ApiException to an ExceptionResponse
+ ExceptionResponse exceptionResponse = ApiExceptionParser.parseException(exception);
+
+ // Set the HTTP response status and content type
+ httpServletResponse.setStatus(exceptionResponse.getStatusCode());
+ httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ // Get the output stream of the response
+ ServletOutputStream out = httpServletResponse.getOutputStream();
+
+ // Write the exception response as JSON to the output stream using the provided ObjectMapper
+ objectMapper.writeValue(out, exceptionResponse);
+ }
+}
diff --git a/server/src/main/java/com/lifeAI/LifeAI/utils/PasswordEncryptionUtils.java b/server/src/main/java/com/lifeAI/LifeAI/utils/PasswordEncryptionUtils.java
new file mode 100644
index 0000000..5e93d0c
--- /dev/null
+++ b/server/src/main/java/com/lifeAI/LifeAI/utils/PasswordEncryptionUtils.java
@@ -0,0 +1,49 @@
+package com.lifeAI.LifeAI.utils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class PasswordEncryptionUtils {
+ // Validate the password by comparing plain text and encrypted
+ public static boolean validatePassword(String plain, String encrypted) {
+ if (notNull(plain) && notNull(encrypted)) {
+ // Split the encrypted password into hash and salt
+ String[] stack = encrypted.split(":");
+
+ if (stack.length != 2) return false;
+
+ // Hash the salt and plain text password and compare with the stored hash
+ return md5(stack[1] + plain).equals(stack[0]);
+ }
+
+ return false;
+ }
+
+ // Check if the value is not null
+ private static boolean notNull(Object value) {
+ if (value instanceof String strValue) {
+ return !strValue.trim().isEmpty() && !strValue.equals("NULL");
+ } else if (value instanceof Integer) {
+ return true;
+ } else if (value instanceof Object[]) {
+ return ((Object[]) value).length > 0;
+ }
+
+ return value != null;
+ }
+
+ // Generate MD5 hash of a string
+ private static String md5(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] messageDigest = md.digest(input.getBytes());
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : messageDigest) {
+ hexString.append(String.format("%02x", b));
+ }
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml
index 9e0d555..fd5a53f 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -3,27 +3,39 @@ management:
web:
exposure:
include: [ 'health', 'healthcheck', 'prometheus' ]
- base-path: '/admin'
+ base-path: /admin
path-mapping:
- prometheus: '/metrics'
+ prometheus: /metrics
+
server:
port: 8080
+ frontend:
+ baseUrl: ${FRONTEND_URL}
+ loginUrl: ${server.frontend.baseUrl}/login
+ finishRegisterUrl: ${server.frontend.baseUrl}/finish-register
+ oauth2RedirectUrl: ${server.frontend.baseUrl}/process-oauth2
+ forgottenPasswordUrl: ${server.frontend.baseUrl}/forgotten-password
+ backend:
+ baseUrl: ${BACKEND_URL}
+
spring:
- application.name: plug-n-play
- ai:
- openai:
- api-key: ${OPENAI_API_KEY}
- assistant-id: ${ASSISTANT_ID}
- positiveAssistant-id: ${POSITIVE_ASSISTANT_ID}
- fileAssistant-id: ${FILE_ASSISTANT_ID}
+ application:
+ name: plug-n-play
+
+ config:
+ import: "classpath:rate-limiter.yaml"
+
+ servlet:
+ multipart:
+ max-file-size: 20MB
+ max-request-size: 20MB
+
datasource:
url: jdbc:${MYSQL_URL}
username: ${MYSQLUSER}
password: ${MYSQLPASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
- flyway:
- enabled: true
- validate-on-migrate: false
+
jpa:
generate-ddl: false
hibernate:
@@ -31,9 +43,53 @@ spring:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
+
+ flyway:
+ enabled: true
+ validate-on-migrate: false
+
+ security:
+ jwt:
+ secret-key: ${JWT_SECRET}
+ expiration: ${JWT_EXPIRATION} # 1 day
+ refresh-token:
+ expiration: ${REFRESH_TOKEN_EXPIRATION} # 7 days
+ oauth2:
+ resourceserver:
+ opaquetoken:
+ introspection-uri: https://www.googleapis.com
+ client-id: ${GOOGLE_CLIENT_ID}
+ client-secret: ${GOOGLE_CLIENT_SECRET}
+
+ mail:
+ host: smtp.gmail.com
+ port: 465
+ protocol: smtps
+ username: ${GMAIL_USERNAME}
+ password: ${GMAIL_PASSWORD}
+ properties:
+ mail:
+ transport:
+ protocol: smtps
+ smtps:
+ auth: true
+ starttls:
+ enable: true
+ timeout: 8000
+
+ ai:
+ openai:
+ api-key: ${OPENAI_API_KEY}
+ assistant-id: ${ASSISTANT_ID}
+ positiveAssistant-id: ${POSITIVE_ASSISTANT_ID}
+ fileAssistant-id: ${FILE_ASSISTANT_ID}
+
+application:
+ upload:
+ directory: server/src/main/resources/static/uploads/
+
springdoc:
api-docs:
path: /apidoc
swagger-ui:
path: /swagger-ui/index.html
-
diff --git a/server/src/main/resources/rate-limiter.yaml b/server/src/main/resources/rate-limiter.yaml
new file mode 100644
index 0000000..09c52db
--- /dev/null
+++ b/server/src/main/resources/rate-limiter.yaml
@@ -0,0 +1,11 @@
+resilience4j:
+ rate-limiter:
+ instances:
+ general_api_rate_limiter:
+ limitForPeriod: 100 # requests
+ limitRefreshPeriod: 30m # refresh period
+ timeoutDuration: 0 # no timeout
+ sensitive_operations_rate_limiter:
+ limitForPeriod: 10
+ limitRefreshPeriod: 1m
+ timeoutDuration: 0
\ No newline at end of file