feature/profile-diploma-topic #1
@ -1,8 +1,6 @@
|
|||||||
package ru.tubryansk.tdms.config;
|
package ru.tubryansk.tdms.config;
|
||||||
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
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.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
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.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@ -42,7 +39,7 @@ public class SecurityConfiguration {
|
|||||||
) throws Exception {
|
) throws Exception {
|
||||||
return httpSecurity
|
return httpSecurity
|
||||||
.authorizeHttpRequests(this::configureHttpAuthorization)
|
.authorizeHttpRequests(this::configureHttpAuthorization)
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable) /* todo: настроить csrf */
|
||||||
.cors(a -> a.configurationSource(cors))
|
.cors(a -> a.configurationSource(cors))
|
||||||
.authenticationManager(authenticationManager)
|
.authenticationManager(authenticationManager)
|
||||||
.sessionManagement(cfg -> {
|
.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
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
|
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
|
||||||
return new ProviderManager(authenticationProvider(userDetailsService));
|
return new ProviderManager(authenticationProvider(userDetailsService));
|
||||||
@ -102,15 +84,22 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
|
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
|
||||||
/* API ROUTES */
|
/* SysInfoController */
|
||||||
|
httpAuthorization.requestMatchers("/api/v1/sysinfo/**").permitAll();
|
||||||
|
/* UserController */
|
||||||
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
|
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
|
||||||
httpAuthorization.requestMatchers("/api/v1/user/login").anonymous();
|
httpAuthorization.requestMatchers("/api/v1/user/login").anonymous();
|
||||||
httpAuthorization.requestMatchers("/api/v1/user/current").permitAll();
|
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();
|
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();
|
httpAuthorization.requestMatchers("/api/**").denyAll();
|
||||||
/* STATIC ROUTES */
|
/* since api already blocked, all other requests are static resources */
|
||||||
httpAuthorization.requestMatchers("/**").permitAll();
|
httpAuthorization.requestMatchers("/**").permitAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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<GroupDTO> getAllGroups() {
|
||||||
|
return groupService.getAllGroups();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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;
|
import ru.tubryansk.tdms.service.StudentService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -15,6 +15,6 @@ public class StudentController {
|
|||||||
|
|
||||||
@GetMapping("/current")
|
@GetMapping("/current")
|
||||||
public StudentDTO getCurrentStudent() {
|
public StudentDTO getCurrentStudent() {
|
||||||
return studentService.getCallerStudent().map(StudentDTO::from).orElse(null);
|
return studentService.getCallerStudentDTO();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,17 @@
|
|||||||
package ru.tubryansk.tdms.controller;
|
package ru.tubryansk.tdms.controller;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import ru.tubryansk.tdms.dto.LoginDTO;
|
import ru.tubryansk.tdms.controller.payload.LoginDTO;
|
||||||
import ru.tubryansk.tdms.dto.UserDTO;
|
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.AuthenticationService;
|
||||||
import ru.tubryansk.tdms.service.CallerService;
|
import ru.tubryansk.tdms.service.CallerService;
|
||||||
|
import ru.tubryansk.tdms.service.UserService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/user")
|
@RequestMapping("/api/v1/user")
|
||||||
@ -16,10 +21,12 @@ public class UserController {
|
|||||||
private AuthenticationService authenticationService;
|
private AuthenticationService authenticationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private CallerService callerService;
|
private CallerService callerService;
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
@GetMapping("/current")
|
@GetMapping("/current")
|
||||||
public UserDTO getCurrentUser() {
|
public UserDTO getCurrentUser() {
|
||||||
return callerService.getCallerUser().map(user -> UserDTO.from(user, true)).orElse(UserDTO.unauthenticated());
|
return callerService.getCallerUserDTO();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
@ -28,7 +35,17 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public void login(@RequestBody LoginDTO loginDTO) {
|
public void login(@RequestBody @Valid LoginDTO loginDTO) {
|
||||||
authenticationService.login(loginDTO.username(), loginDTO.password());
|
authenticationService.login(loginDTO.getUsername(), loginDTO.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public void post(@RequestBody @Valid RegistrationDTO registrationDTO) {
|
||||||
|
userService.registerUser(registrationDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get-all")
|
||||||
|
public List<UserDTO> getAllUsers() {
|
||||||
|
return userService.getAllUsers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package ru.tubryansk.tdms.dto;
|
package ru.tubryansk.tdms.controller.payload;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -8,7 +8,7 @@ public record ErrorResponse(String message, ErrorCode errorCode) {
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public enum ErrorCode {
|
public enum ErrorCode {
|
||||||
BAD_REQUEST(HttpStatus.BAD_REQUEST),
|
BUSINESS_ERROR(HttpStatus.BAD_REQUEST),
|
||||||
VALIDATION_ERROR(HttpStatus.BAD_REQUEST),
|
VALIDATION_ERROR(HttpStatus.BAD_REQUEST),
|
||||||
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
NOT_FOUND(HttpStatus.NOT_FOUND),
|
NOT_FOUND(HttpStatus.NOT_FOUND),
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package ru.tubryansk.tdms.dto;
|
package ru.tubryansk.tdms.controller.payload;
|
||||||
|
|
||||||
|
|
||||||
import ru.tubryansk.tdms.entity.Role;
|
import ru.tubryansk.tdms.entity.Role;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package ru.tubryansk.tdms.dto;
|
package ru.tubryansk.tdms.controller.payload;
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
@ -14,7 +14,6 @@ import java.util.List;
|
|||||||
public record UserDTO(
|
public record UserDTO(
|
||||||
boolean authenticated,
|
boolean authenticated,
|
||||||
String login,
|
String login,
|
||||||
String password,
|
|
||||||
String fullName,
|
String fullName,
|
||||||
String email,
|
String email,
|
||||||
String phone,
|
String phone,
|
||||||
@ -28,13 +27,12 @@ public record UserDTO(
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserDTO from(User user, boolean anonymize) {
|
public static UserDTO from(User user) {
|
||||||
return UserDTO.builder()
|
return UserDTO.builder()
|
||||||
.authenticated(true)
|
.authenticated(true)
|
||||||
.login(user.getLogin())
|
.login(user.getLogin())
|
||||||
.password(anonymize ? "" : user.getPassword())
|
|
||||||
.fullName(user.getFullName())
|
.fullName(user.getFullName())
|
||||||
.email(user.getMail())
|
.email(user.getEmail())
|
||||||
.phone(user.getNumberPhone())
|
.phone(user.getNumberPhone())
|
||||||
.createdAt(user.getCreatedAt())
|
.createdAt(user.getCreatedAt())
|
||||||
.updatedAt(user.getUpdatedAt())
|
.updatedAt(user.getUpdatedAt())
|
||||||
@ -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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package ru.tubryansk.tdms.dto;
|
|
||||||
|
|
||||||
public record LoginDTO(
|
|
||||||
String username,
|
|
||||||
String password) {
|
|
||||||
}
|
|
||||||
@ -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())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,17 +2,15 @@ package ru.tubryansk.tdms.entity;
|
|||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@ToString
|
||||||
@AllArgsConstructor
|
@Entity
|
||||||
@Table(name = "diploma_topic")
|
@Table(name = "diploma_topic")
|
||||||
public class DiplomaTopic {
|
public class DiplomaTopic {
|
||||||
@Id
|
@Id
|
||||||
@ -22,3 +20,4 @@ public class DiplomaTopic {
|
|||||||
@Column(name = "name")
|
@Column(name = "name")
|
||||||
private String name;
|
private String name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,17 +2,15 @@ package ru.tubryansk.tdms.entity;
|
|||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@ToString
|
||||||
@AllArgsConstructor
|
@Entity
|
||||||
@Table(name = "group")
|
@Table(name = "group")
|
||||||
public class Group {
|
public class Group {
|
||||||
@Id
|
@Id
|
||||||
@ -21,7 +19,7 @@ public class Group {
|
|||||||
private Long id;
|
private Long id;
|
||||||
@Column(name = "name")
|
@Column(name = "name")
|
||||||
private String name;
|
private String name;
|
||||||
@ManyToOne()
|
@ManyToOne
|
||||||
@JoinColumn(name = "principal_user_id")
|
@JoinColumn(name = "curator_user_id")
|
||||||
private User principalUser;
|
private User groupCurator;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
package ru.tubryansk.tdms.entity;
|
package ru.tubryansk.tdms.entity;
|
||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.Column;
|
||||||
import lombok.AllArgsConstructor;
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.ToString;
|
||||||
import lombok.Setter;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@ToString
|
||||||
@NoArgsConstructor
|
@Entity
|
||||||
@AllArgsConstructor
|
|
||||||
@Table(name = "`role`")
|
@Table(name = "`role`")
|
||||||
public class Role implements GrantedAuthority {
|
public class Role implements GrantedAuthority {
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "id")
|
@Column(name = "id")
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
private Long id;
|
||||||
@Column(name = "name")
|
@Column(name = "name")
|
||||||
private String name;
|
private String name;
|
||||||
|
|||||||
@ -2,10 +2,14 @@ package ru.tubryansk.tdms.entity;
|
|||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Data;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
|
||||||
@Data
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "student")
|
@Table(name = "student")
|
||||||
public class Student {
|
public class Student {
|
||||||
|
|||||||
@ -2,10 +2,11 @@ package ru.tubryansk.tdms.entity;
|
|||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
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.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@ -15,11 +16,10 @@ import java.util.Collection;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@ToString
|
||||||
@AllArgsConstructor
|
@Entity
|
||||||
@Table(name = "`user`")
|
@Table(name = "`user`")
|
||||||
public class User implements UserDetails {
|
public class User implements UserDetails {
|
||||||
@Id
|
@Id
|
||||||
@ -32,18 +32,21 @@ public class User implements UserDetails {
|
|||||||
private String password;
|
private String password;
|
||||||
@Column(name = "full_name")
|
@Column(name = "full_name")
|
||||||
private String fullName;
|
private String fullName;
|
||||||
@Column(name = "mail")
|
@Column(name = "email")
|
||||||
private String mail;
|
private String email;
|
||||||
@Column(name = "number_phone")
|
@Column(name = "number_phone")
|
||||||
private String numberPhone;
|
private String numberPhone;
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
|
@CreationTimestamp
|
||||||
private ZonedDateTime createdAt;
|
private ZonedDateTime createdAt;
|
||||||
@Column(name = "updated_at")
|
@Column(name = "updated_at")
|
||||||
|
@UpdateTimestamp
|
||||||
private ZonedDateTime updatedAt;
|
private ZonedDateTime updatedAt;
|
||||||
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
@ManyToMany
|
||||||
@JoinTable(name = "user_role",
|
@JoinTable(
|
||||||
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
|
name = "user_role",
|
||||||
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
|
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
|
||||||
private List<Role> roles;
|
private List<Role> roles;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
package ru.tubryansk.tdms.entity.repository;
|
||||||
|
|
||||||
|
public interface DefenceRepository {
|
||||||
|
}
|
||||||
@ -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.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
@ -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<Group, Long> {
|
||||||
|
default Group findByIdThrow(Long id) {
|
||||||
|
return this.findById(id).orElseThrow(() -> new NotFoundException(Group.class, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Role, Long> {
|
||||||
|
}
|
||||||
@ -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.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
@ -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.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import ru.tubryansk.tdms.entity.User;
|
import ru.tubryansk.tdms.entity.User;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package ru.tubryansk.tdms.exception;
|
package ru.tubryansk.tdms.exception;
|
||||||
|
|
||||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||||
|
|
||||||
public class AccessDeniedException extends BusinessException {
|
public class AccessDeniedException extends BusinessException {
|
||||||
public AccessDeniedException() {
|
public AccessDeniedException() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package ru.tubryansk.tdms.exception;
|
package ru.tubryansk.tdms.exception;
|
||||||
|
|
||||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||||
|
|
||||||
public class BusinessException extends RuntimeException {
|
public class BusinessException extends RuntimeException {
|
||||||
public BusinessException(String message) {
|
public BusinessException(String message) {
|
||||||
@ -8,6 +8,6 @@ public class BusinessException extends RuntimeException {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ErrorResponse.ErrorCode getErrorCode() {
|
public ErrorResponse.ErrorCode getErrorCode() {
|
||||||
return ErrorResponse.ErrorCode.INTERNAL_ERROR;
|
return ErrorResponse.ErrorCode.BUSINESS_ERROR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,42 +2,55 @@ package ru.tubryansk.tdms.exception;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
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
|
@RestControllerAdvice
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(BindException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
public ErrorResponse handleMethodArgumentNotValidException(BindException e) {
|
||||||
// todo: make a better error message
|
log.debug("Validation error: {}", e.getMessage());
|
||||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.VALIDATION_ERROR);
|
String validationErrors = e.getAllErrors().stream()
|
||||||
|
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
return new ErrorResponse(validationErrors, ErrorResponse.ErrorCode.VALIDATION_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) {
|
public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) {
|
||||||
|
log.info("Business error", e);
|
||||||
response.setStatus(e.getErrorCode().getHttpStatus().value());
|
response.setStatus(e.getErrorCode().getHttpStatus().value());
|
||||||
return new ErrorResponse(e.getMessage(), e.getErrorCode());
|
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)
|
@ExceptionHandler(NoResourceFoundException.class)
|
||||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) {
|
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) {
|
||||||
// todo: make error page
|
log.error("Resource not found", e);
|
||||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND);
|
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
public ErrorResponse handleUnexpectedException(Exception e) {
|
public ErrorResponse handleUnexpectedException(Exception e) {
|
||||||
// todo: make error page
|
|
||||||
log.error("Unexpected exception.", e);
|
log.error("Unexpected exception.", e);
|
||||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR);
|
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
package ru.tubryansk.tdms.exception;
|
package ru.tubryansk.tdms.exception;
|
||||||
|
|
||||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||||
|
|
||||||
public class NotFoundException extends BusinessException {
|
public class NotFoundException extends BusinessException {
|
||||||
public NotFoundException(Class<?> entityClass, Integer id) {
|
public NotFoundException(Class<?> entityClass, Object id) {
|
||||||
super(entityClass.getSimpleName() + " with id " + id + " not found");
|
super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import org.springframework.security.authentication.AuthenticationManager;
|
|||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import ru.tubryansk.tdms.entity.User;
|
import ru.tubryansk.tdms.entity.User;
|
||||||
|
|
||||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
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) {
|
public void login(String username, String password) {
|
||||||
try {
|
try {
|
||||||
var context = SecurityContextHolder.createEmptyContext();
|
var context = SecurityContextHolder.createEmptyContext();
|
||||||
@ -41,8 +43,8 @@ public class AuthenticationService {
|
|||||||
SecurityContextHolder.setContext(context);
|
SecurityContextHolder.setContext(context);
|
||||||
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to log in user: {}. {}", username, e.getMessage());
|
log.error("Failed to log in user: {}", username, e);
|
||||||
return;
|
throw e;
|
||||||
}
|
}
|
||||||
log.debug("User {} logged in", username);
|
log.debug("User {} logged in", username);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package ru.tubryansk.tdms.service;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import ru.tubryansk.tdms.controller.payload.UserDTO;
|
||||||
import ru.tubryansk.tdms.entity.User;
|
import ru.tubryansk.tdms.entity.User;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -18,4 +19,8 @@ public class CallerService {
|
|||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserDTO getCallerUserDTO() {
|
||||||
|
return getCallerUser().map(UserDTO::from).orElse(UserDTO.unauthenticated());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<GroupDTO> getAllGroups() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<String, Role> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,12 @@ package ru.tubryansk.tdms.service;
|
|||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import ru.tubryansk.tdms.controller.payload.StudentDTO;
|
||||||
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
||||||
import ru.tubryansk.tdms.entity.Student;
|
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.exception.AccessDeniedException;
|
||||||
import ru.tubryansk.tdms.repository.DiplomaTopicRepository;
|
|
||||||
import ru.tubryansk.tdms.repository.StudentRepository;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -43,4 +44,13 @@ public class StudentService {
|
|||||||
public Optional<Student> getCallerStudent() {
|
public Optional<Student> getCallerStudent() {
|
||||||
return studentRepository.findByUser(callerService.getCallerUser().orElse(null));
|
return studentRepository.findByUser(callerService.getCallerUser().orElse(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StudentDTO getCallerStudentDTO() {
|
||||||
|
Student callerStudent = getCallerStudent().orElse(null);
|
||||||
|
if (callerStudent == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StudentDTO.from(callerStudent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,14 +3,21 @@ package ru.tubryansk.tdms.service;
|
|||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
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.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
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@ -18,10 +25,75 @@ import java.util.Optional;
|
|||||||
public class UserService implements UserDetailsService {
|
public class UserService implements UserDetailsService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
@Autowired
|
||||||
|
private GroupRepository groupRepository;
|
||||||
|
@Autowired
|
||||||
|
private StudentRepository studentRepository;
|
||||||
|
@Autowired
|
||||||
|
private RoleService roleService;
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User loadUserByUsername(String username) throws UsernameNotFoundException {
|
public User loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
return userRepository.findUserByLogin(username)
|
log.info("Loading user with username: {}", username);
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
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<UserDTO> getAllUsers() {
|
||||||
|
log.info("Loading all users");
|
||||||
|
List<UserDTO> 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<Role> roles = new ArrayList<>();
|
||||||
|
if (registrationDTO.getStudentData() != null) {
|
||||||
|
roles.add(roleService.getRoleByAuthority(RoleService.Authority.ROLE_STUDENT));
|
||||||
|
}
|
||||||
|
user.setRoles(roles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,13 @@
|
|||||||
create table role
|
create table role
|
||||||
(
|
(
|
||||||
id bigint primary key,
|
id bigint primary key,
|
||||||
|
|
||||||
name text not null unique,
|
name text not null unique,
|
||||||
authority text not null unique
|
authority text not null unique
|
||||||
);
|
);
|
||||||
|
|
||||||
-- COMMENTS
|
-- COMMENTS
|
||||||
|
comment on table role is 'Таблица ролей пользователей';
|
||||||
|
|
||||||
comment on column role.name is 'Человекочитаемое имя роли';
|
comment on column role.name is 'Человекочитаемое имя роли';
|
||||||
comment on column role.authority is 'Имя роли в системе';
|
comment on column role.authority is 'Имя роли в системе';
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
create table "user"
|
create table "user"
|
||||||
(
|
(
|
||||||
id bigserial primary key,
|
id bigserial primary key,
|
||||||
login text not null unique,
|
|
||||||
password text not null,
|
login text not null unique,
|
||||||
full_name text not null,
|
password text not null,
|
||||||
mail text not null unique,
|
full_name text not null,
|
||||||
number_phone text not null unique,
|
email text not null unique,
|
||||||
|
number_phone text not null unique,
|
||||||
|
|
||||||
created_at timestamptz not null,
|
created_at timestamptz not null,
|
||||||
updated_at timestamptz
|
updated_at timestamptz
|
||||||
);
|
);
|
||||||
|
|
||||||
-- COMMENTS
|
-- COMMENTS
|
||||||
|
comment on table "user" is 'Таблица пользователей';
|
||||||
|
|
||||||
comment on column "user".login is 'Логин пользователя';
|
comment on column "user".login is 'Логин пользователя';
|
||||||
comment on column "user".password is 'Пароль пользователя';
|
comment on column "user".password is 'Пароль пользователя';
|
||||||
comment on column "user".full_name 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".number_phone is 'Номер телефона пользователя';
|
||||||
comment on column "user".created_at is 'Дата создания записи';
|
|
||||||
comment on column "user".updated_at is 'Дата последнего обновления записи';
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
create table user_role
|
create table user_role
|
||||||
(
|
(
|
||||||
id bigserial primary key,
|
id bigserial primary key,
|
||||||
|
|
||||||
user_id bigint not null,
|
user_id bigint not null,
|
||||||
role_id bigint not null
|
role_id bigint not null
|
||||||
);
|
);
|
||||||
@ -14,5 +15,7 @@ alter table user_role
|
|||||||
foreign key (role_id) references role (id);
|
foreign key (role_id) references role (id);
|
||||||
|
|
||||||
-- COMMENTS
|
-- COMMENTS
|
||||||
|
comment on table user_role is 'Таблица связи пользователей и ролей';
|
||||||
|
|
||||||
comment on column user_role.user_id is 'Идентификатор пользователя';
|
comment on column user_role.user_id is 'Идентификатор пользователя';
|
||||||
comment on column user_role.role_id is 'Идентификатор роли';
|
comment on column user_role.role_id is 'Идентификатор роли';
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
create table diploma_topic
|
create table diploma_topic
|
||||||
(
|
(
|
||||||
id bigserial primary key,
|
id bigserial primary key,
|
||||||
name text not null unique
|
|
||||||
|
name text not null
|
||||||
);
|
);
|
||||||
|
|
||||||
-- COMMENTS
|
-- COMMENTS
|
||||||
|
comment on table diploma_topic is 'Таблица тем дипломных работ';
|
||||||
|
|
||||||
comment on column diploma_topic.name is 'Название темы дипломной работы';
|
comment on column diploma_topic.name is 'Название темы дипломной работы';
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
create table "group"
|
create table "group"
|
||||||
(
|
(
|
||||||
id bigserial primary key,
|
id bigserial primary key,
|
||||||
name text not null unique,
|
|
||||||
principal_user_id bigint not null
|
name text not null unique,
|
||||||
|
curator_user_id bigint,
|
||||||
|
|
||||||
|
created_at timestamptz not null,
|
||||||
|
updated_at timestamptz
|
||||||
);
|
);
|
||||||
|
|
||||||
-- FOREIGN KEY
|
-- FOREIGN KEY
|
||||||
alter table "group"
|
alter table "group"
|
||||||
add constraint fk_group_principal_user_id
|
add constraint fk_group_curator_user_id
|
||||||
foreign key (principal_user_id) references "user" (id)
|
foreign key (curator_user_id) references "user" (id)
|
||||||
on delete cascade;
|
on delete set null on update cascade;
|
||||||
|
|
||||||
-- COMMENTS
|
-- COMMENTS
|
||||||
|
comment on table "group" is 'Таблица групп студентов';
|
||||||
|
|
||||||
comment on column "group".name is 'Название группы';
|
comment on column "group".name is 'Название группы';
|
||||||
comment on column "group".principal_user_id is 'Идентификатор куратора группы';
|
comment on column "group".curator_user_id is 'Идентификатор куратора группы';
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
create table student
|
create table student
|
||||||
(
|
(
|
||||||
id bigserial primary key,
|
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,
|
form boolean,
|
||||||
protection_order integer not null,
|
protection_day int,
|
||||||
|
protection_order int,
|
||||||
magistracy text,
|
magistracy text,
|
||||||
digital_format_present boolean,
|
digital_format_present boolean,
|
||||||
mark_comment integer,
|
mark_comment int,
|
||||||
mark_practice integer,
|
mark_practice int,
|
||||||
predefence_comment text,
|
predefence_comment text,
|
||||||
normal_control text,
|
normal_control text,
|
||||||
anti_plagiarism int,
|
anti_plagiarism int,
|
||||||
note text,
|
note text,
|
||||||
record_book_returned boolean,
|
record_book_returned boolean,
|
||||||
work text,
|
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,
|
created_at timestamptz not null,
|
||||||
updated_at timestamptz
|
updated_at timestamptz
|
||||||
);
|
);
|
||||||
@ -25,22 +28,30 @@ create table student
|
|||||||
alter table student
|
alter table student
|
||||||
add constraint fk_student_user_id
|
add constraint fk_student_user_id
|
||||||
foreign key (user_id) references "user" (id)
|
foreign key (user_id) references "user" (id)
|
||||||
on delete cascade;
|
on delete cascade on update cascade;
|
||||||
alter table student
|
alter table student
|
||||||
add constraint fk_student_diploma_topic_id
|
add constraint fk_student_diploma_topic_id
|
||||||
foreign key (diploma_topic_id) references 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
|
alter table student
|
||||||
add constraint fk_student_mentor_user_id
|
add constraint fk_student_mentor_user_id
|
||||||
foreign key (mentor_user_id) references "user" (id)
|
foreign key (mentor_user_id) references "user" (id)
|
||||||
on delete cascade;
|
on delete set null on update cascade;
|
||||||
alter table student
|
alter table student
|
||||||
add constraint fk_student_group_id
|
add constraint fk_student_group_id
|
||||||
foreign key (group_id) references "group" (id)
|
foreign key (group_id) references "group" (id)
|
||||||
on delete cascade;
|
on delete set null on update cascade;
|
||||||
|
|
||||||
-- COMMENTS
|
-- 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.form is 'Форма обучения';
|
||||||
|
comment on column student.protection_day is 'День защиты';
|
||||||
comment on column student.protection_order is 'Порядок защиты';
|
comment on column student.protection_order is 'Порядок защиты';
|
||||||
comment on column student.magistracy is 'Магистратура';
|
comment on column student.magistracy is 'Магистратура';
|
||||||
comment on column student.digital_format_present 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.note is 'Примечание';
|
||||||
comment on column student.record_book_returned is 'Ведомость возвращена';
|
comment on column student.record_book_returned is 'Ведомость возвращена';
|
||||||
comment on column student.work 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 'Идентификатор группы';
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
insert into "user" (id, login, password, full_name, mail, number_phone, created_at)
|
insert into "user" (id, login, password, full_name, email, number_phone, created_at)
|
||||||
values (1, 'admin', '{noop}admin', 'Администратор', 'admin@localhost', '+79110000000', now());
|
values (1, 'admin', '{noop}admin', 'Администратор', 'admin@tdms.tu-byransk.ru', '', now());
|
||||||
|
|
||||||
insert into user_role (id, user_id, role_id)
|
insert into user_role (id, user_id, role_id)
|
||||||
values (1, 1, 4);
|
values (1, 1, 4);
|
||||||
|
|||||||
51
web/package-lock.json
generated
51
web/package-lock.json
generated
@ -13,14 +13,17 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mobx": "^6.13.1",
|
"mobx": "^6.13.1",
|
||||||
"mobx-react": "^9.1.1",
|
"mobx-react": "^9.1.1",
|
||||||
"mobx-state-router": "^6.0.1",
|
"mobx-state-router": "^6.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.4",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"uuid": "^11.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-decorators": "^7.25.7",
|
"@babel/plugin-proposal-decorators": "^7.25.7",
|
||||||
@ -2105,6 +2108,12 @@
|
|||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@ -3858,20 +3867,6 @@
|
|||||||
"node": ">= 0.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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@ -4591,7 +4586,7 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"dev": true
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
@ -5974,6 +5969,16 @@
|
|||||||
"websocket-driver": "^0.7.4"
|
"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": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@ -6454,12 +6459,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
|
||||||
"dev": true,
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/value-equal": {
|
"node_modules/value-equal": {
|
||||||
|
|||||||
@ -12,14 +12,17 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mobx": "^6.13.1",
|
"mobx": "^6.13.1",
|
||||||
"mobx-react": "^9.1.1",
|
"mobx-react": "^9.1.1",
|
||||||
"mobx-state-router": "^6.0.1",
|
"mobx-state-router": "^6.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.4",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"uuid": "^11.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-decorators": "^7.25.7",
|
"@babel/plugin-proposal-decorators": "^7.25.7",
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import './index.css'
|
|||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import {RouterContext, RouterView} from "mobx-state-router";
|
import {RouterContext, RouterView} from "mobx-state-router";
|
||||||
import {initApp} from "./utils/init";
|
import {initApp} from "./utils/init";
|
||||||
import {RootStoreContext} from './context/RootStoreContext';
|
import {RootStoreContext} from './store/RootStoreContext';
|
||||||
import {viewMap} from "./router/viewMap";
|
import {viewMap} from "./router/viewMap";
|
||||||
|
|
||||||
const rootStore = initApp();
|
const rootStore = initApp();
|
||||||
|
|||||||
89
web/src/components/NotificationContainer.tsx
Normal file
89
web/src/components/NotificationContainer.tsx
Normal file
@ -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 => (
|
||||||
|
<NotificationPopup key={notification.uuid} notification={notification} type={type}/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div style={{position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 1000}}>
|
||||||
|
{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)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = <FontAwesomeIcon icon={'close'} onClick={this.close}/>;
|
||||||
|
|
||||||
|
return <Card className={`position-relative mt-3 opacity-75 ${this.cardClassName}`}>
|
||||||
|
{
|
||||||
|
hasTitle &&
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<Row>
|
||||||
|
<Col sm={11}>
|
||||||
|
{this.props.notification.title}
|
||||||
|
</Col>
|
||||||
|
<Col className={'text-end'}>
|
||||||
|
{closeIcon}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
}
|
||||||
|
<CardBody>
|
||||||
|
<CardText>
|
||||||
|
<Row>
|
||||||
|
<Col sm={11}>
|
||||||
|
{this.props.notification.message}
|
||||||
|
</Col>
|
||||||
|
{
|
||||||
|
!hasTitle &&
|
||||||
|
<Col className={'text-end'}>
|
||||||
|
{closeIcon}
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
</Row>
|
||||||
|
</CardText>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
}
|
||||||
147
web/src/components/custom/DataTable.tsx
Normal file
147
web/src/components/custom/DataTable.tsx
Normal file
@ -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<T> {
|
||||||
|
tableDescriptor: TableDescriptor<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class DataTable<T> extends ComponentContext<DataTableProps<T>> {
|
||||||
|
constructor(props: DataTableProps<T>) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
return <tr>
|
||||||
|
{this.props.tableDescriptor.columns.map(column => <th className={'text-center'}
|
||||||
|
key={column.key}>{column.title}</th>)}
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
body() {
|
||||||
|
const firstColumnKey = this.props.tableDescriptor.columns[0].key;
|
||||||
|
return this.props.tableDescriptor.data.map(row => {
|
||||||
|
const rowAny = row as any;
|
||||||
|
return <tr key={rowAny[firstColumnKey]}>
|
||||||
|
{
|
||||||
|
this.props.tableDescriptor.columns.map(column => {
|
||||||
|
return <td
|
||||||
|
className={'text-center'}
|
||||||
|
key={column.key}>
|
||||||
|
{column.format(rowAny[column.key])}
|
||||||
|
</td>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <tr className={'text-center'}>
|
||||||
|
<td colSpan={table.columns.length}>
|
||||||
|
<div className={'d-flex justify-content-between'}>
|
||||||
|
<div/>
|
||||||
|
<Pagination className={'mb-0'}>
|
||||||
|
<Pagination.First onClick={this.goFirstPage} disabled={this.isFirstPage()}/>
|
||||||
|
<Pagination.Ellipsis disabled={this.isFirstPage()}/>
|
||||||
|
<Pagination.Prev onClick={this.goPrevPage} disabled={this.isFirstPage()}/>
|
||||||
|
<Pagination.Item active>{this.props.tableDescriptor.page}</Pagination.Item>
|
||||||
|
<Pagination.Next onClick={this.goNextPage} disabled={!this.isLastPage()}/>
|
||||||
|
<Pagination.Ellipsis disabled={!this.isLastPage()}/>
|
||||||
|
<Pagination.Last onClick={this.goLastPage} disabled={!this.isLastPage()}/>
|
||||||
|
</Pagination>
|
||||||
|
<FormSelect className={'w-auto'} onChange={this.changePageSize}>
|
||||||
|
<option>10</option>
|
||||||
|
<option>20</option>
|
||||||
|
<option>50</option>
|
||||||
|
<option>100</option>
|
||||||
|
</FormSelect>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const table = this.props.tableDescriptor;
|
||||||
|
return <Table hover striped>
|
||||||
|
<thead>
|
||||||
|
{this.header()}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.body()}
|
||||||
|
</tbody>
|
||||||
|
{
|
||||||
|
table.pageable &&
|
||||||
|
<tfoot>
|
||||||
|
{this.footer()}
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
}
|
||||||
3
web/src/components/custom/controls/ReactiveControls.css
Normal file
3
web/src/components/custom/controls/ReactiveControls.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.l-no-bg label::after {
|
||||||
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
|
}
|
||||||
116
web/src/components/custom/controls/ReactiveControls.tsx
Normal file
116
web/src/components/custom/controls/ReactiveControls.tsx
Normal file
@ -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<T> {
|
||||||
|
value: ReactiveValue<T>;
|
||||||
|
label?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class StringInput extends React.Component<ReactiveInputProps<string>> {
|
||||||
|
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<HTMLInputElement>) {
|
||||||
|
this.props.value.set(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className={'mb-1 l-no-bg'}>
|
||||||
|
{/*todo: disable background-color for label*/}
|
||||||
|
<FloatingLabel label={this.props.label} className={`${this.props.className} mt-0 mb-0`}>
|
||||||
|
<FormControl type='text' placeholder={this.props.label} disabled={this.props.disabled}
|
||||||
|
onChange={this.onChange} value={this.props.value.value}
|
||||||
|
className={`${this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`}/>
|
||||||
|
</FloatingLabel>
|
||||||
|
<FormText children={this.props.value.firstError} className={`text-danger mt-0 mb-0 d-block`}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
|
||||||
|
@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<HTMLInputElement>) {
|
||||||
|
this.props.value.set(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
toggleShowPassword() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className={'mb-1 l-no-bg'}>
|
||||||
|
<div className={'d-flex justify-content-between align-items-center'}>
|
||||||
|
<FloatingLabel label={this.props.label} className={`${this.props.className} w-100`}>
|
||||||
|
<FormControl type={`${this.showPassword ? 'text' : 'password'}`} placeholder={this.props.label}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
className={`${this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`}
|
||||||
|
onChange={this.onChange} value={this.props.value.value}/>
|
||||||
|
</FloatingLabel>
|
||||||
|
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"}>
|
||||||
|
<FontAwesomeIcon icon={this.showPassword ? 'eye-slash' : 'eye'}/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormText children={this.props.value.firstError} className={'text-danger d-block mt-0 mb-0'}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class SelectButtonInput extends React.Component<ReactiveInputProps<string>> {
|
||||||
|
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<HTMLInputElement>) {
|
||||||
|
this.props.value.set(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
<ButtonGroup className={'d-block l-no-bg'}>
|
||||||
|
<ToggleButton key={'admin'} value={'admin'} id={`radio-admin`} type="radio"
|
||||||
|
variant={'outline-primary'} children={'Администратор'}
|
||||||
|
checked={this.props.value.value === 'admin'} onChange={this.onChange}/>
|
||||||
|
<ToggleButton key={'student'} id={`radio-student`} type="radio" value={'student'}
|
||||||
|
variant={'outline-primary'}
|
||||||
|
checked={this.props.value.value === 'student'} onChange={this.onChange}
|
||||||
|
children={'Студент'}/>
|
||||||
|
</ButtonGroup>
|
||||||
|
<FormText children={this.props.value.firstError} className={'text-danger d-block'}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,42 +1,40 @@
|
|||||||
import {Component, ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
import {Container} from "react-bootstrap";
|
import {Container} from "react-bootstrap";
|
||||||
import Footer from "./Footer";
|
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
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
|
export abstract class DefaultPage extends ComponentContext {
|
||||||
class DefaultPage extends Component<any> {
|
|
||||||
get page(): ReactNode {
|
get page(): ReactNode {
|
||||||
throw new Error('This is not abstract method, ' +
|
throw new Error('This is not abstract method, ' +
|
||||||
'because mobx cant handle abstract methods. ' +
|
'because mobx cant handle abstract methods. ' +
|
||||||
'Please override this method in child class. ' +
|
'Please override this method in child class. ' +
|
||||||
'Do not call it directly.');
|
'Do not call it directly.');
|
||||||
}
|
}
|
||||||
declare context: RootStoreContextType;
|
|
||||||
static contextType = RootStoreContext;
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let isLoading = this.context.pendingStore.isThinking();
|
const thinking = this.thinkStore.isThinking();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Header/>
|
<Header/>
|
||||||
<Container className={"mt-5 mb-5"}>
|
<Container className={"mt-5 mb-5"}>
|
||||||
{
|
{
|
||||||
isLoading &&
|
thinking &&
|
||||||
<div id='fullscreen-loader'>
|
<div id='fullscreen-loader'>
|
||||||
<FontAwesomeIcon icon='gear' size="4x" spin/>
|
<FontAwesomeIcon icon='gear' size="4x" spin/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!isLoading &&
|
!thinking &&
|
||||||
this.page
|
<>
|
||||||
|
<NotificationContainer/>
|
||||||
|
{this.page}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</Container>
|
</Container>
|
||||||
<Footer/>
|
<Footer/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {DefaultPage};
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import {DefaultPage} from "./layout/DefaultPage";
|
import {observer} from "mobx-react";
|
||||||
|
import {DefaultPage} from "./DefaultPage";
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class Error extends DefaultPage {
|
export default class Error extends DefaultPage {
|
||||||
get page() {
|
get page() {
|
||||||
return <h1>Error</h1>
|
return <h1>Error</h1>
|
||||||
41
web/src/components/layout/Footer.tsx
Normal file
41
web/src/components/layout/Footer.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {ComponentContext} from "../../utils/ComponentContext";
|
||||||
|
import {observer} from "mobx-react";
|
||||||
|
import {makeObservable} from "mobx";
|
||||||
|
import {Container, Nav, Navbar, NavbarText, NavLink} from "react-bootstrap";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class Footer extends ComponentContext {
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <footer>
|
||||||
|
<Navbar className="bg-body-tertiary">
|
||||||
|
<Container>
|
||||||
|
<div>
|
||||||
|
<NavbarText>Thesis Defence Management System — </NavbarText>
|
||||||
|
{
|
||||||
|
this.thinkStore.isThinking('updateVersion') &&
|
||||||
|
<FontAwesomeIcon icon='gear' spin/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!this.thinkStore.isThinking('updateVersion') &&
|
||||||
|
<NavbarText>{this.sysInfoStore.version}</NavbarText>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Nav>
|
||||||
|
<NavLink href="https://git.mskobaro.ru/mskobaro/TDMS">
|
||||||
|
<FontAwesomeIcon icon={findIconDefinition({iconName: 'github', prefix: 'fab'})} size="xl"/>
|
||||||
|
</NavLink>
|
||||||
|
</Nav>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,17 @@
|
|||||||
import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
|
import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
|
||||||
import {Component} from "react";
|
|
||||||
import {RouterLink} from "mobx-state-router";
|
import {RouterLink} from "mobx-state-router";
|
||||||
import {IAuthenticated} from "../../../models/user";
|
import {IAuthenticated} from "../../models/user";
|
||||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
import {RootStoreContext, RootStoreContextType} from "../../store/RootStoreContext";
|
||||||
import {observer} from "mobx-react";
|
import {observer} from "mobx-react";
|
||||||
import {post} from "../../../utils/request";
|
import {post} from "../../utils/request";
|
||||||
import {LoginModal} from "../../user/LoginModal";
|
import {LoginModal} from "../user/LoginModal";
|
||||||
import {ModalState} from "../../../state/ModalState";
|
import {ModalState} from "../../utils/modalState";
|
||||||
import {action, makeObservable} from "mobx";
|
import {action, makeObservable} from "mobx";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {ComponentContext} from "../../utils/ComponentContext";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Header extends Component {
|
class Header extends ComponentContext {
|
||||||
declare context: RootStoreContextType;
|
|
||||||
static contextType = RootStoreContext;
|
|
||||||
|
|
||||||
loginModalState = new ModalState();
|
loginModalState = new ModalState();
|
||||||
|
|
||||||
@ -22,13 +20,10 @@ class Header extends Component {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get loginThink() {
|
|
||||||
return this.context.pendingStore.isThinking('updateCurrentUser');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const userStore = this.context.userStore;
|
const userStore = this.context.userStore;
|
||||||
const user = userStore.user;
|
const user = userStore.user;
|
||||||
|
let thinking = this.thinkStore.isThinking('updateCurrentUser');
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<header>
|
<header>
|
||||||
@ -38,23 +33,27 @@ class Header extends Component {
|
|||||||
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
||||||
</Navbar.Brand>
|
</Navbar.Brand>
|
||||||
<Nav>
|
<Nav>
|
||||||
<NavDropdown title="Группы">
|
{
|
||||||
<NavDropdown.Item>Список</NavDropdown.Item>
|
user.authenticated && userStore.isAdministrator() &&
|
||||||
<NavDropdown.Item>Редактировать</NavDropdown.Item>
|
<NavDropdown title="Пользователи">
|
||||||
</NavDropdown>
|
<NavDropdown.Item as={RouterLink} routeName={'userList'} children={'Список'}/>
|
||||||
|
<NavDropdown.Item as={RouterLink} routeName={'userRegistration'}
|
||||||
|
children={'Зарегистрировать'}/>
|
||||||
|
</NavDropdown>
|
||||||
|
}
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
<Nav className="ms-auto">
|
<Nav className="ms-auto">
|
||||||
{
|
{
|
||||||
this.loginThink &&
|
thinking &&
|
||||||
<FontAwesomeIcon icon='gear' spin/>
|
<FontAwesomeIcon icon='gear' spin/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
user.authenticated && !this.loginThink &&
|
user.authenticated && !thinking &&
|
||||||
<AuthenticatedItems/>
|
<AuthenticatedItems/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!user.authenticated && !this.loginThink &&
|
!user.authenticated && !thinking &&
|
||||||
<>
|
<>
|
||||||
<Nav.Link onClick={this.loginModalState.open}>Войти</Nav.Link>
|
<Nav.Link onClick={this.loginModalState.open}>Войти</Nav.Link>
|
||||||
</>
|
</>
|
||||||
@ -68,7 +67,8 @@ class Header extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticatedItems extends Component {
|
@observer
|
||||||
|
class AuthenticatedItems extends ComponentContext<any, any> {
|
||||||
declare context: RootStoreContextType;
|
declare context: RootStoreContextType;
|
||||||
static contextType = RootStoreContext;
|
static contextType = RootStoreContext;
|
||||||
|
|
||||||
9
web/src/components/layout/Home.tsx
Normal file
9
web/src/components/layout/Home.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {DefaultPage} from "./DefaultPage";
|
||||||
|
import {observer} from "mobx-react";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export default class Home extends DefaultPage {
|
||||||
|
get page() {
|
||||||
|
return <h1>Home</h1>
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import {DefaultPage} from "./layout/DefaultPage";
|
|
||||||
import {RootStoreContext, RootStoreContextType} from "../../context/RootStoreContext";
|
|
||||||
|
|
||||||
export default class Home extends DefaultPage {
|
|
||||||
declare context: RootStoreContextType;
|
|
||||||
static contextType = RootStoreContext;
|
|
||||||
|
|
||||||
get page() {
|
|
||||||
return <h1>Home</h1>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import {Container, Nav, Navbar} from "react-bootstrap";
|
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|
||||||
import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
|
|
||||||
|
|
||||||
const Footer = () => {
|
|
||||||
return (
|
|
||||||
<footer>
|
|
||||||
<Navbar className="bg-body-tertiary">
|
|
||||||
<Container>
|
|
||||||
<Navbar.Text>Thesis Defence Management System ©</Navbar.Text>
|
|
||||||
<Nav>
|
|
||||||
<Nav.Link href="https://github.com/Velixeor/Thesis-Defense-Management-System">
|
|
||||||
<FontAwesomeIcon icon={findIconDefinition({iconName:'github', prefix:'fab'})} size="xl"/>
|
|
||||||
</Nav.Link>
|
|
||||||
</Nav>
|
|
||||||
</Container>
|
|
||||||
</Navbar>
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Footer;
|
|
||||||
@ -1,25 +1,22 @@
|
|||||||
import {ChangeEvent, Component} from "react";
|
import {ChangeEvent} from "react";
|
||||||
import {Button, FormControl, FormGroup, FormLabel, FormText, Modal} from "react-bootstrap";
|
import {Button, FormControl, FormGroup, FormLabel, FormText, Modal} from "react-bootstrap";
|
||||||
import {ModalState} from "../../state/ModalState";
|
import {ModalState} from "../../utils/modalState";
|
||||||
import {observer} from "mobx-react";
|
import {observer} from "mobx-react";
|
||||||
import {action, computed, makeObservable, observable, reaction} from "mobx";
|
import {action, computed, makeObservable, observable, reaction} from "mobx";
|
||||||
import {post} from "../../utils/request";
|
import {post} from "../../utils/request";
|
||||||
import {RootStoreContext, RootStoreContextType} from "../../context/RootStoreContext";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {ComponentContext} from "../../utils/ComponentContext";
|
||||||
|
|
||||||
interface LoginModalProps {
|
interface LoginModalProps {
|
||||||
modalState: ModalState;
|
modalState: ModalState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LoginModal extends Component<LoginModalProps> {
|
export class LoginModal extends ComponentContext<LoginModalProps> {
|
||||||
declare context: RootStoreContextType;
|
|
||||||
static contextType = RootStoreContext;
|
|
||||||
modalState = this.props.modalState;
|
|
||||||
@observable login = '';
|
@observable login = '';
|
||||||
@observable loginError = '';
|
@observable loginError = '';
|
||||||
@observable password = '';
|
@observable password = '';
|
||||||
@observable passwordError = '';
|
@observable passwordError = '';
|
||||||
@observable rememberMe = false;
|
|
||||||
|
|
||||||
constructor(props: LoginModalProps) {
|
constructor(props: LoginModalProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -74,48 +71,72 @@ export class LoginModal extends Component<LoginModalProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
onLogin() {
|
tryLogin() {
|
||||||
if (this.loginButtonDisabled)
|
if (this.loginButtonDisabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.thinkStore.think('loginModal');
|
||||||
post('user/login', {
|
post('user/login', {
|
||||||
username: this.login,
|
username: this.login,
|
||||||
password: this.password
|
password: this.password
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.context.userStore.updateCurrentUser();
|
this.userStore.updateCurrentUser((user) => {
|
||||||
this.modalState.close();
|
if (user.authenticated) {
|
||||||
|
this.routerStore.goTo('profile').then();
|
||||||
|
this.notificationStore.success('Вы успешно вошли в систему, ' + user.fullName, 'Успешный вход');
|
||||||
|
} else {
|
||||||
|
this.routerStore.goTo('root').then();
|
||||||
|
this.notificationStore.error('Произошла ошибка при попытке входа в систему', 'Ошибка входа');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.props.modalState.close();
|
||||||
|
this.thinkStore.completeAll('loginModal');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Modal show={this.modalState.isOpen} centered>
|
const open = this.props.modalState.isOpen;
|
||||||
|
const thinking = this.thinkStore.isThinking('loginModal');
|
||||||
|
|
||||||
|
return <Modal show={open} centered>
|
||||||
<Modal.Header>
|
<Modal.Header>
|
||||||
<Modal.Title>Вход</Modal.Title>
|
<Modal.Title>Вход</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
{
|
||||||
<FormGroup className={'mb-3'}>
|
thinking &&
|
||||||
<FormLabel>Имя пользователя</FormLabel>
|
<Modal.Body>
|
||||||
<FormControl type="text" onChange={this.onLoginInput}/>
|
<FontAwesomeIcon icon={'gear'} spin/>
|
||||||
{
|
</Modal.Body>
|
||||||
this.loginError &&
|
}
|
||||||
<FormText className={'text-danger'}>{this.loginError}</FormText>
|
{
|
||||||
}
|
!thinking &&
|
||||||
</FormGroup>
|
<>
|
||||||
<FormGroup className={'mb-3'}>
|
<Modal.Body>
|
||||||
<FormLabel>Пароль</FormLabel>
|
<FormGroup className={'mb-3'}>
|
||||||
<FormControl type="password" onChange={this.onPasswordInput}/>
|
<FormLabel>Имя пользователя</FormLabel>
|
||||||
{
|
<FormControl type="text" onChange={this.onLoginInput}/>
|
||||||
this.passwordError &&
|
{
|
||||||
<FormText className={'text-danger'}>{this.passwordError}</FormText>
|
this.loginError &&
|
||||||
}
|
<FormText className={'text-danger'}>{this.loginError}</FormText>
|
||||||
</FormGroup>
|
}
|
||||||
</Modal.Body>
|
</FormGroup>
|
||||||
<Modal.Footer>
|
<FormGroup className={'mb-3'}>
|
||||||
<Button variant={'primary'} onClick={this.onLogin} disabled={this.loginButtonDisabled}>Войти</Button>
|
<FormLabel>Пароль</FormLabel>
|
||||||
<Button variant={'secondary'} onClick={this.modalState.close}>Закрыть</Button>
|
<FormControl type="password" onChange={this.onPasswordInput}/>
|
||||||
</Modal.Footer>
|
{
|
||||||
|
this.passwordError &&
|
||||||
|
<FormText className={'text-danger'}>{this.passwordError}</FormText>
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant={'primary'} onClick={this.tryLogin}
|
||||||
|
disabled={this.loginButtonDisabled}>Войти</Button>
|
||||||
|
<Button variant={'secondary'} onClick={this.props.modalState.close}>Закрыть</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
60
web/src/components/user/UserList.tsx
Normal file
60
web/src/components/user/UserList.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {observer} from "mobx-react";
|
||||||
|
import {action, makeObservable, observable, reaction, runInAction} from "mobx";
|
||||||
|
import {DefaultPage} from "../layout/DefaultPage";
|
||||||
|
import {IAuthenticated} from "../../models/user";
|
||||||
|
import {DataTable} from "../custom/DataTable";
|
||||||
|
import {get} from "../../utils/request";
|
||||||
|
import {Column, TableDescriptor} from "../../utils/tables";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class UserList extends DefaultPage {
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.requestUsers();
|
||||||
|
reaction(() => this.users, () => {
|
||||||
|
if (typeof this.users === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tableDescriptor = new TableDescriptor(this.userColumns, this.users);
|
||||||
|
}, {fireImmediately: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable users?: IAuthenticated[];
|
||||||
|
@observable tableDescriptor?: TableDescriptor<IAuthenticated>;
|
||||||
|
|
||||||
|
userColumns = [
|
||||||
|
new Column('login', 'Логин'),
|
||||||
|
new Column('fullName', 'Полное имя'),
|
||||||
|
new Column('email', 'Email'),
|
||||||
|
new Column('phone', 'Телефон'),
|
||||||
|
new Column('createdAt', 'Дата создания'),
|
||||||
|
new Column('updatedAt', 'Дата обновления', (value: string) => value ? value : 'Не обновлялось'),
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
requestUsers() {
|
||||||
|
this.thinkStore.think('userList');
|
||||||
|
get<IAuthenticated[]>('user/get-all').then((users) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.users = users;
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
this.thinkStore.completeAll('userList');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get page() {
|
||||||
|
return <>
|
||||||
|
{
|
||||||
|
this.tableDescriptor &&
|
||||||
|
<DataTable tableDescriptor={this.tableDescriptor}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import {DefaultPage} from "../page/layout/DefaultPage";
|
import {DefaultPage} from "../layout/DefaultPage";
|
||||||
import {Col, Form, Row} from "react-bootstrap";
|
import {Col, Form, Row} from "react-bootstrap";
|
||||||
import {observer} from "mobx-react";
|
import {observer} from "mobx-react";
|
||||||
import {RootStoreContext, type RootStoreContextType} from "../../context/RootStoreContext";
|
import {RootStoreContext, type RootStoreContextType} from "../../store/RootStoreContext";
|
||||||
import {IAuthenticated} from "../../models/user";
|
import {IAuthenticated} from "../../models/user";
|
||||||
import {Component} from "react";
|
import {Component} from "react";
|
||||||
import {dateConverter} from "../../utils/converters";
|
import {dateConverter} from "../../utils/converters";
|
||||||
|
|||||||
87
web/src/components/user/UserRegistration.tsx
Normal file
87
web/src/components/user/UserRegistration.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {observer} from "mobx-react";
|
||||||
|
import {DefaultPage} from "../layout/DefaultPage";
|
||||||
|
import {action, computed, makeObservable, observable} from "mobx";
|
||||||
|
import {Button, Col, Form, Row} from "react-bootstrap";
|
||||||
|
import {UserRegistrationDTO} from "../../models/registration";
|
||||||
|
import {post} from "../../utils/request";
|
||||||
|
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
|
||||||
|
import {PasswordInput, SelectButtonInput, StringInput} from "../custom/controls/ReactiveControls";
|
||||||
|
import {
|
||||||
|
email,
|
||||||
|
loginChars,
|
||||||
|
loginLength,
|
||||||
|
nameChars,
|
||||||
|
nameLength,
|
||||||
|
passwordChars,
|
||||||
|
passwordLength,
|
||||||
|
phone,
|
||||||
|
required
|
||||||
|
} from "../../utils/reactive/validators";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class UserRegistration extends DefaultPage {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable login = new ReactiveValue<string>().addValidator(required).addValidator(loginLength).addValidator(loginChars);
|
||||||
|
@observable password = new ReactiveValue<string>().addValidator(required).addValidator(passwordLength).addValidator(passwordChars);
|
||||||
|
@observable fullName = new ReactiveValue<string>().addValidator(required).addValidator(nameLength).addValidator(nameChars);
|
||||||
|
@observable email = new ReactiveValue<string>().addValidator(required).addValidator(email);
|
||||||
|
@observable numberPhone = new ReactiveValue<string>().addValidator(required).addValidator(phone).setAuto('+7');
|
||||||
|
|
||||||
|
|
||||||
|
@observable accountType = new ReactiveValue<string>().addValidator(required).addValidator((value) => {
|
||||||
|
if (!['student', 'admin'].includes(value)) {
|
||||||
|
return 'Тип аккаунта должен быть "СТУДЕНТ" или "АДМИНИСТРАТОР"';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get formInvalid() {
|
||||||
|
return this.login.invalid || !this.login.touched
|
||||||
|
|| this.password.invalid || !this.password.touched
|
||||||
|
|| this.fullName.invalid || !this.fullName.touched
|
||||||
|
|| this.email.invalid || !this.email.touched
|
||||||
|
|| this.numberPhone.invalid || !this.numberPhone.touched
|
||||||
|
|| this.accountType.invalid || !this.accountType.touched;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
submit() {
|
||||||
|
post('user/register', {
|
||||||
|
login: this.login.value,
|
||||||
|
password: this.password.value,
|
||||||
|
fullName: this.fullName.value,
|
||||||
|
email: this.email.value,
|
||||||
|
numberPhone: this.numberPhone.value,
|
||||||
|
// studentData: { groupId: 1 }
|
||||||
|
} as UserRegistrationDTO).then(() => {
|
||||||
|
this.notificationStore.success('Пользователь успешно зарегистрирован');
|
||||||
|
}).catch(() => {
|
||||||
|
this.notificationStore.error('Ошибка регистрации пользователя');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get page() {
|
||||||
|
return <div className={'w-75 ms-auto me-auto'}>
|
||||||
|
<Form>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<StringInput value={this.login} label={"Логин"}/>
|
||||||
|
<PasswordInput value={this.password} label={"Пароль"}/>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<StringInput value={this.fullName} label={"Полное имя"}/>
|
||||||
|
<StringInput value={this.email} label={"Почта"}/>
|
||||||
|
<StringInput value={this.numberPhone} label={"Телефон"}/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<SelectButtonInput value={this.accountType} label={'Тип аккаунта'}/>
|
||||||
|
<Button disabled={this.formInvalid} onClick={this.submit}>Зарегистрировать</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
12
web/src/models/registration.ts
Normal file
12
web/src/models/registration.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface UserRegistrationDTO {
|
||||||
|
login: string,
|
||||||
|
password: string,
|
||||||
|
fullName: string,
|
||||||
|
email: string,
|
||||||
|
numberPhone: string,
|
||||||
|
studentData?: StudentRegistrationDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StudentRegistrationDTO {
|
||||||
|
groupId: number;
|
||||||
|
}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
// todo: update
|
// todo: update
|
||||||
export enum Role {
|
export enum Role {
|
||||||
STUDENT = 'ROLE_STUDENT',
|
ADMINISTRATOR = "ROLE_ADMINISTRATOR",
|
||||||
TUTOR = 'ROLE_TUTOR',
|
STUDENT = "ROLE_STUDENT",
|
||||||
DIRECTOR = 'ROLE_DIRECTOR',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthority {
|
export interface IAuthority {
|
||||||
|
|||||||
@ -6,6 +6,12 @@ export const routes: Route[] = [{
|
|||||||
}, {
|
}, {
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
pattern: '/profile',
|
pattern: '/profile',
|
||||||
|
}, {
|
||||||
|
name: 'userList',
|
||||||
|
pattern: '/users',
|
||||||
|
}, {
|
||||||
|
name: 'userRegistration',
|
||||||
|
pattern: '/user-registration',
|
||||||
}, {
|
}, {
|
||||||
name: 'error',
|
name: 'error',
|
||||||
pattern: '/error',
|
pattern: '/error',
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import {ViewMap} from "mobx-state-router";
|
import {ViewMap} from "mobx-state-router";
|
||||||
import Home from "../components/page/Home";
|
import Home from "../components/layout/Home";
|
||||||
import Error from "../components/page/Error";
|
import Error from "../components/layout/Error";
|
||||||
import UserProfilePage from "../components/user/UserProfilePage";
|
import UserProfilePage from "../components/user/UserProfilePage";
|
||||||
|
import {UserList} from "../components/user/UserList";
|
||||||
|
import {UserRegistration} from "../components/user/UserRegistration";
|
||||||
|
|
||||||
export const viewMap: ViewMap = {
|
export const viewMap: ViewMap = {
|
||||||
root: <Home/>,
|
root: <Home/>,
|
||||||
profile: <UserProfilePage/>,
|
profile: <UserProfilePage/>,
|
||||||
|
userList: <UserList/>,
|
||||||
|
userRegistration: <UserRegistration/>,
|
||||||
error: <Error/>,
|
error: <Error/>,
|
||||||
}
|
}
|
||||||
42
web/src/services/NotificationService.ts
Normal file
42
web/src/services/NotificationService.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {NotificationStore} from "../store/NotificationStore";
|
||||||
|
|
||||||
|
export class NotificationService {
|
||||||
|
static store?: NotificationStore;
|
||||||
|
|
||||||
|
static init(store: NotificationStore) {
|
||||||
|
this.store = store;
|
||||||
|
console.debug('NotificationService initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
static error(message: string, title?: string) {
|
||||||
|
if (!this.store) {
|
||||||
|
console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.store.error(message, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
static warn(message: string, title?: string) {
|
||||||
|
if (!this.store) {
|
||||||
|
console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.store.warn(message, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
static info(message: string, title?: string) {
|
||||||
|
if (!this.store) {
|
||||||
|
console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.store.info(message, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
static success(message: string, title?: string) {
|
||||||
|
if (!this.store) {
|
||||||
|
console.log(`NotificationStore is not initialized\nMessage: ${message}\nTitle: ${title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.store.success(message, title);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ export class RouterService {
|
|||||||
|
|
||||||
static init(router: MyRouterStore) {
|
static init(router: MyRouterStore) {
|
||||||
this.router = router;
|
this.router = router;
|
||||||
|
console.debug('RouterService initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
static redirect(state: string, options?: IRouterOptions) {
|
static redirect(state: string, options?: IRouterOptions) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export class MyRouterStore extends RouterStore {
|
|||||||
init() {
|
init() {
|
||||||
const historyAdapter = new HistoryAdapter(this, browserHistory);
|
const historyAdapter = new HistoryAdapter(this, browserHistory);
|
||||||
historyAdapter.observeRouterStateChanges();
|
historyAdapter.observeRouterStateChanges();
|
||||||
|
console.debug('MyRouterStore initialized');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
93
web/src/store/NotificationStore.ts
Normal file
93
web/src/store/NotificationStore.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import {RootStore} from "./RootStore";
|
||||||
|
import {action, makeObservable, observable, runInAction} from "mobx";
|
||||||
|
import {v7 as uuidGen} from 'uuid';
|
||||||
|
|
||||||
|
export class Notification {
|
||||||
|
uuid: string;
|
||||||
|
message: string;
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
constructor(message: string, title?: string) {
|
||||||
|
this.title = title;
|
||||||
|
this.message = message;
|
||||||
|
this.uuid = uuidGen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
ERROR = 'error',
|
||||||
|
WARNING = 'warning',
|
||||||
|
INFO = 'info',
|
||||||
|
SUCCESS = 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationStore {
|
||||||
|
rootStore: RootStore;
|
||||||
|
@observable errors: Notification[] = [];
|
||||||
|
@observable warnings: Notification[] = [];
|
||||||
|
@observable infos: Notification[] = [];
|
||||||
|
@observable successes: Notification[] = [];
|
||||||
|
timeout: number = 5000;
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
this.rootStore = rootStore;
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
notify(message: string, type: NotificationType, title?: string) {
|
||||||
|
let notification = new Notification(message, title);
|
||||||
|
let container: Notification[];
|
||||||
|
switch (type) {
|
||||||
|
case NotificationType.ERROR:
|
||||||
|
container = this.errors;
|
||||||
|
break;
|
||||||
|
case NotificationType.WARNING:
|
||||||
|
container = this.warnings;
|
||||||
|
break;
|
||||||
|
case NotificationType.INFO:
|
||||||
|
container = this.infos;
|
||||||
|
break;
|
||||||
|
case NotificationType.SUCCESS:
|
||||||
|
container = this.successes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.push(notification);
|
||||||
|
setTimeout(() => runInAction(() => {
|
||||||
|
this.close(notification.uuid);
|
||||||
|
}), this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
error(message: string, title?: string) {
|
||||||
|
this.notify(message, NotificationType.ERROR, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
warn(message: string, title?: string) {
|
||||||
|
this.notify(message, NotificationType.WARNING, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
info(message: string, title?: string) {
|
||||||
|
this.notify(message, NotificationType.INFO, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
success(message: string, title?: string) {
|
||||||
|
this.notify(message, NotificationType.SUCCESS, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
close(uuid: string) {
|
||||||
|
this.errors = this.errors.filter(n => n.uuid !== uuid);
|
||||||
|
this.warnings = this.warnings.filter(n => n.uuid !== uuid);
|
||||||
|
this.infos = this.infos.filter(n => n.uuid !== uuid);
|
||||||
|
this.successes = this.successes.filter(n => n.uuid !== uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.debug('NotificationStore initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import {action, makeObservable, observable} from "mobx";
|
|
||||||
import {RootStore} from "./RootStore";
|
|
||||||
|
|
||||||
export class PendingStore {
|
|
||||||
rootStore: RootStore;
|
|
||||||
@observable pendingMap = new Map<string, number>();
|
|
||||||
|
|
||||||
constructor(rootStore: RootStore) {
|
|
||||||
this.rootStore = rootStore;
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action.bound
|
|
||||||
think(key: string = '$default') {
|
|
||||||
const count = this.pendingMap.get(key) || 0;
|
|
||||||
this.pendingMap.set(key, count + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action.bound
|
|
||||||
completeOne(key: string) {
|
|
||||||
const count = this.pendingMap.get(key);
|
|
||||||
if (count === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pendingMap.set(key, count - 1);
|
|
||||||
if (this.pendingMap.get(key) === 0) {
|
|
||||||
this.pendingMap.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action.bound
|
|
||||||
completeAll(key: string) {
|
|
||||||
this.pendingMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action.bound
|
|
||||||
isThinking(key: string = '$default') {
|
|
||||||
return this.pendingMap.get(key) !== undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +1,23 @@
|
|||||||
import {MyRouterStore} from "./MyRouterStore";
|
import {MyRouterStore} from "./MyRouterStore";
|
||||||
import {UserStore} from "./UserStore";
|
import {UserStore} from "./UserStore";
|
||||||
import {PendingStore} from "./PendingStore";
|
import {ThinkStore} from "./ThinkStore";
|
||||||
|
import {NotificationStore} from "./NotificationStore";
|
||||||
|
import {SysInfoStore} from "./SysInfoStore";
|
||||||
|
|
||||||
export class RootStore {
|
export class RootStore {
|
||||||
pendingStore = new PendingStore(this);
|
thinkStore = new ThinkStore(this);
|
||||||
userStore = new UserStore(this);
|
userStore = new UserStore(this);
|
||||||
routerStore = new MyRouterStore(this);
|
routerStore = new MyRouterStore(this);
|
||||||
|
notificationStore = new NotificationStore(this);
|
||||||
|
sysInfoStore = new SysInfoStore(this);
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.userStore.init();
|
this.thinkStore.init();
|
||||||
this.routerStore.init();
|
this.routerStore.init();
|
||||||
|
this.notificationStore.init();
|
||||||
|
this.sysInfoStore.init();
|
||||||
|
this.userStore.init();
|
||||||
|
console.debug('RootStore initialized');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {RootStore} from "../store/RootStore";
|
import {RootStore} from "./RootStore";
|
||||||
import {ContextType, createContext} from "react";
|
import {ContextType, createContext} from "react";
|
||||||
|
|
||||||
export const RootStoreContext = createContext<RootStore>(new RootStore());
|
export const RootStoreContext = createContext<RootStore>(new RootStore());
|
||||||
32
web/src/store/SysInfoStore.ts
Normal file
32
web/src/store/SysInfoStore.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {RootStore} from "./RootStore";
|
||||||
|
import {action, makeObservable, observable, runInAction} from "mobx";
|
||||||
|
import {get} from "../utils/request";
|
||||||
|
|
||||||
|
export class SysInfoStore {
|
||||||
|
rootStore: RootStore;
|
||||||
|
@observable version: string = 'unknown';
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
makeObservable(this);
|
||||||
|
this.rootStore = rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
updateVersion() {
|
||||||
|
this.rootStore.thinkStore.think('updateVersion');
|
||||||
|
get<string>('/sysinfo/version').then((response) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.version = response;
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.rootStore.thinkStore.completeOne('updateVersion');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.updateVersion();
|
||||||
|
console.debug('SysInfoStore initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
web/src/store/ThinkStore.ts
Normal file
35
web/src/store/ThinkStore.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {action, makeObservable, observable} from "mobx";
|
||||||
|
import {RootStore} from "./RootStore";
|
||||||
|
|
||||||
|
export class ThinkStore {
|
||||||
|
rootStore: RootStore;
|
||||||
|
@observable thinks: string[] = [];
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
this.rootStore = rootStore;
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
think(key: string = '$default') {
|
||||||
|
this.thinks.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
completeOne(key: string) {
|
||||||
|
this.thinks.splice(this.thinks.indexOf(key), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
completeAll(key: string) {
|
||||||
|
this.thinks = this.thinks.filter(k => k !== key);
|
||||||
|
}
|
||||||
|
|
||||||
|
isThinking(key: string = '$default') {
|
||||||
|
return this.thinks.includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.debug('ThinkStore initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,9 +16,8 @@ export class UserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
updateCurrentUser() {
|
updateCurrentUser(callback?: (user: IUser) => void) {
|
||||||
// todo: store token in localStorage
|
this.rootStore.thinkStore.think('updateCurrentUser');
|
||||||
this.rootStore.pendingStore.think('updateCurrentUser');
|
|
||||||
get<IUser>('/user/current').then((response) => {
|
get<IUser>('/user/current').then((response) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.user = response;
|
this.user = response;
|
||||||
@ -32,13 +31,21 @@ export class UserStore {
|
|||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.rootStore.pendingStore.completeOne('updateCurrentUser');
|
this.rootStore.thinkStore.completeOne('updateCurrentUser');
|
||||||
|
if (callback) {
|
||||||
|
callback(this.user);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAdministrator() {
|
||||||
|
return this.user.authenticated && this.user.authorities.some(a => a.authority === Role.ADMINISTRATOR);
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.updateCurrentUser();
|
this.updateCurrentUser();
|
||||||
|
console.debug('UserStore initialized');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
31
web/src/utils/ComponentContext.ts
Normal file
31
web/src/utils/ComponentContext.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {Component} from "react";
|
||||||
|
import {RootStoreContext, RootStoreContextType} from "../store/RootStoreContext";
|
||||||
|
|
||||||
|
export abstract class ComponentContext<P = {}, S = {}, SS = any> extends Component<P, S, SS> {
|
||||||
|
declare context: RootStoreContextType;
|
||||||
|
static contextType = RootStoreContext;
|
||||||
|
|
||||||
|
get ctx() {
|
||||||
|
return this.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
get thinkStore() {
|
||||||
|
return this.context.thinkStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userStore() {
|
||||||
|
return this.context.userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get routerStore() {
|
||||||
|
return this.context.routerStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get notificationStore() {
|
||||||
|
return this.context.notificationStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sysInfoStore() {
|
||||||
|
return this.context.sysInfoStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,47 @@
|
|||||||
import {configure} from "mobx";
|
import {configure} from "mobx";
|
||||||
import {RootStore} from "../store/RootStore";
|
import {RootStore} from "../store/RootStore";
|
||||||
import {RouterService} from "../services/RouterService";
|
import {RouterService} from "../services/RouterService";
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import {library} from '@fortawesome/fontawesome-svg-core';
|
||||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||||
import {fab} from "@fortawesome/free-brands-svg-icons";
|
import {fab} from "@fortawesome/free-brands-svg-icons";
|
||||||
import {far} from "@fortawesome/free-regular-svg-icons";
|
import {far} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import {NotificationService} from "../services/NotificationService";
|
||||||
|
|
||||||
const initMobX = () => {
|
const initMobX = () => {
|
||||||
configure({enforceActions: 'observed'});
|
configure({enforceActions: 'observed'});
|
||||||
|
console.debug('MobX initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
const initFontAwesome = () => {
|
const initFontAwesome = () => {
|
||||||
library.add(fas);
|
library.add(fas);
|
||||||
library.add(fab);
|
library.add(fab);
|
||||||
library.add(far);
|
library.add(far);
|
||||||
|
console.debug('FontAwesome initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initLibs = () => {
|
||||||
|
initMobX();
|
||||||
|
initFontAwesome();
|
||||||
|
console.debug('Libraries initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initServices = (rootStore: RootStore) => {
|
||||||
|
RouterService.init(rootStore.routerStore);
|
||||||
|
NotificationService.init(rootStore.notificationStore);
|
||||||
|
console.debug('Services initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initApp = () => {
|
export const initApp = () => {
|
||||||
initMobX();
|
console.debug('Initializing app');
|
||||||
initFontAwesome();
|
console.debug('>>>>>>>>>>>>>>>>>>>>>>>>');
|
||||||
|
|
||||||
|
initLibs();
|
||||||
|
|
||||||
let rootStore = new RootStore().init();
|
let rootStore = new RootStore().init();
|
||||||
RouterService.init(rootStore.routerStore);
|
|
||||||
|
initServices(rootStore);
|
||||||
|
|
||||||
|
console.debug('<<<<<<<<<<<<<<<<<<<<<<<<');
|
||||||
|
console.debug('App initialized');
|
||||||
return rootStore;
|
return rootStore;
|
||||||
}
|
}
|
||||||
|
|||||||
127
web/src/utils/reactive/reactiveValue.ts
Normal file
127
web/src/utils/reactive/reactiveValue.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import {action, computed, makeObservable, observable, reaction} from "mobx";
|
||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
|
||||||
|
export class ReactiveValue<T> {
|
||||||
|
@observable private val: T;
|
||||||
|
@observable private isTouched: boolean = false;
|
||||||
|
@observable private validators: ((value: T, field: string) => string)[] = [];
|
||||||
|
@observable private errors: string[] = [];
|
||||||
|
@observable private fieldName: string;
|
||||||
|
|
||||||
|
constructor(fireImmediately: boolean = false) {
|
||||||
|
makeObservable(this);
|
||||||
|
reaction(() => ({value: this.val, touched: this.isTouched}), () => {
|
||||||
|
if (!this.isTouched) {
|
||||||
|
this.errors = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.errors = this.validators
|
||||||
|
.map(validator => validator(this.val, this.fieldName))
|
||||||
|
.filter(error => error && error.length > 0);
|
||||||
|
}, {fireImmediately});
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get value() {
|
||||||
|
return this.val;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
set(value: T) {
|
||||||
|
this.val = value;
|
||||||
|
this.isTouched = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
setAuto(value: T) {
|
||||||
|
this.val = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
touch() {
|
||||||
|
this.isTouched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
untouch() {
|
||||||
|
this.isTouched = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get touched() {
|
||||||
|
return this.isTouched;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
addValidator(validator: (value: T, field?: string) => string) {
|
||||||
|
this.validators.push(validator);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get invalid() {
|
||||||
|
return this.errors.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get allErrors() {
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get firstError() {
|
||||||
|
return this.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
setField(field: string) {
|
||||||
|
this.fieldName = field;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get field() {
|
||||||
|
return this.fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
clear() {
|
||||||
|
this.val = null;
|
||||||
|
this.isTouched = false;
|
||||||
|
this.errors = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberField extends ReactiveValue<number> {
|
||||||
|
constructor(fireImmediately: boolean = false) {
|
||||||
|
super(fireImmediately);
|
||||||
|
makeObservable(this);
|
||||||
|
this.addValidator(value => {
|
||||||
|
if (_.isNaN(value)) {
|
||||||
|
return 'Должно быть числом';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
this.set(_.toNumber(event.currentTarget.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BooleanField extends ReactiveValue<boolean> {
|
||||||
|
constructor(fireImmediately: boolean = false) {
|
||||||
|
super(fireImmediately);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
this.set(event.currentTarget.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
web/src/utils/reactive/validators.ts
Normal file
106
web/src/utils/reactive/validators.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
export const required = (value: any, field = 'Поле') => {
|
||||||
|
if (!value || (typeof value === 'string' && value.trim().length === 0)) {
|
||||||
|
return `${field} обязательно для заполнения`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const greaterThan = (min: number) => (value: number, field = 'Значение') => {
|
||||||
|
if (value && value <= min) {
|
||||||
|
return `${field} должно быть больше ${min}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lessThan = (max: number) => (value: number, field = 'Значение') => {
|
||||||
|
if (value && value >= max) {
|
||||||
|
return `${field} должно быть меньше ${max}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const equals = (expected: any) => (value: any, field = 'Поле') => {
|
||||||
|
if (value && value !== expected) {
|
||||||
|
return `${field} должно быть равно ${expected}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const length = (min: number, max: number) => (value: string, field = 'Поле') => {
|
||||||
|
if (value && (value.length < min || value.length > max)) {
|
||||||
|
return `${field} должно содержать от ${min} до ${max} символов`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pattern = (regexp: RegExp, message: string) => (value: string, field = '') => {
|
||||||
|
if (!regexp.test(value)) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const email = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^.+@.+\..+$/.test(value)) {
|
||||||
|
return `${field} должно быть корректным адресом электронной почты`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const phone = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^\+[1-9]\d{6,14}$/.test(value)) {
|
||||||
|
return `${field} должно быть корректным номером телефона`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loginChars = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^[a-zA-Z0-9]*$/.test(value)) {
|
||||||
|
return `${field} должно содержать только латинские буквы, цифры и знак подчеркивания`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loginLength = (value: string, field = 'Поле') => {
|
||||||
|
if (value.length < 5 || value.length > 32) {
|
||||||
|
return `${field} должно содержать от 5 до 32 символов`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const passwordChars = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]*$/.test(value)) {
|
||||||
|
return `${field} должен содержать хотя бы одну цифру, одну заглавную и одну строчную букву`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const passwordLength = (value: string, field = 'Поле') => {
|
||||||
|
if (value.length < 8 || value.length > 32) {
|
||||||
|
return `${field} должен содержать от 8 до 32 символов`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nameChars = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^[a-zA-Zа-яА-ЯёЁ\s]+$/.test(value)) {
|
||||||
|
return `${field} должно содержать только буквы английского или русского алфавита и пробелы`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nameLength = (value: string, field = 'Поле') => {
|
||||||
|
if (value.length < 3) {
|
||||||
|
return `${field} должно содержать минимум 3 символа`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const number = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
return `${field} должно быть числом`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const date = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
|
return `${field} должно быть датой в формате ГГГГ-ММ-ДД`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const time = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^\d{2}:\d{2}$/.test(value)) {
|
||||||
|
return `${field} должно быть временем в формате ЧЧ:ММ`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dateTime = (value: string, field = 'Поле') => {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(value)) {
|
||||||
|
return `${field} должно быть датой и временем в формате ГГГГ-ММ-ДД ЧЧ:ММ`;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
web/src/utils/request.ts
Normal file
32
web/src/utils/request.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import axios, {AxiosError, AxiosRequestConfig} from "axios";
|
||||||
|
import {NotificationService} from "../services/NotificationService";
|
||||||
|
|
||||||
|
export const apiUrl = "http://localhost:8080/api/v1/";
|
||||||
|
|
||||||
|
export const get = async <R>(url: string, data?: any, doReject = true, showError = true) => await request<R>({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
data: data,
|
||||||
|
}, doReject, showError);
|
||||||
|
|
||||||
|
export const post = async <R>(url: string, data?: any, doReject = true, showError = true) => await request<R>({
|
||||||
|
url: url,
|
||||||
|
method: 'POST',
|
||||||
|
data: data,
|
||||||
|
}, doReject, showError);
|
||||||
|
|
||||||
|
export const request = async <R>(config: AxiosRequestConfig<any>, doReject: boolean, showError: boolean) => {
|
||||||
|
return new Promise<R>((resolve, reject) => {
|
||||||
|
axios.request<R>({...config, baseURL: apiUrl, withCredentials: true}).then((response) => {
|
||||||
|
resolve(response.data);
|
||||||
|
}).catch((error: AxiosError) => {
|
||||||
|
if (showError) {
|
||||||
|
if (error.response) NotificationService.error(JSON.stringify(error.response.data), 'Сервер вернул ошибку, код статуса: ' + error.response.status);
|
||||||
|
else if (error.request) NotificationService.error(error.request, 'Не удалось получить ответ от сервера');
|
||||||
|
else NotificationService.error(error.message, 'Запрос отправить не удалось');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doReject) reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import axios, {AxiosRequestConfig} from "axios";
|
|
||||||
|
|
||||||
export const apiUrl = "http://localhost:8080/api/v1/";
|
|
||||||
|
|
||||||
export const get = async <T,> (url: string, data?: any) => {
|
|
||||||
return await request<T>({
|
|
||||||
url: url,
|
|
||||||
method: 'GET',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const post = async <T,> (url: string, data?: any) => {
|
|
||||||
return await request<T>({
|
|
||||||
url: url,
|
|
||||||
method: 'POST',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const request = async <T,> (config: AxiosRequestConfig<any>) => {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
console.debug(`${config.method} ${config.url} request: ${config.method === 'GET' ? JSON.stringify(config.params) : JSON.stringify(config.data)}`);
|
|
||||||
axios.request({...config, baseURL: apiUrl, withCredentials: true}).then((response) => {
|
|
||||||
console.debug(`${config.method} ${config.url} response: ${JSON.stringify(response.data)}`);
|
|
||||||
resolve(response.data);
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(`${config.method} ${config.url} error: ${error}`);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
33
web/src/utils/tables.ts
Normal file
33
web/src/utils/tables.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export class Column {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
format: (value: any) => string;
|
||||||
|
|
||||||
|
constructor(key: string, title: string, format: (value: any) => string = value => value) {
|
||||||
|
this.key = key;
|
||||||
|
this.title = title;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Sort {
|
||||||
|
column: Column;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableDescriptor<T> {
|
||||||
|
columns: Column[];
|
||||||
|
sorts: Sort[];
|
||||||
|
filters: ((row: T) => boolean)[] = [() => true];
|
||||||
|
data: T[];
|
||||||
|
pageable = false;
|
||||||
|
pageSize = 10;
|
||||||
|
page = 0;
|
||||||
|
|
||||||
|
constructor(columns: Column[], data: T[], sorts: Sort[] = [], filters: ((row: T) => boolean)[] = [() => true]) {
|
||||||
|
this.columns = columns;
|
||||||
|
this.data = data;
|
||||||
|
this.sorts = sorts;
|
||||||
|
this.filters = filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,10 +15,11 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
|
||||||
/* lint */
|
/* lint */
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user