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} + }