diff --git a/build.gradle b/build.gradle index bd00b16..d1c6244 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.security:spring-security-crypto:6.5.3' + implementation("io.jsonwebtoken:jjwt:0.13.0") compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' diff --git a/src/main/java/ru/soune/nocopy/adminpanel/config/AppConfig.java b/src/main/java/ru/soune/nocopy/adminpanel/config/AppConfig.java new file mode 100644 index 0000000..d369206 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/config/AppConfig.java @@ -0,0 +1,44 @@ +package ru.soune.nocopy.adminpanel.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import ru.soune.nocopy.adminpanel.handler.AdminUserHandler; +import ru.soune.nocopy.adminpanel.handler.RequestHandler; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class AppConfig { + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } + + @Bean + public Map handlers(AdminUserHandler adminUserHandler) { + Map map = new HashMap<>(); + map.put(100, adminUserHandler); + + return map; + } + +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/controller/AdminController.java b/src/main/java/ru/soune/nocopy/adminpanel/controller/AdminController.java new file mode 100644 index 0000000..d408846 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/controller/AdminController.java @@ -0,0 +1,58 @@ +package ru.soune.nocopy.adminpanel.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.soune.nocopy.adminpanel.dto.BaseRequest; +import ru.soune.nocopy.adminpanel.dto.BaseResponse; +import ru.soune.nocopy.adminpanel.handler.RequestHandler; +import ru.soune.nocopy.adminpanel.util.MessageCode; + +import java.util.Map; + +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +@Slf4j +public class AdminController { + + private final Map handlers; + + @PostMapping + public ResponseEntity handleRequest(@RequestBody BaseRequest request) { + log.info("Received request: msg_id={}, version={}", request.getMsgId(), request.getVersion()); + + try { + RequestHandler handler = handlers.get(request.getMsgId()); + + if (handler == null) { + log.warn("No handler found for msg_id: {}", request.getMsgId()); + return ResponseEntity.ok( + BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.INVALID_ACTION.getCode()) + .messageDesc("No handler for msg_id: " + request.getMsgId()) + .build() + ); + } + + BaseResponse response = handler.handle(request); + log.info("Request processed successfully: msg_id={}", request.getMsgId()); + return ResponseEntity.ok(response); + + } catch (Exception e) { + log.error("Error processing request: msg_id={}", request.getMsgId(), e); + return ResponseEntity.ok( + BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.INVALID_ACTION.getCode()) + .messageDesc("Internal server error: " + e.getMessage()) + .build() + ); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/dto/ActionResponse.java b/src/main/java/ru/soune/nocopy/adminpanel/dto/ActionResponse.java new file mode 100644 index 0000000..be5d535 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/dto/ActionResponse.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.adminpanel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ActionResponse { + @JsonProperty("action") + private String action; + + @JsonProperty("available_actions") + private List availableActions; +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/dto/AdminUserRequest.java b/src/main/java/ru/soune/nocopy/adminpanel/dto/AdminUserRequest.java new file mode 100644 index 0000000..c016b18 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/dto/AdminUserRequest.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.adminpanel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class AdminUserRequest { + @JsonProperty("action") + private String action; + + @JsonProperty("admin_id") + private Long adminId; + + @JsonProperty("full_name") + private String fullName; + + @JsonProperty("email") + private String email; + + @JsonProperty("password") + private String password; + + @JsonProperty("is_super_admin") + private Boolean isSuperAdmin; + + @JsonProperty("is_active") + private Boolean isActive; +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/dto/AdminUserResponse.java b/src/main/java/ru/soune/nocopy/adminpanel/dto/AdminUserResponse.java new file mode 100644 index 0000000..a06f8e2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/dto/AdminUserResponse.java @@ -0,0 +1,28 @@ +package ru.soune.nocopy.adminpanel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AdminUserResponse { + @JsonProperty("id") + private Long id; + + @JsonProperty("full_name") + private String fullName; + + @JsonProperty("email") + private String email; + + @JsonProperty("is_active") + private Boolean isActive; + + @JsonProperty("is_super_admin") + private Boolean isSuperAdmin; + + @JsonProperty("created_at") + private String createdAt; +} + diff --git a/src/main/java/ru/soune/nocopy/adminpanel/dto/BaseRequest.java b/src/main/java/ru/soune/nocopy/adminpanel/dto/BaseRequest.java new file mode 100644 index 0000000..d960005 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/dto/BaseRequest.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.adminpanel.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) +public class BaseRequest { + @JsonProperty("version") + Integer version; + + @JsonProperty("msg_id") + Integer msgId; + + @JsonProperty("token") + String token; + + @JsonProperty("message_body") + Object messageBody; +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/dto/BaseResponse.java b/src/main/java/ru/soune/nocopy/adminpanel/dto/BaseResponse.java new file mode 100644 index 0000000..54b84e3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/dto/BaseResponse.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.adminpanel.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +@Builder +@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) +public class BaseResponse { + @JsonProperty("msg_id") + private Integer msgId; + + @JsonProperty("message_code") + private Integer messageCode; + + @JsonProperty("message_desc") + private String messageDesc; + + @JsonProperty("message_body") + private Object messageBody; +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/dto/LoginResponse.java b/src/main/java/ru/soune/nocopy/adminpanel/dto/LoginResponse.java new file mode 100644 index 0000000..c1f66ed --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/dto/LoginResponse.java @@ -0,0 +1,18 @@ +package ru.soune.nocopy.adminpanel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class LoginResponse { + @JsonProperty("success") + private Boolean success; + + @JsonProperty("token") + private String token; + + @JsonProperty("admin") + private AdminUserResponse admin; +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/entity/AdminUser.java b/src/main/java/ru/soune/nocopy/adminpanel/entity/AdminUser.java new file mode 100644 index 0000000..0d99281 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/entity/AdminUser.java @@ -0,0 +1,59 @@ +package ru.soune.nocopy.adminpanel.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.adminpanel.permission.AdminPermission; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "admin_users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString(exclude = {"password", "permissions"}) +@EntityListeners(AuditingEntityListener.class) +public class AdminUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "full_name") + private String fullName; + + @Size(max = 1024) + @Column(name = "email", length = 1024, unique = true) + private String email; + + @Column(nullable = false) + @Size(min = 6) + @JsonIgnore + private String password; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @Column(name = "is_super_admin", nullable = false) + private Boolean isSuperAdmin = false; + + @Column(name = "permissions", nullable = false) + private Long permissions = AdminPermission.DEFAULT_PERMISSIONS; + +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/exceptions/AdminUserNotFoundException.java b/src/main/java/ru/soune/nocopy/adminpanel/exceptions/AdminUserNotFoundException.java new file mode 100644 index 0000000..89c02cd --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/exceptions/AdminUserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.adminpanel.exceptions; + +public class AdminUserNotFoundException extends RuntimeException { + public AdminUserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/exceptions/EmailAlreadyExistsException.java b/src/main/java/ru/soune/nocopy/adminpanel/exceptions/EmailAlreadyExistsException.java new file mode 100644 index 0000000..38395dc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/exceptions/EmailAlreadyExistsException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.adminpanel.exceptions; + +public class EmailAlreadyExistsException extends RuntimeException { + public EmailAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/handler/AdminUserHandler.java b/src/main/java/ru/soune/nocopy/adminpanel/handler/AdminUserHandler.java new file mode 100644 index 0000000..45b00bb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/handler/AdminUserHandler.java @@ -0,0 +1,267 @@ +package ru.soune.nocopy.adminpanel.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.adminpanel.dto.*; +import ru.soune.nocopy.adminpanel.entity.AdminUser; +import ru.soune.nocopy.adminpanel.exceptions.AdminUserNotFoundException; +import ru.soune.nocopy.adminpanel.exceptions.EmailAlreadyExistsException; +import ru.soune.nocopy.adminpanel.permission.AdminPermission; +import ru.soune.nocopy.adminpanel.service.AdminUserService; +import ru.soune.nocopy.adminpanel.util.JwtUtil; +import ru.soune.nocopy.adminpanel.util.MessageCode; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AdminUserHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final AdminUserService adminUserService; + + private final JwtUtil jwtUtil; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + AdminUserRequest adminRequest = objectMapper.convertValue(request.getMessageBody(), AdminUserRequest.class); + String action = adminRequest.getAction(); + + return switch (action) { + case "login" -> handleLogin(request, adminRequest); + case "create" -> handleCreate(request, adminRequest); + case "get_all" -> handleGetAll(request); + case "get_by_id" -> handleGetById(request, adminRequest); + case "update" -> handleUpdate(request, adminRequest); + case "set_super_admin" -> handleSetSuperAdmin(request, adminRequest); + case "toggle_active" -> handleToggleActive(request, adminRequest); + case "delete" -> handleDelete(request, adminRequest); + default -> handleInvalidAction(request, action); + }; + } + + private BaseResponse handleLogin(BaseRequest request, AdminUserRequest adminRequest) { + String email = adminRequest.getEmail(); + String password = adminRequest.getPassword(); + + boolean authenticated = adminUserService.authenticate(email, password); + + if (!authenticated) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.INVALID_CREDENTIALS.getCode()) + .messageDesc(MessageCode.INVALID_CREDENTIALS.getDescription()) + .build(); + } + + try { + AdminUser admin = adminUserService.findByEmail(email); + + if (!admin.getIsActive()) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_INACTIVE.getCode()) + .messageDesc(MessageCode.ADMIN_INACTIVE.getDescription()) + .build(); + } + + String token = jwtUtil.generateToken(admin.getEmail(), admin.getId()); + + LoginResponse loginResponse = LoginResponse.builder() + .success(true) + .token(token) + .admin(convertToResponse(admin)) + .build(); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(loginResponse) + .build(); + } catch (AdminUserNotFoundException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_NOT_FOUND.getCode()) + .messageDesc(MessageCode.ADMIN_NOT_FOUND.getDescription()) + .build(); + } + } + + private BaseResponse handleCreate(BaseRequest request, AdminUserRequest adminRequest) { + try { + boolean superAdmin = adminRequest.getIsSuperAdmin() != null && adminRequest.getIsSuperAdmin(); + AdminUser admin = adminUserService.create( + adminRequest.getFullName(), + adminRequest.getEmail(), + adminRequest.getPassword(), + superAdmin + ); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(convertToResponse(admin)) + .build(); + } catch (EmailAlreadyExistsException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.EMAIL_ALREADY_EXISTS.getCode()) + .messageDesc(MessageCode.EMAIL_ALREADY_EXISTS.getDescription()) + .build(); + } + } + + private BaseResponse handleGetAll(BaseRequest request) { + List admins = adminUserService.findAll().stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(admins) + .build(); + } + + private BaseResponse handleGetById(BaseRequest request, AdminUserRequest adminRequest) { + try { + AdminUser admin = adminUserService.findById(adminRequest.getAdminId()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(convertToResponse(admin)) + .build(); + } catch (AdminUserNotFoundException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_NOT_FOUND.getCode()) + .messageDesc(MessageCode.ADMIN_NOT_FOUND.getDescription()) + .build(); + } + } + + private BaseResponse handleUpdate(BaseRequest request, AdminUserRequest adminRequest) { + try { + AdminUser admin = adminUserService.update( + adminRequest.getAdminId(), + adminRequest.getFullName(), + adminRequest.getEmail(), + adminRequest.getPassword() + ); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(convertToResponse(admin)) + .build(); + } catch (AdminUserNotFoundException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_NOT_FOUND.getCode()) + .messageDesc(MessageCode.ADMIN_NOT_FOUND.getDescription()) + .build(); + } catch (EmailAlreadyExistsException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.EMAIL_ALREADY_EXISTS.getCode()) + .messageDesc(MessageCode.EMAIL_ALREADY_EXISTS.getDescription()) + .build(); + } + } + + private BaseResponse handleSetSuperAdmin(BaseRequest request, AdminUserRequest adminRequest) { + try { + AdminUser admin = adminUserService.setSuperAdmin(adminRequest.getAdminId(), adminRequest.getIsSuperAdmin()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(convertToResponse(admin)) + .build(); + } catch (AdminUserNotFoundException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_NOT_FOUND.getCode()) + .messageDesc(MessageCode.ADMIN_NOT_FOUND.getDescription()) + .build(); + } + } + + private BaseResponse handleToggleActive(BaseRequest request, AdminUserRequest adminRequest) { + try { + adminUserService.toggleActive(adminRequest.getAdminId(), adminRequest.getIsActive()); + AdminUser admin = adminUserService.findById(adminRequest.getAdminId()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody(convertToResponse(admin)) + .build(); + } catch (AdminUserNotFoundException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_NOT_FOUND.getCode()) + .messageDesc(MessageCode.ADMIN_NOT_FOUND.getDescription()) + .build(); + } + } + + private BaseResponse handleDelete(BaseRequest request, AdminUserRequest adminRequest) { + try { + adminUserService.delete(adminRequest.getAdminId()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.SUCCESS.getCode()) + .messageDesc(MessageCode.SUCCESS.getDescription()) + .messageBody("Admin deleted successfully") + .build(); + } catch (AdminUserNotFoundException e) { + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.ADMIN_NOT_FOUND.getCode()) + .messageDesc(MessageCode.ADMIN_NOT_FOUND.getDescription()) + .build(); + } + } + + private BaseResponse handleInvalidAction(BaseRequest request, String action) { + ActionResponse response = ActionResponse.builder() + .action(action) + .availableActions(Arrays.asList( + "login", "create", "get_all", "get_by_id", + "update", "set_super_admin", "toggle_active", "delete")) + .build(); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageCode(MessageCode.INVALID_ACTION.getCode()) + .messageDesc(MessageCode.INVALID_ACTION.getDescription()) + .messageBody(response) + .build(); + } + + private AdminUserResponse convertToResponse(AdminUser admin) { + return AdminUserResponse.builder() + .id(admin.getId()) + .fullName(admin.getFullName()) + .email(admin.getEmail()) + .isActive(admin.getIsActive()) + .isSuperAdmin(AdminPermission.isSuperAdmin(admin.getPermissions())) + .createdAt(admin.getCreatedAt().toString()) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/handler/RequestHandler.java b/src/main/java/ru/soune/nocopy/adminpanel/handler/RequestHandler.java new file mode 100644 index 0000000..260fdbc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/handler/RequestHandler.java @@ -0,0 +1,8 @@ +package ru.soune.nocopy.adminpanel.handler; + +import ru.soune.nocopy.adminpanel.dto.BaseRequest; +import ru.soune.nocopy.adminpanel.dto.BaseResponse; + +public interface RequestHandler { + BaseResponse handle(BaseRequest request) throws Exception; +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/permission/AdminPermission.java b/src/main/java/ru/soune/nocopy/adminpanel/permission/AdminPermission.java new file mode 100644 index 0000000..e2728b3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/permission/AdminPermission.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.adminpanel.permission; + +public class AdminPermission { + public static final long ADMIN_BIT = 1L; + public static final long SUPER_ADMIN_BIT = 1L << 1; + + public static final long DEFAULT_PERMISSIONS = ADMIN_BIT; + public static final long SUPER_ADMIN_PERMISSIONS = ADMIN_BIT | SUPER_ADMIN_BIT; + + public static boolean hasPermission(long userPermissions, long permissionBit) { + return (userPermissions & permissionBit) != 0; + } + + public static boolean isAdmin(long permissions) { + return hasPermission(permissions, ADMIN_BIT); + } + + public static boolean isSuperAdmin(long permissions) { + return hasPermission(permissions, SUPER_ADMIN_BIT); + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/permission/PermissionChecker.java b/src/main/java/ru/soune/nocopy/adminpanel/permission/PermissionChecker.java new file mode 100644 index 0000000..bb3ad9c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/permission/PermissionChecker.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.adminpanel.permission; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.adminpanel.entity.AdminUser; + +@Component +@RequiredArgsConstructor +public class PermissionChecker { + + public boolean isSuperAdmin(AdminUser admin) { + if (admin == null || !admin.getIsActive()) return false; + return AdminPermission.isSuperAdmin(admin.getPermissions()); + } + + public void checkSuperAdmin(AdminUser admin) { + if (!isSuperAdmin(admin)) { + throw new SecurityException("Super admin permission required"); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/repository/AdminUserRepository.java b/src/main/java/ru/soune/nocopy/adminpanel/repository/AdminUserRepository.java new file mode 100644 index 0000000..b382e9f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/repository/AdminUserRepository.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.adminpanel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.adminpanel.entity.AdminUser; + +import java.util.Optional; + +@Repository +public interface AdminUserRepository extends JpaRepository { + Optional findByEmail(String email); + + boolean existsByEmail(String email); +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/service/AdminUserService.java b/src/main/java/ru/soune/nocopy/adminpanel/service/AdminUserService.java new file mode 100644 index 0000000..dbdcb6b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/service/AdminUserService.java @@ -0,0 +1,99 @@ +package ru.soune.nocopy.adminpanel.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.adminpanel.entity.AdminUser; +import ru.soune.nocopy.adminpanel.exceptions.AdminUserNotFoundException; +import ru.soune.nocopy.adminpanel.exceptions.EmailAlreadyExistsException; +import ru.soune.nocopy.adminpanel.permission.AdminPermission; +import ru.soune.nocopy.adminpanel.repository.AdminUserRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AdminUserService { + + private final AdminUserRepository adminUserRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public AdminUser create(String fullName, String email, String password, boolean superAdmin) { + if (adminUserRepository.existsByEmail(email)) { + throw new EmailAlreadyExistsException("Email already exists: " + email); + } + + AdminUser admin = new AdminUser(); + admin.setFullName(fullName); + admin.setEmail(email); + admin.setPassword(passwordEncoder.encode(password)); + admin.setCreatedAt(LocalDateTime.now()); + admin.setIsActive(true); + admin.setPermissions(superAdmin ? AdminPermission.SUPER_ADMIN_PERMISSIONS : AdminPermission.DEFAULT_PERMISSIONS); + + return adminUserRepository.save(admin); + } + + public AdminUser findById(Long id) { + return adminUserRepository.findById(id) + .orElseThrow(() -> new AdminUserNotFoundException("Admin not found: " + id)); + } + + public AdminUser findByEmail(String email) { + return adminUserRepository.findByEmail(email) + .orElseThrow(() -> new AdminUserNotFoundException("Admin not found: " + email)); + } + + public List findAll() { + return adminUserRepository.findAll(); + } + + @Transactional + public AdminUser update(Long id, String fullName, String email, String password) { + AdminUser admin = findById(id); + + if (fullName != null) admin.setFullName(fullName); + + if (email != null && !email.equals(admin.getEmail())) { + if (adminUserRepository.existsByEmail(email)) { + throw new EmailAlreadyExistsException("Email already exists: " + email); + } + admin.setEmail(email); + } + + if (password != null && !password.isEmpty()) { + admin.setPassword(passwordEncoder.encode(password)); + } + + return adminUserRepository.save(admin); + } + + @Transactional + public AdminUser setSuperAdmin(Long id, boolean superAdmin) { + AdminUser admin = findById(id); + admin.setPermissions(superAdmin ? AdminPermission.SUPER_ADMIN_PERMISSIONS : AdminPermission.DEFAULT_PERMISSIONS); + return adminUserRepository.save(admin); + } + + @Transactional + public void toggleActive(Long id, Boolean isActive) { + AdminUser admin = findById(id); + admin.setIsActive(isActive); + adminUserRepository.save(admin); + } + + @Transactional + public void delete(Long id) { + adminUserRepository.deleteById(id); + } + + public boolean authenticate(String email, String password) { + return adminUserRepository.findByEmail(email) + .filter(AdminUser::getIsActive) + .map(admin -> passwordEncoder.matches(password, admin.getPassword())) + .orElse(false); + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/util/JwtAuthFilter.java b/src/main/java/ru/soune/nocopy/adminpanel/util/JwtAuthFilter.java new file mode 100644 index 0000000..31304a2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/util/JwtAuthFilter.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.adminpanel.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import ru.soune.nocopy.adminpanel.dto.BaseResponse; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + + if (!jwtUtil.validateToken(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + + BaseResponse errorResponse = BaseResponse.builder() + .messageCode(MessageCode.INVALID_CREDENTIALS.getCode()) + .messageDesc("Invalid or expired token") + .build(); + + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + return; + } + + request.setAttribute("userId", jwtUtil.extractUserId(token)); + request.setAttribute("email", jwtUtil.extractEmail(token)); + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/util/JwtUtil.java b/src/main/java/ru/soune/nocopy/adminpanel/util/JwtUtil.java new file mode 100644 index 0000000..129be81 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/util/JwtUtil.java @@ -0,0 +1,64 @@ +package ru.soune.nocopy.adminpanel.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:Z29nb3Bvd2VycmFuZ2VydHVtYmF5YW1iZjMyNDIyMjh3aW5lcndpbmVy}") + private String secret; + + @Value("${jwt.expiration:3600000}") + private Long expiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String email, Long userId) { + return Jwts.builder() + .setSubject(email) + .claim("userId", userId) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public Claims extractClaims(String token) { + return Jwts.parser() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String extractEmail(String token) { + return extractClaims(token).getSubject(); + } + + public Long extractUserId(String token) { + return extractClaims(token).get("userId", Long.class); + } + + public boolean isTokenExpired(String token) { + return extractClaims(token).getExpiration().before(new Date()); + } + + public boolean validateToken(String token) { + try { + return !isTokenExpired(token); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/adminpanel/util/MessageCode.java b/src/main/java/ru/soune/nocopy/adminpanel/util/MessageCode.java new file mode 100644 index 0000000..b3e7ecf --- /dev/null +++ b/src/main/java/ru/soune/nocopy/adminpanel/util/MessageCode.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.adminpanel.util; + +import lombok.Getter; + +@Getter +public enum MessageCode { + SUCCESS(0, "Success"), + INVALID_ACTION(2, "Invalid action"), + ADMIN_NOT_FOUND(4, "Admin not found"), + EMAIL_ALREADY_EXISTS(2, "Email already exists"), + INVALID_CREDENTIALS(2, "Invalid credentials"), + PERMISSION_DENIED(2, "Permission denied"), + ADMIN_INACTIVE(2, "Admin is inactive"); + + private final int code; + private final String description; + + MessageCode(int code, String description) { + this.code = code; + this.description = description; + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index eebf038..4248496 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -9,4 +9,8 @@ spring: ddl-auto: validate server: - port: 8082 \ No newline at end of file + port: 8082 + +jwt: + secret: Z29nb3Bvd2VycmFuZ2VydHVtYmF5YW1iZjMyNDIyMjh3aW5lcndpbmVy + expiration: 3600000 \ No newline at end of file