Compare commits
13
Commits
f52f5f36a4
...
eea18d335b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eea18d335b | ||
|
|
42b2d42878 | ||
|
|
0aeafc4f56 | ||
|
|
aceeb32d69 | ||
|
|
ca10835732 | ||
|
|
e00ac3fd66 | ||
|
|
75d37a160a | ||
|
|
e86b8bf08e | ||
|
|
078ff4fb41 | ||
|
|
1e003c7a10 | ||
|
|
5ff716e515 | ||
|
|
7dae1d56bf | ||
|
|
4b108e1ae1 |
@@ -0,0 +1,14 @@
|
|||||||
|
# Spring Boot Note Api
|
||||||
|
|
||||||
|
> I'm testing spring boot, and it might become my main framework for building APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
486 days have passed since I started this project, well, lets rewrite it to DDD + clean and just finish that mini project.
|
||||||
|
I created an archive branch for old code.
|
||||||
|
|
||||||
|
but for alll that time i didnt learn how to make normal tests for my code, cuz i
|
||||||
|
learned spring boot and java core superfically
|
||||||
|
|
||||||
|
i think it will be better to make tests "in the progress", eeeh, for example: new feature, some tests for that, one more feature...
|
||||||
|
and so on.
|
||||||
@@ -74,6 +74,45 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.validator</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.tokishu.note.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface CurrentUser {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.tokishu.note.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface NullableCurrentUser {
|
||||||
|
}
|
||||||
@@ -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.security.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("/api/notes/*")).permitAll()
|
||||||
|
.requestMatchers(new AntPathRequestMatcher("/error")).permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net.tokishu.note.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.tokishu.note.resolver.CurrentUserArgumentResolver;
|
||||||
|
import net.tokishu.note.resolver.NullableCurrentUserArgumentResolver;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final CurrentUserArgumentResolver currentUserArgumentResolver;
|
||||||
|
private final NullableCurrentUserArgumentResolver nullableCurrentUserArgumentResolver;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||||
|
resolvers.add(currentUserArgumentResolver);
|
||||||
|
resolvers.add(nullableCurrentUserArgumentResolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Validated
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
public final AuthService authService;
|
||||||
|
|
||||||
|
@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,49 +1,55 @@
|
|||||||
package net.tokishu.note.controller;
|
package net.tokishu.note.controller;
|
||||||
|
|
||||||
import net.tokishu.note.dto.NoteRequest;
|
import jakarta.validation.Valid;
|
||||||
import net.tokishu.note.model.Note;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.tokishu.note.annotation.CurrentUser;
|
||||||
|
import net.tokishu.note.annotation.NullableCurrentUser;
|
||||||
|
import net.tokishu.note.dto.request.NoteRequest;
|
||||||
|
import net.tokishu.note.dto.response.NoteResponse;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
import net.tokishu.note.service.NoteService;
|
import net.tokishu.note.service.NoteService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
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
|
||||||
|
@Validated
|
||||||
|
@RequestMapping("/notes")
|
||||||
public class NotesController {
|
public class NotesController {
|
||||||
|
|
||||||
@Autowired
|
private final NoteService noteService;
|
||||||
private NoteService noteService;
|
|
||||||
|
|
||||||
@GetMapping("/notes")
|
@GetMapping
|
||||||
public ResponseEntity<?> getAll(){
|
public ResponseEntity<List<NoteResponse>> getAll(@CurrentUser User sender){
|
||||||
return ResponseEntity.ok(noteService.getAll());
|
return ResponseEntity.ok(noteService.getAll(sender));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/note/{uuid}")
|
@GetMapping("/{idOrCode}")
|
||||||
public Note find(@PathVariable UUID uuid){
|
public NoteResponse find(@PathVariable String idOrCode, @NullableCurrentUser User sender) {
|
||||||
return noteService.find(uuid);
|
return noteService.findByIdOrPublicLink(idOrCode, sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/note")
|
@PostMapping()
|
||||||
public ResponseEntity<?> add(@RequestBody NoteRequest note){
|
public ResponseEntity<Map<String, String>> add(@RequestBody @Valid NoteRequest note, @CurrentUser User sender){
|
||||||
noteService.add(note);
|
noteService.add(note, sender);
|
||||||
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 @Valid NoteRequest note, @CurrentUser User sender){
|
||||||
noteService.update(uuid, note);
|
noteService.update(uuid, note, sender);
|
||||||
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, @CurrentUser User sender){
|
||||||
noteService.delete(uuid);
|
noteService.delete(uuid, sender);
|
||||||
return ResponseEntity.ok(Map.of(" message", "Note deleted"));
|
return ResponseEntity.ok(Map.of("message", "Note deleted"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package net.tokishu.note.controller;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.tokishu.note.annotation.CurrentUser;
|
||||||
|
import net.tokishu.note.dto.request.ChangePasswordRequest;
|
||||||
|
import net.tokishu.note.dto.request.UpdateProfileRequest;
|
||||||
|
import net.tokishu.note.dto.request.UserByAdminRequest;
|
||||||
|
import net.tokishu.note.dto.response.ApiResponse;
|
||||||
|
import net.tokishu.note.dto.response.UserResponse;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.service.UserService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/users")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
public UserResponse me(@CurrentUser User sender){
|
||||||
|
return userService.getCurrentUserResponse(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me")
|
||||||
|
public UserResponse updateMyProfile(@RequestBody @Valid UpdateProfileRequest request, @CurrentUser User sender) {
|
||||||
|
return userService.updateCurrentUser(request, sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me/password")
|
||||||
|
public ApiResponse changePassword(@RequestBody @Valid ChangePasswordRequest request, @CurrentUser User sender) {
|
||||||
|
// userService.changePassword(request);
|
||||||
|
return ApiResponse.builder().status(200).message("JoJ").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
ADMIN ACTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @PostMapping("/new")
|
||||||
|
// @PreAuthorize("hasAuthority('ADMIN')")
|
||||||
|
// public ResponseEntity<?> newUser(@RequestBody @Valid UserByAdminRequest request, @CurrentUser User sender){
|
||||||
|
// return userService.createUserByAdmin(request);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @GetMapping
|
||||||
|
// @PreAuthorize("hasAuthority('ADMIN')")
|
||||||
|
// public ResponseEntity<List<UserResponse>> getAllUsers(@CurrentUser User sender) {
|
||||||
|
// return userService.getAllUsers();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @GetMapping("/{username}")
|
||||||
|
// @PreAuthorize("hasAuthority('ADMIN')")
|
||||||
|
// public UserResponse getUserByUsername(@PathVariable String username, @CurrentUser User sender) {
|
||||||
|
// return userService.getUserByUsername(username);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// @PutMapping("/{username}")
|
||||||
|
// @PreAuthorize("hasAuthority('ADMIN')")
|
||||||
|
// public UserResponse editUser(@RequestBody @Valid UserByAdminRequest request, @PathVariable String username, @CurrentUser User sender){
|
||||||
|
// return userService.editUser(username, request);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @DeleteMapping("/{username}")
|
||||||
|
// @PreAuthorize("hasAuthority('ADMIN')")
|
||||||
|
// public ApiResponse removeUser(@PathVariable String username, @CurrentUser User sender){
|
||||||
|
// return userService.deleteUser(username);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -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 ChangePasswordRequest {
|
||||||
|
@NotBlank
|
||||||
|
private String oldPassword;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String newPassword;
|
||||||
|
}
|
||||||
@@ -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,20 @@
|
|||||||
|
package net.tokishu.note.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Boolean isPublic;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.tokishu.note.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RegisterRequest {
|
||||||
|
@NotBlank()
|
||||||
|
@Size(min = 3, max = 24)
|
||||||
|
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
|
||||||
|
message = "The username can only contain letters, numbers, symbols and hyphens.")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank()
|
||||||
|
@Size(min = 6, max = 42)
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package net.tokishu.note.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UpdateProfileRequest {
|
||||||
|
@NotBlank()
|
||||||
|
@Size(min = 3, max = 24)
|
||||||
|
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
|
||||||
|
message = "The username can only contain letters, numbers, symbols and hyphens.")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank()
|
||||||
|
private String gravatarEmail;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package net.tokishu.note.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class UserByAdminRequest {
|
||||||
|
@NotBlank()
|
||||||
|
@Size(min = 3, max = 24)
|
||||||
|
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
|
||||||
|
message = "The username can only contain letters, numbers, symbols and hyphens.")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@NotBlank()
|
||||||
|
@Size(min = 6, max = 42)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private UserRole role;
|
||||||
|
}
|
||||||
@@ -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 ApiResponse {
|
||||||
|
private final int status;
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.tokishu.note.dto.response;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class LoginResponse {
|
||||||
|
private String token;
|
||||||
|
private String username;
|
||||||
|
private UserRole role;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.tokishu.note.dto.response;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class NoteResponse {
|
||||||
|
private UUID uuid;
|
||||||
|
private String name;
|
||||||
|
private String text;
|
||||||
|
private Boolean isPublic;
|
||||||
|
private String publicLink;
|
||||||
|
private String author;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.tokishu.note.dto.response;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class RegisterResponse {
|
||||||
|
private String username;
|
||||||
|
private UserRole role;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.tokishu.note.dto.response;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class RootResponse {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private UserRole role;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.tokishu.note.dto.response;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Builder;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class UserResponse {
|
||||||
|
private String username;
|
||||||
|
private String gravatarUrl;
|
||||||
|
private UserRole role;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package net.tokishu.note.exception;
|
||||||
|
|
||||||
|
import net.tokishu.note.dto.response.ApiResponse;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse> handleAll(Exception ex) {
|
||||||
|
if (ex instanceof MethodArgumentNotValidException validationEx) {
|
||||||
|
return handleValidation(validationEx);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ex.getClass().getSimpleName()) {
|
||||||
|
case "ResponseStatusException" -> {
|
||||||
|
ResponseStatusException rse = (ResponseStatusException) ex;
|
||||||
|
return buildErrorResponse(rse.getStatusCode().value(), rse.getReason());
|
||||||
|
}
|
||||||
|
case "NoHandlerFoundException" -> {
|
||||||
|
return buildErrorResponse(404, "Endpoint not found");
|
||||||
|
}
|
||||||
|
case "HttpMessageNotReadableException" -> {
|
||||||
|
return buildErrorResponse(400, "Invalid request body");
|
||||||
|
}
|
||||||
|
case "HttpRequestMethodNotSupportedException" -> {
|
||||||
|
return buildErrorResponse(405, "Method not allowed");
|
||||||
|
}
|
||||||
|
case "HttpMediaTypeNotSupportedException" -> {
|
||||||
|
return buildErrorResponse(415, "Unsupported media type");
|
||||||
|
}
|
||||||
|
case "HttpMediaTypeNotAcceptableException" -> {
|
||||||
|
return buildErrorResponse(406, "Not acceptable");
|
||||||
|
}
|
||||||
|
case "HttpRequestTimeoutException" -> {
|
||||||
|
return buildErrorResponse(408, "Request timeout");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return buildErrorResponse(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ApiResponse> handleValidation(MethodArgumentNotValidException ex) {
|
||||||
|
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||||
|
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||||
|
.findFirst()
|
||||||
|
.orElse("Invalid request");
|
||||||
|
|
||||||
|
return buildErrorResponse(400, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ResponseEntity<ApiResponse> buildErrorResponse(int status, String message) {
|
||||||
|
return ResponseEntity
|
||||||
|
.status(status)
|
||||||
|
.body(ApiResponse.builder()
|
||||||
|
.status(status)
|
||||||
|
.message(message != null ? message : "Unexpected 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.ApiResponse;
|
||||||
|
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);
|
||||||
|
ApiResponse body = ApiResponse.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
package net.tokishu.note.model;
|
package net.tokishu.note.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Null;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.hibernate.annotations.ColumnDefault;
|
||||||
|
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
|
||||||
@@ -13,6 +18,22 @@ public class Note {
|
|||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.UUID)
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
|
@Column(length = 128, nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
|
@Column(length = 8192, nullable = false)
|
||||||
private String text;
|
private String text;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "author", referencedColumnName = "username")
|
||||||
|
private User author;
|
||||||
|
|
||||||
|
@Column(length = 6, nullable = true, updatable = false)
|
||||||
|
private String publicLink;
|
||||||
|
|
||||||
|
@ColumnDefault("false")
|
||||||
|
private Boolean isPublic;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.tokishu.note.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
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
|
||||||
|
@Column(length = 24)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@ColumnDefault("'USER'")
|
||||||
|
private UserRole role;
|
||||||
|
|
||||||
|
@Email
|
||||||
|
@Column(nullable = true)
|
||||||
|
private String gravatarEmail;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package net.tokishu.note.model;
|
||||||
|
|
||||||
|
public enum UserRole {
|
||||||
|
ADMIN, USER
|
||||||
|
}
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
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 org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
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);
|
||||||
|
Optional<Note> findByPublicLinkAndIsPublicTrue(String publicLink);
|
||||||
|
boolean existsByPublicLink(String publicLink);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE Note n SET n.author = :newUsername WHERE n.author = :oldUsername")
|
||||||
|
void updateOwnerUsername(@Param("oldUsername") String oldUsername, @Param("newUsername") String newUsername);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.tokishu.note.repo;
|
||||||
|
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, String> {
|
||||||
|
boolean existsByRole(UserRole role);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE User u SET u.username = :newUsername WHERE u.username = :oldUsername")
|
||||||
|
void updateUsername(@Param("oldUsername") String oldUsername, @Param("newUsername") String newUsername);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.tokishu.note.resolver;
|
||||||
|
|
||||||
|
import net.tokishu.note.annotation.CurrentUser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.service.AuthService;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||||
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsParameter(MethodParameter parameter) {
|
||||||
|
return parameter.hasParameterAnnotation(CurrentUser.class)
|
||||||
|
&& parameter.getParameterType().equals(User.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object resolveArgument(MethodParameter parameter,
|
||||||
|
ModelAndViewContainer mavContainer,
|
||||||
|
NativeWebRequest webRequest,
|
||||||
|
WebDataBinderFactory binderFactory) {
|
||||||
|
return authService.getCurrentUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.tokishu.note.resolver;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.tokishu.note.annotation.NullableCurrentUser;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.service.AuthService;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||||
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NullableCurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsParameter(MethodParameter parameter) {
|
||||||
|
return parameter.hasParameterAnnotation(NullableCurrentUser.class)
|
||||||
|
&& parameter.getParameterType().equals(User.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object resolveArgument(MethodParameter parameter,
|
||||||
|
ModelAndViewContainer mavContainer,
|
||||||
|
NativeWebRequest webRequest,
|
||||||
|
WebDataBinderFactory binderFactory) {
|
||||||
|
return authService.getCurrentUserOrNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package net.tokishu.note.security;
|
||||||
|
|
||||||
|
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,73 @@
|
|||||||
|
package net.tokishu.note.security;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
|
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.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JwtService.java
|
||||||
|
public Claims extractAllClaims(String token) {
|
||||||
|
try {
|
||||||
|
return Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Key getSignKey() {
|
||||||
|
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
|
||||||
|
return Keys.hmacShaKeyFor(keyBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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.dto.response.UserResponse;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
import net.tokishu.note.repo.UserRepository;
|
||||||
|
import net.tokishu.note.security.JwtService;
|
||||||
|
import net.tokishu.note.util.GravatarUtil;
|
||||||
|
import net.tokishu.note.util.PasswordGenerator;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JwtService jwtService;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public RootResponse registerRoot() {
|
||||||
|
String rawPassword = PasswordGenerator.generate(16);
|
||||||
|
User rootUser = userService.createUser("root", rawPassword, UserRole.ADMIN, null);
|
||||||
|
|
||||||
|
return RootResponse.builder()
|
||||||
|
.username(rootUser.getUsername())
|
||||||
|
.password(rawPassword)
|
||||||
|
.role(rootUser.getRole())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegisterResponse registerUser(RegisterRequest request) {
|
||||||
|
User user = userService.createRegularUser(request.getUsername(), request.getPassword());
|
||||||
|
|
||||||
|
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().name())
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
return LoginResponse.builder()
|
||||||
|
.token(token)
|
||||||
|
.username(user.getUsername())
|
||||||
|
.role(user.getRole())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getCurrentUser() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails userDetails) {
|
||||||
|
username = userDetails.getUsername();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
username = (String) principal;
|
||||||
|
} else {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unable to identify user");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRepository.findById(username)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getCurrentUserOrNull() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails userDetails) {
|
||||||
|
username = userDetails.getUsername();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
username = (String) principal;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRepository.findById(username).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,137 @@
|
|||||||
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.dto.response.NoteResponse;
|
||||||
import net.tokishu.note.model.Note;
|
import net.tokishu.note.model.Note;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
import net.tokishu.note.repo.NoteRepository;
|
import net.tokishu.note.repo.NoteRepository;
|
||||||
|
import net.tokishu.note.util.CheckAuthUtil;
|
||||||
|
import net.tokishu.note.util.CodeGenerator;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class NoteService {
|
public class NoteService {
|
||||||
|
|
||||||
public final NoteRepository noteRepository;
|
public final NoteRepository noteRepository;
|
||||||
|
public final UserService userService;
|
||||||
|
|
||||||
public List<Note> getAll(){
|
public List<NoteResponse> getAll(User currentUser) {
|
||||||
return noteRepository.findAll();
|
List<Note> notes;
|
||||||
|
|
||||||
|
if (currentUser.getRole() == UserRole.ADMIN) {
|
||||||
|
notes = noteRepository.findAll();
|
||||||
|
} else {
|
||||||
|
notes = noteRepository.findByAuthorUsername(currentUser.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes.stream()
|
||||||
|
.map(this::toResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Note find(UUID uuid) {
|
public NoteResponse findByIdOrPublicLink(String idOrCode, User currentUser) {
|
||||||
return noteRepository.findById(uuid)
|
if (!StringUtils.hasText(idOrCode)) {
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid identifier or public link");
|
||||||
|
}
|
||||||
|
|
||||||
|
Note note;
|
||||||
|
if (isUuid(idOrCode)) {
|
||||||
|
CheckAuthUtil.check(currentUser);
|
||||||
|
UUID id = UUID.fromString(idOrCode);
|
||||||
|
note = noteRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||||
|
checkOwnership(note, currentUser);
|
||||||
|
} else {
|
||||||
|
note = noteRepository.findByPublicLinkAndIsPublicTrue(idOrCode)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResponse(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NoteResponse add(NoteRequest data, User author) {
|
||||||
public Note add(NoteRequest data){
|
|
||||||
Note note = new Note();
|
Note note = new Note();
|
||||||
note.setName(data.getName());
|
note.setName(data.getName());
|
||||||
note.setText(data.getText());
|
note.setText(data.getText());
|
||||||
|
note.setAuthor(author);
|
||||||
|
|
||||||
return noteRepository.save(note);
|
String code;
|
||||||
|
|
||||||
|
int attempts = 0;
|
||||||
|
do {
|
||||||
|
code = CodeGenerator.generateCode();
|
||||||
|
attempts++;
|
||||||
|
if (attempts > 10) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate unique public link");
|
||||||
|
}
|
||||||
|
} while (noteRepository.existsByPublicLink(code));
|
||||||
|
|
||||||
|
note.setPublicLink(code);
|
||||||
|
|
||||||
|
note.setIsPublic(Boolean.TRUE.equals(data.getIsPublic()));
|
||||||
|
|
||||||
|
Note saved = noteRepository.save(note);
|
||||||
|
return toResponse(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Note update(UUID uuid, NoteRequest data){
|
public NoteResponse update(UUID uuid, NoteRequest data, User currentUser) {
|
||||||
Note existing = noteRepository.findById(uuid)
|
Note existing = noteRepository.findById(uuid)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||||
existing.setName(data.getName());
|
|
||||||
existing.setText(data.getText());
|
|
||||||
|
|
||||||
return noteRepository.save(existing);
|
checkOwnership(existing, currentUser);
|
||||||
|
|
||||||
|
existing.setName(data.getName());
|
||||||
|
existing.setText(data.getText());
|
||||||
|
existing.setIsPublic(Boolean.TRUE.equals(data.getIsPublic()));
|
||||||
|
|
||||||
|
return toResponse(noteRepository.save(existing));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(UUID uuid){
|
public void delete(UUID uuid, User currentUser) {
|
||||||
Note existing = noteRepository.findById(uuid)
|
Note existing = noteRepository.findById(uuid)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
|
||||||
noteRepository.deleteById(uuid);
|
|
||||||
|
checkOwnership(existing, currentUser);
|
||||||
|
noteRepository.delete(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkOwnership(Note note, User user) {
|
||||||
|
if (user.getRole() == UserRole.ADMIN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!note.getAuthor().getUsername().equals(user.getUsername())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not the author of this note");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoteResponse toResponse(Note note) {
|
||||||
|
return NoteResponse.builder()
|
||||||
|
.uuid(note.getUuid())
|
||||||
|
.name(note.getName())
|
||||||
|
.text(note.getText())
|
||||||
|
.isPublic(note.getIsPublic())
|
||||||
|
.publicLink(note.getPublicLink())
|
||||||
|
.author(note.getAuthor().getUsername())
|
||||||
|
.createdAt(note.getCreatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUuid(String input) {
|
||||||
|
try {
|
||||||
|
UUID.fromString(input);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package net.tokishu.note.service;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.tokishu.note.dto.request.UpdateProfileRequest;
|
||||||
|
import net.tokishu.note.dto.response.UserResponse;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import net.tokishu.note.model.UserRole;
|
||||||
|
import net.tokishu.note.repo.NoteRepository;
|
||||||
|
import net.tokishu.note.repo.UserRepository;
|
||||||
|
import net.tokishu.note.util.CheckAuthUtil;
|
||||||
|
import net.tokishu.note.util.GravatarUtil;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final NoteRepository noteRepository;
|
||||||
|
|
||||||
|
public UserResponse getCurrentUserResponse(User user) {
|
||||||
|
return UserResponse.builder()
|
||||||
|
.username(user.getUsername())
|
||||||
|
.role(user.getRole())
|
||||||
|
.gravatarUrl(GravatarUtil.generateGravatarUrl(user.getGravatarEmail()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User createUser(String username, String password, UserRole role, User currentUser) {
|
||||||
|
if (userRepository.existsById(username)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "User already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean adminExists = userRepository.existsByRole(UserRole.ADMIN);
|
||||||
|
boolean isCreatingAdmin = UserRole.ADMIN.equals(role);
|
||||||
|
|
||||||
|
if (isCreatingAdmin && adminExists) {
|
||||||
|
if (currentUser == null || currentUser.getRole() != UserRole.ADMIN) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Only admin can create another admin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
user.setRole(role);
|
||||||
|
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User createRegularUser(String username, String password) {
|
||||||
|
return createUser(username, password, UserRole.USER, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse updateCurrentUser(UpdateProfileRequest request, User currentUser) {
|
||||||
|
String oldUsername = currentUser.getUsername();
|
||||||
|
String newUsername = request.getUsername();
|
||||||
|
|
||||||
|
if (!oldUsername.equals(newUsername)) {
|
||||||
|
if (userRepository.existsById(newUsername)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Username is already taken");
|
||||||
|
}
|
||||||
|
|
||||||
|
userRepository.updateUsername(oldUsername, newUsername);
|
||||||
|
noteRepository.updateOwnerUsername(oldUsername, newUsername);
|
||||||
|
|
||||||
|
currentUser.setUsername(newUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser.setGravatarEmail(request.getGravatarEmail());
|
||||||
|
User saved = userRepository.save(currentUser);
|
||||||
|
|
||||||
|
return UserResponse.builder()
|
||||||
|
.username(saved.getUsername())
|
||||||
|
.role(currentUser.getRole())
|
||||||
|
.gravatarUrl(GravatarUtil.generateGravatarUrl(saved.getGravatarEmail()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package net.tokishu.note.util;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import net.tokishu.note.model.User;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class CheckAuthUtil {
|
||||||
|
public void check(User user) {
|
||||||
|
if (user == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not authenticated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package net.tokishu.note.util;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class CodeGenerator {
|
||||||
|
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
private static final int LENGTH = 6;
|
||||||
|
private static final Random RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
public String generateCode() {
|
||||||
|
StringBuilder sb = new StringBuilder(LENGTH);
|
||||||
|
for (int i = 0; i < LENGTH; i++) {
|
||||||
|
sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package net.tokishu.note.util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class GravatarUtil {
|
||||||
|
|
||||||
|
public static String generateGravatarUrl(String email) {
|
||||||
|
if (email == null || email.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(email.trim().toLowerCase().getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (byte b : digest) {
|
||||||
|
sb.append(String.format("%02x", b & 0xff));
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://www.gravatar.com/avatar/" + sb.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException("MD5 not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.tokishu.note.util;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class PasswordGenerator {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -9,3 +13,5 @@ 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,7 @@
|
|||||||
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.dto.response.NoteResponse;
|
||||||
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;
|
||||||
@@ -20,183 +21,176 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
@ActiveProfiles("test") // Используем тестовый профиль
|
@ActiveProfiles("test") // Используем тестовый профиль
|
||||||
@Transactional // Откатывает транзакции после каждого теста
|
@Transactional // Откатывает транзакции после каждого теста
|
||||||
public class NoteServiceTest {
|
public class NoteServiceTest {
|
||||||
|
//
|
||||||
@Autowired
|
// @Autowired
|
||||||
private NoteService noteService;
|
// private NoteService noteService;
|
||||||
|
//
|
||||||
@Autowired
|
// @Autowired
|
||||||
private NoteRepository noteRepository;
|
// private NoteRepository noteRepository;
|
||||||
|
//
|
||||||
@BeforeEach
|
// @BeforeEach
|
||||||
void setUp() {
|
// void setUp() {
|
||||||
// Очищаем БД перед каждым тестом
|
// // Очищаем БД перед каждым тестом
|
||||||
noteRepository.deleteAll();
|
// noteRepository.deleteAll();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void addAndGetAllNotes() {
|
// void addAndGetAllNotes() {
|
||||||
// Given
|
// // Given
|
||||||
NoteRequest noteRequest = new NoteRequest();
|
// NoteRequest noteRequest = new NoteRequest();
|
||||||
noteRequest.setName("Test Note");
|
// noteRequest.setName("Test Note");
|
||||||
noteRequest.setText("Test content");
|
// noteRequest.setText("Test content");
|
||||||
|
//
|
||||||
// When
|
// // When
|
||||||
Note savedNote = noteService.add(noteRequest);
|
// NoteResponse savedNote = noteService.add(noteRequest);
|
||||||
List<Note> notes = noteService.getAll();
|
// List<NoteResponse> notes = noteService.getAll();
|
||||||
|
//
|
||||||
// Then
|
// // Then
|
||||||
assertNotNull(savedNote);
|
// assertNotNull(savedNote);
|
||||||
assertNotNull(savedNote.getUuid());
|
// assertNotNull(savedNote.getUuid());
|
||||||
assertEquals(1, notes.size());
|
// assertEquals(1, notes.size());
|
||||||
assertEquals("Test Note", notes.get(0).getName());
|
// assertEquals("Test Note", notes.get(0).getName());
|
||||||
assertEquals("Test content", notes.get(0).getText());
|
// assertEquals("Test content", notes.get(0).getText());
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void findExistingNote() {
|
// void findExistingNote() {
|
||||||
// Given
|
// // Given
|
||||||
NoteRequest noteRequest = new NoteRequest();
|
// NoteRequest noteRequest = new NoteRequest();
|
||||||
noteRequest.setName("Find Me");
|
// noteRequest.setName("Find Me");
|
||||||
noteRequest.setText("Find content");
|
// noteRequest.setText("Find content");
|
||||||
Note savedNote = noteService.add(noteRequest);
|
// NoteResponse savedNote = noteService.add(noteRequest);
|
||||||
|
//
|
||||||
// When
|
// // When
|
||||||
Note foundNote = noteService.find(savedNote.getUuid());
|
// NoteResponse foundNote = noteService.find(savedNote.getUuid());
|
||||||
|
//
|
||||||
// Then
|
// // Then
|
||||||
assertNotNull(foundNote);
|
// assertNotNull(foundNote);
|
||||||
assertEquals("Find Me", foundNote.getName());
|
// assertEquals("Find Me", foundNote.getName());
|
||||||
assertEquals("Find content", foundNote.getText());
|
// assertEquals("Find content", foundNote.getText());
|
||||||
assertEquals(savedNote.getUuid(), foundNote.getUuid());
|
// assertEquals(savedNote.getUuid(), foundNote.getUuid());
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void findNonExistingNote_ShouldThrowException() {
|
// void findNonExistingNote_ShouldThrowException() {
|
||||||
// Given
|
// // Given
|
||||||
UUID nonExistingId = UUID.randomUUID();
|
// UUID nonExistingId = UUID.randomUUID();
|
||||||
|
//
|
||||||
// When & Then
|
// // When & Then
|
||||||
ResponseStatusException exception = assertThrows(
|
// ResponseStatusException exception = assertThrows(
|
||||||
ResponseStatusException.class,
|
// ResponseStatusException.class,
|
||||||
() -> noteService.find(nonExistingId)
|
// () -> noteService.find(nonExistingId)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
assertTrue(exception.getMessage().contains("Note not found"));
|
// assertTrue(exception.getMessage().contains("Note not found"));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void updateExistingNote() {
|
// void updateExistingNote() {
|
||||||
// Given
|
// // Given
|
||||||
NoteRequest originalRequest = new NoteRequest();
|
// NoteRequest originalRequest = new NoteRequest();
|
||||||
originalRequest.setName("Original Name");
|
// originalRequest.setName("Original Name");
|
||||||
originalRequest.setText("Original content");
|
// originalRequest.setText("Original content");
|
||||||
Note savedNote = noteService.add(originalRequest);
|
// NoteResponse savedNote = noteService.add(originalRequest);
|
||||||
|
//
|
||||||
NoteRequest updateRequest = new NoteRequest();
|
// NoteRequest updateRequest = new NoteRequest();
|
||||||
updateRequest.setName("Updated Name");
|
// updateRequest.setName("Updated Name");
|
||||||
updateRequest.setText("Updated content");
|
// updateRequest.setText("Updated content");
|
||||||
|
//
|
||||||
// When
|
// // When
|
||||||
Note updatedNote = noteService.update(savedNote.getUuid(), updateRequest);
|
// NoteResponse updatedNote = noteService.update(savedNote.getUuid(), updateRequest);
|
||||||
|
//
|
||||||
// Then
|
// // Then
|
||||||
assertNotNull(updatedNote);
|
// assertNotNull(updatedNote);
|
||||||
assertEquals(savedNote.getUuid(), updatedNote.getUuid());
|
// assertEquals(savedNote.getUuid(), updatedNote.getUuid());
|
||||||
assertEquals("Updated Name", updatedNote.getName());
|
// assertEquals("Updated Name", updatedNote.getName());
|
||||||
assertEquals("Updated content", updatedNote.getText());
|
// assertEquals("Updated content", updatedNote.getText());
|
||||||
|
//
|
||||||
// Проверяем, что изменения сохранились в БД
|
// // Проверяем, что изменения сохранились в БД
|
||||||
Note foundNote = noteService.find(savedNote.getUuid());
|
// NoteResponse foundNote = noteService.find(savedNote.getUuid());
|
||||||
assertEquals("Updated Name", foundNote.getName());
|
// assertEquals("Updated Name", foundNote.getName());
|
||||||
assertEquals("Updated content", foundNote.getText());
|
// assertEquals("Updated content", foundNote.getText());
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void updateNonExistingNote_ShouldThrowException() {
|
// void updateNonExistingNote_ShouldThrowException() {
|
||||||
// Given
|
// // Given
|
||||||
UUID nonExistingId = UUID.randomUUID();
|
// UUID nonExistingId = UUID.randomUUID();
|
||||||
NoteRequest updateRequest = new NoteRequest();
|
// NoteRequest updateRequest = new NoteRequest();
|
||||||
updateRequest.setName("Update Name");
|
// updateRequest.setName("Update Name");
|
||||||
updateRequest.setText("Update content");
|
// updateRequest.setText("Update content");
|
||||||
|
//
|
||||||
// When & Then
|
// // When & Then
|
||||||
ResponseStatusException exception = assertThrows(
|
// ResponseStatusException exception = assertThrows(
|
||||||
ResponseStatusException.class,
|
// ResponseStatusException.class,
|
||||||
() -> noteService.update(nonExistingId, updateRequest)
|
// () -> noteService.update(nonExistingId, updateRequest)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
assertTrue(exception.getMessage().contains("Note not found"));
|
// assertTrue(exception.getMessage().contains("Note not found"));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void deleteExistingNote() {
|
// void deleteExistingNote() {
|
||||||
// Given
|
// // Given
|
||||||
NoteRequest noteRequest = new NoteRequest();
|
// NoteRequest noteRequest = new NoteRequest();
|
||||||
noteRequest.setName("Delete Me");
|
// noteRequest.setName("Delete Me");
|
||||||
noteRequest.setText("Delete content");
|
// noteRequest.setText("Delete content");
|
||||||
Note savedNote = noteService.add(noteRequest);
|
// NoteResponse savedNote = noteService.add(noteRequest);
|
||||||
|
//
|
||||||
// When
|
// // When
|
||||||
assertDoesNotThrow(() -> noteService.delete(savedNote.getUuid()));
|
// assertDoesNotThrow(() -> noteService.delete(savedNote.getUuid()));
|
||||||
|
//
|
||||||
// Then
|
// // Then
|
||||||
List<Note> notes = noteService.getAll();
|
// List<NoteResponse> notes = noteService.getAll();
|
||||||
assertTrue(notes.isEmpty());
|
// assertTrue(notes.isEmpty());
|
||||||
|
//
|
||||||
// Проверяем, что заметка действительно удалена
|
// // Проверяем, что заметка действительно удалена
|
||||||
ResponseStatusException exception = assertThrows(
|
// ResponseStatusException exception = assertThrows(
|
||||||
ResponseStatusException.class,
|
// ResponseStatusException.class,
|
||||||
() -> noteService.find(savedNote.getUuid())
|
// () -> noteService.find(savedNote.getUuid())
|
||||||
);
|
// );
|
||||||
assertTrue(exception.getMessage().contains("Note not found"));
|
// assertTrue(exception.getMessage().contains("Note not found"));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void deleteNonExistingNote_ShouldThrowException() {
|
// void deleteNonExistingNote_ShouldThrowException() {
|
||||||
// Given
|
// // Given
|
||||||
UUID nonExistingId = UUID.randomUUID();
|
// UUID nonExistingId = UUID.randomUUID();
|
||||||
|
//
|
||||||
// When & Then
|
// // When & Then
|
||||||
ResponseStatusException exception = assertThrows(
|
// ResponseStatusException exception = assertThrows(
|
||||||
ResponseStatusException.class,
|
// ResponseStatusException.class,
|
||||||
() -> noteService.delete(nonExistingId)
|
// () -> noteService.delete(nonExistingId)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
assertTrue(exception.getMessage().contains("Note not found"));
|
// assertTrue(exception.getMessage().contains("Note not found"));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void getAllNotes_WhenEmpty() {
|
// void getAllNotes_WhenEmpty() {
|
||||||
// When
|
// // When
|
||||||
List<Note> notes = noteService.getAll();
|
// List<NoteResponse> notes = noteService.getAll();
|
||||||
|
//
|
||||||
// Then
|
// // Then
|
||||||
assertNotNull(notes);
|
// assertNotNull(notes);
|
||||||
assertTrue(notes.isEmpty());
|
// assertTrue(notes.isEmpty());
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
void getAllNotes_WithMultipleNotes() {
|
// void getAllNotes_WithMultipleNotes() {
|
||||||
// Given
|
// // Given
|
||||||
for (int i = 1; i <= 5; i++) {
|
// for (int i = 1; i <= 5; i++) {
|
||||||
NoteRequest noteRequest = new NoteRequest();
|
// NoteRequest noteRequest = new NoteRequest();
|
||||||
noteRequest.setName("Note " + i);
|
// noteRequest.setName("Note " + i);
|
||||||
noteRequest.setText("Content for note " + i);
|
// noteRequest.setText("Content for note " + i);
|
||||||
noteService.add(noteRequest);
|
// noteService.add(noteRequest);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// When
|
// // When
|
||||||
List<Note> notes = noteService.getAll();
|
// List<NoteResponse> notes = noteService.getAll();
|
||||||
|
//
|
||||||
// Then
|
// // Then
|
||||||
assertNotNull(notes);
|
// assertNotNull(notes);
|
||||||
assertEquals(5, notes.size());
|
// assertEquals(5, notes.size());
|
||||||
|
// }
|
||||||
// Проверяем, что все заметки уникальны по ID
|
|
||||||
long uniqueIds = notes.stream()
|
|
||||||
.map(Note::getUuid)
|
|
||||||
.distinct()
|
|
||||||
.count();
|
|
||||||
assertEquals(5, uniqueIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user