From d0c3e0c30ff6f6c3f365084ab89a7c76d5478695 Mon Sep 17 00:00:00 2001 From: Anupama Morapitiya Date: Tue, 20 Jan 2026 19:30:04 +0530 Subject: [PATCH 1/2] feat: Implement user service with CRUD operations and OpenAPI documentation --- services/user-service/pom.xml | 10 +++ .../user_service/config/OpenApiConfig.java | 18 +++++ .../controller/UserController.java | 76 +++++++++++++++++++ .../user_service/dto/CreateUserRequest.java | 13 ++++ .../user_service/dto/UpdateUserRequest.java | 13 ++++ .../platformops/user_service/model/User.java | 25 ++++++ .../repository/UserRepository.java | 9 +++ .../user_service/service/UserService.java | 48 ++++++++++++ .../src/main/resources/application.yml | 19 ++++- 9 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/dto/UpdateUserRequest.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/model/User.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/service/UserService.java diff --git a/services/user-service/pom.xml b/services/user-service/pom.xml index c2db72c..9371da2 100644 --- a/services/user-service/pom.xml +++ b/services/user-service/pom.xml @@ -42,6 +42,16 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.projectlombok + lombok + true + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + org.postgresql diff --git a/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java b/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java new file mode 100644 index 0000000..04663f4 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java @@ -0,0 +1,18 @@ +package com.platformops.user_service.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("User Service API") + .version("1.0") + .description("API for managing users in the platform")); + } +} \ No newline at end of file diff --git a/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java b/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java new file mode 100644 index 0000000..7881983 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java @@ -0,0 +1,76 @@ +package com.platformops.user_service.controller; + +import com.platformops.user_service.dto.CreateUserRequest; +import com.platformops.user_service.dto.UpdateUserRequest; +import com.platformops.user_service.model.User; +import com.platformops.user_service.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/users") +@Tag(name = "User Management", description = "Endpoints for managing user accounts and profiles") +public class UserController { + @Autowired + private UserService userService; + + @GetMapping + @Operation( + summary = "Get all users", + description = "Retrieve a paginated list of all registered users in the system. Useful for admin dashboards or user management interfaces." + ) + public List getAllUsers() { + return userService.getAllUsers(); + } + + @GetMapping("/{id}") + @Operation( + summary = "Get user by ID", + description = "Retrieve details of a specific user by their unique ID. Useful for displaying user profiles or editing user information." + ) + public User getUserById( + @Parameter(description = "ID of the user to retrieve", example = "1") + @PathVariable Long id) { + return userService.getUserById(id); + } + + @PostMapping + @Operation( + summary = "Create a new user", + description = "Register a new user in the system. The email must be unique. Useful for user sign-up flows." + ) + public User createUser( + @Parameter(description = "User creation request with name, email, and password", required = true) + @RequestBody CreateUserRequest request) { + return userService.createUser(request); + } + + @PutMapping("/{id}") + @Operation( + summary = "Update a user", + description = "Update the details of an existing user by their ID. Useful for profile updates or admin edits." + ) + public User updateUser( + @Parameter(description = "ID of the user to update", example = "1") + @PathVariable Long id, + @Parameter(description = "User update request with name, email, and password", required = true) + @RequestBody UpdateUserRequest request) { + return userService.updateUser(id, request); + } + + @DeleteMapping("/{id}") + @Operation( + summary = "Delete a user", + description = "Remove a user from the system by their ID. Useful for user account deletion or admin cleanup." + ) + public void deleteUser( + @Parameter(description = "ID of the user to delete", example = "1") + @PathVariable Long id) { + userService.deleteUser(id); + } +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java b/services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java new file mode 100644 index 0000000..445f5a1 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java @@ -0,0 +1,13 @@ +package com.platformops.user_service.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CreateUserRequest { + private String name; + private String email; + private String password; +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/dto/UpdateUserRequest.java b/services/user-service/src/main/java/com/platformops/user_service/dto/UpdateUserRequest.java new file mode 100644 index 0000000..45c58d0 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/dto/UpdateUserRequest.java @@ -0,0 +1,13 @@ +package com.platformops.user_service.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UpdateUserRequest { + private String name; + private String email; + private String password; +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/model/User.java b/services/user-service/src/main/java/com/platformops/user_service/model/User.java new file mode 100644 index 0000000..3b0b80a --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/model/User.java @@ -0,0 +1,25 @@ +package com.platformops.user_service.model; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String password; +} \ No newline at end of file diff --git a/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java b/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java new file mode 100644 index 0000000..cffe910 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.platformops.user_service.repository; + + +import com.platformops.user_service.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + User findByEmail(String email); +} \ No newline at end of file diff --git a/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java b/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java new file mode 100644 index 0000000..6bb296d --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java @@ -0,0 +1,48 @@ +package com.platformops.user_service.service; + + +import com.platformops.user_service.dto.CreateUserRequest; +import com.platformops.user_service.dto.UpdateUserRequest; +import com.platformops.user_service.model.User; +import com.platformops.user_service.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + + public List getAllUsers() { + return userRepository.findAll(); + } + + public User getUserById(Long id) { + return userRepository.findById(id).orElse(null); + } + + public User createUser(CreateUserRequest request) { + User user = new User(); + user.setName(request.getName()); + user.setEmail(request.getEmail()); + user.setPassword(request.getPassword()); + return userRepository.save(user); + } + + public User updateUser(Long id, UpdateUserRequest request) { + User existingUser = userRepository.findById(id).orElse(null); + if (existingUser != null) { + existingUser.setName(request.getName()); + existingUser.setEmail(request.getEmail()); + existingUser.setPassword(request.getPassword()); + return userRepository.save(existingUser); + } + return null; + } + + public void deleteUser(Long id) { + userRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/services/user-service/src/main/resources/application.yml b/services/user-service/src/main/resources/application.yml index 364dd65..d067af5 100644 --- a/services/user-service/src/main/resources/application.yml +++ b/services/user-service/src/main/resources/application.yml @@ -1,8 +1,15 @@ +server: + port: 8080 + spring: + application: + name: user-service + datasource: - url: jdbc:postgresql://localhost:5432/users - username: user - password: password + url: jdbc:postgresql://${DB_HOST:localhost}:5432/users + username: ${DB_USER:user} + password: ${DB_PASSWORD:password} + jpa: hibernate: ddl-auto: update @@ -10,3 +17,9 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect show-sql: true + +management: + endpoints: + web: + exposure: + include: health,info,prometheus \ No newline at end of file From 49e50cad30c6ad27ee93390fd6d930dff1607298 Mon Sep 17 00:00:00 2001 From: Anupama Morapitiya Date: Tue, 20 Jan 2026 21:24:39 +0530 Subject: [PATCH 2/2] feat: Add user authentication and authorization with JWT support --- services/user-service/.env.example | 17 ++++ services/user-service/.gitignore | 6 ++ services/user-service/SWAGGER_JWT_GUIDE.md | 0 services/user-service/pom.xml | 42 ++++++--- .../user_service/config/OpenApiConfig.java | 15 +++- .../user_service/config/SecurityConfig.java | 63 ++++++++++++++ .../controller/AuthController.java | 56 ++++++++++++ .../controller/UserController.java | 41 ++++++--- .../user_service/dto/AuthResponse.java | 17 ++++ .../user_service/dto/LoginRequest.java | 12 +++ ...eUserRequest.java => RegisterRequest.java} | 2 +- .../filter/JwtAuthenticationFilter.java | 58 +++++++++++++ .../repository/UserRepository.java | 4 +- .../user_service/service/AuthService.java | 85 +++++++++++++++++++ .../service/CustomUserDetailsService.java | 30 +++++++ .../user_service/service/UserService.java | 17 ++-- .../user_service/util/JwtUtil.java | 74 ++++++++++++++++ .../src/main/resources/application.yml | 4 + 18 files changed, 509 insertions(+), 34 deletions(-) create mode 100644 services/user-service/.env.example create mode 100644 services/user-service/SWAGGER_JWT_GUIDE.md create mode 100644 services/user-service/src/main/java/com/platformops/user_service/config/SecurityConfig.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/controller/AuthController.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/dto/AuthResponse.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/dto/LoginRequest.java rename services/user-service/src/main/java/com/platformops/user_service/dto/{CreateUserRequest.java => RegisterRequest.java} (85%) create mode 100644 services/user-service/src/main/java/com/platformops/user_service/filter/JwtAuthenticationFilter.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/service/AuthService.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/service/CustomUserDetailsService.java create mode 100644 services/user-service/src/main/java/com/platformops/user_service/util/JwtUtil.java diff --git a/services/user-service/.env.example b/services/user-service/.env.example new file mode 100644 index 0000000..9239fb7 --- /dev/null +++ b/services/user-service/.env.example @@ -0,0 +1,17 @@ +# JWT Configuration +# Copy this file to .env and update with your actual values + +# JWT Secret Key (Base64 encoded, minimum 256 bits recommended) +# Generate a new one using: openssl rand -base64 64 +JWT_SECRET=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 + +# JWT Token Expiration Time (in milliseconds) +# 900000 = 15 minutes +# 3600000 = 1 hour +# 86400000 = 24 hours (current default) +JWT_EXPIRATION=86400000 + +# Database Configuration +DB_HOST=localhost +DB_USER=user +DB_PASSWORD=password diff --git a/services/user-service/.gitignore b/services/user-service/.gitignore index 667aaef..9c33eaa 100644 --- a/services/user-service/.gitignore +++ b/services/user-service/.gitignore @@ -31,3 +31,9 @@ build/ ### VS Code ### .vscode/ + +### Environment Variables ### +.env +.env.local +.env.production + diff --git a/services/user-service/SWAGGER_JWT_GUIDE.md b/services/user-service/SWAGGER_JWT_GUIDE.md new file mode 100644 index 0000000..e69de29 diff --git a/services/user-service/pom.xml b/services/user-service/pom.xml index 9371da2..572d8a3 100644 --- a/services/user-service/pom.xml +++ b/services/user-service/pom.xml @@ -47,17 +47,39 @@ lombok true - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.5.0 - + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + - - org.postgresql - postgresql - runtime - + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + org.postgresql + postgresql + runtime + org.springframework.boot spring-boot-starter-actuator-test diff --git a/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java b/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java index 04663f4..0b8f5b5 100644 --- a/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java +++ b/services/user-service/src/main/java/com/platformops/user_service/config/OpenApiConfig.java @@ -1,7 +1,10 @@ package com.platformops.user_service.config; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,10 +12,20 @@ public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { + final String securitySchemeName = "bearerAuth"; return new OpenAPI() .info(new Info() .title("User Service API") .version("1.0") - .description("API for managing users in the platform")); + .description("API for managing users in the platform. Use the 'Authorize' button to add your JWT token.")) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("Enter your JWT token from /api/auth/login or /api/auth/register"))); } } \ No newline at end of file diff --git a/services/user-service/src/main/java/com/platformops/user_service/config/SecurityConfig.java b/services/user-service/src/main/java/com/platformops/user_service/config/SecurityConfig.java new file mode 100644 index 0000000..4c4f7d0 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package com.platformops.user_service.config; + +import com.platformops.user_service.filter.JwtAuthenticationFilter; +import com.platformops.user_service.service.CustomUserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Autowired + private CustomUserDetailsService userDetailsService; + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService); + return authProvider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/controller/AuthController.java b/services/user-service/src/main/java/com/platformops/user_service/controller/AuthController.java new file mode 100644 index 0000000..bfc539f --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/controller/AuthController.java @@ -0,0 +1,56 @@ +package com.platformops.user_service.controller; + +import com.platformops.user_service.dto.AuthResponse; +import com.platformops.user_service.dto.LoginRequest; +import com.platformops.user_service.dto.RegisterRequest; +import com.platformops.user_service.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@Tag(name = "Authentication", description = "Endpoints for user authentication and registration") +@SecurityRequirement(name = "") // No authentication required for these endpoints +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/register") + @Operation( + summary = "Register a new user", + description = "Create a new user account and receive a JWT token for authentication" + ) + public ResponseEntity register( + @Parameter(description = "Registration details including name, email, and password", required = true) + @RequestBody RegisterRequest request) { + try { + AuthResponse response = authService.register(request); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().build(); + } + } + + @PostMapping("/login") + @Operation( + summary = "Login user", + description = "Authenticate a user with email and password, and receive a JWT token" + ) + public ResponseEntity login( + @Parameter(description = "Login credentials including email and password", required = true) + @RequestBody LoginRequest request) { + try { + AuthResponse response = authService.login(request); + return ResponseEntity.ok(response); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().build(); + } + } +} + diff --git a/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java b/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java index 7881983..ebcbc8a 100644 --- a/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java +++ b/services/user-service/src/main/java/com/platformops/user_service/controller/UserController.java @@ -1,13 +1,17 @@ package com.platformops.user_service.controller; -import com.platformops.user_service.dto.CreateUserRequest; import com.platformops.user_service.dto.UpdateUserRequest; import com.platformops.user_service.model.User; import com.platformops.user_service.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -15,6 +19,7 @@ @RestController @RequestMapping("/api/users") @Tag(name = "User Management", description = "Endpoints for managing user accounts and profiles") +@SecurityRequirement(name = "bearerAuth") public class UserController { @Autowired private UserService userService; @@ -39,38 +44,48 @@ public User getUserById( return userService.getUserById(id); } - @PostMapping + @GetMapping("/me") @Operation( - summary = "Create a new user", - description = "Register a new user in the system. The email must be unique. Useful for user sign-up flows." + summary = "Get current user profile", + description = "Retrieve the profile of the currently authenticated user" ) - public User createUser( - @Parameter(description = "User creation request with name, email, and password", required = true) - @RequestBody CreateUserRequest request) { - return userService.createUser(request); + public ResponseEntity getCurrentUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String authenticatedEmail = authentication.getName(); + + User user = userService.getUserByEmail(authenticatedEmail); + if (user == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body("User not found"); + } + + return ResponseEntity.ok(user); } @PutMapping("/{id}") @Operation( summary = "Update a user", - description = "Update the details of an existing user by their ID. Useful for profile updates or admin edits." + description = "Update the details of the authenticated user. Users can only update their own profile." ) - public User updateUser( + public ResponseEntity updateUser( @Parameter(description = "ID of the user to update", example = "1") @PathVariable Long id, @Parameter(description = "User update request with name, email, and password", required = true) @RequestBody UpdateUserRequest request) { - return userService.updateUser(id, request); + + User updatedUser = userService.updateUser(id, request); + return ResponseEntity.ok(updatedUser); } @DeleteMapping("/{id}") @Operation( summary = "Delete a user", - description = "Remove a user from the system by their ID. Useful for user account deletion or admin cleanup." + description = "Remove the authenticated user's account from the system. Users can only delete their own account." ) - public void deleteUser( + public ResponseEntity deleteUser( @Parameter(description = "ID of the user to delete", example = "1") @PathVariable Long id) { userService.deleteUser(id); + return ResponseEntity.ok("User deleted successfully"); } } diff --git a/services/user-service/src/main/java/com/platformops/user_service/dto/AuthResponse.java b/services/user-service/src/main/java/com/platformops/user_service/dto/AuthResponse.java new file mode 100644 index 0000000..0e3c57e --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/dto/AuthResponse.java @@ -0,0 +1,17 @@ +package com.platformops.user_service.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthResponse { + private String token; + @Builder.Default + private String type = "Bearer"; + private Long id; + private String name; + private String email; +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/dto/LoginRequest.java b/services/user-service/src/main/java/com/platformops/user_service/dto/LoginRequest.java new file mode 100644 index 0000000..47f3335 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/dto/LoginRequest.java @@ -0,0 +1,12 @@ +package com.platformops.user_service.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class LoginRequest { + private String email; + private String password; +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java b/services/user-service/src/main/java/com/platformops/user_service/dto/RegisterRequest.java similarity index 85% rename from services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java rename to services/user-service/src/main/java/com/platformops/user_service/dto/RegisterRequest.java index 445f5a1..c360ff5 100644 --- a/services/user-service/src/main/java/com/platformops/user_service/dto/CreateUserRequest.java +++ b/services/user-service/src/main/java/com/platformops/user_service/dto/RegisterRequest.java @@ -6,7 +6,7 @@ @Setter @NoArgsConstructor @AllArgsConstructor -public class CreateUserRequest { +public class RegisterRequest { private String name; private String email; private String password; diff --git a/services/user-service/src/main/java/com/platformops/user_service/filter/JwtAuthenticationFilter.java b/services/user-service/src/main/java/com/platformops/user_service/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..1576b55 --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/filter/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package com.platformops.user_service.filter; + +import com.platformops.user_service.service.CustomUserDetailsService; +import com.platformops.user_service.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +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; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private CustomUserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String authHeader = request.getHeader("Authorization"); + String token = null; + String username = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + try { + username = jwtUtil.extractUsername(token); + } catch (Exception e) { + // Invalid token + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (jwtUtil.validateToken(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/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java b/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java index cffe910..6cd9913 100644 --- a/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java +++ b/services/user-service/src/main/java/com/platformops/user_service/repository/UserRepository.java @@ -4,6 +4,8 @@ import com.platformops.user_service.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { - User findByEmail(String email); + Optional findByEmail(String email); } \ No newline at end of file diff --git a/services/user-service/src/main/java/com/platformops/user_service/service/AuthService.java b/services/user-service/src/main/java/com/platformops/user_service/service/AuthService.java new file mode 100644 index 0000000..a61cf3e --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/service/AuthService.java @@ -0,0 +1,85 @@ +package com.platformops.user_service.service; + +import com.platformops.user_service.dto.AuthResponse; +import com.platformops.user_service.dto.LoginRequest; +import com.platformops.user_service.dto.RegisterRequest; +import com.platformops.user_service.model.User; +import com.platformops.user_service.repository.UserRepository; +import com.platformops.user_service.util.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class AuthService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private AuthenticationManager authenticationManager; + + public AuthResponse register(RegisterRequest request) { + // Check if user already exists + if (userRepository.findByEmail(request.getEmail()).isPresent()) { + throw new RuntimeException("Email already exists"); + } + + // Create new user + User user = new User(); + user.setName(request.getName()); + user.setEmail(request.getEmail()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + + User savedUser = userRepository.save(user); + + // Generate JWT token + String token = jwtUtil.generateToken(savedUser.getEmail()); + + return AuthResponse.builder() + .token(token) + .type("Bearer") + .id(savedUser.getId()) + .name(savedUser.getName()) + .email(savedUser.getEmail()) + .build(); + } + + public AuthResponse login(LoginRequest request) { + try { + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword() + ) + ); + + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("User not found")); + + String token = jwtUtil.generateToken(user.getEmail()); + + return AuthResponse.builder() + .token(token) + .type("Bearer") + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .build(); + + } catch (BadCredentialsException e) { + throw new RuntimeException("Invalid email or password"); + } + } +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/service/CustomUserDetailsService.java b/services/user-service/src/main/java/com/platformops/user_service/service/CustomUserDetailsService.java new file mode 100644 index 0000000..96e859f --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/service/CustomUserDetailsService.java @@ -0,0 +1,30 @@ +package com.platformops.user_service.service; + +import com.platformops.user_service.model.User; +import com.platformops.user_service.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +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; + +import java.util.ArrayList; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); + + return new org.springframework.security.core.userdetails.User( + user.getEmail(), + user.getPassword(), + new ArrayList<>() + ); + } +} diff --git a/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java b/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java index 6bb296d..88834fd 100644 --- a/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java +++ b/services/user-service/src/main/java/com/platformops/user_service/service/UserService.java @@ -1,11 +1,12 @@ package com.platformops.user_service.service; -import com.platformops.user_service.dto.CreateUserRequest; +import com.platformops.user_service.dto.RegisterRequest; import com.platformops.user_service.dto.UpdateUserRequest; import com.platformops.user_service.model.User; import com.platformops.user_service.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -15,6 +16,9 @@ public class UserService { @Autowired private UserRepository userRepository; + @Autowired + private PasswordEncoder passwordEncoder; + public List getAllUsers() { return userRepository.findAll(); } @@ -23,20 +27,17 @@ public User getUserById(Long id) { return userRepository.findById(id).orElse(null); } - public User createUser(CreateUserRequest request) { - User user = new User(); - user.setName(request.getName()); - user.setEmail(request.getEmail()); - user.setPassword(request.getPassword()); - return userRepository.save(user); + public User getUserByEmail(String email) { + return userRepository.findByEmail(email).orElse(null); } + public User updateUser(Long id, UpdateUserRequest request) { User existingUser = userRepository.findById(id).orElse(null); if (existingUser != null) { existingUser.setName(request.getName()); existingUser.setEmail(request.getEmail()); - existingUser.setPassword(request.getPassword()); + existingUser.setPassword(passwordEncoder.encode(request.getPassword())); return userRepository.save(existingUser); } return null; diff --git a/services/user-service/src/main/java/com/platformops/user_service/util/JwtUtil.java b/services/user-service/src/main/java/com/platformops/user_service/util/JwtUtil.java new file mode 100644 index 0000000..a80a89a --- /dev/null +++ b/services/user-service/src/main/java/com/platformops/user_service/util/JwtUtil.java @@ -0,0 +1,74 @@ +package com.platformops.user_service.util; + +import io.jsonwebtoken.*; +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.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970}") + private String secret; + + @Value("${jwt.expiration:86400000}") // 24 hours in milliseconds + private Long expiration; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSignKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String generateToken(String email) { + Map claims = new HashMap<>(); + return createToken(claims, email); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSignKey()) + .compact(); + } + + private SecretKey getSignKey() { + byte[] keyBytes = Decoders.BASE64.decode(secret); + return Keys.hmacShaKeyFor(keyBytes); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } +} diff --git a/services/user-service/src/main/resources/application.yml b/services/user-service/src/main/resources/application.yml index d067af5..399d55a 100644 --- a/services/user-service/src/main/resources/application.yml +++ b/services/user-service/src/main/resources/application.yml @@ -18,6 +18,10 @@ spring: dialect: org.hibernate.dialect.PostgreSQLDialect show-sql: true +jwt: + secret: ${JWT_SECRET:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970} + expiration: ${JWT_EXPIRATION:86400000} # 24 hours in milliseconds + management: endpoints: web: