Pre admin

This commit is contained in:
Tokishu
2025-06-02 22:23:43 +02:00
parent aceeb32d69
commit 0aeafc4f56
28 changed files with 475 additions and 112 deletions
@@ -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,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);
}
}
@@ -26,7 +26,6 @@ import java.util.Map;
public class AuthController {
public final AuthService authService;
public final UserService userService;
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(@RequestBody @Valid RegisterRequest request) {
@@ -2,8 +2,11 @@ package net.tokishu.note.controller;
import jakarta.validation.Valid;
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 org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -23,30 +26,30 @@ public class NotesController {
private final NoteService noteService;
@GetMapping
public ResponseEntity<List<NoteResponse>> getAll(){
return ResponseEntity.ok(noteService.getAll());
public ResponseEntity<List<NoteResponse>> getAll(@CurrentUser User sender){
return ResponseEntity.ok(noteService.getAll(sender));
}
@GetMapping("/{idOrCode}")
public NoteResponse find(@PathVariable String idOrCode) {
return noteService.findByIdOrPublicLink(idOrCode);
public NoteResponse find(@PathVariable String idOrCode, @NullableCurrentUser User sender) {
return noteService.findByIdOrPublicLink(idOrCode, sender);
}
@PostMapping()
public ResponseEntity<Map<String, String>> add(@RequestBody @Valid NoteRequest note){
noteService.add(note);
public ResponseEntity<Map<String, String>> add(@RequestBody @Valid NoteRequest note, @CurrentUser User sender){
noteService.add(note, sender);
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "Note added"));
}
@PutMapping("/{uuid}")
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody @Valid NoteRequest note){
noteService.update(uuid, note);
public ResponseEntity<?> update(@PathVariable("uuid") UUID uuid, @RequestBody @Valid NoteRequest note, @CurrentUser User sender){
noteService.update(uuid, note, sender);
return ResponseEntity.ok(Map.of("message", "Note updated"));
}
@DeleteMapping("/{uuid}")
public ResponseEntity<?> delete(@PathVariable UUID uuid){
noteService.delete(uuid);
public ResponseEntity<?> delete(@PathVariable UUID uuid, @CurrentUser User sender){
noteService.delete(uuid, sender);
return ResponseEntity.ok(Map.of("message", "Note deleted"));
}
}
@@ -1,12 +1,79 @@
package net.tokishu.note.controller;
import jakarta.validation.Valid;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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);
// }
}
@@ -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;
}
@@ -1,16 +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=1, max=24)
@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
@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;
}
@@ -5,7 +5,7 @@ import lombok.Getter;
@Getter
@Builder
public class ApiErrorResponse {
public class ApiResponse {
private final int status;
private final String message;
}
@@ -2,11 +2,12 @@ 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 String role;
private UserRole role;
}
@@ -2,11 +2,13 @@ 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 String role;
private UserRole role;
private String message;
}
@@ -2,11 +2,12 @@ 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 String role;
private UserRole role;
}
@@ -2,10 +2,12 @@ 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 role;
private String gravatarUrl;
private UserRole role;
}
@@ -1,20 +1,17 @@
package net.tokishu.note.exception;
import jakarta.servlet.http.HttpServletRequest;
import net.tokishu.note.dto.response.ApiErrorResponse;
import net.tokishu.note.dto.response.ApiResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
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;
import org.springframework.web.servlet.NoHandlerFoundException;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleAll(Exception ex) {
public ResponseEntity<ApiResponse> handleAll(Exception ex) {
if (ex instanceof MethodArgumentNotValidException validationEx) {
return handleValidation(validationEx);
}
@@ -49,7 +46,7 @@ public class GlobalExceptionHandler {
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
public ResponseEntity<ApiResponse> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.findFirst()
@@ -59,10 +56,10 @@ public class GlobalExceptionHandler {
}
private ResponseEntity<ApiErrorResponse> buildErrorResponse(int status, String message) {
private ResponseEntity<ApiResponse> buildErrorResponse(int status, String message) {
return ResponseEntity
.status(status)
.body(ApiErrorResponse.builder()
.body(ApiResponse.builder()
.status(status)
.message(message != null ? message : "Unexpected error")
.build());
@@ -2,7 +2,7 @@ package net.tokishu.note.exception;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.tokishu.note.dto.response.ApiErrorResponse;
import net.tokishu.note.dto.response.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
@@ -20,7 +20,7 @@ public class SecurityExceptionHandler implements AuthenticationEntryPoint, Acces
private void writeErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ApiErrorResponse body = ApiErrorResponse.builder()
ApiResponse body = ApiResponse.builder()
.status(status)
.message(message)
.build();
+10 -1
View File
@@ -1,6 +1,7 @@
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;
@@ -13,11 +14,19 @@ import java.util.UUID;
@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 String role;
private UserRole role;
@Email
@Column(nullable = true)
private String gravatarEmail;
@CreationTimestamp
@Column(updatable = false)
@@ -0,0 +1,5 @@
package net.tokishu.note.model;
public enum UserRole {
ADMIN, USER
}
@@ -2,6 +2,9 @@ package net.tokishu.note.repo;
import net.tokishu.note.model.Note;
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;
@@ -12,4 +15,8 @@ public interface NoteRepository extends JpaRepository<Note, UUID> {
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);
}
@@ -1,10 +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(String role);
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();
}
}
@@ -6,11 +6,17 @@ 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;
@@ -19,24 +25,14 @@ import org.springframework.web.server.ResponseStatusException;
@RequiredArgsConstructor
public class AuthService {
public final UserRepository userRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final UserService userService;
public RootResponse registerRoot() {
if (userRepository.existsByRole("ADMIN")) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Administrator already exists");
}
String rawPassword = PasswordGenerator.generate(16);
String hashedPassword = passwordEncoder.encode(rawPassword);
User rootUser = new User();
rootUser.setUsername("root");
rootUser.setPassword(hashedPassword);
rootUser.setRole("ADMIN");
userRepository.save(rootUser);
User rootUser = userService.createUser("root", rawPassword, UserRole.ADMIN, null);
return RootResponse.builder()
.username(rootUser.getUsername())
@@ -45,18 +41,8 @@ public class AuthService {
.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);
User user = userService.createRegularUser(request.getUsername(), request.getPassword());
return RegisterResponse.builder()
.username(user.getUsername())
@@ -77,7 +63,7 @@ public class AuthService {
org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.roles(user.getRole().name())
.build()
);
@@ -88,4 +74,46 @@ public class AuthService {
.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);
}
}
@@ -5,7 +5,9 @@ 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.User;
import net.tokishu.note.model.UserRole;
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.stereotype.Service;
@@ -22,11 +24,10 @@ public class NoteService {
public final NoteRepository noteRepository;
public final UserService userService;
public List<NoteResponse> getAll() {
User currentUser = userService.getCurrentUser();
public List<NoteResponse> getAll(User currentUser) {
List<Note> notes;
if ("ADMIN".equalsIgnoreCase(currentUser.getRole())) {
if (currentUser.getRole() == UserRole.ADMIN) {
notes = noteRepository.findAll();
} else {
notes = noteRepository.findByAuthorUsername(currentUser.getUsername());
@@ -37,18 +38,17 @@ public class NoteService {
.collect(Collectors.toList());
}
public NoteResponse findByIdOrPublicLink(String idOrCode) {
public NoteResponse findByIdOrPublicLink(String idOrCode, User currentUser) {
if (!StringUtils.hasText(idOrCode)) {
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"));
User currentUser = userService.getCurrentUser();
checkOwnership(note, currentUser);
} else {
note = noteRepository.findByPublicLinkAndIsPublicTrue(idOrCode)
@@ -58,10 +58,7 @@ public class NoteService {
return toResponse(note);
}
public NoteResponse add(NoteRequest data) {
User author = userService.getCurrentUser();
public NoteResponse add(NoteRequest data, User author) {
Note note = new Note();
note.setName(data.getName());
note.setText(data.getText());
@@ -86,12 +83,10 @@ public class NoteService {
return toResponse(saved);
}
public NoteResponse update(UUID uuid, NoteRequest data) {
public NoteResponse update(UUID uuid, NoteRequest data, User currentUser) {
Note existing = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
User currentUser = userService.getCurrentUser();
checkOwnership(existing, currentUser);
existing.setName(data.getName());
@@ -101,17 +96,16 @@ public class NoteService {
return toResponse(noteRepository.save(existing));
}
public void delete(UUID uuid) {
public void delete(UUID uuid, User currentUser) {
Note existing = noteRepository.findById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Note not found"));
User currentUser = userService.getCurrentUser();
checkOwnership(existing, currentUser);
noteRepository.delete(existing);
}
private void checkOwnership(Note note, User user) {
if ("ADMIN".equalsIgnoreCase(user.getRole())) { // TODO: use enum
if (user.getRole() == UserRole.ADMIN) {
return;
}
@@ -1,10 +1,15 @@
package net.tokishu.note.service;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import net.tokishu.note.dto.request.RegisterRequest;
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;
@@ -13,44 +18,70 @@ 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;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final NoteRepository noteRepository;
public List<User> getAll(){
return userRepository.findAll();
}
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 UserResponse getCurrentUserResponse() {
User user = getCurrentUser();
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,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);
}
}
}