diff --git a/backend/pom.xml b/backend/pom.xml
index bc08d3f..901365d 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -88,6 +88,29 @@
springdoc-openapi-starter-webmvc-ui
2.3.0
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.3
+
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.3
+ runtime
+
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.3
+ runtime
+
+
diff --git a/backend/src/main/java/com/llm_service/llm_service/config/CustomLogoutHandler.java b/backend/src/main/java/com/llm_service/llm_service/config/CustomLogoutHandler.java
new file mode 100644
index 0000000..8696edf
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/config/CustomLogoutHandler.java
@@ -0,0 +1,33 @@
+package com.llm_service.llm_service.config;
+
+import com.llm_service.llm_service.persistance.entities.TokenEntity;
+import com.llm_service.llm_service.persistance.repositories.token.TokenRepository;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+
+@Configuration
+@RequiredArgsConstructor
+public class CustomLogoutHandler implements LogoutHandler {
+ private final TokenRepository tokenRepository;
+
+ @Override
+ public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+ String authHeader = request.getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ return;
+ }
+
+ String token = authHeader.substring(7);
+ TokenEntity storedToken = tokenRepository.findByToken(token).orElse(null);
+
+ if (storedToken != null) {
+ storedToken.setLoggedOut(true);
+ tokenRepository.save(storedToken);
+ }
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/config/SecurityConfiguration.java b/backend/src/main/java/com/llm_service/llm_service/config/SecurityConfiguration.java
index 8527b0b..11014e2 100644
--- a/backend/src/main/java/com/llm_service/llm_service/config/SecurityConfiguration.java
+++ b/backend/src/main/java/com/llm_service/llm_service/config/SecurityConfiguration.java
@@ -1,21 +1,67 @@
package com.llm_service.llm_service.config;
+import com.llm_service.llm_service.persistance.entities.Role;
+import com.llm_service.llm_service.service.jwt.filter.JwtAuthenticationFilter;
+import com.llm_service.llm_service.service.user.UserDetailsServiceImp;
+import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.Customizer;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.HttpStatusEntryPoint;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
+@RequiredArgsConstructor
public class SecurityConfiguration {
+ private final UserDetailsServiceImp userDetailsServiceImp;
+
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ private final CustomLogoutHandler logoutHandler;
+
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+
+ // TODO fix the swagger security config
return http.csrf(AbstractHttpConfigurer::disable)
- .authorizeHttpRequests(req -> req.anyRequest().permitAll())
- .httpBasic(Customizer.withDefaults())
+ .authorizeHttpRequests(req -> req.requestMatchers(
+ "/login/**", "/register/**", "/forget-password/**", "/swagger-ui/**", "/v3/api-docs/**")
+ .permitAll()
+ .requestMatchers("/paid/**")
+ .hasAuthority(Role.PAID.name())
+ .anyRequest()
+ .authenticated())
+ .userDetailsService(userDetailsServiceImp)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+ .exceptionHandling(e -> e.accessDeniedHandler((request, response, accessDeniedException) ->
+ response.setStatus(HttpStatus.FORBIDDEN.value()))
+ .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
+ .logout(l -> l.logoutUrl("/logout")
+ .addLogoutHandler(logoutHandler)
+ .logoutSuccessHandler(
+ (request, response, authentication) -> SecurityContextHolder.clearContext()))
.build();
}
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+ return configuration.getAuthenticationManager();
+ }
}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationController.java b/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationController.java
index d1f8e2c..9163e78 100644
--- a/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationController.java
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationController.java
@@ -91,6 +91,14 @@ public ResponseEntity> continueConversation(
.body(discussions.stream().map(conversationApiMapper::map).toList());
}
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Update a conversation",
+ content = {@Content(mediaType = "application/json")})
+ })
+ @Operation(summary = "update conversation title")
@PutMapping("/{id}")
public ResponseEntity editConversation(
@PathVariable UUID id, @RequestBody ConversationTitleRequest conversationTitleRequest)
@@ -102,6 +110,14 @@ public ResponseEntity editConversation(
return ResponseEntity.status(HttpStatus.OK).body(conversationApiMapper.map(conversation));
}
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Deletes a conversation",
+ content = {@Content(mediaType = "application/json")})
+ })
+ @Operation(summary = "deletes a conversation")
@DeleteMapping("/{id}")
public ResponseEntity deleteConversation(@PathVariable UUID id) throws ConversationNotFoundException {
conversationService.getByID(id).orElseThrow(() -> new ConversationNotFoundException(id));
@@ -109,6 +125,14 @@ public ResponseEntity deleteConversation(@PathVariable UUID id) throws Con
return ResponseEntity.status(HttpStatus.OK).body(null);
}
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Deletes all conversation",
+ content = {@Content(mediaType = "application/json")})
+ })
+ @Operation(summary = "deletes all conversations")
@DeleteMapping
public ResponseEntity deleteConversation() {
conversationService.deleteAll();
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationRequest.java b/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationRequest.java
index adfce82..2bfe797 100644
--- a/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationRequest.java
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationRequest.java
@@ -1,6 +1,7 @@
package com.llm_service.llm_service.controller.conversation;
import lombok.Builder;
+import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@@ -8,5 +9,6 @@
@Builder
@Jacksonized
public class ConversationRequest {
+ @NonNull
String text;
}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationTitleRequest.java b/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationTitleRequest.java
index 8f1b1f9..3d67fea 100644
--- a/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationTitleRequest.java
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/conversation/ConversationTitleRequest.java
@@ -1,6 +1,7 @@
package com.llm_service.llm_service.controller.conversation;
import lombok.Builder;
+import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@@ -8,5 +9,6 @@
@Builder
@Jacksonized
public class ConversationTitleRequest {
+ @NonNull
String title;
}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/LoginRequest.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/LoginRequest.java
new file mode 100644
index 0000000..cb933ca
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/LoginRequest.java
@@ -0,0 +1,17 @@
+package com.llm_service.llm_service.controller.user;
+
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+
+@Value
+@Builder
+@Jacksonized
+public class LoginRequest {
+ @NonNull
+ String username;
+
+ @NonNull
+ String password;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/LoginResponse.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/LoginResponse.java
new file mode 100644
index 0000000..77d3482
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/LoginResponse.java
@@ -0,0 +1,12 @@
+package com.llm_service.llm_service.controller.user;
+
+import lombok.Builder;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+
+@Value
+@Builder
+@Jacksonized
+public class LoginResponse {
+ String token;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java
new file mode 100644
index 0000000..5282295
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserApiMapper.java
@@ -0,0 +1,14 @@
+package com.llm_service.llm_service.controller.user;
+
+import com.llm_service.llm_service.dto.User;
+import com.llm_service.llm_service.service.jwt.AuthenticationResponse;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = "spring")
+public interface UserApiMapper {
+ LoginResponse map(AuthenticationResponse response);
+
+ @Mapping(target = "name", source = "username")
+ UserResponse map(User user);
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java
new file mode 100644
index 0000000..8dc6631
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserController.java
@@ -0,0 +1,80 @@
+package com.llm_service.llm_service.controller.user;
+
+import com.llm_service.llm_service.dto.User;
+import com.llm_service.llm_service.exception.user.UserAlreadyExistsException;
+import com.llm_service.llm_service.exception.user.UserNotFoundException;
+import com.llm_service.llm_service.exception.user.UsernameAlreadyExistsException;
+import com.llm_service.llm_service.service.jwt.AuthenticationResponse;
+import com.llm_service.llm_service.service.jwt.AuthenticationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.web.bind.annotation.*;
+
+@CrossOrigin("http://localhost:4040")
+@RestController
+public class UserController {
+ private final AuthenticationService authenticationService;
+ private final UserApiMapper userApiMapper;
+
+ public UserController(AuthenticationService authenticationService, UserApiMapper userApiMapper) {
+ this.authenticationService = authenticationService;
+ this.userApiMapper = userApiMapper;
+ }
+
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Creates a new user",
+ content = {@Content(mediaType = "application/json")})
+ })
+ @Operation(summary = "register the user")
+ @PostMapping("/register")
+ public ResponseEntity register(@RequestBody UserRequest userRequest)
+ throws UsernameAlreadyExistsException {
+ User user = authenticationService.register(userRequest);
+ return ResponseEntity.status(HttpStatus.OK).body(userApiMapper.map(user));
+ }
+
+ @ApiResponses(
+ value = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Logs the user into the system",
+ content = {@Content(mediaType = "application/json")})
+ })
+ @Operation(summary = "login phase of the user")
+ @PostMapping("/login")
+ public ResponseEntity login(@RequestBody LoginRequest loginRequest)
+ throws UserNotFoundException, AuthenticationException {
+ AuthenticationResponse authenticationResponse = authenticationService.authenticate(loginRequest);
+ return ResponseEntity.ok(userApiMapper.map(authenticationResponse));
+ }
+
+ @ExceptionHandler(UserAlreadyExistsException.class)
+ ResponseEntity handleUsernameAlreadyExistsExceptions(
+ UserAlreadyExistsException usernameAlreadyExistsException) {
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(usernameAlreadyExistsException.getMessage());
+ }
+
+ @ExceptionHandler(UsernameAlreadyExistsException.class)
+ ResponseEntity handleUsernameAlreadyExistsExceptions(
+ UsernameAlreadyExistsException usernameAlreadyExistsException) {
+ return ResponseEntity.status(HttpStatus.CONFLICT).body(usernameAlreadyExistsException.getMessage());
+ }
+
+ @ExceptionHandler(UserNotFoundException.class)
+ ResponseEntity handleUsernameNotFoundExceptions(UserNotFoundException usernameNotFoundException) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(usernameNotFoundException.getMessage());
+ }
+
+ @ExceptionHandler(AuthenticationException.class)
+ ResponseEntity handleAuthenticationException(AuthenticationException authenticationException) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(authenticationException.getMessage());
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserRequest.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserRequest.java
new file mode 100644
index 0000000..527d84a
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserRequest.java
@@ -0,0 +1,25 @@
+package com.llm_service.llm_service.controller.user;
+
+import com.llm_service.llm_service.persistance.entities.Role;
+import lombok.*;
+import lombok.extern.jackson.Jacksonized;
+
+@Value
+@Builder
+@Jacksonized
+public class UserRequest {
+ @NonNull
+ String username;
+
+ @NonNull
+ String password;
+
+ @NonNull
+ String firstName;
+
+ @NonNull
+ String lastName;
+
+ @NonNull
+ Role role;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java
new file mode 100644
index 0000000..b70bf58
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/controller/user/UserResponse.java
@@ -0,0 +1,12 @@
+package com.llm_service.llm_service.controller.user;
+
+import lombok.Value;
+import lombok.experimental.SuperBuilder;
+import lombok.extern.jackson.Jacksonized;
+
+@Value
+@Jacksonized
+@SuperBuilder
+public class UserResponse {
+ String name;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/dto/User.java b/backend/src/main/java/com/llm_service/llm_service/dto/User.java
new file mode 100644
index 0000000..7e13547
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/dto/User.java
@@ -0,0 +1,17 @@
+package com.llm_service.llm_service.dto;
+
+import com.llm_service.llm_service.persistance.entities.Role;
+import java.util.UUID;
+import lombok.Builder;
+import lombok.Value;
+
+@Value
+@Builder(toBuilder = true)
+public class User {
+ UUID id;
+ String firstName;
+ String lastName;
+ String username;
+ String password;
+ Role role;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/exception/user/UserNotFoundException.java b/backend/src/main/java/com/llm_service/llm_service/exception/user/UserNotFoundException.java
index 9738b0a..e9915fa 100644
--- a/backend/src/main/java/com/llm_service/llm_service/exception/user/UserNotFoundException.java
+++ b/backend/src/main/java/com/llm_service/llm_service/exception/user/UserNotFoundException.java
@@ -6,4 +6,8 @@ public class UserNotFoundException extends Exception {
public UserNotFoundException(UUID id) {
super("User with id " + id + " is not found");
}
+
+ public UserNotFoundException() {
+ super("User with is not found");
+ }
}
diff --git a/backend/src/main/java/com/llm_service/llm_service/exception/user/UsernameAlreadyExistsException.java b/backend/src/main/java/com/llm_service/llm_service/exception/user/UsernameAlreadyExistsException.java
new file mode 100644
index 0000000..32d279b
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/exception/user/UsernameAlreadyExistsException.java
@@ -0,0 +1,7 @@
+package com.llm_service.llm_service.exception.user;
+
+public class UsernameAlreadyExistsException extends Exception {
+ public UsernameAlreadyExistsException(String username) {
+ super("Username with name " + username + " already exists");
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/entities/FullName.java b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/FullName.java
new file mode 100644
index 0000000..275e0d5
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/FullName.java
@@ -0,0 +1,15 @@
+package com.llm_service.llm_service.persistance.entities;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.Data;
+
+@Data
+@Embeddable
+public class FullName {
+ @Column(name = "first_name")
+ private String firstName;
+
+ @Column(name = "last_name")
+ private String lastName;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/entities/Role.java b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/Role.java
new file mode 100644
index 0000000..11432df
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/Role.java
@@ -0,0 +1,6 @@
+package com.llm_service.llm_service.persistance.entities;
+
+public enum Role {
+ PAID,
+ FREE
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/entities/TokenEntity.java b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/TokenEntity.java
new file mode 100644
index 0000000..3b18fae
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/TokenEntity.java
@@ -0,0 +1,21 @@
+package com.llm_service.llm_service.persistance.entities;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Entity
+@Table(name = "token")
+@Data
+public class TokenEntity extends BaseEntity {
+ @Column(name = "token")
+ private String token;
+
+ @Column(name = "logged_out")
+ private boolean loggedOut;
+
+ @ManyToOne
+ @JoinColumn(name = "user_id")
+ private UserEntity user;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/entities/UserEntity.java b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/UserEntity.java
new file mode 100644
index 0000000..13f8d61
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/entities/UserEntity.java
@@ -0,0 +1,59 @@
+package com.llm_service.llm_service.persistance.entities;
+
+import jakarta.persistence.*;
+import java.util.Collection;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+@EqualsAndHashCode(callSuper = true)
+@Entity
+@Table(name = "users")
+@Data
+public class UserEntity extends BaseEntity implements UserDetails {
+
+ @Embedded
+ private FullName fullName;
+
+ @Column(name = "username")
+ private String username;
+
+ @Column(name = "password")
+ private String password;
+
+ @Enumerated(value = EnumType.STRING)
+ @Column(name = "role")
+ private Role role;
+
+ @OneToMany(mappedBy = "user")
+ private List tokens;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(new SimpleGrantedAuthority(role.name()));
+ }
+
+ // TODO the following should be researched to be figure out how it us being used in SecurityConfig
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenEntityMapper.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenEntityMapper.java
new file mode 100644
index 0000000..95ca484
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenEntityMapper.java
@@ -0,0 +1,12 @@
+package com.llm_service.llm_service.persistance.repositories.token;
+
+import com.llm_service.llm_service.persistance.entities.TokenEntity;
+import com.llm_service.llm_service.service.jwt.Token;
+import org.mapstruct.Mapper;
+
+@Mapper(componentModel = "spring")
+public interface TokenEntityMapper {
+ Token map(TokenEntity tokenEntity);
+
+ TokenEntity map(Token token);
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenJpaPersistenceManager.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenJpaPersistenceManager.java
new file mode 100644
index 0000000..ef6d965
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenJpaPersistenceManager.java
@@ -0,0 +1,51 @@
+package com.llm_service.llm_service.persistance.repositories.token;
+
+import com.llm_service.llm_service.service.jwt.Token;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TokenJpaPersistenceManager implements TokenPersistenceManager {
+
+ private final TokenRepository tokenRepository;
+ private final TokenEntityMapper tokenEntityMapper;
+
+ @Autowired
+ public TokenJpaPersistenceManager(TokenRepository tokenRepository, TokenEntityMapper tokenEntityMapper) {
+ this.tokenRepository = tokenRepository;
+ this.tokenEntityMapper = tokenEntityMapper;
+ }
+
+ @Override
+ public List findAll() {
+ return tokenRepository.findAll().stream().map(tokenEntityMapper::map).toList();
+ }
+
+ @Override
+ public List findAllTokensByUser(UUID id) {
+ return tokenRepository.findAllTokensByUserId(id).stream()
+ .map(tokenEntityMapper::map)
+ .toList();
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ return tokenRepository.findById(id).map(tokenEntityMapper::map);
+ }
+
+ @Override
+ public void save(Token token) {
+ tokenRepository.save(tokenEntityMapper.map(token));
+ }
+
+ @Override
+ public void saveAll(List tokens) {}
+
+ @Override
+ public void delete(UUID id) {
+ tokenRepository.deleteById(id);
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenPersistenceManager.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenPersistenceManager.java
new file mode 100644
index 0000000..fa2cb88
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenPersistenceManager.java
@@ -0,0 +1,21 @@
+package com.llm_service.llm_service.persistance.repositories.token;
+
+import com.llm_service.llm_service.service.jwt.Token;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface TokenPersistenceManager {
+
+ List findAll();
+
+ List findAllTokensByUser(UUID id);
+
+ Optional findById(UUID id);
+
+ void save(Token token);
+
+ void saveAll(List tokens);
+
+ void delete(UUID id);
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenRepository.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenRepository.java
new file mode 100644
index 0000000..5c0bbf7
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/token/TokenRepository.java
@@ -0,0 +1,14 @@
+package com.llm_service.llm_service.persistance.repositories.token;
+
+import com.llm_service.llm_service.persistance.entities.TokenEntity;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TokenRepository extends JpaRepository {
+
+ List findAllTokensByUserId(UUID id);
+
+ Optional findByToken(String token);
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserEntityMapper.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserEntityMapper.java
new file mode 100644
index 0000000..52a97e8
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserEntityMapper.java
@@ -0,0 +1,21 @@
+package com.llm_service.llm_service.persistance.repositories.user;
+
+import com.llm_service.llm_service.dto.User;
+import com.llm_service.llm_service.persistance.entities.FullName;
+import com.llm_service.llm_service.persistance.entities.UserEntity;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = "spring")
+public interface UserEntityMapper {
+
+ @Mapping(target = "firstName", source = "userEntity.fullName.firstName")
+ @Mapping(target = "lastName", source = "userEntity.fullName.lastName")
+ User map(UserEntity userEntity);
+
+ FullName map(String firstName, String lastName);
+
+ @Mapping(target = "fullName.firstName", source = "firstName")
+ @Mapping(target = "fullName.lastName", source = "lastName")
+ UserEntity map(User user);
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserJpaPersistenceManager.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserJpaPersistenceManager.java
new file mode 100644
index 0000000..ae888ea
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserJpaPersistenceManager.java
@@ -0,0 +1,46 @@
+package com.llm_service.llm_service.persistance.repositories.user;
+
+import com.llm_service.llm_service.dto.User;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserJpaPersistenceManager implements UserPersistenceManager {
+ private final UserRepository userRepository;
+
+ private final UserEntityMapper userEntityMapper;
+
+ @Autowired
+ public UserJpaPersistenceManager(UserRepository userRepository, UserEntityMapper customerEntityMapper) {
+ this.userRepository = userRepository;
+ this.userEntityMapper = customerEntityMapper;
+ }
+
+ @Override
+ public List findAll() {
+ return userRepository.findAll().stream().map(userEntityMapper::map).toList();
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ return userRepository.findById(id).map(userEntityMapper::map);
+ }
+
+ @Override
+ public Optional findByUsername(String username) {
+ return userRepository.findByUsername(username).map(userEntityMapper::map);
+ }
+
+ @Override
+ public User save(User customer) {
+ return userEntityMapper.map(userRepository.save(userEntityMapper.map(customer)));
+ }
+
+ @Override
+ public void delete(UUID id) {
+ userRepository.deleteById(id);
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserPersistenceManager.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserPersistenceManager.java
new file mode 100644
index 0000000..58c1dc6
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserPersistenceManager.java
@@ -0,0 +1,18 @@
+package com.llm_service.llm_service.persistance.repositories.user;
+
+import com.llm_service.llm_service.dto.User;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface UserPersistenceManager {
+ List findAll();
+
+ Optional findById(UUID id);
+
+ Optional findByUsername(String username);
+
+ User save(User customer);
+
+ void delete(UUID id); // TODO to be removed in the future
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserRepository.java b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserRepository.java
new file mode 100644
index 0000000..565f1fd
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/persistance/repositories/user/UserRepository.java
@@ -0,0 +1,10 @@
+package com.llm_service.llm_service.persistance.repositories.user;
+
+import com.llm_service.llm_service.persistance.entities.UserEntity;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserRepository extends JpaRepository {
+ Optional findByUsername(String username);
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationResponse.java b/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationResponse.java
new file mode 100644
index 0000000..5d81f37
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationResponse.java
@@ -0,0 +1,14 @@
+package com.llm_service.llm_service.service.jwt;
+
+import lombok.Data;
+
+@Data
+public class AuthenticationResponse {
+ private String token;
+ private String message;
+
+ public AuthenticationResponse(String token, String message) {
+ this.token = token;
+ this.message = message;
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java b/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java
new file mode 100644
index 0000000..e2bc2a5
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/service/jwt/AuthenticationService.java
@@ -0,0 +1,96 @@
+package com.llm_service.llm_service.service.jwt;
+
+import com.llm_service.llm_service.controller.user.LoginRequest;
+import com.llm_service.llm_service.controller.user.UserRequest;
+import com.llm_service.llm_service.dto.User;
+import com.llm_service.llm_service.exception.user.UserNotFoundException;
+import com.llm_service.llm_service.exception.user.UsernameAlreadyExistsException;
+import com.llm_service.llm_service.persistance.entities.UserEntity;
+import com.llm_service.llm_service.persistance.repositories.token.TokenPersistenceManager;
+import com.llm_service.llm_service.persistance.repositories.user.UserEntityMapper;
+import com.llm_service.llm_service.persistance.repositories.user.UserPersistenceManager;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class AuthenticationService {
+
+ private final UserPersistenceManager userPersistenceManager;
+
+ private final PasswordEncoder passwordEncoder;
+ private final JwtService jwtService;
+
+ private final TokenPersistenceManager tokenPersistenceManager;
+
+ private final AuthenticationManager authenticationManager;
+
+ private final UserEntityMapper userEntityMapper;
+
+ public User register(UserRequest userRequest) throws UsernameAlreadyExistsException {
+ User user = User.builder()
+ .role(userRequest.getRole())
+ .username(userRequest.getUsername())
+ .password(userRequest.getPassword())
+ .firstName(userRequest.getFirstName())
+ .lastName(userRequest.getLastName())
+ .build();
+
+ // check if user already exist. if exist than authenticate the user
+ if (userPersistenceManager.findByUsername(user.getUsername()).isPresent()) {
+ throw new UsernameAlreadyExistsException(user.getUsername());
+ }
+
+ User newUser = user.toBuilder()
+ .password(this.passwordEncoder.encode(user.getPassword()))
+ .build();
+
+ UserEntity userEntity = userEntityMapper.map(newUser);
+
+ User savedUser = userPersistenceManager.save(userEntityMapper.map(userEntity));
+
+ String jwt = jwtService.generateToken(savedUser);
+
+ saveUserToken(jwt, savedUser);
+
+ return savedUser;
+ }
+
+ public AuthenticationResponse authenticate(LoginRequest loginRequest)
+ throws UserNotFoundException, AuthenticationException {
+ authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
+
+ User user = userPersistenceManager
+ .findByUsername(loginRequest.getUsername())
+ .orElseThrow(UserNotFoundException::new);
+ String jwt = jwtService.generateToken(user);
+
+ revokeAllTokenByUser(user);
+ saveUserToken(jwt, user);
+
+ return new AuthenticationResponse(jwt, "User login was successful");
+ }
+
+ private void revokeAllTokenByUser(User user) {
+ List validTokens = tokenPersistenceManager.findAllTokensByUser(user.getId());
+ if (validTokens.isEmpty()) {
+ return;
+ }
+
+ validTokens.forEach(t -> t.toBuilder().loggedOut(false).build());
+
+ tokenPersistenceManager.saveAll(validTokens);
+ }
+
+ private void saveUserToken(String jwt, User user) {
+ Token token = Token.builder().token(jwt).loggedOut(false).user(user).build();
+
+ tokenPersistenceManager.save(token);
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/service/jwt/JwtService.java b/backend/src/main/java/com/llm_service/llm_service/service/jwt/JwtService.java
new file mode 100644
index 0000000..fef9059
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/service/jwt/JwtService.java
@@ -0,0 +1,72 @@
+package com.llm_service.llm_service.service.jwt;
+
+import com.llm_service.llm_service.dto.User;
+import com.llm_service.llm_service.persistance.repositories.token.TokenRepository;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import java.util.Date;
+import java.util.function.Function;
+import javax.crypto.SecretKey;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JwtService {
+ private final String SECRET_KEY = "4bb6d1dfbafb64a681139d1586b6f1160d18159afd57c8c79136d7490630407c";
+ private final TokenRepository tokenRepository;
+
+ public JwtService(TokenRepository tokenRepository) {
+ this.tokenRepository = tokenRepository;
+ }
+
+ public String extractUsername(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ public boolean isValid(String token, UserDetails user) {
+ String username = extractUsername(token);
+
+ boolean validToken =
+ tokenRepository.findByToken(token).map(t -> !t.isLoggedOut()).orElse(false);
+
+ return (username.equals(user.getUsername())) && !isTokenExpired(token) && validToken;
+ }
+
+ private boolean isTokenExpired(String token) {
+ return extractExpiration(token).before(new Date());
+ }
+
+ private Date extractExpiration(String token) {
+ return extractClaim(token, Claims::getExpiration);
+ }
+
+ public T extractClaim(String token, Function resolver) {
+ Claims claims = extractAllClaims(token);
+ return resolver.apply(claims);
+ }
+
+ private Claims extractAllClaims(String token) {
+ return Jwts.parser()
+ .verifyWith(getSignInKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+
+ public String generateToken(User user) {
+
+ return Jwts.builder()
+ .subject(user.getUsername())
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
+ .signWith(getSignInKey())
+ .compact();
+ }
+
+ private SecretKey getSignInKey() {
+ byte[] keyBytes = Decoders.BASE64URL.decode(SECRET_KEY);
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/service/jwt/Token.java b/backend/src/main/java/com/llm_service/llm_service/service/jwt/Token.java
new file mode 100644
index 0000000..289c92b
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/service/jwt/Token.java
@@ -0,0 +1,15 @@
+package com.llm_service.llm_service.service.jwt;
+
+import com.llm_service.llm_service.dto.User;
+import java.util.UUID;
+import lombok.Builder;
+import lombok.Value;
+
+@Value
+@Builder(toBuilder = true)
+public class Token {
+ UUID id;
+ String token;
+ boolean loggedOut;
+ User user;
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/service/jwt/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/llm_service/llm_service/service/jwt/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..ebfdc97
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/service/jwt/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,61 @@
+package com.llm_service.llm_service.service.jwt.filter;
+
+import com.llm_service.llm_service.service.jwt.JwtService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import org.springframework.lang.NonNull;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtService jwtService;
+ private final UserDetailsService userDetailsService;
+
+ public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
+ this.jwtService = jwtService;
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain)
+ throws ServletException, IOException {
+
+ String authHeader = request.getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String token = authHeader.substring(7);
+ String username = jwtService.extractUsername(token);
+
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+
+ if (jwtService.isValid(token, userDetails)) {
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+ }
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/backend/src/main/java/com/llm_service/llm_service/service/user/UserDetailsServiceImp.java b/backend/src/main/java/com/llm_service/llm_service/service/user/UserDetailsServiceImp.java
new file mode 100644
index 0000000..b681f17
--- /dev/null
+++ b/backend/src/main/java/com/llm_service/llm_service/service/user/UserDetailsServiceImp.java
@@ -0,0 +1,27 @@
+package com.llm_service.llm_service.service.user;
+
+import com.llm_service.llm_service.persistance.repositories.user.UserEntityMapper;
+import com.llm_service.llm_service.persistance.repositories.user.UserPersistenceManager;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserDetailsServiceImp implements UserDetailsService {
+ private final UserPersistenceManager userPersistenceManager;
+ private final UserEntityMapper userEntityMapper;
+
+ public UserDetailsServiceImp(UserPersistenceManager userPersistenceManager, UserEntityMapper userEntityMapper) {
+ this.userPersistenceManager = userPersistenceManager;
+ this.userEntityMapper = userEntityMapper;
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userPersistenceManager
+ .findByUsername(username)
+ .map(userEntityMapper::map)
+ .orElseThrow(() -> new UsernameNotFoundException(username));
+ }
+}