From 1e003c7a105a533fe16c22c854c349d21690a090 Mon Sep 17 00:00:00 2001 From: Tokishu Date: Tue, 27 May 2025 02:23:52 +0200 Subject: [PATCH] Added JWT --- pom.xml | 33 +++++++ .../note/config/JwtAuthenticationFilter.java | 65 +++++++++++++ .../tokishu/note/config/SecurityConfig.java | 46 +++++++++ .../note/controller/AuthController.java | 47 ++++++++++ .../note/controller/NotesController.java | 20 ++-- .../note/controller/UserController.java | 12 +++ .../net/tokishu/note/dto/NoteRequest.java | 9 -- .../note/dto/request/LoginRequest.java | 13 +++ .../tokishu/note/dto/request/NoteRequest.java | 16 ++++ .../note/dto/request/RegisterRequest.java | 16 ++++ .../tokishu/note/dto/request/UserRequest.java | 5 + .../note/dto/response/ApiErrorResponse.java | 11 +++ .../note/dto/response/LoginResponse.java | 12 +++ .../note/dto/response/RegisterResponse.java | 12 +++ .../note/dto/response/RootResponse.java | 12 +++ .../note/dto/response/UserResponse.java | 11 +++ .../exception/GlobalExceptionHandler.java | 59 ++++++++++++ .../exception/SecurityExceptionHandler.java | 41 ++++++++ .../java/net/tokishu/note/model/Note.java | 10 ++ .../java/net/tokishu/note/model/User.java | 25 +++++ .../net/tokishu/note/repo/NoteRepository.java | 6 +- .../net/tokishu/note/repo/UserRepository.java | 9 ++ .../serurity/CustomUserDetailsService.java | 28 ++++++ .../net/tokishu/note/serurity/JwtService.java | 64 +++++++++++++ .../note/serurity/PasswordService.java | 20 ++++ .../net/tokishu/note/service/AuthService.java | 93 +++++++++++++++++++ .../net/tokishu/note/service/NoteService.java | 3 +- .../net/tokishu/note/service/UserService.java | 22 +++++ src/main/resources/application.properties | 8 +- .../tokishu/note/service/NoteServiceTest.java | 2 +- 30 files changed, 706 insertions(+), 24 deletions(-) create mode 100644 src/main/java/net/tokishu/note/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/net/tokishu/note/config/SecurityConfig.java create mode 100644 src/main/java/net/tokishu/note/controller/AuthController.java create mode 100644 src/main/java/net/tokishu/note/controller/UserController.java delete mode 100644 src/main/java/net/tokishu/note/dto/NoteRequest.java create mode 100644 src/main/java/net/tokishu/note/dto/request/LoginRequest.java create mode 100644 src/main/java/net/tokishu/note/dto/request/NoteRequest.java create mode 100644 src/main/java/net/tokishu/note/dto/request/RegisterRequest.java create mode 100644 src/main/java/net/tokishu/note/dto/request/UserRequest.java create mode 100644 src/main/java/net/tokishu/note/dto/response/ApiErrorResponse.java create mode 100644 src/main/java/net/tokishu/note/dto/response/LoginResponse.java create mode 100644 src/main/java/net/tokishu/note/dto/response/RegisterResponse.java create mode 100644 src/main/java/net/tokishu/note/dto/response/RootResponse.java create mode 100644 src/main/java/net/tokishu/note/dto/response/UserResponse.java create mode 100644 src/main/java/net/tokishu/note/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/net/tokishu/note/exception/SecurityExceptionHandler.java create mode 100644 src/main/java/net/tokishu/note/model/User.java create mode 100644 src/main/java/net/tokishu/note/repo/UserRepository.java create mode 100644 src/main/java/net/tokishu/note/serurity/CustomUserDetailsService.java create mode 100644 src/main/java/net/tokishu/note/serurity/JwtService.java create mode 100644 src/main/java/net/tokishu/note/serurity/PasswordService.java create mode 100644 src/main/java/net/tokishu/note/service/AuthService.java create mode 100644 src/main/java/net/tokishu/note/service/UserService.java diff --git a/pom.xml b/pom.xml index 1a49bf4..9a7fd03 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,39 @@ org.springframework.data spring-data-jpa + + org.springframework.boot + spring-boot-starter-validation + + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-core + 6.5.0 + + + diff --git a/src/main/java/net/tokishu/note/config/JwtAuthenticationFilter.java b/src/main/java/net/tokishu/note/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..2d332d3 --- /dev/null +++ b/src/main/java/net/tokishu/note/config/JwtAuthenticationFilter.java @@ -0,0 +1,65 @@ +package net.tokishu.note.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import net.tokishu.note.serurity.JwtService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String username; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + username = jwtService.extractUsername(jwt); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if (jwtService.isTokenValid(jwt, 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/src/main/java/net/tokishu/note/config/SecurityConfig.java b/src/main/java/net/tokishu/note/config/SecurityConfig.java new file mode 100644 index 0000000..b97758c --- /dev/null +++ b/src/main/java/net/tokishu/note/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package net.tokishu.note.config; + +import lombok.RequiredArgsConstructor; +import net.tokishu.note.exception.SecurityExceptionHandler; +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.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; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@RequiredArgsConstructor +@Configuration +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthFilter; + private final SecurityExceptionHandler securityExceptionHandler; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(securityExceptionHandler) + .accessDeniedHandler(securityExceptionHandler) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers(new AntPathRequestMatcher("/api/auth/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/net/tokishu/note/controller/AuthController.java b/src/main/java/net/tokishu/note/controller/AuthController.java new file mode 100644 index 0000000..ecf7d1f --- /dev/null +++ b/src/main/java/net/tokishu/note/controller/AuthController.java @@ -0,0 +1,47 @@ +package net.tokishu.note.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import net.tokishu.note.dto.request.LoginRequest; +import net.tokishu.note.dto.request.RegisterRequest; +import net.tokishu.note.dto.response.LoginResponse; +import net.tokishu.note.dto.response.RegisterResponse; +import net.tokishu.note.dto.response.RootResponse; +import net.tokishu.note.model.User; +import net.tokishu.note.service.AuthService; +import net.tokishu.note.service.UserService; +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.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/auth") +public class AuthController { + + public final AuthService authService; + public final UserService userService; + + @PostMapping("/register") + public ResponseEntity register(@RequestBody @Valid RegisterRequest request) { + RegisterResponse response = authService.registerUser(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody @Valid LoginRequest request) { + LoginResponse response = authService.authorizeUser(request); + return ResponseEntity.ok(response); + } + + + @PostMapping("/new-root") + public ResponseEntity newRoot() { + RootResponse response = authService.registerRoot(); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/net/tokishu/note/controller/NotesController.java b/src/main/java/net/tokishu/note/controller/NotesController.java index d417e56..53a7c17 100644 --- a/src/main/java/net/tokishu/note/controller/NotesController.java +++ b/src/main/java/net/tokishu/note/controller/NotesController.java @@ -1,6 +1,7 @@ package net.tokishu.note.controller; -import net.tokishu.note.dto.NoteRequest; +import lombok.RequiredArgsConstructor; +import net.tokishu.note.dto.request.NoteRequest; import net.tokishu.note.model.Note; import net.tokishu.note.service.NoteService; import org.springframework.beans.factory.annotation.Autowired; @@ -8,40 +9,39 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.UUID; +@RequiredArgsConstructor @RestController +@RequestMapping("/notes") public class NotesController { - @Autowired - private NoteService noteService; + private final NoteService noteService; - @GetMapping("/notes") + @GetMapping public ResponseEntity getAll(){ return ResponseEntity.ok(noteService.getAll()); } - @GetMapping("/note/{uuid}") + @GetMapping("/{uuid}") public Note find(@PathVariable UUID uuid){ return noteService.find(uuid); } - @PostMapping("/note") + @PostMapping() public ResponseEntity add(@RequestBody NoteRequest note){ noteService.add(note); return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Note added")); } - @PutMapping("/note/{uuid}") + @PutMapping("/{uuid}") public ResponseEntity update(@PathVariable("uuid") UUID uuid, @RequestBody NoteRequest note){ noteService.update(uuid, note); return ResponseEntity.ok(Map.of("message", "Note updated")); } - @DeleteMapping("/note/{uuid}") + @DeleteMapping("/{uuid}") public ResponseEntity delete(@PathVariable UUID uuid){ noteService.delete(uuid); return ResponseEntity.ok(Map.of(" message", "Note deleted")); diff --git a/src/main/java/net/tokishu/note/controller/UserController.java b/src/main/java/net/tokishu/note/controller/UserController.java new file mode 100644 index 0000000..b3c8fdb --- /dev/null +++ b/src/main/java/net/tokishu/note/controller/UserController.java @@ -0,0 +1,12 @@ +package net.tokishu.note.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/users") +public class UserController { +} diff --git a/src/main/java/net/tokishu/note/dto/NoteRequest.java b/src/main/java/net/tokishu/note/dto/NoteRequest.java deleted file mode 100644 index 83a1cc8..0000000 --- a/src/main/java/net/tokishu/note/dto/NoteRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.tokishu.note.dto; - -import lombok.Data; - -@Data -public class NoteRequest { - private String name; - private String text; -} diff --git a/src/main/java/net/tokishu/note/dto/request/LoginRequest.java b/src/main/java/net/tokishu/note/dto/request/LoginRequest.java new file mode 100644 index 0000000..e3c6388 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/request/LoginRequest.java @@ -0,0 +1,13 @@ +package net.tokishu.note.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class LoginRequest { + @NotBlank + private String username; + + @NotBlank + private String password; +} diff --git a/src/main/java/net/tokishu/note/dto/request/NoteRequest.java b/src/main/java/net/tokishu/note/dto/request/NoteRequest.java new file mode 100644 index 0000000..a5d1a01 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/request/NoteRequest.java @@ -0,0 +1,16 @@ +package net.tokishu.note.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class NoteRequest { + @NotBlank + @Size(min=1, max=128) + private String name; + + @NotBlank + @Size(min=1, max=8192) + private String text; +} diff --git a/src/main/java/net/tokishu/note/dto/request/RegisterRequest.java b/src/main/java/net/tokishu/note/dto/request/RegisterRequest.java new file mode 100644 index 0000000..c2045b9 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/request/RegisterRequest.java @@ -0,0 +1,16 @@ +package net.tokishu.note.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class RegisterRequest { + @NotBlank + @Size(min=1, max=24) + private String username; + + @NotBlank + @Size(min=6, max=42) + private String password; +} diff --git a/src/main/java/net/tokishu/note/dto/request/UserRequest.java b/src/main/java/net/tokishu/note/dto/request/UserRequest.java new file mode 100644 index 0000000..5923b04 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/request/UserRequest.java @@ -0,0 +1,5 @@ +package net.tokishu.note.dto.request; + +public class UserRequest { + +} diff --git a/src/main/java/net/tokishu/note/dto/response/ApiErrorResponse.java b/src/main/java/net/tokishu/note/dto/response/ApiErrorResponse.java new file mode 100644 index 0000000..fa576d8 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/response/ApiErrorResponse.java @@ -0,0 +1,11 @@ +package net.tokishu.note.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ApiErrorResponse { + private final int status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/net/tokishu/note/dto/response/LoginResponse.java b/src/main/java/net/tokishu/note/dto/response/LoginResponse.java new file mode 100644 index 0000000..13633eb --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/response/LoginResponse.java @@ -0,0 +1,12 @@ +package net.tokishu.note.dto.response; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class LoginResponse { + private String token; + private String username; + private String role; +} diff --git a/src/main/java/net/tokishu/note/dto/response/RegisterResponse.java b/src/main/java/net/tokishu/note/dto/response/RegisterResponse.java new file mode 100644 index 0000000..7807ee5 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/response/RegisterResponse.java @@ -0,0 +1,12 @@ +package net.tokishu.note.dto.response; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class RegisterResponse { + private String username; + private String role; + private String message; +} diff --git a/src/main/java/net/tokishu/note/dto/response/RootResponse.java b/src/main/java/net/tokishu/note/dto/response/RootResponse.java new file mode 100644 index 0000000..4a71157 --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/response/RootResponse.java @@ -0,0 +1,12 @@ +package net.tokishu.note.dto.response; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class RootResponse { + private String username; + private String password; + private String role; +} diff --git a/src/main/java/net/tokishu/note/dto/response/UserResponse.java b/src/main/java/net/tokishu/note/dto/response/UserResponse.java new file mode 100644 index 0000000..6219f1f --- /dev/null +++ b/src/main/java/net/tokishu/note/dto/response/UserResponse.java @@ -0,0 +1,11 @@ +package net.tokishu.note.dto.response; + +import lombok.Data; +import lombok.Builder; + +@Data +@Builder +public class UserResponse { + private String username; + private String role; +} \ No newline at end of file diff --git a/src/main/java/net/tokishu/note/exception/GlobalExceptionHandler.java b/src/main/java/net/tokishu/note/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..71fd2b0 --- /dev/null +++ b/src/main/java/net/tokishu/note/exception/GlobalExceptionHandler.java @@ -0,0 +1,59 @@ +package net.tokishu.note.exception; + +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.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.NoHandlerFoundException; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity 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 handleBadRequest(HttpMessageNotReadableException ex) { + return ResponseEntity + .badRequest() + .body(ApiErrorResponse.builder() + .status(400) + .message("Invalid request body") + .build() + ); + } + + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity handleNotFound(NoHandlerFoundException ex) { + return ResponseEntity + .status(404) + .body(ApiErrorResponse.builder() + .status(404) + .message("Endpoint not found") + .build() + ); + } + + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneric(Exception ex) { + return ResponseEntity + .status(500) + .body(ApiErrorResponse.builder() + .status(500) + .message("Internal server error") + .build() + ); + } +} diff --git a/src/main/java/net/tokishu/note/exception/SecurityExceptionHandler.java b/src/main/java/net/tokishu/note/exception/SecurityExceptionHandler.java new file mode 100644 index 0000000..4712e70 --- /dev/null +++ b/src/main/java/net/tokishu/note/exception/SecurityExceptionHandler.java @@ -0,0 +1,41 @@ +package net.tokishu.note.exception; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.tokishu.note.dto.response.ApiErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class SecurityExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private void writeErrorResponse(HttpServletResponse response, int status, String message) throws IOException { + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + ApiErrorResponse body = ApiErrorResponse.builder() + .status(status) + .message(message) + .build(); + response.getWriter().write(objectMapper.writeValueAsString(body)); + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + org.springframework.security.core.AuthenticationException authException) throws IOException { + writeErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + writeErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); + } +} diff --git a/src/main/java/net/tokishu/note/model/Note.java b/src/main/java/net/tokishu/note/model/Note.java index 514ec75..dd80086 100644 --- a/src/main/java/net/tokishu/note/model/Note.java +++ b/src/main/java/net/tokishu/note/model/Note.java @@ -2,8 +2,10 @@ package net.tokishu.note.model; import jakarta.persistence.*; import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; import org.springframework.validation.annotation.Validated; +import java.time.LocalDateTime; import java.util.UUID; @Entity @@ -15,4 +17,12 @@ public class Note { private UUID uuid; private String name; private String text; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author", referencedColumnName = "username") + private User author; + + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; } \ No newline at end of file diff --git a/src/main/java/net/tokishu/note/model/User.java b/src/main/java/net/tokishu/note/model/User.java new file mode 100644 index 0000000..25be19d --- /dev/null +++ b/src/main/java/net/tokishu/note/model/User.java @@ -0,0 +1,25 @@ +package net.tokishu.note.model; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Data +@Table(name = "Users") +public class User { + @Id + private String username; + private String password; + + @ColumnDefault("'USER'") + private String role; + + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/src/main/java/net/tokishu/note/repo/NoteRepository.java b/src/main/java/net/tokishu/note/repo/NoteRepository.java index c13f9ab..cf7238a 100644 --- a/src/main/java/net/tokishu/note/repo/NoteRepository.java +++ b/src/main/java/net/tokishu/note/repo/NoteRepository.java @@ -1,8 +1,12 @@ package net.tokishu.note.repo; +import net.tokishu.note.model.Note; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.UUID; -public interface NoteRepository extends JpaRepository { +public interface NoteRepository extends JpaRepository { + List findByAuthorUsername(String username); + List findByAuthorUsernameOrderByCreatedAtDesc(String username); } diff --git a/src/main/java/net/tokishu/note/repo/UserRepository.java b/src/main/java/net/tokishu/note/repo/UserRepository.java new file mode 100644 index 0000000..a733c6f --- /dev/null +++ b/src/main/java/net/tokishu/note/repo/UserRepository.java @@ -0,0 +1,9 @@ +package net.tokishu.note.repo; + +import net.tokishu.note.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/net/tokishu/note/serurity/CustomUserDetailsService.java b/src/main/java/net/tokishu/note/serurity/CustomUserDetailsService.java new file mode 100644 index 0000000..112e46c --- /dev/null +++ b/src/main/java/net/tokishu/note/serurity/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package net.tokishu.note.serurity; + +import lombok.RequiredArgsConstructor; +import net.tokishu.note.model.User; +import net.tokishu.note.repo.UserRepository; +import org.springframework.security.core.userdetails.*; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) { + User user = userRepository.findById(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())) + ); + } +} diff --git a/src/main/java/net/tokishu/note/serurity/JwtService.java b/src/main/java/net/tokishu/note/serurity/JwtService.java new file mode 100644 index 0000000..f725810 --- /dev/null +++ b/src/main/java/net/tokishu/note/serurity/JwtService.java @@ -0,0 +1,64 @@ +package net.tokishu.note.serurity; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +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.Service; + +import java.security.Key; +import java.util.Date; +import java.util.function.Function; + +@Service +public class JwtService { + @Value("${jwt.secret}") + private String secretKey; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + public String generateToken(UserDetails userDetails) { + return Jwts.builder() + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24 hours + .signWith(getSignKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .setSigningKey(getSignKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Key getSignKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/net/tokishu/note/serurity/PasswordService.java b/src/main/java/net/tokishu/note/serurity/PasswordService.java new file mode 100644 index 0000000..be478a4 --- /dev/null +++ b/src/main/java/net/tokishu/note/serurity/PasswordService.java @@ -0,0 +1,20 @@ +package net.tokishu.note.serurity; + +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; + +@Service +public class PasswordService { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static final SecureRandom RANDOM = new SecureRandom(); + + public String generate(int length) { + StringBuilder password = new StringBuilder(); + for (int i = 0; i < length; i++) { + password.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + return password.toString(); + } +} diff --git a/src/main/java/net/tokishu/note/service/AuthService.java b/src/main/java/net/tokishu/note/service/AuthService.java new file mode 100644 index 0000000..4f9c417 --- /dev/null +++ b/src/main/java/net/tokishu/note/service/AuthService.java @@ -0,0 +1,93 @@ +package net.tokishu.note.service; + +import lombok.RequiredArgsConstructor; +import net.tokishu.note.dto.request.LoginRequest; +import net.tokishu.note.dto.request.RegisterRequest; +import net.tokishu.note.dto.response.LoginResponse; +import net.tokishu.note.dto.response.RegisterResponse; +import net.tokishu.note.dto.response.RootResponse; +import net.tokishu.note.model.User; +import net.tokishu.note.repo.UserRepository; +import net.tokishu.note.serurity.JwtService; +import net.tokishu.note.serurity.PasswordService; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +@RequiredArgsConstructor +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"); + }); + + String rawPassword = passwordService.generate(16); + String hashedPassword = passwordEncoder.encode(rawPassword); + + User rootUser = new User(); + rootUser.setUsername("root"); + rootUser.setPassword(hashedPassword); + rootUser.setRole("ADMIN"); + + userRepository.save(rootUser); + + return RootResponse.builder() + .username(rootUser.getUsername()) + .password(rawPassword) + .role(rootUser.getRole()) + .build(); + } + + + public RegisterResponse registerUser(RegisterRequest request) { + if (userRepository.existsById(request.getUsername())) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "User already exists"); + } + + User user = new User(); + user.setUsername(request.getUsername()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setRole("USER"); + + userRepository.save(user); + + return RegisterResponse.builder() + .username(user.getUsername()) + .role(user.getRole()) + .message("Registration successful") + .build(); + } + + public LoginResponse authorizeUser(LoginRequest request) { + User user = userRepository.findById(request.getUsername()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials"); + } + + String token = jwtService.generateToken( + org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) + .roles(user.getRole()) + .build() + ); + + return LoginResponse.builder() + .token(token) + .username(user.getUsername()) + .role(user.getRole()) + .build(); + } + +} diff --git a/src/main/java/net/tokishu/note/service/NoteService.java b/src/main/java/net/tokishu/note/service/NoteService.java index 5bff837..4c225af 100644 --- a/src/main/java/net/tokishu/note/service/NoteService.java +++ b/src/main/java/net/tokishu/note/service/NoteService.java @@ -1,7 +1,7 @@ package net.tokishu.note.service; import lombok.RequiredArgsConstructor; -import net.tokishu.note.dto.NoteRequest; +import net.tokishu.note.dto.request.NoteRequest; import net.tokishu.note.model.Note; import net.tokishu.note.repo.NoteRepository; import org.springframework.http.HttpStatus; @@ -9,7 +9,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import java.util.*; -import java.util.concurrent.atomic.AtomicLong; @Service @RequiredArgsConstructor diff --git a/src/main/java/net/tokishu/note/service/UserService.java b/src/main/java/net/tokishu/note/service/UserService.java new file mode 100644 index 0000000..d0cb3b8 --- /dev/null +++ b/src/main/java/net/tokishu/note/service/UserService.java @@ -0,0 +1,22 @@ +package net.tokishu.note.service; + +import lombok.RequiredArgsConstructor; +import net.tokishu.note.dto.request.RegisterRequest; +import net.tokishu.note.model.User; +import net.tokishu.note.repo.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + public final UserRepository userRepository; + + public List getAll(){ + return userRepository.findAll(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a539af7..9df386d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,10 @@ spring.application.name=notes spring.mvc.servlet.path=/api +?Error +spring.mvc.throw-exception-if-no-handler-found=true +spring.web.resources.add-mappings=false + # DB spring.datasource.url=jdbc:postgresql://localhost:5432/notes spring.datasource.username=postgres @@ -8,4 +12,6 @@ spring.datasource.password=password spring.jpa.hibernate.ddl-auto=update # Disable trace -server.error.include-stacktrace=never \ No newline at end of file +server.error.include-stacktrace=never + +jwt.secret=f037435dc264ba72492096d3f3eb2f3da046f75ce45c85bc9bb487caa2bedb60 \ No newline at end of file diff --git a/src/test/java/net/tokishu/note/service/NoteServiceTest.java b/src/test/java/net/tokishu/note/service/NoteServiceTest.java index bffaa87..00a6730 100644 --- a/src/test/java/net/tokishu/note/service/NoteServiceTest.java +++ b/src/test/java/net/tokishu/note/service/NoteServiceTest.java @@ -1,6 +1,6 @@ package net.tokishu.note.service; -import net.tokishu.note.dto.NoteRequest; +import net.tokishu.note.dto.request.NoteRequest; import net.tokishu.note.model.Note; import net.tokishu.note.repo.NoteRepository; import org.junit.jupiter.api.BeforeEach;