From bafb636cbe5e800b6a615270536b10e627b0f118 Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Thu, 18 Jun 2026 00:22:50 +0300 Subject: [PATCH] added security and fix edit profile --- build.gradle | 6 ++ .../refound/controller/AuthController.java | 39 ++++++++-- .../refound/controller/PostController.java | 5 ++ .../refound/controller/UserController.java | 30 ++++++++ .../com/krylov/refound/dto/AuthResponse.java | 2 +- .../com/krylov/refound/dto/CheckLoginDto.java | 9 +++ .../com/krylov/refound/dto/PostResponse.java | 1 + .../java/com/krylov/refound/dto/UserDto.java | 13 ++++ .../krylov/refound/dto/UserResponseDto.java | 15 ++++ .../com/krylov/refound/dto/UserUpdateDto.java | 15 ++++ .../java/com/krylov/refound/entity/User.java | 3 + .../com/krylov/refound/enums/ErrorCode.java | 4 +- .../com/krylov/refound/mapper/PostMapper.java | 1 + .../refound/repository/PostRepository.java | 37 +-------- .../security/JwtAuthenticationFilter.java | 50 ++++++++++++ .../krylov/refound/security/JwtService.java | 77 +++++++++++++++++++ .../refound/security/SecurityConfig.java | 45 +++++++++++ .../krylov/refound/service/AvatarService.java | 26 +++++++ .../krylov/refound/service/PostService.java | 10 +-- .../krylov/refound/service/UserService.java | 57 +++++++++++++- src/main/resources/application.yaml | 6 +- .../db/changelog/add/004-add-fields-user.yaml | 10 +++ .../changelog/add/005-add-user-avatarUrl.yaml | 9 +++ .../db/changelog/db.changelog-master.yaml | 4 +- 24 files changed, 418 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/krylov/refound/controller/UserController.java create mode 100644 src/main/java/com/krylov/refound/dto/CheckLoginDto.java create mode 100644 src/main/java/com/krylov/refound/dto/UserDto.java create mode 100644 src/main/java/com/krylov/refound/dto/UserResponseDto.java create mode 100644 src/main/java/com/krylov/refound/dto/UserUpdateDto.java create mode 100644 src/main/java/com/krylov/refound/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/krylov/refound/security/JwtService.java create mode 100644 src/main/java/com/krylov/refound/security/SecurityConfig.java create mode 100644 src/main/java/com/krylov/refound/service/AvatarService.java create mode 100644 src/main/resources/db/changelog/add/004-add-fields-user.yaml create mode 100644 src/main/resources/db/changelog/add/005-add-user-avatarUrl.yaml diff --git a/build.gradle b/build.gradle index a309cb8..cac55bd 100644 --- a/build.gradle +++ b/build.gradle @@ -29,10 +29,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.liquibase:liquibase-core' implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.mapstruct:mapstruct:1.5.5.Final' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' diff --git a/src/main/java/com/krylov/refound/controller/AuthController.java b/src/main/java/com/krylov/refound/controller/AuthController.java index f6fe51c..bb542b8 100644 --- a/src/main/java/com/krylov/refound/controller/AuthController.java +++ b/src/main/java/com/krylov/refound/controller/AuthController.java @@ -2,18 +2,20 @@ package com.krylov.refound.controller; import com.krylov.refound.dto.AuthRequest; import com.krylov.refound.dto.AuthResponse; +import com.krylov.refound.dto.CheckLoginDto; import com.krylov.refound.dto.RegisterRequest; +import com.krylov.refound.dto.UserDto; import com.krylov.refound.entity.User; import com.krylov.refound.repository.UserRepository; +import com.krylov.refound.security.JwtService; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.security.crypto.password.PasswordEncoder; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -23,6 +25,8 @@ import org.springframework.web.bind.annotation.RestController; public class AuthController { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; @PostMapping("/register") public ResponseEntity register(@RequestBody RegisterRequest request) { @@ -38,18 +42,37 @@ public class AuthController { User user = new User(); user.setName(request.getName()); user.setLogin(request.getLogin()); - user.setPassword(request.getPassword()); // позже добавить BCrypt + user.setPassword(passwordEncoder.encode(request.getPassword())); userRepository.save(user); - String token = "fake-jwt-token"; + String token = jwtService.generateToken(user.getLogin()); - return ResponseEntity.ok(new AuthResponse(token, user)); + UserDto userDto = new UserDto(user.getId(), user.getName(), user.getLogin(), user.getEmail()); + return ResponseEntity.ok(new AuthResponse(token, userDto)); } - @GetMapping("/check-login") - public ResponseEntity checkLogin(@RequestParam String login) { - boolean exists = userRepository.existsByLogin(login); + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthRequest request) { + + User user = userRepository.findByLogin(request.getLogin()) + .orElse(null); + + if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { + return ResponseEntity + .badRequest() + .body(Map.of("message", "Неверный логин или пароль")); + } + + String token = jwtService.generateToken(user.getLogin()); + + UserDto userDto = new UserDto(user.getId(), user.getName(), user.getLogin(), user.getEmail()); + return ResponseEntity.ok(new AuthResponse(token, userDto)); + } + + @PostMapping("/check-login") + public ResponseEntity checkLogin(@RequestBody CheckLoginDto request) { + boolean exists = userRepository.existsByLogin(request.getLogin()); return ResponseEntity.ok(Map.of("exists", exists)); } diff --git a/src/main/java/com/krylov/refound/controller/PostController.java b/src/main/java/com/krylov/refound/controller/PostController.java index 99f47e2..02eb16b 100644 --- a/src/main/java/com/krylov/refound/controller/PostController.java +++ b/src/main/java/com/krylov/refound/controller/PostController.java @@ -42,6 +42,11 @@ public class PostController { return service.getFeed(); } + @GetMapping("/{id}") + public List getById(@PathVariable Long id) { + return service.getById(id); + } + @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public PostResponse update( @PathVariable Long id, diff --git a/src/main/java/com/krylov/refound/controller/UserController.java b/src/main/java/com/krylov/refound/controller/UserController.java new file mode 100644 index 0000000..84ca0dd --- /dev/null +++ b/src/main/java/com/krylov/refound/controller/UserController.java @@ -0,0 +1,30 @@ +package com.krylov.refound.controller; + +import com.krylov.refound.dto.UserResponseDto; +import com.krylov.refound.dto.UserUpdateDto; +import com.krylov.refound.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +@Slf4j +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @PutMapping("/{id}") + public ResponseEntity updateUser(@PathVariable Long id, @ModelAttribute UserUpdateDto userUpdateDto) { + log.info("updateUser, id={}", id); + UserResponseDto userResponseDto = userService.updateUser(id, userUpdateDto); + return ResponseEntity.ok(userResponseDto); + } + +} diff --git a/src/main/java/com/krylov/refound/dto/AuthResponse.java b/src/main/java/com/krylov/refound/dto/AuthResponse.java index 939b030..f2633ad 100644 --- a/src/main/java/com/krylov/refound/dto/AuthResponse.java +++ b/src/main/java/com/krylov/refound/dto/AuthResponse.java @@ -9,5 +9,5 @@ import lombok.Data; public class AuthResponse { private String token; - private User user; + private UserDto user; } diff --git a/src/main/java/com/krylov/refound/dto/CheckLoginDto.java b/src/main/java/com/krylov/refound/dto/CheckLoginDto.java new file mode 100644 index 0000000..33cfef4 --- /dev/null +++ b/src/main/java/com/krylov/refound/dto/CheckLoginDto.java @@ -0,0 +1,9 @@ +package com.krylov.refound.dto; + +import lombok.Data; + +@Data +public class CheckLoginDto { + private String login; + private String password; +} diff --git a/src/main/java/com/krylov/refound/dto/PostResponse.java b/src/main/java/com/krylov/refound/dto/PostResponse.java index 4aa9a6d..01a641f 100644 --- a/src/main/java/com/krylov/refound/dto/PostResponse.java +++ b/src/main/java/com/krylov/refound/dto/PostResponse.java @@ -22,4 +22,5 @@ public class PostResponse { private String phone; private List images; private boolean isFavorite; + private Long userId; } diff --git a/src/main/java/com/krylov/refound/dto/UserDto.java b/src/main/java/com/krylov/refound/dto/UserDto.java new file mode 100644 index 0000000..22ce59d --- /dev/null +++ b/src/main/java/com/krylov/refound/dto/UserDto.java @@ -0,0 +1,13 @@ +package com.krylov.refound.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class UserDto { + private Long id; + private String name; + private String login; + private String email; +} diff --git a/src/main/java/com/krylov/refound/dto/UserResponseDto.java b/src/main/java/com/krylov/refound/dto/UserResponseDto.java new file mode 100644 index 0000000..832055c --- /dev/null +++ b/src/main/java/com/krylov/refound/dto/UserResponseDto.java @@ -0,0 +1,15 @@ +package com.krylov.refound.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserResponseDto { + private Long id; + private String login; + private String name; + private String phone; + private String email; + private String avatar; +} diff --git a/src/main/java/com/krylov/refound/dto/UserUpdateDto.java b/src/main/java/com/krylov/refound/dto/UserUpdateDto.java new file mode 100644 index 0000000..f045691 --- /dev/null +++ b/src/main/java/com/krylov/refound/dto/UserUpdateDto.java @@ -0,0 +1,15 @@ +package com.krylov.refound.dto; + +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Data +public class UserUpdateDto { + private Long id; + private String login; + private String firstName; + private String lastName; + private String phone; + private String email; + private MultipartFile avatar; +} diff --git a/src/main/java/com/krylov/refound/entity/User.java b/src/main/java/com/krylov/refound/entity/User.java index 61e8cd9..a963dbe 100644 --- a/src/main/java/com/krylov/refound/entity/User.java +++ b/src/main/java/com/krylov/refound/entity/User.java @@ -23,13 +23,16 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; + private String lastName; @Column(unique = true) private String login; private String password; private LocalDateTime createdAt; private String email; + private String phone; @Enumerated(EnumType.STRING) private Role role = Role.USER; + private String avatarUrl; @PrePersist public void prePersist() { diff --git a/src/main/java/com/krylov/refound/enums/ErrorCode.java b/src/main/java/com/krylov/refound/enums/ErrorCode.java index 50ea4e8..543efaa 100644 --- a/src/main/java/com/krylov/refound/enums/ErrorCode.java +++ b/src/main/java/com/krylov/refound/enums/ErrorCode.java @@ -4,5 +4,7 @@ public enum ErrorCode { VALIDATION_ERROR, NOT_FOUND, INTERNAL_ERROR, - BAD_REQUEST + BAD_REQUEST, + UNAUTHORIZED, + FORBIDDEN } diff --git a/src/main/java/com/krylov/refound/mapper/PostMapper.java b/src/main/java/com/krylov/refound/mapper/PostMapper.java index 04d3217..6469007 100644 --- a/src/main/java/com/krylov/refound/mapper/PostMapper.java +++ b/src/main/java/com/krylov/refound/mapper/PostMapper.java @@ -16,6 +16,7 @@ public interface PostMapper { @Mapping(source = "category", target = "category", qualifiedByName = "stringToPostCategory") Post toEntity(PostRequest request); + @Mapping(source = "user.id", target = "userId") @Mapping(source = "user.login", target = "userEmail") @Mapping(source = "images", target = "images", qualifiedByName = "imagesToUrls") @Mapping(target = "category", expression = "java(post.getCategory().getDisplayName())") diff --git a/src/main/java/com/krylov/refound/repository/PostRepository.java b/src/main/java/com/krylov/refound/repository/PostRepository.java index ccc37c3..12eee3d 100644 --- a/src/main/java/com/krylov/refound/repository/PostRepository.java +++ b/src/main/java/com/krylov/refound/repository/PostRepository.java @@ -1,45 +1,12 @@ package com.krylov.refound.repository; import com.krylov.refound.entity.Post; -import com.krylov.refound.enums.PostType; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; public interface PostRepository extends JpaRepository, JpaSpecificationExecutor { - Page findByType(PostType type, Pageable pageable); - - @Query(value = """ - SELECT * FROM Posts p - WHERE - (:type IS NULL OR p.type = :type) - AND (:city IS NULL OR p.city = :city) - AND (:category IS NULL OR p.category = :category) - AND ( - :lat IS NULL OR :lng IS NULL OR :radius IS NULL OR - ( - 6371 * acos( - cos(radians(:lat)) * cos(radians(p.latitude)) * - cos(radians(p.longitude) - radians(:lng)) + - sin(radians(:lat)) * sin(radians(p.latitude)) - ) - ) <= :radius - ) - """, - countQuery = "SELECT count(*) FROM Posts p", - nativeQuery = true) - Page searchWithGeo( - @Param("type") String type, - @Param("city") String city, - @Param("category") String category, - @Param("lat") Double lat, - @Param("lng") Double lng, - @Param("radius") Double radius, - Pageable pageable - ); + List findByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/krylov/refound/security/JwtAuthenticationFilter.java b/src/main/java/com/krylov/refound/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..7d7489f --- /dev/null +++ b/src/main/java/com/krylov/refound/security/JwtAuthenticationFilter.java @@ -0,0 +1,50 @@ +package com.krylov.refound.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + final String token = authHeader.substring(7); + + try { + if (jwtService.isTokenValid(token) && SecurityContextHolder.getContext().getAuthentication() == null) { + String login = jwtService.extractLogin(token); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(login, null, new ArrayList<>()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } catch (Exception e) { + // Token invalid — continue as unauthenticated + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/krylov/refound/security/JwtService.java b/src/main/java/com/krylov/refound/security/JwtService.java new file mode 100644 index 0000000..56130f5 --- /dev/null +++ b/src/main/java/com/krylov/refound/security/JwtService.java @@ -0,0 +1,77 @@ +package com.krylov.refound.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + public String generateToken(String login) { + Map claims = new HashMap<>(); + return generateToken(claims, login); + } + + public String generateToken(Map extraClaims, String login) { + return Jwts.builder() + .claims(extraClaims) + .subject(login) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + public String extractLogin(String token) { + return extractClaim(token, Claims::getSubject); + } + + public boolean isTokenValid(String token) { + try { + String login = extractLogin(token); + return login != null && !isTokenExpired(token); + } catch (Exception e) { + return false; + } + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private SecretKey getSigningKey() { + byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/com/krylov/refound/security/SecurityConfig.java b/src/main/java/com/krylov/refound/security/SecurityConfig.java new file mode 100644 index 0000000..ed65675 --- /dev/null +++ b/src/main/java/com/krylov/refound/security/SecurityConfig.java @@ -0,0 +1,45 @@ +package com.krylov.refound.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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 +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Public endpoints + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers(org.springframework.http.HttpMethod.GET, "/api/v1/posts/**").permitAll() + .requestMatchers("/files/**", "/uploads/**").permitAll() + .requestMatchers("/ws/**").permitAll() + // All other endpoints require authentication + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/krylov/refound/service/AvatarService.java b/src/main/java/com/krylov/refound/service/AvatarService.java new file mode 100644 index 0000000..eae7706 --- /dev/null +++ b/src/main/java/com/krylov/refound/service/AvatarService.java @@ -0,0 +1,26 @@ +package com.krylov.refound.service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class AvatarService { + private final Path uploadDir = Paths.get("uploads/avatars"); + + public String save(MultipartFile file) { + try { + Files.createDirectories(uploadDir); + String filename = UUID.randomUUID() + "_" + file.getOriginalFilename(); + Path target = uploadDir.resolve(filename); + file.transferTo(target); + return "/uploads/avatars/" + filename; // или полный URL, если есть статик-сервер/CDN + } catch (IOException e) { + throw new RuntimeException("Failed to save avatar", e); + } + } +} diff --git a/src/main/java/com/krylov/refound/service/PostService.java b/src/main/java/com/krylov/refound/service/PostService.java index 9d99beb..0666e1f 100644 --- a/src/main/java/com/krylov/refound/service/PostService.java +++ b/src/main/java/com/krylov/refound/service/PostService.java @@ -36,7 +36,6 @@ public class PostService { private final ImageRepository imageRepository; private final NominatimService nominatimService; - @CacheEvict(value = "posts", allEntries = true) public PostRequest create(PostRequest request, MultipartFile image) { Post post = mapper.toEntity(request); @@ -74,15 +73,10 @@ public class PostService { } - @Cacheable(value = "post", key = "#id") - public PostResponse getById(Long id) { - Post post = repository.findById(id) - .orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND)); - - return mapper.toResponse(post); + public List getById(Long id) { + return repository.findByUserId(id).stream().map(mapper::toResponse).toList(); } - @CacheEvict(value = {"posts", "post"}, allEntries = true) public PostResponse update(Long id, PostRequest request, MultipartFile image) { Post post = repository.findById(id) diff --git a/src/main/java/com/krylov/refound/service/UserService.java b/src/main/java/com/krylov/refound/service/UserService.java index 90ca9fa..3751619 100644 --- a/src/main/java/com/krylov/refound/service/UserService.java +++ b/src/main/java/com/krylov/refound/service/UserService.java @@ -1,25 +1,80 @@ package com.krylov.refound.service; +import com.krylov.refound.dto.UserResponseDto; +import com.krylov.refound.dto.UserUpdateDto; import com.krylov.refound.entity.User; import com.krylov.refound.enums.ErrorCode; import com.krylov.refound.exception.ApiException; import com.krylov.refound.repository.UserRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; 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.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; @Service +@Slf4j @RequiredArgsConstructor public class UserService { private final UserRepository repository; + private final AvatarService avatarService; public User getCurrentUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return repository.findByLogin("moscow_city231@rambler.ru") + if (authentication == null || authentication.getPrincipal() == null) { + throw new ApiException( + ErrorCode.UNAUTHORIZED, + "Пользователь не авторизован", + HttpStatus.UNAUTHORIZED + ); + } + + String login = (String) authentication.getPrincipal(); + + return repository.findByLogin(login) .orElseThrow(() -> new ApiException( ErrorCode.NOT_FOUND, "User not found", HttpStatus.NOT_FOUND )); } + + public UserResponseDto updateUser(Long id, UserUpdateDto dto) { + User user = repository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + + user.setName(dto.getFirstName()); + user.setLogin(dto.getLastName()); + user.setPhone(dto.getPhone()); + user.setEmail(dto.getEmail()); + user.setLogin(dto.getLogin()); + + MultipartFile avatar = dto.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + String url = avatarService.save(avatar); // сохранение на диск/в S3/Yandex Object Storage + user.setAvatarUrl(url); + } + + repository.save(user); + + UserResponseDto userResponseDto = getUserResponseDto(user); + log.info("User updated: {}", userResponseDto); + return userResponseDto; + } + + private static UserResponseDto getUserResponseDto(User user) { + return UserResponseDto.builder() + .id(user.getId()) + .login(user.getLogin()) + .name(user.getName()) + .phone(user.getPhone()) + .email(user.getEmail()) + .avatar(user.getAvatarUrl()) + .build(); + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 14183e3..dbc314c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -33,4 +33,8 @@ file: upload-dir: uploads/ cache: - ttl: 300 \ No newline at end of file + ttl: 300 + +jwt: + secret: mySuperSecretKeyForJwtTokenGenerationThatIsLongEnough2024!ReFound + expiration: 86400000 # 24 hours in milliseconds \ No newline at end of file diff --git a/src/main/resources/db/changelog/add/004-add-fields-user.yaml b/src/main/resources/db/changelog/add/004-add-fields-user.yaml new file mode 100644 index 0000000..bf12c11 --- /dev/null +++ b/src/main/resources/db/changelog/add/004-add-fields-user.yaml @@ -0,0 +1,10 @@ +databaseChangeLog: + - changeSet: + id: 004-add-lastName-phone + author: you + changes: + - addColumn: + tableName: users + columns: + - column: { name: last_name, type: VARCHAR(255) } + - column: { name: phone, type: VARCHAR(255) } \ No newline at end of file diff --git a/src/main/resources/db/changelog/add/005-add-user-avatarUrl.yaml b/src/main/resources/db/changelog/add/005-add-user-avatarUrl.yaml new file mode 100644 index 0000000..c347e19 --- /dev/null +++ b/src/main/resources/db/changelog/add/005-add-user-avatarUrl.yaml @@ -0,0 +1,9 @@ +databaseChangeLog: + - changeSet: + id: 005-add-user-avatarUrl + author: you + changes: + - addColumn: + tableName: users + columns: + - column: { name: avatar_url, type: VARCHAR(255) } \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 6bd179e..e70d24f 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -11,4 +11,6 @@ databaseChangeLog: - include: { file: db/changelog/alter/010-alter-user.yaml } - include: { file: db/changelog/add/011-add-name-user.yaml } - include: { file: db/changelog/add/012-add-email-role.yaml } - - include: { file: db/changelog/add/003-add-phone-posts.yaml } \ No newline at end of file + - include: { file: db/changelog/add/003-add-phone-posts.yaml } + - include: { file: db/changelog/add/004-add-fields-user.yaml } + - include: { file: db/changelog/add/005-add-user-avatarUrl.yaml } \ No newline at end of file