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;