Just push
This commit is contained in:
@@ -106,6 +106,12 @@
|
||||
<version>6.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SecurityConfig {
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(new AntPathRequestMatcher("/api/auth/**")).permitAll()
|
||||
.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
|
||||
.requestMatchers(new AntPathRequestMatcher("/api/notes/*")).permitAll()
|
||||
.requestMatchers(new AntPathRequestMatcher("/error")).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
@@ -14,12 +14,14 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
@@ -38,7 +40,6 @@ public class AuthController {
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/new-root")
|
||||
public ResponseEntity<RootResponse> newRoot() {
|
||||
RootResponse response = authService.registerRoot();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package net.tokishu.note.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.tokishu.note.dto.request.NoteRequest;
|
||||
import net.tokishu.note.dto.response.NoteResponse;
|
||||
import net.tokishu.note.service.NoteService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -14,6 +16,7 @@ import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/notes")
|
||||
public class NotesController {
|
||||
|
||||
@@ -24,19 +27,19 @@ public class NotesController {
|
||||
return ResponseEntity.ok(noteService.getAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
public NoteResponse find(@PathVariable UUID uuid){
|
||||
return noteService.find(uuid);
|
||||
@GetMapping("/{idOrCode}")
|
||||
public NoteResponse find(@PathVariable String idOrCode) {
|
||||
return noteService.findByIdOrPublicLink(idOrCode);
|
||||
}
|
||||
|
||||
@PostMapping()
|
||||
public ResponseEntity<Map<String, String>> add(@RequestBody NoteRequest note){
|
||||
public ResponseEntity<Map<String, String>> add(@RequestBody @Valid NoteRequest note){
|
||||
noteService.add(note);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Note added"));
|
||||
}
|
||||
|
||||
@PutMapping("/{uuid}")
|
||||
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody NoteRequest note){
|
||||
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody @Valid NoteRequest note){
|
||||
noteService.update(uuid, note);
|
||||
return ResponseEntity.ok(Map.of("message", "Note updated"));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.tokishu.note.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -13,4 +14,7 @@ public class NoteRequest {
|
||||
@NotBlank
|
||||
@Size(min=1, max=8192)
|
||||
private String text;
|
||||
|
||||
@NotNull
|
||||
private Boolean isPublic;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public class NoteResponse {
|
||||
private UUID uuid;
|
||||
private String name;
|
||||
private String text;
|
||||
private Boolean isPublic;
|
||||
private String publicLink;
|
||||
private String author;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import net.tokishu.note.dto.response.ApiErrorResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
@@ -12,48 +13,58 @@ import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<ApiErrorResponse> handle(ResponseStatusException ex) {
|
||||
return ResponseEntity
|
||||
.status(ex.getStatusCode())
|
||||
.body(ApiErrorResponse.builder()
|
||||
.status(ex.getStatusCode().value())
|
||||
.message(ex.getReason() != null ? ex.getReason() : "Unexpected error")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<ApiErrorResponse> handleBadRequest(HttpMessageNotReadableException ex) {
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
.body(ApiErrorResponse.builder()
|
||||
.status(400)
|
||||
.message("Invalid request body")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public ResponseEntity<ApiErrorResponse> handleNotFound(NoHandlerFoundException ex) {
|
||||
return ResponseEntity
|
||||
.status(404)
|
||||
.body(ApiErrorResponse.builder()
|
||||
.status(404)
|
||||
.message("Endpoint not found")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiErrorResponse> handleGeneric(Exception ex) {
|
||||
public ResponseEntity<ApiErrorResponse> handleAll(Exception ex) {
|
||||
if (ex instanceof MethodArgumentNotValidException validationEx) {
|
||||
return handleValidation(validationEx);
|
||||
}
|
||||
|
||||
switch (ex.getClass().getSimpleName()) {
|
||||
case "ResponseStatusException" -> {
|
||||
ResponseStatusException rse = (ResponseStatusException) ex;
|
||||
return buildErrorResponse(rse.getStatusCode().value(), rse.getReason());
|
||||
}
|
||||
case "NoHandlerFoundException" -> {
|
||||
return buildErrorResponse(404, "Endpoint not found");
|
||||
}
|
||||
case "HttpMessageNotReadableException" -> {
|
||||
return buildErrorResponse(400, "Invalid request body");
|
||||
}
|
||||
case "HttpRequestMethodNotSupportedException" -> {
|
||||
return buildErrorResponse(405, "Method not allowed");
|
||||
}
|
||||
case "HttpMediaTypeNotSupportedException" -> {
|
||||
return buildErrorResponse(415, "Unsupported media type");
|
||||
}
|
||||
case "HttpMediaTypeNotAcceptableException" -> {
|
||||
return buildErrorResponse(406, "Not acceptable");
|
||||
}
|
||||
case "HttpRequestTimeoutException" -> {
|
||||
return buildErrorResponse(408, "Request timeout");
|
||||
}
|
||||
default -> {
|
||||
return buildErrorResponse(500, "Internal server error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||
.findFirst()
|
||||
.orElse("Invalid request");
|
||||
|
||||
return buildErrorResponse(400, message);
|
||||
}
|
||||
|
||||
|
||||
private ResponseEntity<ApiErrorResponse> buildErrorResponse(int status, String message) {
|
||||
return ResponseEntity
|
||||
.status(500)
|
||||
.status(status)
|
||||
.body(ApiErrorResponse.builder()
|
||||
.status(500)
|
||||
.message("Internal server error")
|
||||
.build()
|
||||
);
|
||||
.status(status)
|
||||
.message(message != null ? message : "Unexpected error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package net.tokishu.note.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@@ -15,13 +18,21 @@ public class Note {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID uuid;
|
||||
@Column(length = 128, nullable = false)
|
||||
private String name;
|
||||
@Column(length = 8192, nullable = false)
|
||||
private String text;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "author", referencedColumnName = "username")
|
||||
private User author;
|
||||
|
||||
@Column(length = 6, nullable = true, updatable = false)
|
||||
private String publicLink;
|
||||
|
||||
@ColumnDefault("false")
|
||||
private Boolean isPublic;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -4,9 +4,12 @@ import net.tokishu.note.model.Note;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface NoteRepository extends JpaRepository<Note, UUID> {
|
||||
List<Note> findByAuthorUsername(String username);
|
||||
List<Note> findByAuthorUsernameOrderByCreatedAtDesc(String username);
|
||||
Optional<Note> findByPublicLinkAndIsPublicTrue(String publicLink);
|
||||
boolean existsByPublicLink(String publicLink);
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, String> {
|
||||
boolean existsByRole(String role);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import net.tokishu.note.dto.response.RootResponse;
|
||||
import net.tokishu.note.model.User;
|
||||
import net.tokishu.note.repo.UserRepository;
|
||||
import net.tokishu.note.security.JwtService;
|
||||
import net.tokishu.note.security.PasswordService;
|
||||
import net.tokishu.note.util.PasswordGenerator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -20,17 +20,15 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
public class AuthService {
|
||||
|
||||
public final UserRepository userRepository;
|
||||
public final UserService userService;
|
||||
public final PasswordService passwordService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public RootResponse registerRoot() {
|
||||
userRepository.findById("root").ifPresent(user -> {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Root user already exists");
|
||||
});
|
||||
if (userRepository.existsByRole("ADMIN")) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Administrator already exists");
|
||||
}
|
||||
|
||||
String rawPassword = passwordService.generate(16);
|
||||
String rawPassword = PasswordGenerator.generate(16);
|
||||
String hashedPassword = passwordEncoder.encode(rawPassword);
|
||||
|
||||
User rootUser = new User();
|
||||
|
||||
@@ -6,9 +6,10 @@ import net.tokishu.note.dto.response.NoteResponse;
|
||||
import net.tokishu.note.model.Note;
|
||||
import net.tokishu.note.model.User;
|
||||
import net.tokishu.note.repo.NoteRepository;
|
||||
import net.tokishu.note.repo.UserRepository;
|
||||
import net.tokishu.note.util.CodeGenerator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.*;
|
||||
@@ -20,7 +21,6 @@ public class NoteService {
|
||||
|
||||
public final NoteRepository noteRepository;
|
||||
public final UserService userService;
|
||||
public final UserRepository userRepository;
|
||||
|
||||
public List<NoteResponse> getAll() {
|
||||
User currentUser = userService.getCurrentUser();
|
||||
@@ -37,15 +37,28 @@ public class NoteService {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public NoteResponse find(UUID uuid) {
|
||||
Note note = noteRepository.findById(uuid)
|
||||
public NoteResponse findByIdOrPublicLink(String idOrCode) {
|
||||
if (!StringUtils.hasText(idOrCode)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid identifier or public link");
|
||||
}
|
||||
|
||||
Note note;
|
||||
if (isUuid(idOrCode)) {
|
||||
UUID id = UUID.fromString(idOrCode);
|
||||
note = noteRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||
|
||||
User currentUser = userService.getCurrentUser();
|
||||
checkOwnership(note, currentUser);
|
||||
} else {
|
||||
note = noteRepository.findByPublicLinkAndIsPublicTrue(idOrCode)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||
}
|
||||
|
||||
return toResponse(note);
|
||||
}
|
||||
|
||||
|
||||
public NoteResponse add(NoteRequest data) {
|
||||
User author = userService.getCurrentUser();
|
||||
|
||||
@@ -54,10 +67,26 @@ public class NoteService {
|
||||
note.setText(data.getText());
|
||||
note.setAuthor(author);
|
||||
|
||||
String code;
|
||||
|
||||
int attempts = 0;
|
||||
do {
|
||||
code = CodeGenerator.generateCode();
|
||||
attempts++;
|
||||
if (attempts > 10) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate unique public link");
|
||||
}
|
||||
} while (noteRepository.existsByPublicLink(code));
|
||||
|
||||
note.setPublicLink(code);
|
||||
|
||||
note.setIsPublic(Boolean.TRUE.equals(data.getIsPublic()));
|
||||
|
||||
Note saved = noteRepository.save(note);
|
||||
return toResponse(saved);
|
||||
}
|
||||
|
||||
|
||||
public NoteResponse update(UUID uuid, NoteRequest data) {
|
||||
Note existing = noteRepository.findById(uuid)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||
@@ -67,6 +96,7 @@ public class NoteService {
|
||||
|
||||
existing.setName(data.getName());
|
||||
existing.setText(data.getText());
|
||||
existing.setIsPublic(Boolean.TRUE.equals(data.getIsPublic()));
|
||||
|
||||
return toResponse(noteRepository.save(existing));
|
||||
}
|
||||
@@ -81,7 +111,7 @@ public class NoteService {
|
||||
}
|
||||
|
||||
private void checkOwnership(Note note, User user) {
|
||||
if ("ADMIN".equalsIgnoreCase(user.getRole())) {
|
||||
if ("ADMIN".equalsIgnoreCase(user.getRole())) { // TODO: use enum
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,8 +125,19 @@ public class NoteService {
|
||||
.uuid(note.getUuid())
|
||||
.name(note.getName())
|
||||
.text(note.getText())
|
||||
.isPublic(note.getIsPublic())
|
||||
.publicLink(note.getPublicLink())
|
||||
.author(note.getAuthor().getUsername())
|
||||
.createdAt(note.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isUuid(String input) {
|
||||
try {
|
||||
UUID.fromString(input);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.tokishu.note.util;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
|
||||
@UtilityClass
|
||||
public class CodeGenerator {
|
||||
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
private static final int LENGTH = 6;
|
||||
private static final Random RANDOM = new SecureRandom();
|
||||
|
||||
public String generateCode() {
|
||||
StringBuilder sb = new StringBuilder(LENGTH);
|
||||
for (int i = 0; i < LENGTH; i++) {
|
||||
sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -1,13 +1,12 @@
|
||||
package net.tokishu.note.security;
|
||||
package net.tokishu.note.util;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
@Service
|
||||
public class PasswordService {
|
||||
|
||||
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@UtilityClass
|
||||
public class PasswordGenerator {
|
||||
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*!$%&@";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
public String generate(int length) {
|
||||
@@ -1,7 +1,7 @@
|
||||
spring.application.name=notes
|
||||
spring.mvc.servlet.path=/api
|
||||
|
||||
?Error
|
||||
# Error
|
||||
spring.mvc.throw-exception-if-no-handler-found=true
|
||||
spring.web.resources.add-mappings=false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user