I accidentally pushed into that branch :(
Added JWT
This commit is contained in:
@@ -74,6 +74,39 @@
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-jpa</artifactId>
|
||||
</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>
|
||||
|
||||
<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;
|
||||
|
||||
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"));
|
||||
|
||||
@@ -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 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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<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;
|
||||
|
||||
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
|
||||
|
||||
@@ -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.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
|
||||
server.error.include-stacktrace=never
|
||||
|
||||
jwt.secret=f037435dc264ba72492096d3f3eb2f3da046f75ce45c85bc9bb487caa2bedb60
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user