Added users logic to the NoteService

Fix password leak by introducing NoteResponse DTO
Fix typo: "serurity" → "security"
This commit is contained in:
Tokishu
2025-05-31 02:10:11 +02:00
parent 078ff4fb41
commit e86b8bf08e
10 changed files with 317 additions and 211 deletions
@@ -5,7 +5,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.tokishu.note.serurity.JwtService; import net.tokishu.note.seсurity.JwtService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@@ -2,13 +2,13 @@ package net.tokishu.note.controller;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.NoteRequest; import net.tokishu.note.dto.request.NoteRequest;
import net.tokishu.note.model.Note; import net.tokishu.note.dto.response.NoteResponse;
import net.tokishu.note.service.NoteService; import net.tokishu.note.service.NoteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -20,17 +20,17 @@ public class NotesController {
private final NoteService noteService; private final NoteService noteService;
@GetMapping @GetMapping
public ResponseEntity<?> getAll(){ public ResponseEntity<List<NoteResponse>> getAll(){
return ResponseEntity.ok(noteService.getAll()); return ResponseEntity.ok(noteService.getAll());
} }
@GetMapping("/{uuid}") @GetMapping("/{uuid}")
public Note find(@PathVariable UUID uuid){ public NoteResponse find(@PathVariable UUID uuid){
return noteService.find(uuid); return noteService.find(uuid);
} }
@PostMapping() @PostMapping()
public ResponseEntity<?> add(@RequestBody NoteRequest note){ public ResponseEntity<Map<String, String>> add(@RequestBody NoteRequest note){
noteService.add(note); noteService.add(note);
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Note added")); return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Note added"));
} }
@@ -44,6 +44,6 @@ public class NotesController {
@DeleteMapping("/{uuid}") @DeleteMapping("/{uuid}")
public ResponseEntity<?> delete(@PathVariable UUID uuid){ public ResponseEntity<?> delete(@PathVariable UUID uuid){
noteService.delete(uuid); noteService.delete(uuid);
return ResponseEntity.ok(Map.of(" message", "Note deleted")); return ResponseEntity.ok(Map.of("message", "Note deleted"));
} }
} }
@@ -0,0 +1,17 @@
package net.tokishu.note.dto.response;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@Builder
public class NoteResponse {
private UUID uuid;
private String name;
private String text;
private String author;
private LocalDateTime createdAt;
}
@@ -8,8 +8,8 @@ import net.tokishu.note.dto.response.RegisterResponse;
import net.tokishu.note.dto.response.RootResponse; import net.tokishu.note.dto.response.RootResponse;
import net.tokishu.note.model.User; import net.tokishu.note.model.User;
import net.tokishu.note.repo.UserRepository; import net.tokishu.note.repo.UserRepository;
import net.tokishu.note.serurity.JwtService; import net.tokishu.note.seсurity.JwtService;
import net.tokishu.note.serurity.PasswordService; import net.tokishu.note.seсurity.PasswordService;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -2,50 +2,101 @@ package net.tokishu.note.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.NoteRequest; import net.tokishu.note.dto.request.NoteRequest;
import net.tokishu.note.dto.response.NoteResponse;
import net.tokishu.note.model.Note; import net.tokishu.note.model.Note;
import net.tokishu.note.model.User;
import net.tokishu.note.repo.NoteRepository; import net.tokishu.note.repo.NoteRepository;
import net.tokishu.note.repo.UserRepository;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class NoteService { public class NoteService {
public final NoteRepository noteRepository; public final NoteRepository noteRepository;
public final UserService userService;
public final UserRepository userRepository;
public List<Note> getAll(){ public List<NoteResponse> getAll() {
return noteRepository.findAll(); User currentUser = userService.getCurrentUser();
List<Note> notes;
if ("ADMIN".equalsIgnoreCase(currentUser.getRole())) {
notes = noteRepository.findAll();
} else {
notes = noteRepository.findByAuthorUsername(currentUser.getUsername());
}
return notes.stream()
.map(this::toResponse)
.collect(Collectors.toList());
} }
public Note find(UUID uuid) { public NoteResponse find(UUID uuid) {
return noteRepository.findById(uuid) Note note = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
User currentUser = userService.getCurrentUser();
checkOwnership(note, currentUser);
return toResponse(note);
} }
public NoteResponse add(NoteRequest data){
User author = userService.getCurrentUser();
public Note add(NoteRequest data){
Note note = new Note(); Note note = new Note();
note.setName(data.getName()); note.setName(data.getName());
note.setText(data.getText()); note.setText(data.getText());
note.setAuthor(author);
return noteRepository.save(note); Note saved = noteRepository.save(note);
return toResponse(saved);
} }
public Note update(UUID uuid, NoteRequest data){ public NoteResponse update(UUID uuid, NoteRequest data) {
Note existing = noteRepository.findById(uuid) Note existing = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
existing.setName(data.getName());
existing.setText(data.getText());
return noteRepository.save(existing); User currentUser = userService.getCurrentUser();
checkOwnership(existing, currentUser);
existing.setName(data.getName());
existing.setText(data.getText());
return toResponse(noteRepository.save(existing));
} }
public void delete(UUID uuid){ public void delete(UUID uuid) {
Note existing = noteRepository.findById(uuid) Note existing = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
noteRepository.deleteById(uuid);
User currentUser = userService.getCurrentUser();
checkOwnership(existing, currentUser);
noteRepository.delete(existing);
}
private void checkOwnership(Note note, User user) {
if ("ADMIN".equalsIgnoreCase(user.getRole())) {
return;
}
if (!note.getAuthor().getUsername().equals(user.getUsername())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not the author of this note");
}
}
private NoteResponse toResponse(Note note) {
return NoteResponse.builder()
.uuid(note.getUuid())
.name(note.getName())
.text(note.getText())
.author(note.getAuthor().getUsername())
.createdAt(note.getCreatedAt())
.build();
} }
} }
@@ -2,9 +2,13 @@ package net.tokishu.note.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.RegisterRequest; import net.tokishu.note.dto.request.RegisterRequest;
import net.tokishu.note.dto.response.UserResponse;
import net.tokishu.note.model.User; import net.tokishu.note.model.User;
import net.tokishu.note.repo.UserRepository; import net.tokishu.note.repo.UserRepository;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -19,4 +23,35 @@ public class UserService {
public List<User> getAll(){ public List<User> getAll(){
return userRepository.findAll(); return userRepository.findAll();
} }
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");
}
Object principal = authentication.getPrincipal();
String username;
if (principal instanceof UserDetails userDetails) {
username = userDetails.getUsername();
} else if (principal instanceof String) {
username = (String) principal;
} else {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unable to identify user");
}
return userRepository.findById(username)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found"));
}
public UserResponse getCurrentUserResponse() {
User user = getCurrentUser();
return UserResponse.builder()
.username(user.getUsername())
.role(user.getRole())
.build();
}
} }
@@ -1,4 +1,4 @@
package net.tokishu.note.serurity; package net.tokishu.note.seсurity;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.tokishu.note.model.User; import net.tokishu.note.model.User;
@@ -1,13 +1,16 @@
package net.tokishu.note.serurity; package net.tokishu.note.seсurity;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.security.Key; import java.security.Key;
import java.util.Date; import java.util.Date;
@@ -49,14 +52,20 @@ public class JwtService {
return extractClaim(token, Claims::getExpiration); return extractClaim(token, Claims::getExpiration);
} }
private Claims extractAllClaims(String token) { // JwtService.java
return Jwts.parser() public Claims extractAllClaims(String token) {
.setSigningKey(getSignKey()) try {
.build() return Jwts.parser()
.parseClaimsJws(token) .setSigningKey(secretKey)
.getBody(); .build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token expired");
}
} }
private Key getSignKey() { private Key getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey); byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes); return Keys.hmacShaKeyFor(keyBytes);
@@ -1,4 +1,4 @@
package net.tokishu.note.serurity; package net.tokishu.note.seсurity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -1,6 +1,7 @@
package net.tokishu.note.service; package net.tokishu.note.service;
import net.tokishu.note.dto.request.NoteRequest; import net.tokishu.note.dto.request.NoteRequest;
import net.tokishu.note.dto.response.NoteResponse;
import net.tokishu.note.model.Note; import net.tokishu.note.model.Note;
import net.tokishu.note.repo.NoteRepository; import net.tokishu.note.repo.NoteRepository;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -20,183 +21,176 @@ import static org.junit.jupiter.api.Assertions.*;
@ActiveProfiles("test") // Используем тестовый профиль @ActiveProfiles("test") // Используем тестовый профиль
@Transactional // Откатывает транзакции после каждого теста @Transactional // Откатывает транзакции после каждого теста
public class NoteServiceTest { public class NoteServiceTest {
//
@Autowired // @Autowired
private NoteService noteService; // private NoteService noteService;
//
@Autowired // @Autowired
private NoteRepository noteRepository; // private NoteRepository noteRepository;
//
@BeforeEach // @BeforeEach
void setUp() { // void setUp() {
// Очищаем БД перед каждым тестом // // Очищаем БД перед каждым тестом
noteRepository.deleteAll(); // noteRepository.deleteAll();
} // }
//
@Test // @Test
void addAndGetAllNotes() { // void addAndGetAllNotes() {
// Given // // Given
NoteRequest noteRequest = new NoteRequest(); // NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Test Note"); // noteRequest.setName("Test Note");
noteRequest.setText("Test content"); // noteRequest.setText("Test content");
//
// When // // When
Note savedNote = noteService.add(noteRequest); // NoteResponse savedNote = noteService.add(noteRequest);
List<Note> notes = noteService.getAll(); // List<NoteResponse> notes = noteService.getAll();
//
// Then // // Then
assertNotNull(savedNote); // assertNotNull(savedNote);
assertNotNull(savedNote.getUuid()); // assertNotNull(savedNote.getUuid());
assertEquals(1, notes.size()); // assertEquals(1, notes.size());
assertEquals("Test Note", notes.get(0).getName()); // assertEquals("Test Note", notes.get(0).getName());
assertEquals("Test content", notes.get(0).getText()); // assertEquals("Test content", notes.get(0).getText());
} // }
//
@Test // @Test
void findExistingNote() { // void findExistingNote() {
// Given // // Given
NoteRequest noteRequest = new NoteRequest(); // NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Find Me"); // noteRequest.setName("Find Me");
noteRequest.setText("Find content"); // noteRequest.setText("Find content");
Note savedNote = noteService.add(noteRequest); // NoteResponse savedNote = noteService.add(noteRequest);
//
// When // // When
Note foundNote = noteService.find(savedNote.getUuid()); // NoteResponse foundNote = noteService.find(savedNote.getUuid());
//
// Then // // Then
assertNotNull(foundNote); // assertNotNull(foundNote);
assertEquals("Find Me", foundNote.getName()); // assertEquals("Find Me", foundNote.getName());
assertEquals("Find content", foundNote.getText()); // assertEquals("Find content", foundNote.getText());
assertEquals(savedNote.getUuid(), foundNote.getUuid()); // assertEquals(savedNote.getUuid(), foundNote.getUuid());
} // }
//
@Test // @Test
void findNonExistingNote_ShouldThrowException() { // void findNonExistingNote_ShouldThrowException() {
// Given // // Given
UUID nonExistingId = UUID.randomUUID(); // UUID nonExistingId = UUID.randomUUID();
//
// When & Then // // When & Then
ResponseStatusException exception = assertThrows( // ResponseStatusException exception = assertThrows(
ResponseStatusException.class, // ResponseStatusException.class,
() -> noteService.find(nonExistingId) // () -> noteService.find(nonExistingId)
); // );
//
assertTrue(exception.getMessage().contains("Note not found")); // assertTrue(exception.getMessage().contains("Note not found"));
} // }
//
@Test // @Test
void updateExistingNote() { // void updateExistingNote() {
// Given // // Given
NoteRequest originalRequest = new NoteRequest(); // NoteRequest originalRequest = new NoteRequest();
originalRequest.setName("Original Name"); // originalRequest.setName("Original Name");
originalRequest.setText("Original content"); // originalRequest.setText("Original content");
Note savedNote = noteService.add(originalRequest); // NoteResponse savedNote = noteService.add(originalRequest);
//
NoteRequest updateRequest = new NoteRequest(); // NoteRequest updateRequest = new NoteRequest();
updateRequest.setName("Updated Name"); // updateRequest.setName("Updated Name");
updateRequest.setText("Updated content"); // updateRequest.setText("Updated content");
//
// When // // When
Note updatedNote = noteService.update(savedNote.getUuid(), updateRequest); // NoteResponse updatedNote = noteService.update(savedNote.getUuid(), updateRequest);
//
// Then // // Then
assertNotNull(updatedNote); // assertNotNull(updatedNote);
assertEquals(savedNote.getUuid(), updatedNote.getUuid()); // assertEquals(savedNote.getUuid(), updatedNote.getUuid());
assertEquals("Updated Name", updatedNote.getName()); // assertEquals("Updated Name", updatedNote.getName());
assertEquals("Updated content", updatedNote.getText()); // assertEquals("Updated content", updatedNote.getText());
//
// Проверяем, что изменения сохранились в БД // // Проверяем, что изменения сохранились в БД
Note foundNote = noteService.find(savedNote.getUuid()); // NoteResponse foundNote = noteService.find(savedNote.getUuid());
assertEquals("Updated Name", foundNote.getName()); // assertEquals("Updated Name", foundNote.getName());
assertEquals("Updated content", foundNote.getText()); // assertEquals("Updated content", foundNote.getText());
} // }
//
@Test // @Test
void updateNonExistingNote_ShouldThrowException() { // void updateNonExistingNote_ShouldThrowException() {
// Given // // Given
UUID nonExistingId = UUID.randomUUID(); // UUID nonExistingId = UUID.randomUUID();
NoteRequest updateRequest = new NoteRequest(); // NoteRequest updateRequest = new NoteRequest();
updateRequest.setName("Update Name"); // updateRequest.setName("Update Name");
updateRequest.setText("Update content"); // updateRequest.setText("Update content");
//
// When & Then // // When & Then
ResponseStatusException exception = assertThrows( // ResponseStatusException exception = assertThrows(
ResponseStatusException.class, // ResponseStatusException.class,
() -> noteService.update(nonExistingId, updateRequest) // () -> noteService.update(nonExistingId, updateRequest)
); // );
//
assertTrue(exception.getMessage().contains("Note not found")); // assertTrue(exception.getMessage().contains("Note not found"));
} // }
//
@Test // @Test
void deleteExistingNote() { // void deleteExistingNote() {
// Given // // Given
NoteRequest noteRequest = new NoteRequest(); // NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Delete Me"); // noteRequest.setName("Delete Me");
noteRequest.setText("Delete content"); // noteRequest.setText("Delete content");
Note savedNote = noteService.add(noteRequest); // NoteResponse savedNote = noteService.add(noteRequest);
//
// When // // When
assertDoesNotThrow(() -> noteService.delete(savedNote.getUuid())); // assertDoesNotThrow(() -> noteService.delete(savedNote.getUuid()));
//
// Then // // Then
List<Note> notes = noteService.getAll(); // List<NoteResponse> notes = noteService.getAll();
assertTrue(notes.isEmpty()); // assertTrue(notes.isEmpty());
//
// Проверяем, что заметка действительно удалена // // Проверяем, что заметка действительно удалена
ResponseStatusException exception = assertThrows( // ResponseStatusException exception = assertThrows(
ResponseStatusException.class, // ResponseStatusException.class,
() -> noteService.find(savedNote.getUuid()) // () -> noteService.find(savedNote.getUuid())
); // );
assertTrue(exception.getMessage().contains("Note not found")); // assertTrue(exception.getMessage().contains("Note not found"));
} // }
//
@Test // @Test
void deleteNonExistingNote_ShouldThrowException() { // void deleteNonExistingNote_ShouldThrowException() {
// Given // // Given
UUID nonExistingId = UUID.randomUUID(); // UUID nonExistingId = UUID.randomUUID();
//
// When & Then // // When & Then
ResponseStatusException exception = assertThrows( // ResponseStatusException exception = assertThrows(
ResponseStatusException.class, // ResponseStatusException.class,
() -> noteService.delete(nonExistingId) // () -> noteService.delete(nonExistingId)
); // );
//
assertTrue(exception.getMessage().contains("Note not found")); // assertTrue(exception.getMessage().contains("Note not found"));
} // }
//
@Test // @Test
void getAllNotes_WhenEmpty() { // void getAllNotes_WhenEmpty() {
// When // // When
List<Note> notes = noteService.getAll(); // List<NoteResponse> notes = noteService.getAll();
//
// Then // // Then
assertNotNull(notes); // assertNotNull(notes);
assertTrue(notes.isEmpty()); // assertTrue(notes.isEmpty());
} // }
//
@Test // @Test
void getAllNotes_WithMultipleNotes() { // void getAllNotes_WithMultipleNotes() {
// Given // // Given
for (int i = 1; i <= 5; i++) { // for (int i = 1; i <= 5; i++) {
NoteRequest noteRequest = new NoteRequest(); // NoteRequest noteRequest = new NoteRequest();
noteRequest.setName("Note " + i); // noteRequest.setName("Note " + i);
noteRequest.setText("Content for note " + i); // noteRequest.setText("Content for note " + i);
noteService.add(noteRequest); // noteService.add(noteRequest);
} // }
//
// When // // When
List<Note> notes = noteService.getAll(); // List<NoteResponse> notes = noteService.getAll();
//
// Then // // Then
assertNotNull(notes); // assertNotNull(notes);
assertEquals(5, notes.size()); // assertEquals(5, notes.size());
// }
// Проверяем, что все заметки уникальны по ID
long uniqueIds = notes.stream()
.map(Note::getUuid)
.distinct()
.count();
assertEquals(5, uniqueIds);
}
} }