From c2d19a7724319628e49f4bbb8838744dbf2647b6 Mon Sep 17 00:00:00 2001 From: Maksim Skobaro Date: Fri, 7 Feb 2025 07:05:15 +0300 Subject: [PATCH] improved, more featured, fixed Exceptions and Errors are better Files structure is better New ComponentContext.ts New DataTable.tsx tables.ts Massive components refactoring New Group.java New LoggingRequestFilter.java LoggingSessionListener.java New NotificationStore.ts SysInfoStore.ts New reactiveValue.ts ReactiveControls.tsx New dependencies And much more --- .../tdms/config/SecurityConfiguration.java | 35 ++--- .../tdms/controller/GroupController.java | 20 +++ .../tdms/controller/StudentController.java | 4 +- .../tdms/controller/SysInfoController.java | 21 +++ .../tdms/controller/UserController.java | 27 +++- .../payload}/ErrorResponse.java | 4 +- .../tdms/controller/payload/GroupDTO.java | 14 ++ .../tdms/controller/payload/LoginDTO.java | 12 ++ .../controller/payload/RegistrationDTO.java | 34 ++++ .../{dto => controller/payload}/RoleDTO.java | 2 +- .../tdms/controller/payload/StudentDTO.java | 50 ++++++ .../{dto => controller/payload}/UserDTO.java | 8 +- .../java/ru/tubryansk/tdms/dto/GroupDTO.java | 13 -- .../java/ru/tubryansk/tdms/dto/LoginDTO.java | 6 - .../ru/tubryansk/tdms/dto/StudentDTO.java | 51 ------ .../tubryansk/tdms/entity/DiplomaTopic.java | 9 +- .../java/ru/tubryansk/tdms/entity/Group.java | 14 +- .../java/ru/tubryansk/tdms/entity/Role.java | 17 +- .../ru/tubryansk/tdms/entity/Student.java | 8 +- .../java/ru/tubryansk/tdms/entity/User.java | 25 +-- .../entity/repository/DefenceRepository.java | 4 + .../repository/DiplomaTopicRepository.java | 2 +- .../entity/repository/GroupRepository.java | 13 ++ .../entity/repository/RoleRepository.java | 9 ++ .../repository/StudentRepository.java | 2 +- .../repository/UserRepository.java | 3 +- .../tdms/exception/AccessDeniedException.java | 2 +- .../tdms/exception/BusinessException.java | 4 +- .../exception/GlobalExceptionHandler.java | 31 ++-- .../tdms/exception/NotFoundException.java | 6 +- .../tdms/service/AuthenticationService.java | 6 +- .../tubryansk/tdms/service/CallerService.java | 5 + .../tubryansk/tdms/service/GroupService.java | 15 ++ .../tubryansk/tdms/service/RoleService.java | 38 +++++ .../tdms/service/StudentService.java | 14 +- .../tdms/service/SysInfoService.java | 14 ++ .../tubryansk/tdms/service/UserService.java | 82 +++++++++- .../tdms/web/LoggingRequestFilter.java | 29 ++++ .../tdms/web/LoggingSessionListener.java | 23 +++ .../ru/tubryansk/tdms/web/RequestLogger.java | 27 ---- .../migration/V00010__Create__role_table.sql | 3 + .../migration/V00020__Create__user_table.sql | 18 ++- .../V00030__Create__user_role_table.sql | 3 + .../V00040__Create__diploma_topic_table.sql | 5 +- .../migration/V00050__Create__group_table.sql | 20 ++- .../V00060__Create__student_table.sql | 37 +++-- .../V00510__Insert_administrator.sql | 4 +- web/package-lock.json | 51 +++--- web/package.json | 5 +- web/src/Application.tsx | 2 +- web/src/components/NotificationContainer.tsx | 89 +++++++++++ web/src/components/custom/DataTable.tsx | 147 ++++++++++++++++++ .../custom/controls/ReactiveControls.css | 3 + .../custom/controls/ReactiveControls.tsx | 116 ++++++++++++++ .../{page => }/layout/DefaultPage.tsx | 28 ++-- web/src/components/{page => layout}/Error.tsx | 4 +- web/src/components/layout/Footer.tsx | 41 +++++ .../components/{page => }/layout/Header.tsx | 42 ++--- web/src/components/layout/Home.tsx | 9 ++ web/src/components/page/Home.tsx | 11 -- web/src/components/page/layout/Footer.tsx | 22 --- web/src/components/user/LoginModal.tsx | 93 ++++++----- web/src/components/user/UserList.tsx | 60 +++++++ web/src/components/user/UserProfilePage.tsx | 4 +- web/src/components/user/UserRegistration.tsx | 87 +++++++++++ web/src/models/registration.ts | 12 ++ web/src/models/role.ts | 5 +- web/src/router/routes.ts | 6 + web/src/router/viewMap.tsx | 8 +- web/src/services/NotificationService.ts | 42 +++++ web/src/services/RouterService.ts | 1 + web/src/store/MyRouterStore.ts | 1 + web/src/store/NotificationStore.ts | 93 +++++++++++ web/src/store/PendingStore.ts | 41 ----- web/src/store/RootStore.ts | 14 +- .../{context => store}/RootStoreContext.ts | 2 +- web/src/store/SysInfoStore.ts | 32 ++++ web/src/store/ThinkStore.ts | 35 +++++ web/src/store/UserStore.ts | 17 +- web/src/utils/ComponentContext.ts | 31 ++++ web/src/utils/init.ts | 32 +++- .../ModalState.ts => utils/modalState.ts} | 0 web/src/utils/reactive/reactiveValue.ts | 127 +++++++++++++++ web/src/utils/reactive/validators.ts | 106 +++++++++++++ web/src/utils/request.ts | 32 ++++ web/src/utils/request.tsx | 32 ---- web/src/utils/tables.ts | 33 ++++ web/tsconfig.json | 7 +- 88 files changed, 1858 insertions(+), 458 deletions(-) create mode 100644 server/src/main/java/ru/tubryansk/tdms/controller/GroupController.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/controller/SysInfoController.java rename server/src/main/java/ru/tubryansk/tdms/{dto => controller/payload}/ErrorResponse.java (84%) create mode 100644 server/src/main/java/ru/tubryansk/tdms/controller/payload/GroupDTO.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/controller/payload/LoginDTO.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/controller/payload/RegistrationDTO.java rename server/src/main/java/ru/tubryansk/tdms/{dto => controller/payload}/RoleDTO.java (89%) create mode 100644 server/src/main/java/ru/tubryansk/tdms/controller/payload/StudentDTO.java rename server/src/main/java/ru/tubryansk/tdms/{dto => controller/payload}/UserDTO.java (81%) delete mode 100644 server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java delete mode 100644 server/src/main/java/ru/tubryansk/tdms/dto/LoginDTO.java delete mode 100644 server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/entity/repository/DefenceRepository.java rename server/src/main/java/ru/tubryansk/tdms/{ => entity}/repository/DiplomaTopicRepository.java (91%) create mode 100644 server/src/main/java/ru/tubryansk/tdms/entity/repository/GroupRepository.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/entity/repository/RoleRepository.java rename server/src/main/java/ru/tubryansk/tdms/{ => entity}/repository/StudentRepository.java (92%) rename server/src/main/java/ru/tubryansk/tdms/{ => entity}/repository/UserRepository.java (70%) create mode 100644 server/src/main/java/ru/tubryansk/tdms/service/GroupService.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/service/RoleService.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/service/SysInfoService.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/web/LoggingRequestFilter.java create mode 100644 server/src/main/java/ru/tubryansk/tdms/web/LoggingSessionListener.java delete mode 100644 server/src/main/java/ru/tubryansk/tdms/web/RequestLogger.java create mode 100644 web/src/components/NotificationContainer.tsx create mode 100644 web/src/components/custom/DataTable.tsx create mode 100644 web/src/components/custom/controls/ReactiveControls.css create mode 100644 web/src/components/custom/controls/ReactiveControls.tsx rename web/src/components/{page => }/layout/DefaultPage.tsx (59%) rename web/src/components/{page => layout}/Error.tsx (53%) create mode 100644 web/src/components/layout/Footer.tsx rename web/src/components/{page => }/layout/Header.tsx (65%) create mode 100644 web/src/components/layout/Home.tsx delete mode 100644 web/src/components/page/Home.tsx delete mode 100644 web/src/components/page/layout/Footer.tsx create mode 100644 web/src/components/user/UserList.tsx create mode 100644 web/src/components/user/UserRegistration.tsx create mode 100644 web/src/models/registration.ts create mode 100644 web/src/services/NotificationService.ts create mode 100644 web/src/store/NotificationStore.ts delete mode 100644 web/src/store/PendingStore.ts rename web/src/{context => store}/RootStoreContext.ts (81%) create mode 100644 web/src/store/SysInfoStore.ts create mode 100644 web/src/store/ThinkStore.ts create mode 100644 web/src/utils/ComponentContext.ts rename web/src/{state/ModalState.ts => utils/modalState.ts} (100%) create mode 100644 web/src/utils/reactive/reactiveValue.ts create mode 100644 web/src/utils/reactive/validators.ts create mode 100644 web/src/utils/request.ts delete mode 100644 web/src/utils/request.tsx create mode 100644 web/src/utils/tables.ts diff --git a/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java b/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java index c09b22a..926951b 100644 --- a/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java +++ b/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java @@ -1,8 +1,6 @@ package ru.tubryansk.tdms.config; -import jakarta.servlet.http.HttpSessionEvent; -import jakarta.servlet.http.HttpSessionListener; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; @@ -19,7 +17,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @@ -42,7 +39,7 @@ public class SecurityConfiguration { ) throws Exception { return httpSecurity .authorizeHttpRequests(this::configureHttpAuthorization) - .csrf(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) /* todo: настроить csrf */ .cors(a -> a.configurationSource(cors)) .authenticationManager(authenticationManager) .sessionManagement(cfg -> { @@ -76,21 +73,6 @@ public class SecurityConfiguration { }; } - @Bean - public HttpSessionListener httpSessionListener() { - return new HttpSessionListener() { - @Override - public void sessionCreated(HttpSessionEvent se) { - log.debug("Session created: {}, user {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName()); - } - - @Override - public void sessionDestroyed(HttpSessionEvent se) { - log.debug("Session destroyed: {}, user: {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName()); - } - }; - } - @Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) { return new ProviderManager(authenticationProvider(userDetailsService)); @@ -102,15 +84,22 @@ public class SecurityConfiguration { } private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry httpAuthorization) { - /* API ROUTES */ + /* SysInfoController */ + httpAuthorization.requestMatchers("/api/v1/sysinfo/**").permitAll(); + /* UserController */ httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated(); httpAuthorization.requestMatchers("/api/v1/user/login").anonymous(); httpAuthorization.requestMatchers("/api/v1/user/current").permitAll(); - + httpAuthorization.requestMatchers("/api/v1/user/get-all").hasAuthority("ROLE_ADMINISTRATOR"); + httpAuthorization.requestMatchers("/api/v1/user/register").hasAuthority("ROLE_ADMINISTRATOR"); + httpAuthorization.requestMatchers("/api/v1/user/validate-registration").hasAuthority("ROLE_ADMINISTRATOR"); + /* StudentController */ httpAuthorization.requestMatchers("/api/v1/student/current").permitAll(); - + /* GroupController */ + httpAuthorization.requestMatchers("/api/v1/group/get-all").permitAll(); + /* deny all other api requests */ httpAuthorization.requestMatchers("/api/**").denyAll(); - /* STATIC ROUTES */ + /* since api already blocked, all other requests are static resources */ httpAuthorization.requestMatchers("/**").permitAll(); } diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/GroupController.java b/server/src/main/java/ru/tubryansk/tdms/controller/GroupController.java new file mode 100644 index 0000000..504df1a --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/GroupController.java @@ -0,0 +1,20 @@ +package ru.tubryansk.tdms.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.tubryansk.tdms.controller.payload.GroupDTO; +import ru.tubryansk.tdms.service.GroupService; + +import java.util.Collection; + +@RestController("/api/v1/group/") +public class GroupController { + @Autowired + private GroupService groupService; + + @GetMapping("/get-all-groups") + public Collection getAllGroups() { + return groupService.getAllGroups(); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java b/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java index 3bb6523..d8ef6d2 100644 --- a/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java +++ b/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java @@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import ru.tubryansk.tdms.dto.StudentDTO; +import ru.tubryansk.tdms.controller.payload.StudentDTO; import ru.tubryansk.tdms.service.StudentService; @RestController @@ -15,6 +15,6 @@ public class StudentController { @GetMapping("/current") public StudentDTO getCurrentStudent() { - return studentService.getCallerStudent().map(StudentDTO::from).orElse(null); + return studentService.getCallerStudentDTO(); } } diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/SysInfoController.java b/server/src/main/java/ru/tubryansk/tdms/controller/SysInfoController.java new file mode 100644 index 0000000..003d9c0 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/SysInfoController.java @@ -0,0 +1,21 @@ +package ru.tubryansk.tdms.controller; + +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.tubryansk.tdms.service.SysInfoService; + +@RestController +@RequestMapping("/api/v1/sysinfo") +public class SysInfoController { + @Autowired + private SysInfoService sysInfoService; + + @SneakyThrows + @GetMapping("/version") + public String getVersion() { + return sysInfoService.getVersion(); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java b/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java index 7d75f9a..905570f 100644 --- a/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java +++ b/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java @@ -1,12 +1,17 @@ package ru.tubryansk.tdms.controller; +import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import ru.tubryansk.tdms.dto.LoginDTO; -import ru.tubryansk.tdms.dto.UserDTO; +import ru.tubryansk.tdms.controller.payload.LoginDTO; +import ru.tubryansk.tdms.controller.payload.RegistrationDTO; +import ru.tubryansk.tdms.controller.payload.UserDTO; import ru.tubryansk.tdms.service.AuthenticationService; import ru.tubryansk.tdms.service.CallerService; +import ru.tubryansk.tdms.service.UserService; + +import java.util.List; @RestController @RequestMapping("/api/v1/user") @@ -16,10 +21,12 @@ public class UserController { private AuthenticationService authenticationService; @Autowired private CallerService callerService; + @Autowired + private UserService userService; @GetMapping("/current") public UserDTO getCurrentUser() { - return callerService.getCallerUser().map(user -> UserDTO.from(user, true)).orElse(UserDTO.unauthenticated()); + return callerService.getCallerUserDTO(); } @PostMapping("/logout") @@ -28,7 +35,17 @@ public class UserController { } @PostMapping("/login") - public void login(@RequestBody LoginDTO loginDTO) { - authenticationService.login(loginDTO.username(), loginDTO.password()); + public void login(@RequestBody @Valid LoginDTO loginDTO) { + authenticationService.login(loginDTO.getUsername(), loginDTO.getPassword()); + } + + @PostMapping("/register") + public void post(@RequestBody @Valid RegistrationDTO registrationDTO) { + userService.registerUser(registrationDTO); + } + + @GetMapping("/get-all") + public List getAllUsers() { + return userService.getAllUsers(); } } diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/ErrorResponse.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/ErrorResponse.java similarity index 84% rename from server/src/main/java/ru/tubryansk/tdms/dto/ErrorResponse.java rename to server/src/main/java/ru/tubryansk/tdms/controller/payload/ErrorResponse.java index 8981986..c5f94ff 100644 --- a/server/src/main/java/ru/tubryansk/tdms/dto/ErrorResponse.java +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/ErrorResponse.java @@ -1,4 +1,4 @@ -package ru.tubryansk.tdms.dto; +package ru.tubryansk.tdms.controller.payload; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -8,7 +8,7 @@ public record ErrorResponse(String message, ErrorCode errorCode) { @RequiredArgsConstructor @Getter public enum ErrorCode { - BAD_REQUEST(HttpStatus.BAD_REQUEST), + BUSINESS_ERROR(HttpStatus.BAD_REQUEST), VALIDATION_ERROR(HttpStatus.BAD_REQUEST), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), NOT_FOUND(HttpStatus.NOT_FOUND), diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/payload/GroupDTO.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/GroupDTO.java new file mode 100644 index 0000000..bbe8931 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/GroupDTO.java @@ -0,0 +1,14 @@ +package ru.tubryansk.tdms.controller.payload; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class GroupDTO { + private String name; + private String principalName; + private Boolean isMePrincipal; +} diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/payload/LoginDTO.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/LoginDTO.java new file mode 100644 index 0000000..3d12fd5 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/LoginDTO.java @@ -0,0 +1,12 @@ +package ru.tubryansk.tdms.controller.payload; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; + +@Getter +public class LoginDTO { + @NotEmpty(message = "Логин не может быть пустым") + private String username; + @NotEmpty(message = "Пароль не может быть пустым") + private String password; +} \ No newline at end of file diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/payload/RegistrationDTO.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/RegistrationDTO.java new file mode 100644 index 0000000..6ad9060 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/RegistrationDTO.java @@ -0,0 +1,34 @@ +package ru.tubryansk.tdms.controller.payload; + +import jakarta.validation.constraints.*; +import lombok.Getter; +import org.hibernate.validator.constraints.Length; + +@Getter +public class RegistrationDTO { + @NotEmpty(message = "Логин не может быть пустым") + @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Логин должен содержать только латинские буквы, цифры и знак подчеркивания") + @Size(min = 5, message = "Логин должен содержать минимум 5 символов") + @Size(max = 32, message = "Логин должен содержать максимум 32 символов") + private String login; + @NotEmpty(message = "Пароль не может быть пустым") + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "Пароль должен содержать хотя бы одну цифру, одну заглавную и одну строчную букву, минимум 8 символов") + private String password; + @NotEmpty(message = "Имя не может быть пустым") + @Length(min = 3, message = "Имя должно содержать минимум 3 символа") + @Pattern(regexp = "^[a-zA-Zа-яА-ЯёЁ\\s]+$", message = "Имя должно содержать только буквы английского или русского алфавита и пробелы") + private String fullName; + @NotNull(message = "Почта не может быть пустой") + @Email(message = "Почта должна быть валидным адресом электронной почты") + private String email; + @NotNull(message = "Номер телефона не может быть пустым") + @Pattern(regexp = "^\\+[1-9]\\d{6,14}$", message = "Номер телефона должен начинаться с '+' и содержать от 7 до 15 цифр") + private String numberPhone; + private StudentRegistrationDTO studentData; + + @Getter + public static class StudentRegistrationDTO { + @NotNull(message = "Группа не может быть пустой") + private Long groupId; + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/RoleDTO.java similarity index 89% rename from server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java rename to server/src/main/java/ru/tubryansk/tdms/controller/payload/RoleDTO.java index dc9ebea..bcf67b1 100644 --- a/server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/RoleDTO.java @@ -1,4 +1,4 @@ -package ru.tubryansk.tdms.dto; +package ru.tubryansk.tdms.controller.payload; import ru.tubryansk.tdms.entity.Role; diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/payload/StudentDTO.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/StudentDTO.java new file mode 100644 index 0000000..48e144d --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/StudentDTO.java @@ -0,0 +1,50 @@ +package ru.tubryansk.tdms.controller.payload; + + +import lombok.Data; +import ru.tubryansk.tdms.entity.Student; + + +@Data +public class StudentDTO { + // private Boolean form; + // private Integer protectionOrder; + // private String magistracy; + // private Boolean digitalFormatPresent; + // private Integer markComment; + // private Integer markPractice; + // private String predefenceComment; + // private String normalControl; + // private Integer antiPlagiarism; + // private String note; + // private Boolean recordBookReturned; + // private String work; + // private UserDTO user; + // private String diplomaTopic; + // private UserDTO mentorUser; + // private GroupDTO group; + + public static StudentDTO from(Student student) { + StudentDTO studentDTO = new StudentDTO(); + // studentDTO.setForm(student.getForm()); + // return studentDTO; + // student.getForm(), + // student.getProtectionOrder(), + // student.getMagistracy(), + // student.getDigitalFormatPresent(), + // student.getMarkComment(), + // student.getMarkPractice(), + // student.getPredefenceComment(), + // student.getNormalControl(), + // student.getAntiPlagiarism(), + // student.getNote(), + // student.getRecordBookReturned(), + // student.getWork(), + // UserDTO.from(student.getUser()), + // student.getDiplomaTopic().getName(), + // UserDTO.from(student.getMentorUser()), + // GroupDTO.from(student.getGroup()) + // ); + return studentDTO; + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java b/server/src/main/java/ru/tubryansk/tdms/controller/payload/UserDTO.java similarity index 81% rename from server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java rename to server/src/main/java/ru/tubryansk/tdms/controller/payload/UserDTO.java index 9a24e53..19bb83c 100644 --- a/server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java +++ b/server/src/main/java/ru/tubryansk/tdms/controller/payload/UserDTO.java @@ -1,4 +1,4 @@ -package ru.tubryansk.tdms.dto; +package ru.tubryansk.tdms.controller.payload; import com.fasterxml.jackson.annotation.JsonInclude; @@ -14,7 +14,6 @@ import java.util.List; public record UserDTO( boolean authenticated, String login, - String password, String fullName, String email, String phone, @@ -28,13 +27,12 @@ public record UserDTO( .build(); } - public static UserDTO from(User user, boolean anonymize) { + public static UserDTO from(User user) { return UserDTO.builder() .authenticated(true) .login(user.getLogin()) - .password(anonymize ? "" : user.getPassword()) .fullName(user.getFullName()) - .email(user.getMail()) + .email(user.getEmail()) .phone(user.getNumberPhone()) .createdAt(user.getCreatedAt()) .updatedAt(user.getUpdatedAt()) diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java deleted file mode 100644 index 9458955..0000000 --- a/server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.tubryansk.tdms.dto; - -import ru.tubryansk.tdms.entity.Group; - -public record GroupDTO(String name, UserDTO principalUser) { - - public static GroupDTO from(Group group) { - return new GroupDTO( - group.getName(), - UserDTO.from(group.getPrincipalUser(), true) - ); - } -} diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/LoginDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/LoginDTO.java deleted file mode 100644 index a2810c3..0000000 --- a/server/src/main/java/ru/tubryansk/tdms/dto/LoginDTO.java +++ /dev/null @@ -1,6 +0,0 @@ -package ru.tubryansk.tdms.dto; - -public record LoginDTO( - String username, - String password) { -} \ No newline at end of file diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java deleted file mode 100644 index 726d623..0000000 --- a/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java +++ /dev/null @@ -1,51 +0,0 @@ -package ru.tubryansk.tdms.dto; - - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import ru.tubryansk.tdms.entity.Student; - - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class StudentDTO { - private Boolean form; - private Integer protectionOrder; - private String magistracy; - private Boolean digitalFormatPresent; - private Integer markComment; - private Integer markPractice; - private String predefenceComment; - private String normalControl; - private Integer antiPlagiarism; - private String note; - private Boolean recordBookReturned; - private String work; - private UserDTO user; - private String diplomaTopic; - private UserDTO mentorUser; - private GroupDTO group; - - public static StudentDTO from(Student student) { - return new StudentDTO( - student.getForm(), - student.getProtectionOrder(), - student.getMagistracy(), - student.getDigitalFormatPresent(), - student.getMarkComment(), - student.getMarkPractice(), - student.getPredefenceComment(), - student.getNormalControl(), - student.getAntiPlagiarism(), - student.getNote(), - student.getRecordBookReturned(), - student.getWork(), - UserDTO.from(student.getUser(), true), - student.getDiplomaTopic().getName(), - UserDTO.from(student.getMentorUser(), true), - GroupDTO.from(student.getGroup()) - ); - } -} diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/DiplomaTopic.java b/server/src/main/java/ru/tubryansk/tdms/entity/DiplomaTopic.java index 616a46e..dc08e04 100644 --- a/server/src/main/java/ru/tubryansk/tdms/entity/DiplomaTopic.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/DiplomaTopic.java @@ -2,17 +2,15 @@ package ru.tubryansk.tdms.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; -@Entity @Getter @Setter -@NoArgsConstructor -@AllArgsConstructor +@ToString +@Entity @Table(name = "diploma_topic") public class DiplomaTopic { @Id @@ -22,3 +20,4 @@ public class DiplomaTopic { @Column(name = "name") private String name; } + diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/Group.java b/server/src/main/java/ru/tubryansk/tdms/entity/Group.java index 218c15d..99eea52 100644 --- a/server/src/main/java/ru/tubryansk/tdms/entity/Group.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/Group.java @@ -2,17 +2,15 @@ package ru.tubryansk.tdms.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; -@Entity @Getter @Setter -@NoArgsConstructor -@AllArgsConstructor +@ToString +@Entity @Table(name = "group") public class Group { @Id @@ -21,7 +19,7 @@ public class Group { private Long id; @Column(name = "name") private String name; - @ManyToOne() - @JoinColumn(name = "principal_user_id") - private User principalUser; + @ManyToOne + @JoinColumn(name = "curator_user_id") + private User groupCurator; } diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/Role.java b/server/src/main/java/ru/tubryansk/tdms/entity/Role.java index d9ad154..77dd152 100644 --- a/server/src/main/java/ru/tubryansk/tdms/entity/Role.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/Role.java @@ -1,25 +1,22 @@ package ru.tubryansk.tdms.entity; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.ToString; import org.springframework.security.core.GrantedAuthority; - -@Entity @Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor +@ToString +@Entity @Table(name = "`role`") public class Role implements GrantedAuthority { @Id @Column(name = "id") - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name") private String name; diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/Student.java b/server/src/main/java/ru/tubryansk/tdms/entity/Student.java index 720b2b0..eae5aa1 100644 --- a/server/src/main/java/ru/tubryansk/tdms/entity/Student.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/Student.java @@ -2,10 +2,14 @@ package ru.tubryansk.tdms.entity; import jakarta.persistence.*; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; -@Data +@Getter +@Setter +@ToString @Entity @Table(name = "student") public class Student { diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/User.java b/server/src/main/java/ru/tubryansk/tdms/entity/User.java index e0f11b0..c76df19 100644 --- a/server/src/main/java/ru/tubryansk/tdms/entity/User.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/User.java @@ -2,10 +2,11 @@ package ru.tubryansk.tdms.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -15,11 +16,10 @@ import java.util.Collection; import java.util.List; -@Entity @Getter @Setter -@NoArgsConstructor -@AllArgsConstructor +@ToString +@Entity @Table(name = "`user`") public class User implements UserDetails { @Id @@ -32,18 +32,21 @@ public class User implements UserDetails { private String password; @Column(name = "full_name") private String fullName; - @Column(name = "mail") - private String mail; + @Column(name = "email") + private String email; @Column(name = "number_phone") private String numberPhone; @Column(name = "created_at") + @CreationTimestamp private ZonedDateTime createdAt; @Column(name = "updated_at") + @UpdateTimestamp private ZonedDateTime updatedAt; - @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) - @JoinTable(name = "user_role", - joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), - inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) + @ManyToMany + @JoinTable( + name = "user_role", + joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) private List roles; @Override diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/repository/DefenceRepository.java b/server/src/main/java/ru/tubryansk/tdms/entity/repository/DefenceRepository.java new file mode 100644 index 0000000..cbe22e5 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/entity/repository/DefenceRepository.java @@ -0,0 +1,4 @@ +package ru.tubryansk.tdms.entity.repository; + +public interface DefenceRepository { +} diff --git a/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java b/server/src/main/java/ru/tubryansk/tdms/entity/repository/DiplomaTopicRepository.java similarity index 91% rename from server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java rename to server/src/main/java/ru/tubryansk/tdms/entity/repository/DiplomaTopicRepository.java index 1ce61e6..aa9a364 100644 --- a/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/repository/DiplomaTopicRepository.java @@ -1,4 +1,4 @@ -package ru.tubryansk.tdms.repository; +package ru.tubryansk.tdms.entity.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/repository/GroupRepository.java b/server/src/main/java/ru/tubryansk/tdms/entity/repository/GroupRepository.java new file mode 100644 index 0000000..f874a35 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/entity/repository/GroupRepository.java @@ -0,0 +1,13 @@ +package ru.tubryansk.tdms.entity.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.tubryansk.tdms.entity.Group; +import ru.tubryansk.tdms.exception.NotFoundException; + +@Repository +public interface GroupRepository extends JpaRepository { + default Group findByIdThrow(Long id) { + return this.findById(id).orElseThrow(() -> new NotFoundException(Group.class, id)); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/repository/RoleRepository.java b/server/src/main/java/ru/tubryansk/tdms/entity/repository/RoleRepository.java new file mode 100644 index 0000000..5d11c42 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/entity/repository/RoleRepository.java @@ -0,0 +1,9 @@ +package ru.tubryansk.tdms.entity.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.tubryansk.tdms.entity.Role; + +@Repository +public interface RoleRepository extends JpaRepository { +} diff --git a/server/src/main/java/ru/tubryansk/tdms/repository/StudentRepository.java b/server/src/main/java/ru/tubryansk/tdms/entity/repository/StudentRepository.java similarity index 92% rename from server/src/main/java/ru/tubryansk/tdms/repository/StudentRepository.java rename to server/src/main/java/ru/tubryansk/tdms/entity/repository/StudentRepository.java index c41a842..d7bc259 100644 --- a/server/src/main/java/ru/tubryansk/tdms/repository/StudentRepository.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/repository/StudentRepository.java @@ -1,4 +1,4 @@ -package ru.tubryansk.tdms.repository; +package ru.tubryansk.tdms.entity.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/server/src/main/java/ru/tubryansk/tdms/repository/UserRepository.java b/server/src/main/java/ru/tubryansk/tdms/entity/repository/UserRepository.java similarity index 70% rename from server/src/main/java/ru/tubryansk/tdms/repository/UserRepository.java rename to server/src/main/java/ru/tubryansk/tdms/entity/repository/UserRepository.java index 8abd893..8abd792 100644 --- a/server/src/main/java/ru/tubryansk/tdms/repository/UserRepository.java +++ b/server/src/main/java/ru/tubryansk/tdms/entity/repository/UserRepository.java @@ -1,7 +1,6 @@ -package ru.tubryansk.tdms.repository; +package ru.tubryansk.tdms.entity.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.security.core.userdetails.UserDetails; import ru.tubryansk.tdms.entity.User; import java.util.Optional; diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java b/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java index 8c28bef..02d2410 100644 --- a/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java +++ b/server/src/main/java/ru/tubryansk/tdms/exception/AccessDeniedException.java @@ -1,6 +1,6 @@ package ru.tubryansk.tdms.exception; -import ru.tubryansk.tdms.dto.ErrorResponse; +import ru.tubryansk.tdms.controller.payload.ErrorResponse; public class AccessDeniedException extends BusinessException { public AccessDeniedException() { diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java b/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java index 10dbe2b..652afa7 100644 --- a/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java +++ b/server/src/main/java/ru/tubryansk/tdms/exception/BusinessException.java @@ -1,6 +1,6 @@ package ru.tubryansk.tdms.exception; -import ru.tubryansk.tdms.dto.ErrorResponse; +import ru.tubryansk.tdms.controller.payload.ErrorResponse; public class BusinessException extends RuntimeException { public BusinessException(String message) { @@ -8,6 +8,6 @@ public class BusinessException extends RuntimeException { } public ErrorResponse.ErrorCode getErrorCode() { - return ErrorResponse.ErrorCode.INTERNAL_ERROR; + return ErrorResponse.ErrorCode.BUSINESS_ERROR; } } diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java b/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java index 288e4d5..781e9f1 100644 --- a/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/ru/tubryansk/tdms/exception/GlobalExceptionHandler.java @@ -2,42 +2,55 @@ package ru.tubryansk.tdms.exception; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.resource.NoResourceFoundException; -import ru.tubryansk.tdms.dto.ErrorResponse; +import ru.tubryansk.tdms.controller.payload.ErrorResponse; + +import java.util.stream.Collectors; @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { - @ExceptionHandler(MethodArgumentNotValidException.class) + @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - // todo: make a better error message - return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.VALIDATION_ERROR); + public ErrorResponse handleMethodArgumentNotValidException(BindException e) { + log.debug("Validation error: {}", e.getMessage()); + String validationErrors = e.getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + return new ErrorResponse(validationErrors, ErrorResponse.ErrorCode.VALIDATION_ERROR); } @ExceptionHandler(BusinessException.class) public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) { + log.info("Business error", e); response.setStatus(e.getErrorCode().getHttpStatus().value()); return new ErrorResponse(e.getMessage(), e.getErrorCode()); } + @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ErrorResponse handleAccessDeniedException(AccessDeniedException e) { + log.debug("Access denied", e); + return new ErrorResponse("", ErrorResponse.ErrorCode.ACCESS_DENIED); + } + + @ExceptionHandler(NoResourceFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) { - // todo: make error page + log.error("Resource not found", e); return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleUnexpectedException(Exception e) { - // todo: make error page log.error("Unexpected exception.", e); return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR); } diff --git a/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java b/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java index 1fd0cfa..19ce606 100644 --- a/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java +++ b/server/src/main/java/ru/tubryansk/tdms/exception/NotFoundException.java @@ -1,10 +1,10 @@ package ru.tubryansk.tdms.exception; -import ru.tubryansk.tdms.dto.ErrorResponse; +import ru.tubryansk.tdms.controller.payload.ErrorResponse; public class NotFoundException extends BusinessException { - public NotFoundException(Class entityClass, Integer id) { - super(entityClass.getSimpleName() + " with id " + id + " not found"); + public NotFoundException(Class entityClass, Object id) { + super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден"); } @Override diff --git a/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java b/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java index 5b12321..761c0fa 100644 --- a/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java +++ b/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java @@ -8,6 +8,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.tubryansk.tdms.entity.User; import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; @@ -32,6 +33,7 @@ public class AuthenticationService { } } + @Transactional public void login(String username, String password) { try { var context = SecurityContextHolder.createEmptyContext(); @@ -41,8 +43,8 @@ public class AuthenticationService { SecurityContextHolder.setContext(context); request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); } catch (Exception e) { - log.error("Failed to log in user: {}. {}", username, e.getMessage()); - return; + log.error("Failed to log in user: {}", username, e); + throw e; } log.debug("User {} logged in", username); } diff --git a/server/src/main/java/ru/tubryansk/tdms/service/CallerService.java b/server/src/main/java/ru/tubryansk/tdms/service/CallerService.java index 4fdebb5..7ac9437 100644 --- a/server/src/main/java/ru/tubryansk/tdms/service/CallerService.java +++ b/server/src/main/java/ru/tubryansk/tdms/service/CallerService.java @@ -3,6 +3,7 @@ package ru.tubryansk.tdms.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import ru.tubryansk.tdms.controller.payload.UserDTO; import ru.tubryansk.tdms.entity.User; import java.util.Optional; @@ -18,4 +19,8 @@ public class CallerService { } return Optional.empty(); } + + public UserDTO getCallerUserDTO() { + return getCallerUser().map(UserDTO::from).orElse(UserDTO.unauthenticated()); + } } diff --git a/server/src/main/java/ru/tubryansk/tdms/service/GroupService.java b/server/src/main/java/ru/tubryansk/tdms/service/GroupService.java new file mode 100644 index 0000000..14d7e22 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/service/GroupService.java @@ -0,0 +1,15 @@ +package ru.tubryansk.tdms.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.tubryansk.tdms.controller.payload.GroupDTO; + +import java.util.Collection; + +@Service +@Transactional +public class GroupService { + public Collection getAllGroups() { + return null; + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/service/RoleService.java b/server/src/main/java/ru/tubryansk/tdms/service/RoleService.java new file mode 100644 index 0000000..fba7db6 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/service/RoleService.java @@ -0,0 +1,38 @@ +package ru.tubryansk.tdms.service; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.tubryansk.tdms.entity.Role; +import ru.tubryansk.tdms.entity.repository.RoleRepository; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class RoleService { + public enum Authority { + ROLE_ADMINISTRATOR, + ROLE_COMMISSION_MEMBER, + ROLE_TEACHER, + ROLE_SECRETARY, + ROLE_STUDENT, + } + + public transient Map roles; + + @Autowired + private RoleRepository roleRepository; + + @PostConstruct + @Transactional + public void init() { + roles = new ConcurrentHashMap<>(); + roleRepository.findAll().forEach(role -> roles.put(role.getAuthority(), role)); + } + + public Role getRoleByAuthority(Authority authority) { + return roles.get(authority.name()); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java b/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java index 1117798..bd41725 100644 --- a/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java +++ b/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java @@ -3,11 +3,12 @@ package ru.tubryansk.tdms.service; import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import ru.tubryansk.tdms.controller.payload.StudentDTO; import ru.tubryansk.tdms.entity.DiplomaTopic; import ru.tubryansk.tdms.entity.Student; +import ru.tubryansk.tdms.entity.repository.DiplomaTopicRepository; +import ru.tubryansk.tdms.entity.repository.StudentRepository; import ru.tubryansk.tdms.exception.AccessDeniedException; -import ru.tubryansk.tdms.repository.DiplomaTopicRepository; -import ru.tubryansk.tdms.repository.StudentRepository; import java.util.Map; import java.util.Optional; @@ -43,4 +44,13 @@ public class StudentService { public Optional getCallerStudent() { return studentRepository.findByUser(callerService.getCallerUser().orElse(null)); } + + public StudentDTO getCallerStudentDTO() { + Student callerStudent = getCallerStudent().orElse(null); + if (callerStudent == null) { + return null; + } + + return StudentDTO.from(callerStudent); + } } diff --git a/server/src/main/java/ru/tubryansk/tdms/service/SysInfoService.java b/server/src/main/java/ru/tubryansk/tdms/service/SysInfoService.java new file mode 100644 index 0000000..877c0d3 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/service/SysInfoService.java @@ -0,0 +1,14 @@ +package ru.tubryansk.tdms.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class SysInfoService { + @Value("${application.version}") + private String version; + + public String getVersion() { + return version; + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/service/UserService.java b/server/src/main/java/ru/tubryansk/tdms/service/UserService.java index b6d4b71..1485fd6 100644 --- a/server/src/main/java/ru/tubryansk/tdms/service/UserService.java +++ b/server/src/main/java/ru/tubryansk/tdms/service/UserService.java @@ -3,14 +3,21 @@ package ru.tubryansk.tdms.service; import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import ru.tubryansk.tdms.controller.payload.RegistrationDTO; +import ru.tubryansk.tdms.controller.payload.UserDTO; +import ru.tubryansk.tdms.entity.Role; +import ru.tubryansk.tdms.entity.Student; import ru.tubryansk.tdms.entity.User; -import ru.tubryansk.tdms.repository.UserRepository; +import ru.tubryansk.tdms.entity.repository.GroupRepository; +import ru.tubryansk.tdms.entity.repository.StudentRepository; +import ru.tubryansk.tdms.entity.repository.UserRepository; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; @Service @Transactional @@ -18,10 +25,75 @@ import java.util.Optional; public class UserService implements UserDetailsService { @Autowired private UserRepository userRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private StudentRepository studentRepository; + @Autowired + private RoleService roleService; + @Autowired + private PasswordEncoder passwordEncoder; @Override public User loadUserByUsername(String username) throws UsernameNotFoundException { - return userRepository.findUserByLogin(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); + log.info("Loading user with username: {}", username); + User user = userRepository.findUserByLogin(username).orElseThrow(() -> { + log.info("User with login {} not found", username); + return new UsernameNotFoundException("User with login " + username + " not found"); + }); + log.info("User with login {} loaded", username); + return user; + } + + public List getAllUsers() { + log.info("Loading all users"); + List users = userRepository.findAll().stream() + .map(UserDTO::from) + .toList(); + log.info("{} users loaded", users.size()); + return users; + } + + public void registerUser(RegistrationDTO registrationDTO) { + log.info("Registering new user with login: {}", registrationDTO.getLogin()); + User user = transientUser(registrationDTO); + Student student = transientStudent(registrationDTO.getStudentData()); + fillRoles(user, registrationDTO); + + log.info("Saving new user: {}", user); + userRepository.save(user); + if (student != null) { + student.setUser(user); + log.info("User is student, saving student: {}", student); + studentRepository.save(student); + } + } + + private User transientUser(RegistrationDTO registrationDTO) { + User user = new User(); + user.setLogin(registrationDTO.getLogin()); + user.setPassword(passwordEncoder.encode(registrationDTO.getPassword())); + user.setFullName(registrationDTO.getFullName()); + user.setEmail(registrationDTO.getEmail()); + user.setNumberPhone(registrationDTO.getNumberPhone()); + return user; + } + + private Student transientStudent(RegistrationDTO.StudentRegistrationDTO studentData) { + if (studentData == null) { + return null; + } + + Student student = new Student(); + student.setGroup(groupRepository.findByIdThrow(studentData.getGroupId())); + return student; + } + + private void fillRoles(User user, RegistrationDTO registrationDTO) { + List roles = new ArrayList<>(); + if (registrationDTO.getStudentData() != null) { + roles.add(roleService.getRoleByAuthority(RoleService.Authority.ROLE_STUDENT)); + } + user.setRoles(roles); } } diff --git a/server/src/main/java/ru/tubryansk/tdms/web/LoggingRequestFilter.java b/server/src/main/java/ru/tubryansk/tdms/web/LoggingRequestFilter.java new file mode 100644 index 0000000..0287622 --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/web/LoggingRequestFilter.java @@ -0,0 +1,29 @@ +package ru.tubryansk.tdms.web; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Slf4j +public class LoggingRequestFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + long startTime = System.currentTimeMillis(); + log.info("Making request: {}. user: {}, session: {}, remote ip: {}", + request.getRequestURI(), request.getRemoteUser(), + request.getSession().getId(), request.getRemoteAddr()); + try { + filterChain.doFilter(request, response); + } finally { + long duration = System.currentTimeMillis() - startTime; + log.info("Request finished with {} status. duration: {} ms", response.getStatus(), duration); + } + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/web/LoggingSessionListener.java b/server/src/main/java/ru/tubryansk/tdms/web/LoggingSessionListener.java new file mode 100644 index 0000000..95e96db --- /dev/null +++ b/server/src/main/java/ru/tubryansk/tdms/web/LoggingSessionListener.java @@ -0,0 +1,23 @@ +package ru.tubryansk.tdms.web; + +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class LoggingSessionListener implements HttpSessionListener { + @Override + public void sessionCreated(HttpSessionEvent se) { + log.debug("Session created: {}, user {}", + se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName()); + } + + @Override + public void sessionDestroyed(HttpSessionEvent se) { + log.debug("Session destroyed: {}, user: {}", + se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName()); + } +} diff --git a/server/src/main/java/ru/tubryansk/tdms/web/RequestLogger.java b/server/src/main/java/ru/tubryansk/tdms/web/RequestLogger.java deleted file mode 100644 index 24835b5..0000000 --- a/server/src/main/java/ru/tubryansk/tdms/web/RequestLogger.java +++ /dev/null @@ -1,27 +0,0 @@ -package ru.tubryansk.tdms.web; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.AbstractRequestLoggingFilter; - -@Component -@Slf4j -public class RequestLogger extends AbstractRequestLoggingFilter { - - @Override - protected void beforeRequest(HttpServletRequest request, String message) { - String logMessage = message + - ", method=" + request.getMethod() + - ", uri=" + request.getRequestURI() + - ", query=" + request.getQueryString() + - ", remote=" + request.getRemoteAddr() + - ", user=" + request.getRemoteUser(); - log.debug(logMessage); - } - - @Override - protected void afterRequest(HttpServletRequest request, String message) { - // do nothing - } -} diff --git a/server/src/main/resources/db/migration/V00010__Create__role_table.sql b/server/src/main/resources/db/migration/V00010__Create__role_table.sql index 9418189..84dff44 100644 --- a/server/src/main/resources/db/migration/V00010__Create__role_table.sql +++ b/server/src/main/resources/db/migration/V00010__Create__role_table.sql @@ -1,10 +1,13 @@ create table role ( id bigint primary key, + name text not null unique, authority text not null unique ); -- COMMENTS +comment on table role is 'Таблица ролей пользователей'; + comment on column role.name is 'Человекочитаемое имя роли'; comment on column role.authority is 'Имя роли в системе'; diff --git a/server/src/main/resources/db/migration/V00020__Create__user_table.sql b/server/src/main/resources/db/migration/V00020__Create__user_table.sql index 8de350a..7fdce94 100644 --- a/server/src/main/resources/db/migration/V00020__Create__user_table.sql +++ b/server/src/main/resources/db/migration/V00020__Create__user_table.sql @@ -1,20 +1,22 @@ create table "user" ( id bigserial primary key, - login text not null unique, - password text not null, - full_name text not null, - mail text not null unique, - number_phone text not null unique, + + login text not null unique, + password text not null, + full_name text not null, + email text not null unique, + number_phone text not null unique, + created_at timestamptz not null, updated_at timestamptz ); -- COMMENTS +comment on table "user" is 'Таблица пользователей'; + comment on column "user".login is 'Логин пользователя'; comment on column "user".password is 'Пароль пользователя'; comment on column "user".full_name is 'Полное имя пользователя в формате Фамилия Имя Отчество'; -comment on column "user".mail is 'Почта пользователя'; +comment on column "user".email is 'Почта пользователя'; comment on column "user".number_phone is 'Номер телефона пользователя'; -comment on column "user".created_at is 'Дата создания записи'; -comment on column "user".updated_at is 'Дата последнего обновления записи'; diff --git a/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql b/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql index 8f60378..bf084c9 100644 --- a/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql +++ b/server/src/main/resources/db/migration/V00030__Create__user_role_table.sql @@ -1,6 +1,7 @@ create table user_role ( id bigserial primary key, + user_id bigint not null, role_id bigint not null ); @@ -14,5 +15,7 @@ alter table user_role foreign key (role_id) references role (id); -- COMMENTS +comment on table user_role is 'Таблица связи пользователей и ролей'; + comment on column user_role.user_id is 'Идентификатор пользователя'; comment on column user_role.role_id is 'Идентификатор роли'; diff --git a/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql b/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql index 3d70917..4c9d3c9 100644 --- a/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql +++ b/server/src/main/resources/db/migration/V00040__Create__diploma_topic_table.sql @@ -1,8 +1,11 @@ create table diploma_topic ( id bigserial primary key, - name text not null unique + + name text not null ); -- COMMENTS +comment on table diploma_topic is 'Таблица тем дипломных работ'; + comment on column diploma_topic.name is 'Название темы дипломной работы'; diff --git a/server/src/main/resources/db/migration/V00050__Create__group_table.sql b/server/src/main/resources/db/migration/V00050__Create__group_table.sql index cc3e484..e907daa 100644 --- a/server/src/main/resources/db/migration/V00050__Create__group_table.sql +++ b/server/src/main/resources/db/migration/V00050__Create__group_table.sql @@ -1,16 +1,22 @@ create table "group" ( - id bigserial primary key, - name text not null unique, - principal_user_id bigint not null + id bigserial primary key, + + name text not null unique, + curator_user_id bigint, + + created_at timestamptz not null, + updated_at timestamptz ); -- FOREIGN KEY alter table "group" - add constraint fk_group_principal_user_id - foreign key (principal_user_id) references "user" (id) - on delete cascade; + add constraint fk_group_curator_user_id + foreign key (curator_user_id) references "user" (id) + on delete set null on update cascade; -- COMMENTS +comment on table "group" is 'Таблица групп студентов'; + comment on column "group".name is 'Название группы'; -comment on column "group".principal_user_id is 'Идентификатор куратора группы'; +comment on column "group".curator_user_id is 'Идентификатор куратора группы'; diff --git a/server/src/main/resources/db/migration/V00060__Create__student_table.sql b/server/src/main/resources/db/migration/V00060__Create__student_table.sql index edb29d0..cc9bc47 100644 --- a/server/src/main/resources/db/migration/V00060__Create__student_table.sql +++ b/server/src/main/resources/db/migration/V00060__Create__student_table.sql @@ -1,22 +1,25 @@ create table student ( id bigserial primary key, + user_id bigint not null, + diploma_topic_id bigint not null, + mentor_user_id bigint not null, + group_id bigint not null, + form boolean, - protection_order integer not null, + protection_day int, + protection_order int, magistracy text, digital_format_present boolean, - mark_comment integer, - mark_practice integer, + mark_comment int, + mark_practice int, predefence_comment text, normal_control text, anti_plagiarism int, note text, record_book_returned boolean, work text, - user_id bigint not null, - diploma_topic_id bigint not null, - mentor_user_id bigint not null, - group_id bigint not null, + created_at timestamptz not null, updated_at timestamptz ); @@ -25,22 +28,30 @@ create table student alter table student add constraint fk_student_user_id foreign key (user_id) references "user" (id) - on delete cascade; + on delete cascade on update cascade; alter table student add constraint fk_student_diploma_topic_id foreign key (diploma_topic_id) references diploma_topic (id) - on delete cascade; + on delete set null on update cascade; alter table student add constraint fk_student_mentor_user_id foreign key (mentor_user_id) references "user" (id) - on delete cascade; + on delete set null on update cascade; alter table student add constraint fk_student_group_id foreign key (group_id) references "group" (id) - on delete cascade; + on delete set null on update cascade; -- COMMENTS +comment on table student is 'Таблица студентов'; + +comment on column student.user_id is 'Идентификатор пользователя'; +comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы'; +comment on column student.mentor_user_id is 'Идентификатор научного руководителя'; +comment on column student.group_id is 'Идентификатор группы'; + comment on column student.form is 'Форма обучения'; +comment on column student.protection_day is 'День защиты'; comment on column student.protection_order is 'Порядок защиты'; comment on column student.magistracy is 'Магистратура'; comment on column student.digital_format_present is 'Предоставлен в электронном виде'; @@ -52,7 +63,3 @@ comment on column student.anti_plagiarism is 'Антиплагиат'; comment on column student.note is 'Примечание'; comment on column student.record_book_returned is 'Ведомость возвращена'; comment on column student.work is 'Работа'; -comment on column student.user_id is 'Идентификатор пользователя'; -comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы'; -comment on column student.mentor_user_id is 'Идентификатор научного руководителя'; -comment on column student.group_id is 'Идентификатор группы'; diff --git a/server/src/main/resources/db/migration/V00510__Insert_administrator.sql b/server/src/main/resources/db/migration/V00510__Insert_administrator.sql index 0c23f02..3317163 100644 --- a/server/src/main/resources/db/migration/V00510__Insert_administrator.sql +++ b/server/src/main/resources/db/migration/V00510__Insert_administrator.sql @@ -1,5 +1,5 @@ -insert into "user" (id, login, password, full_name, mail, number_phone, created_at) -values (1, 'admin', '{noop}admin', 'Администратор', 'admin@localhost', '+79110000000', now()); +insert into "user" (id, login, password, full_name, email, number_phone, created_at) +values (1, 'admin', '{noop}admin', 'Администратор', 'admin@tdms.tu-byransk.ru', '', now()); insert into user_role (id, user_id, role_id) values (1, 1, 4); diff --git a/web/package-lock.json b/web/package-lock.json index e1a0ae9..b71c94c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,14 +13,17 @@ "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", + "@types/lodash": "^4.17.15", "axios": "^1.7.7", "bootstrap": "^5.3.3", + "lodash": "^4.17.21", "mobx": "^6.13.1", "mobx-react": "^9.1.1", "mobx-state-router": "^6.0.1", "react": "^18.2.0", "react-bootstrap": "^2.10.4", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "uuid": "^11.0.5" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.25.7", @@ -2105,6 +2108,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3858,20 +3867,6 @@ "node": ">= 0.6" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4591,7 +4586,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -5974,6 +5969,16 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6454,12 +6459,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/value-equal": { diff --git a/web/package.json b/web/package.json index 1fb0428..63c358b 100644 --- a/web/package.json +++ b/web/package.json @@ -12,14 +12,17 @@ "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", + "@types/lodash": "^4.17.15", "axios": "^1.7.7", "bootstrap": "^5.3.3", + "lodash": "^4.17.21", "mobx": "^6.13.1", "mobx-react": "^9.1.1", "mobx-state-router": "^6.0.1", "react": "^18.2.0", "react-bootstrap": "^2.10.4", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "uuid": "^11.0.5" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.25.7", diff --git a/web/src/Application.tsx b/web/src/Application.tsx index b827e65..f678a52 100644 --- a/web/src/Application.tsx +++ b/web/src/Application.tsx @@ -3,7 +3,7 @@ import './index.css' import 'bootstrap/dist/css/bootstrap.min.css'; import {RouterContext, RouterView} from "mobx-state-router"; import {initApp} from "./utils/init"; -import {RootStoreContext} from './context/RootStoreContext'; +import {RootStoreContext} from './store/RootStoreContext'; import {viewMap} from "./router/viewMap"; const rootStore = initApp(); diff --git a/web/src/components/NotificationContainer.tsx b/web/src/components/NotificationContainer.tsx new file mode 100644 index 0000000..f85f22f --- /dev/null +++ b/web/src/components/NotificationContainer.tsx @@ -0,0 +1,89 @@ +import {ComponentContext} from "../utils/ComponentContext"; +import {observer} from "mobx-react"; +import {Notification, NotificationType} from "../store/NotificationStore"; +import {Card, CardBody, CardHeader, CardText, CardTitle, Col, Row} from "react-bootstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {action, makeObservable} from "mobx"; + +@observer +export class NotificationContainer extends ComponentContext { + forEachNotificationRender(notifications: Notification[], type: NotificationType) { + return notifications.map(notification => ( + + )); + } + + render() { + return
+ {this.forEachNotificationRender(this.notificationStore.errors, NotificationType.ERROR)} + {this.forEachNotificationRender(this.notificationStore.successes, NotificationType.SUCCESS)} + {this.forEachNotificationRender(this.notificationStore.warnings, NotificationType.WARNING)} + {this.forEachNotificationRender(this.notificationStore.infos, NotificationType.INFO)} +
+ } +} + +@observer +class NotificationPopup extends ComponentContext<{ notification: Notification, type: NotificationType }> { + + constructor(props: { notification: Notification, type: NotificationType }) { + super(props); + makeObservable(this); + } + + @action.bound + close() { + this.notificationStore.close(this.props.notification.uuid); + } + + get cardClassName() { + switch (this.props.type) { + case NotificationType.ERROR: + return 'text-bg-danger'; + case NotificationType.WARNING: + return 'text-bg-warning'; + case NotificationType.INFO: + return 'text-bg-info'; + case NotificationType.SUCCESS: + return 'text-bg-success'; + } + } + + render() { + const hasTitle = !!this.props.notification.title && this.props.notification.title.length > 0; + const closeIcon = ; + + return + { + hasTitle && + + + + + {this.props.notification.title} + + + {closeIcon} + + + + + } + + + + + {this.props.notification.message} + + { + !hasTitle && + + {closeIcon} + + } + + + + + } +} \ No newline at end of file diff --git a/web/src/components/custom/DataTable.tsx b/web/src/components/custom/DataTable.tsx new file mode 100644 index 0000000..1dd04ba --- /dev/null +++ b/web/src/components/custom/DataTable.tsx @@ -0,0 +1,147 @@ +import {ComponentContext} from "../../utils/ComponentContext"; +import {TableDescriptor} from "../../utils/tables"; +import {observer} from "mobx-react"; +import {action, makeObservable} from "mobx"; +import {FormSelect, Pagination, Table} from "react-bootstrap"; + +export interface DataTableProps { + tableDescriptor: TableDescriptor; +} + +@observer +export class DataTable extends ComponentContext> { + constructor(props: DataTableProps) { + super(props); + makeObservable(this); + } + + header() { + return + {this.props.tableDescriptor.columns.map(column => {column.title})} + + } + + body() { + const firstColumnKey = this.props.tableDescriptor.columns[0].key; + return this.props.tableDescriptor.data.map(row => { + const rowAny = row as any; + return + { + this.props.tableDescriptor.columns.map(column => { + return + {column.format(rowAny[column.key])} + + }) + } + + }); + } + + isFirstPage() { + if (typeof this.props.tableDescriptor.page === 'undefined') { + return true; + } + + return this.props.tableDescriptor.page === 0; + } + + isLastPage() { + if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') { + return true; + } + + return this.props.tableDescriptor.page === (this.props.tableDescriptor.data.length / this.props.tableDescriptor.pageSize); + } + + @action.bound + goFirstPage() { + if (typeof this.props.tableDescriptor.page === 'undefined') { + return; + } + + this.props.tableDescriptor.page = 0; + } + + @action.bound + goLastPage() { + if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') { + return; + } + + this.props.tableDescriptor.page = this.props.tableDescriptor.data.length / this.props.tableDescriptor.pageSize; + } + + @action.bound + goNextPage() { + if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') { + return; + } + + this.props.tableDescriptor.page++; + } + + @action.bound + goPrevPage() { + if (typeof this.props.tableDescriptor.page === 'undefined') { + return; + } + + this.props.tableDescriptor.page--; + } + + @action.bound + changePageSize(e: any) { + this.props.tableDescriptor.pageSize = parseInt(e.target.value); + } + + footer() { + const table = this.props.tableDescriptor; + if (typeof table.page === 'undefined' || typeof table.pageSize === 'undefined') { + return null; + } + + return + +
+
+ + + + + {this.props.tableDescriptor.page} + + + + + + + + + + +
+ + + } + + render() { + const table = this.props.tableDescriptor; + return + + {this.header()} + + + {this.body()} + + { + table.pageable && + + {this.footer()} + + } +
+ } +} \ No newline at end of file diff --git a/web/src/components/custom/controls/ReactiveControls.css b/web/src/components/custom/controls/ReactiveControls.css new file mode 100644 index 0000000..9efa40b --- /dev/null +++ b/web/src/components/custom/controls/ReactiveControls.css @@ -0,0 +1,3 @@ +.l-no-bg label::after { + background-color: rgba(0, 0, 0, 0) !important; +} \ No newline at end of file diff --git a/web/src/components/custom/controls/ReactiveControls.tsx b/web/src/components/custom/controls/ReactiveControls.tsx new file mode 100644 index 0000000..14b381d --- /dev/null +++ b/web/src/components/custom/controls/ReactiveControls.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import {ReactiveValue} from "../../../utils/reactive/reactiveValue"; +import {observer} from "mobx-react"; +import {action, makeObservable, observable} from "mobx"; +import {Button, ButtonGroup, FloatingLabel, FormControl, FormText, ToggleButton} from "react-bootstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import './ReactiveControls.css'; + +export interface ReactiveInputProps { + value: ReactiveValue; + label?: string; + disabled?: boolean; + className?: string; +} + +@observer +export class StringInput extends React.Component> { + constructor(props: any) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto(''); + } + this.props.value.setField(this.props.label); + } + + @action.bound + onChange(event: React.ChangeEvent) { + this.props.value.set(event.currentTarget.value); + } + + render() { + return
+ {/*todo: disable background-color for label*/} + + + + +
+ } +} + +@observer +export class PasswordInput extends React.Component> { + @observable showPassword = false; + + constructor(props: any) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto(''); + } + this.props.value.setField(this.props.label); + } + + @action.bound + onChange(event: React.ChangeEvent) { + this.props.value.set(event.currentTarget.value); + } + + @action.bound + toggleShowPassword() { + this.showPassword = !this.showPassword; + } + + render() { + return
+
+ + + + +
+ +
+ } +} + +@observer +export class SelectButtonInput extends React.Component> { + constructor(props: any) { + super(props); + makeObservable(this); + if (this.props.value.value === undefined) { + this.props.value.setAuto(''); + } + this.props.value.setField(this.props.label); + } + + @action.bound + onChange(event: React.ChangeEvent) { + this.props.value.set(event.currentTarget.value); + } + + render() { + return <> + + + + + + + } +} \ No newline at end of file diff --git a/web/src/components/page/layout/DefaultPage.tsx b/web/src/components/layout/DefaultPage.tsx similarity index 59% rename from web/src/components/page/layout/DefaultPage.tsx rename to web/src/components/layout/DefaultPage.tsx index 21f8ac7..8c377bc 100644 --- a/web/src/components/page/layout/DefaultPage.tsx +++ b/web/src/components/layout/DefaultPage.tsx @@ -1,42 +1,40 @@ -import {Component, ReactNode} from "react"; +import {ReactNode} from "react"; import {Container} from "react-bootstrap"; -import Footer from "./Footer"; import Header from "./Header"; -import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {observer} from "mobx-react"; +import {ComponentContext} from "../../utils/ComponentContext"; +import {NotificationContainer} from "../NotificationContainer"; +import {Footer} from "./Footer"; -@observer -class DefaultPage extends Component { +export abstract class DefaultPage extends ComponentContext { get page(): ReactNode { throw new Error('This is not abstract method, ' + 'because mobx cant handle abstract methods. ' + 'Please override this method in child class. ' + 'Do not call it directly.'); } - declare context: RootStoreContextType; - static contextType = RootStoreContext; render() { - let isLoading = this.context.pendingStore.isThinking(); + const thinking = this.thinkStore.isThinking(); return <>
{ - isLoading && + thinking &&
} { - !isLoading && - this.page + !thinking && + <> + + {this.page} + }