Added JWT
This commit is contained in:
@@ -74,6 +74,39 @@
|
|||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-jpa</artifactId>
|
<artifactId>spring-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Security -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-core</artifactId>
|
||||||
|
<version>6.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RegisterResponse> register(@RequestBody @Valid RegisterRequest request) {
|
||||||
|
RegisterResponse response = authService.registerUser(request);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
|
||||||
|
LoginResponse response = authService.authorizeUser(request);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/new-root")
|
||||||
|
public ResponseEntity<RootResponse> newRoot() {
|
||||||
|
RootResponse response = authService.registerRoot();
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.tokishu.note.controller;
|
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.model.Note;
|
||||||
import net.tokishu.note.service.NoteService;
|
import net.tokishu.note.service.NoteService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -8,40 +9,39 @@ 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.Optional;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/notes")
|
||||||
public class NotesController {
|
public class NotesController {
|
||||||
|
|
||||||
@Autowired
|
private final NoteService noteService;
|
||||||
private NoteService noteService;
|
|
||||||
|
|
||||||
@GetMapping("/notes")
|
@GetMapping
|
||||||
public ResponseEntity<?> getAll(){
|
public ResponseEntity<?> getAll(){
|
||||||
return ResponseEntity.ok(noteService.getAll());
|
return ResponseEntity.ok(noteService.getAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/note/{uuid}")
|
@GetMapping("/{uuid}")
|
||||||
public Note find(@PathVariable UUID uuid){
|
public Note find(@PathVariable UUID uuid){
|
||||||
return noteService.find(uuid);
|
return noteService.find(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/note")
|
@PostMapping()
|
||||||
public ResponseEntity<?> add(@RequestBody NoteRequest note){
|
public ResponseEntity<?> 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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/note/{uuid}")
|
@PutMapping("/{uuid}")
|
||||||
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody NoteRequest note){
|
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody NoteRequest note){
|
||||||
noteService.update(uuid, note);
|
noteService.update(uuid, note);
|
||||||
return ResponseEntity.ok(Map.of("message", "Note updated"));
|
return ResponseEntity.ok(Map.of("message", "Note updated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/note/{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,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 {
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package net.tokishu.note.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class NoteRequest {
|
|
||||||
private String name;
|
|
||||||
private String text;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package net.tokishu.note.dto.request;
|
||||||
|
|
||||||
|
public class UserRequest {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<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) {
|
||||||
|
return ResponseEntity
|
||||||
|
.status(500)
|
||||||
|
.body(ApiErrorResponse.builder()
|
||||||
|
.status(500)
|
||||||
|
.message("Internal server error")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ package net.tokishu.note.model;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -15,4 +17,12 @@ public class Note {
|
|||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
private String name;
|
private String name;
|
||||||
private String text;
|
private String text;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "author", referencedColumnName = "username")
|
||||||
|
private User author;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
package net.tokishu.note.repo;
|
package net.tokishu.note.repo;
|
||||||
|
|
||||||
|
import net.tokishu.note.model.Note;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface NoteRepository extends JpaRepository<net.tokishu.note.model.Note, UUID> {
|
public interface NoteRepository extends JpaRepository<Note, UUID> {
|
||||||
|
List<Note> findByAuthorUsername(String username);
|
||||||
|
List<Note> findByAuthorUsernameOrderByCreatedAtDesc(String username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<User, String> {
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> T extractClaim(String token, Function<Claims, T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package net.tokishu.note.service;
|
package net.tokishu.note.service;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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.model.Note;
|
||||||
import net.tokishu.note.repo.NoteRepository;
|
import net.tokishu.note.repo.NoteRepository;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -9,7 +9,6 @@ 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.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|||||||
@@ -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<User> getAll(){
|
||||||
|
return userRepository.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
spring.application.name=notes
|
spring.application.name=notes
|
||||||
spring.mvc.servlet.path=/api
|
spring.mvc.servlet.path=/api
|
||||||
|
|
||||||
|
?Error
|
||||||
|
spring.mvc.throw-exception-if-no-handler-found=true
|
||||||
|
spring.web.resources.add-mappings=false
|
||||||
|
|
||||||
# DB
|
# DB
|
||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/notes
|
spring.datasource.url=jdbc:postgresql://localhost:5432/notes
|
||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
@@ -8,4 +12,6 @@ spring.datasource.password=password
|
|||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
|
||||||
# Disable trace
|
# Disable trace
|
||||||
server.error.include-stacktrace=never
|
server.error.include-stacktrace=never
|
||||||
|
|
||||||
|
jwt.secret=f037435dc264ba72492096d3f3eb2f3da046f75ce45c85bc9bb487caa2bedb60
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package net.tokishu.note.service;
|
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.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;
|
||||||
|
|||||||
Reference in New Issue
Block a user