improved, more featured, fixed
Exceptions and Errors are better Files structure is better New ComponentContext.ts New DataTable.tsx tables.ts Massive components refactoring New Group.java New LoggingRequestFilter.java LoggingSessionListener.java New NotificationStore.ts SysInfoStore.ts New reactiveValue.ts ReactiveControls.tsx New dependencies And much more
This commit is contained in:
parent
96ffb3ad41
commit
c2d19a7724
@ -1,8 +1,6 @@
|
||||
package ru.tubryansk.tdms.config;
|
||||
|
||||
|
||||
import jakarta.servlet.http.HttpSessionEvent;
|
||||
import jakarta.servlet.http.HttpSessionListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@ -19,7 +17,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@ -42,7 +39,7 @@ public class SecurityConfiguration {
|
||||
) throws Exception {
|
||||
return httpSecurity
|
||||
.authorizeHttpRequests(this::configureHttpAuthorization)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.csrf(AbstractHttpConfigurer::disable) /* todo: настроить csrf */
|
||||
.cors(a -> a.configurationSource(cors))
|
||||
.authenticationManager(authenticationManager)
|
||||
.sessionManagement(cfg -> {
|
||||
@ -76,21 +73,6 @@ public class SecurityConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpSessionListener httpSessionListener() {
|
||||
return new HttpSessionListener() {
|
||||
@Override
|
||||
public void sessionCreated(HttpSessionEvent se) {
|
||||
log.debug("Session created: {}, user {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionDestroyed(HttpSessionEvent se) {
|
||||
log.debug("Session destroyed: {}, user: {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
|
||||
return new ProviderManager(authenticationProvider(userDetailsService));
|
||||
@ -102,15 +84,22 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<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/login").anonymous();
|
||||
httpAuthorization.requestMatchers("/api/v1/user/current").permitAll();
|
||||
|
||||
httpAuthorization.requestMatchers("/api/v1/user/get-all").hasAuthority("ROLE_ADMINISTRATOR");
|
||||
httpAuthorization.requestMatchers("/api/v1/user/register").hasAuthority("ROLE_ADMINISTRATOR");
|
||||
httpAuthorization.requestMatchers("/api/v1/user/validate-registration").hasAuthority("ROLE_ADMINISTRATOR");
|
||||
/* StudentController */
|
||||
httpAuthorization.requestMatchers("/api/v1/student/current").permitAll();
|
||||
|
||||
/* GroupController */
|
||||
httpAuthorization.requestMatchers("/api/v1/group/get-all").permitAll();
|
||||
/* deny all other api requests */
|
||||
httpAuthorization.requestMatchers("/api/**").denyAll();
|
||||
/* STATIC ROUTES */
|
||||
/* since api already blocked, all other requests are static resources */
|
||||
httpAuthorization.requestMatchers("/**").permitAll();
|
||||
}
|
||||
|
||||
|
||||
@ -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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import ru.tubryansk.tdms.dto.StudentDTO;
|
||||
import ru.tubryansk.tdms.controller.payload.StudentDTO;
|
||||
import ru.tubryansk.tdms.service.StudentService;
|
||||
|
||||
@RestController
|
||||
@ -15,6 +15,6 @@ public class StudentController {
|
||||
|
||||
@GetMapping("/current")
|
||||
public StudentDTO getCurrentStudent() {
|
||||
return studentService.getCallerStudent().map(StudentDTO::from).orElse(null);
|
||||
return studentService.getCallerStudentDTO();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.tubryansk.tdms.dto.LoginDTO;
|
||||
import ru.tubryansk.tdms.dto.UserDTO;
|
||||
import ru.tubryansk.tdms.controller.payload.LoginDTO;
|
||||
import ru.tubryansk.tdms.controller.payload.RegistrationDTO;
|
||||
import ru.tubryansk.tdms.controller.payload.UserDTO;
|
||||
import ru.tubryansk.tdms.service.AuthenticationService;
|
||||
import ru.tubryansk.tdms.service.CallerService;
|
||||
import ru.tubryansk.tdms.service.UserService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/user")
|
||||
@ -16,10 +21,12 @@ public class UserController {
|
||||
private AuthenticationService authenticationService;
|
||||
@Autowired
|
||||
private CallerService callerService;
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/current")
|
||||
public UserDTO getCurrentUser() {
|
||||
return callerService.getCallerUser().map(user -> UserDTO.from(user, true)).orElse(UserDTO.unauthenticated());
|
||||
return callerService.getCallerUserDTO();
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
@ -28,7 +35,17 @@ public class UserController {
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public void login(@RequestBody LoginDTO loginDTO) {
|
||||
authenticationService.login(loginDTO.username(), loginDTO.password());
|
||||
public void login(@RequestBody @Valid LoginDTO loginDTO) {
|
||||
authenticationService.login(loginDTO.getUsername(), loginDTO.getPassword());
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public void post(@RequestBody @Valid RegistrationDTO registrationDTO) {
|
||||
userService.registerUser(registrationDTO);
|
||||
}
|
||||
|
||||
@GetMapping("/get-all")
|
||||
public List<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.RequiredArgsConstructor;
|
||||
@ -8,7 +8,7 @@ public record ErrorResponse(String message, ErrorCode errorCode) {
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum ErrorCode {
|
||||
BAD_REQUEST(HttpStatus.BAD_REQUEST),
|
||||
BUSINESS_ERROR(HttpStatus.BAD_REQUEST),
|
||||
VALIDATION_ERROR(HttpStatus.BAD_REQUEST),
|
||||
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
NOT_FOUND(HttpStatus.NOT_FOUND),
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -14,7 +14,6 @@ import java.util.List;
|
||||
public record UserDTO(
|
||||
boolean authenticated,
|
||||
String login,
|
||||
String password,
|
||||
String fullName,
|
||||
String email,
|
||||
String phone,
|
||||
@ -28,13 +27,12 @@ public record UserDTO(
|
||||
.build();
|
||||
}
|
||||
|
||||
public static UserDTO from(User user, boolean anonymize) {
|
||||
public static UserDTO from(User user) {
|
||||
return UserDTO.builder()
|
||||
.authenticated(true)
|
||||
.login(user.getLogin())
|
||||
.password(anonymize ? "" : user.getPassword())
|
||||
.fullName(user.getFullName())
|
||||
.email(user.getMail())
|
||||
.email(user.getEmail())
|
||||
.phone(user.getNumberPhone())
|
||||
.createdAt(user.getCreatedAt())
|
||||
.updatedAt(user.getUpdatedAt())
|
||||
@ -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 lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
@Entity
|
||||
@Table(name = "diploma_topic")
|
||||
public class DiplomaTopic {
|
||||
@Id
|
||||
@ -22,3 +20,4 @@ public class DiplomaTopic {
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
}
|
||||
|
||||
|
||||
@ -2,17 +2,15 @@ package ru.tubryansk.tdms.entity;
|
||||
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
@Entity
|
||||
@Table(name = "group")
|
||||
public class Group {
|
||||
@Id
|
||||
@ -21,7 +19,7 @@ public class Group {
|
||||
private Long id;
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
@ManyToOne()
|
||||
@JoinColumn(name = "principal_user_id")
|
||||
private User principalUser;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "curator_user_id")
|
||||
private User groupCurator;
|
||||
}
|
||||
|
||||
@ -1,25 +1,22 @@
|
||||
package ru.tubryansk.tdms.entity;
|
||||
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
@Entity
|
||||
@Table(name = "`role`")
|
||||
public class Role implements GrantedAuthority {
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@ -2,10 +2,14 @@ package ru.tubryansk.tdms.entity;
|
||||
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
||||
@Data
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@Entity
|
||||
@Table(name = "student")
|
||||
public class Student {
|
||||
|
||||
@ -2,10 +2,11 @@ package ru.tubryansk.tdms.entity;
|
||||
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
@ -15,11 +16,10 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
@Entity
|
||||
@Table(name = "`user`")
|
||||
public class User implements UserDetails {
|
||||
@Id
|
||||
@ -32,18 +32,21 @@ public class User implements UserDetails {
|
||||
private String password;
|
||||
@Column(name = "full_name")
|
||||
private String fullName;
|
||||
@Column(name = "mail")
|
||||
private String mail;
|
||||
@Column(name = "email")
|
||||
private String email;
|
||||
@Column(name = "number_phone")
|
||||
private String numberPhone;
|
||||
@Column(name = "created_at")
|
||||
@CreationTimestamp
|
||||
private ZonedDateTime createdAt;
|
||||
@Column(name = "updated_at")
|
||||
@UpdateTimestamp
|
||||
private ZonedDateTime updatedAt;
|
||||
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||
@JoinTable(name = "user_role",
|
||||
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "user_role",
|
||||
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
|
||||
private List<Role> roles;
|
||||
|
||||
@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.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.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.security.core.userdetails.UserDetails;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -1,6 +1,6 @@
|
||||
package ru.tubryansk.tdms.exception;
|
||||
|
||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
||||
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||
|
||||
public class AccessDeniedException extends BusinessException {
|
||||
public AccessDeniedException() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package ru.tubryansk.tdms.exception;
|
||||
|
||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
||||
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||
|
||||
public class BusinessException extends RuntimeException {
|
||||
public BusinessException(String message) {
|
||||
@ -8,6 +8,6 @@ public class BusinessException extends RuntimeException {
|
||||
}
|
||||
|
||||
public ErrorResponse.ErrorCode getErrorCode() {
|
||||
return ErrorResponse.ErrorCode.INTERNAL_ERROR;
|
||||
return ErrorResponse.ErrorCode.BUSINESS_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,42 +2,55 @@ package ru.tubryansk.tdms.exception;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
||||
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
// todo: make a better error message
|
||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.VALIDATION_ERROR);
|
||||
public ErrorResponse handleMethodArgumentNotValidException(BindException e) {
|
||||
log.debug("Validation error: {}", e.getMessage());
|
||||
String validationErrors = e.getAllErrors().stream()
|
||||
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
return new ErrorResponse(validationErrors, ErrorResponse.ErrorCode.VALIDATION_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) {
|
||||
log.info("Business error", e);
|
||||
response.setStatus(e.getErrorCode().getHttpStatus().value());
|
||||
return new ErrorResponse(e.getMessage(), e.getErrorCode());
|
||||
}
|
||||
|
||||
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public ErrorResponse handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.debug("Access denied", e);
|
||||
return new ErrorResponse("", ErrorResponse.ErrorCode.ACCESS_DENIED);
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) {
|
||||
// todo: make error page
|
||||
log.error("Resource not found", e);
|
||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ErrorResponse handleUnexpectedException(Exception e) {
|
||||
// todo: make error page
|
||||
log.error("Unexpected exception.", e);
|
||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package ru.tubryansk.tdms.exception;
|
||||
|
||||
import ru.tubryansk.tdms.dto.ErrorResponse;
|
||||
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||
|
||||
public class NotFoundException extends BusinessException {
|
||||
public NotFoundException(Class<?> entityClass, Integer id) {
|
||||
super(entityClass.getSimpleName() + " with id " + id + " not found");
|
||||
public NotFoundException(Class<?> entityClass, Object id) {
|
||||
super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -8,6 +8,7 @@ import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
|
||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||
@ -32,6 +33,7 @@ public class AuthenticationService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void login(String username, String password) {
|
||||
try {
|
||||
var context = SecurityContextHolder.createEmptyContext();
|
||||
@ -41,8 +43,8 @@ public class AuthenticationService {
|
||||
SecurityContextHolder.setContext(context);
|
||||
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to log in user: {}. {}", username, e.getMessage());
|
||||
return;
|
||||
log.error("Failed to log in user: {}", username, e);
|
||||
throw e;
|
||||
}
|
||||
log.debug("User {} logged in", username);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package ru.tubryansk.tdms.service;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.tubryansk.tdms.controller.payload.UserDTO;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -18,4 +19,8 @@ public class CallerService {
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public UserDTO getCallerUserDTO() {
|
||||
return getCallerUser().map(UserDTO::from).orElse(UserDTO.unauthenticated());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.tubryansk.tdms.controller.payload.StudentDTO;
|
||||
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
||||
import ru.tubryansk.tdms.entity.Student;
|
||||
import ru.tubryansk.tdms.entity.repository.DiplomaTopicRepository;
|
||||
import ru.tubryansk.tdms.entity.repository.StudentRepository;
|
||||
import ru.tubryansk.tdms.exception.AccessDeniedException;
|
||||
import ru.tubryansk.tdms.repository.DiplomaTopicRepository;
|
||||
import ru.tubryansk.tdms.repository.StudentRepository;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -43,4 +44,13 @@ public class StudentService {
|
||||
public Optional<Student> getCallerStudent() {
|
||||
return studentRepository.findByUser(callerService.getCallerUser().orElse(null));
|
||||
}
|
||||
|
||||
public StudentDTO getCallerStudentDTO() {
|
||||
Student callerStudent = getCallerStudent().orElse(null);
|
||||
if (callerStudent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StudentDTO.from(callerStudent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.tubryansk.tdms.controller.payload.RegistrationDTO;
|
||||
import ru.tubryansk.tdms.controller.payload.UserDTO;
|
||||
import ru.tubryansk.tdms.entity.Role;
|
||||
import ru.tubryansk.tdms.entity.Student;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
import ru.tubryansk.tdms.repository.UserRepository;
|
||||
import ru.tubryansk.tdms.entity.repository.GroupRepository;
|
||||
import ru.tubryansk.tdms.entity.repository.StudentRepository;
|
||||
import ru.tubryansk.tdms.entity.repository.UserRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@ -18,10 +25,75 @@ import java.util.Optional;
|
||||
public class UserService implements UserDetailsService {
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Autowired
|
||||
private GroupRepository groupRepository;
|
||||
@Autowired
|
||||
private StudentRepository studentRepository;
|
||||
@Autowired
|
||||
private RoleService roleService;
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public User loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
return userRepository.findUserByLogin(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
log.info("Loading user with username: {}", username);
|
||||
User user = userRepository.findUserByLogin(username).orElseThrow(() -> {
|
||||
log.info("User with login {} not found", username);
|
||||
return new UsernameNotFoundException("User with login " + username + " not found");
|
||||
});
|
||||
log.info("User with login {} loaded", username);
|
||||
return user;
|
||||
}
|
||||
|
||||
public List<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
|
||||
(
|
||||
id bigint primary key,
|
||||
|
||||
name text not null unique,
|
||||
authority text not null unique
|
||||
);
|
||||
|
||||
-- COMMENTS
|
||||
comment on table role is 'Таблица ролей пользователей';
|
||||
|
||||
comment on column role.name is 'Человекочитаемое имя роли';
|
||||
comment on column role.authority is 'Имя роли в системе';
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
create table "user"
|
||||
(
|
||||
id bigserial primary key,
|
||||
login text not null unique,
|
||||
password text not null,
|
||||
full_name text not null,
|
||||
mail text not null unique,
|
||||
number_phone text not null unique,
|
||||
|
||||
login text not null unique,
|
||||
password text not null,
|
||||
full_name text not null,
|
||||
email text not null unique,
|
||||
number_phone text not null unique,
|
||||
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
-- COMMENTS
|
||||
comment on table "user" is 'Таблица пользователей';
|
||||
|
||||
comment on column "user".login is 'Логин пользователя';
|
||||
comment on column "user".password is 'Пароль пользователя';
|
||||
comment on column "user".full_name is 'Полное имя пользователя в формате Фамилия Имя Отчество';
|
||||
comment on column "user".mail is 'Почта пользователя';
|
||||
comment on column "user".email is 'Почта пользователя';
|
||||
comment on column "user".number_phone is 'Номер телефона пользователя';
|
||||
comment on column "user".created_at is 'Дата создания записи';
|
||||
comment on column "user".updated_at is 'Дата последнего обновления записи';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
create table user_role
|
||||
(
|
||||
id bigserial primary key,
|
||||
|
||||
user_id bigint not null,
|
||||
role_id bigint not null
|
||||
);
|
||||
@ -14,5 +15,7 @@ alter table user_role
|
||||
foreign key (role_id) references role (id);
|
||||
|
||||
-- COMMENTS
|
||||
comment on table user_role is 'Таблица связи пользователей и ролей';
|
||||
|
||||
comment on column user_role.user_id is 'Идентификатор пользователя';
|
||||
comment on column user_role.role_id is 'Идентификатор роли';
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
create table diploma_topic
|
||||
(
|
||||
id bigserial primary key,
|
||||
name text not null unique
|
||||
|
||||
name text not null
|
||||
);
|
||||
|
||||
-- COMMENTS
|
||||
comment on table diploma_topic is 'Таблица тем дипломных работ';
|
||||
|
||||
comment on column diploma_topic.name is 'Название темы дипломной работы';
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
create table "group"
|
||||
(
|
||||
id bigserial primary key,
|
||||
name text not null unique,
|
||||
principal_user_id bigint not null
|
||||
id bigserial primary key,
|
||||
|
||||
name text not null unique,
|
||||
curator_user_id bigint,
|
||||
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
-- FOREIGN KEY
|
||||
alter table "group"
|
||||
add constraint fk_group_principal_user_id
|
||||
foreign key (principal_user_id) references "user" (id)
|
||||
on delete cascade;
|
||||
add constraint fk_group_curator_user_id
|
||||
foreign key (curator_user_id) references "user" (id)
|
||||
on delete set null on update cascade;
|
||||
|
||||
-- COMMENTS
|
||||
comment on table "group" is 'Таблица групп студентов';
|
||||
|
||||
comment on column "group".name is 'Название группы';
|
||||
comment on column "group".principal_user_id is 'Идентификатор куратора группы';
|
||||
comment on column "group".curator_user_id is 'Идентификатор куратора группы';
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
create table student
|
||||
(
|
||||
id bigserial primary key,
|
||||
user_id bigint not null,
|
||||
diploma_topic_id bigint not null,
|
||||
mentor_user_id bigint not null,
|
||||
group_id bigint not null,
|
||||
|
||||
form boolean,
|
||||
protection_order integer not null,
|
||||
protection_day int,
|
||||
protection_order int,
|
||||
magistracy text,
|
||||
digital_format_present boolean,
|
||||
mark_comment integer,
|
||||
mark_practice integer,
|
||||
mark_comment int,
|
||||
mark_practice int,
|
||||
predefence_comment text,
|
||||
normal_control text,
|
||||
anti_plagiarism int,
|
||||
note text,
|
||||
record_book_returned boolean,
|
||||
work text,
|
||||
user_id bigint not null,
|
||||
diploma_topic_id bigint not null,
|
||||
mentor_user_id bigint not null,
|
||||
group_id bigint not null,
|
||||
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz
|
||||
);
|
||||
@ -25,22 +28,30 @@ create table student
|
||||
alter table student
|
||||
add constraint fk_student_user_id
|
||||
foreign key (user_id) references "user" (id)
|
||||
on delete cascade;
|
||||
on delete cascade on update cascade;
|
||||
alter table student
|
||||
add constraint fk_student_diploma_topic_id
|
||||
foreign key (diploma_topic_id) references diploma_topic (id)
|
||||
on delete cascade;
|
||||
on delete set null on update cascade;
|
||||
alter table student
|
||||
add constraint fk_student_mentor_user_id
|
||||
foreign key (mentor_user_id) references "user" (id)
|
||||
on delete cascade;
|
||||
on delete set null on update cascade;
|
||||
alter table student
|
||||
add constraint fk_student_group_id
|
||||
foreign key (group_id) references "group" (id)
|
||||
on delete cascade;
|
||||
on delete set null on update cascade;
|
||||
|
||||
-- COMMENTS
|
||||
comment on table student is 'Таблица студентов';
|
||||
|
||||
comment on column student.user_id is 'Идентификатор пользователя';
|
||||
comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы';
|
||||
comment on column student.mentor_user_id is 'Идентификатор научного руководителя';
|
||||
comment on column student.group_id is 'Идентификатор группы';
|
||||
|
||||
comment on column student.form is 'Форма обучения';
|
||||
comment on column student.protection_day is 'День защиты';
|
||||
comment on column student.protection_order is 'Порядок защиты';
|
||||
comment on column student.magistracy is 'Магистратура';
|
||||
comment on column student.digital_format_present is 'Предоставлен в электронном виде';
|
||||
@ -52,7 +63,3 @@ comment on column student.anti_plagiarism is 'Антиплагиат';
|
||||
comment on column student.note is 'Примечание';
|
||||
comment on column student.record_book_returned is 'Ведомость возвращена';
|
||||
comment on column student.work is 'Работа';
|
||||
comment on column student.user_id is 'Идентификатор пользователя';
|
||||
comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы';
|
||||
comment on column student.mentor_user_id is 'Идентификатор научного руководителя';
|
||||
comment on column student.group_id is 'Идентификатор группы';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
insert into "user" (id, login, password, full_name, mail, number_phone, created_at)
|
||||
values (1, 'admin', '{noop}admin', 'Администратор', 'admin@localhost', '+79110000000', now());
|
||||
insert into "user" (id, login, password, full_name, email, number_phone, created_at)
|
||||
values (1, 'admin', '{noop}admin', 'Администратор', 'admin@tdms.tu-byransk.ru', '', now());
|
||||
|
||||
insert into user_role (id, user_id, role_id)
|
||||
values (1, 1, 4);
|
||||
|
||||
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-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"axios": "^1.7.7",
|
||||
"bootstrap": "^5.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mobx": "^6.13.1",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-state-router": "^6.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.25.7",
|
||||
@ -2105,6 +2108,12 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -3858,20 +3867,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@ -4591,7 +4586,7 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@ -5974,6 +5969,16 @@
|
||||
"websocket-driver": "^0.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -6454,12 +6459,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/value-equal": {
|
||||
|
||||
@ -12,14 +12,17 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"axios": "^1.7.7",
|
||||
"bootstrap": "^5.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mobx": "^6.13.1",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-state-router": "^6.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.25.7",
|
||||
|
||||
@ -3,7 +3,7 @@ import './index.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import {RouterContext, RouterView} from "mobx-state-router";
|
||||
import {initApp} from "./utils/init";
|
||||
import {RootStoreContext} from './context/RootStoreContext';
|
||||
import {RootStoreContext} from './store/RootStoreContext';
|
||||
import {viewMap} from "./router/viewMap";
|
||||
|
||||
const rootStore = initApp();
|
||||
|
||||
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 Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {observer} from "mobx-react";
|
||||
import {ComponentContext} from "../../utils/ComponentContext";
|
||||
import {NotificationContainer} from "../NotificationContainer";
|
||||
import {Footer} from "./Footer";
|
||||
|
||||
@observer
|
||||
class DefaultPage extends Component<any> {
|
||||
export abstract class DefaultPage extends ComponentContext {
|
||||
get page(): ReactNode {
|
||||
throw new Error('This is not abstract method, ' +
|
||||
'because mobx cant handle abstract methods. ' +
|
||||
'Please override this method in child class. ' +
|
||||
'Do not call it directly.');
|
||||
}
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
|
||||
render() {
|
||||
let isLoading = this.context.pendingStore.isThinking();
|
||||
const thinking = this.thinkStore.isThinking();
|
||||
|
||||
return <>
|
||||
<Header/>
|
||||
<Container className={"mt-5 mb-5"}>
|
||||
{
|
||||
isLoading &&
|
||||
thinking &&
|
||||
<div id='fullscreen-loader'>
|
||||
<FontAwesomeIcon icon='gear' size="4x" spin/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!isLoading &&
|
||||
this.page
|
||||
!thinking &&
|
||||
<>
|
||||
<NotificationContainer/>
|
||||
{this.page}
|
||||
</>
|
||||
}
|
||||
</Container>
|
||||
<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 {
|
||||
get page() {
|
||||
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 {Component} from "react";
|
||||
import {RouterLink} from "mobx-state-router";
|
||||
import {IAuthenticated} from "../../../models/user";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
||||
import {IAuthenticated} from "../../models/user";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../store/RootStoreContext";
|
||||
import {observer} from "mobx-react";
|
||||
import {post} from "../../../utils/request";
|
||||
import {LoginModal} from "../../user/LoginModal";
|
||||
import {ModalState} from "../../../state/ModalState";
|
||||
import {post} from "../../utils/request";
|
||||
import {LoginModal} from "../user/LoginModal";
|
||||
import {ModalState} from "../../utils/modalState";
|
||||
import {action, makeObservable} from "mobx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {ComponentContext} from "../../utils/ComponentContext";
|
||||
|
||||
@observer
|
||||
class Header extends Component {
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
class Header extends ComponentContext {
|
||||
|
||||
loginModalState = new ModalState();
|
||||
|
||||
@ -22,13 +20,10 @@ class Header extends Component {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get loginThink() {
|
||||
return this.context.pendingStore.isThinking('updateCurrentUser');
|
||||
}
|
||||
|
||||
render() {
|
||||
const userStore = this.context.userStore;
|
||||
const user = userStore.user;
|
||||
let thinking = this.thinkStore.isThinking('updateCurrentUser');
|
||||
|
||||
return <>
|
||||
<header>
|
||||
@ -38,23 +33,27 @@ class Header extends Component {
|
||||
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
||||
</Navbar.Brand>
|
||||
<Nav>
|
||||
<NavDropdown title="Группы">
|
||||
<NavDropdown.Item>Список</NavDropdown.Item>
|
||||
<NavDropdown.Item>Редактировать</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
{
|
||||
user.authenticated && userStore.isAdministrator() &&
|
||||
<NavDropdown title="Пользователи">
|
||||
<NavDropdown.Item as={RouterLink} routeName={'userList'} children={'Список'}/>
|
||||
<NavDropdown.Item as={RouterLink} routeName={'userRegistration'}
|
||||
children={'Зарегистрировать'}/>
|
||||
</NavDropdown>
|
||||
}
|
||||
</Nav>
|
||||
|
||||
<Nav className="ms-auto">
|
||||
{
|
||||
this.loginThink &&
|
||||
thinking &&
|
||||
<FontAwesomeIcon icon='gear' spin/>
|
||||
}
|
||||
{
|
||||
user.authenticated && !this.loginThink &&
|
||||
user.authenticated && !thinking &&
|
||||
<AuthenticatedItems/>
|
||||
}
|
||||
{
|
||||
!user.authenticated && !this.loginThink &&
|
||||
!user.authenticated && !thinking &&
|
||||
<>
|
||||
<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;
|
||||
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 {ModalState} from "../../state/ModalState";
|
||||
import {ModalState} from "../../utils/modalState";
|
||||
import {observer} from "mobx-react";
|
||||
import {action, computed, makeObservable, observable, reaction} from "mobx";
|
||||
import {post} from "../../utils/request";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../context/RootStoreContext";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {ComponentContext} from "../../utils/ComponentContext";
|
||||
|
||||
interface LoginModalProps {
|
||||
modalState: ModalState;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class LoginModal extends Component<LoginModalProps> {
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
modalState = this.props.modalState;
|
||||
export class LoginModal extends ComponentContext<LoginModalProps> {
|
||||
@observable login = '';
|
||||
@observable loginError = '';
|
||||
@observable password = '';
|
||||
@observable passwordError = '';
|
||||
@observable rememberMe = false;
|
||||
|
||||
constructor(props: LoginModalProps) {
|
||||
super(props);
|
||||
@ -74,48 +71,72 @@ export class LoginModal extends Component<LoginModalProps> {
|
||||
}
|
||||
|
||||
@action.bound
|
||||
onLogin() {
|
||||
tryLogin() {
|
||||
if (this.loginButtonDisabled)
|
||||
return;
|
||||
|
||||
this.thinkStore.think('loginModal');
|
||||
post('user/login', {
|
||||
username: this.login,
|
||||
password: this.password
|
||||
}).then(() => {
|
||||
this.context.userStore.updateCurrentUser();
|
||||
this.modalState.close();
|
||||
this.userStore.updateCurrentUser((user) => {
|
||||
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() {
|
||||
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.Title>Вход</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormGroup className={'mb-3'}>
|
||||
<FormLabel>Имя пользователя</FormLabel>
|
||||
<FormControl type="text" onChange={this.onLoginInput}/>
|
||||
{
|
||||
this.loginError &&
|
||||
<FormText className={'text-danger'}>{this.loginError}</FormText>
|
||||
}
|
||||
</FormGroup>
|
||||
<FormGroup className={'mb-3'}>
|
||||
<FormLabel>Пароль</FormLabel>
|
||||
<FormControl type="password" onChange={this.onPasswordInput}/>
|
||||
{
|
||||
this.passwordError &&
|
||||
<FormText className={'text-danger'}>{this.passwordError}</FormText>
|
||||
}
|
||||
</FormGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant={'primary'} onClick={this.onLogin} disabled={this.loginButtonDisabled}>Войти</Button>
|
||||
<Button variant={'secondary'} onClick={this.modalState.close}>Закрыть</Button>
|
||||
</Modal.Footer>
|
||||
{
|
||||
thinking &&
|
||||
<Modal.Body>
|
||||
<FontAwesomeIcon icon={'gear'} spin/>
|
||||
</Modal.Body>
|
||||
}
|
||||
{
|
||||
!thinking &&
|
||||
<>
|
||||
<Modal.Body>
|
||||
<FormGroup className={'mb-3'}>
|
||||
<FormLabel>Имя пользователя</FormLabel>
|
||||
<FormControl type="text" onChange={this.onLoginInput}/>
|
||||
{
|
||||
this.loginError &&
|
||||
<FormText className={'text-danger'}>{this.loginError}</FormText>
|
||||
}
|
||||
</FormGroup>
|
||||
<FormGroup className={'mb-3'}>
|
||||
<FormLabel>Пароль</FormLabel>
|
||||
<FormControl type="password" onChange={this.onPasswordInput}/>
|
||||
{
|
||||
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>
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
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 {observer} from "mobx-react";
|
||||
import {RootStoreContext, type RootStoreContextType} from "../../context/RootStoreContext";
|
||||
import {RootStoreContext, type RootStoreContextType} from "../../store/RootStoreContext";
|
||||
import {IAuthenticated} from "../../models/user";
|
||||
import {Component} from "react";
|
||||
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
|
||||
export enum Role {
|
||||
STUDENT = 'ROLE_STUDENT',
|
||||
TUTOR = 'ROLE_TUTOR',
|
||||
DIRECTOR = 'ROLE_DIRECTOR',
|
||||
ADMINISTRATOR = "ROLE_ADMINISTRATOR",
|
||||
STUDENT = "ROLE_STUDENT",
|
||||
}
|
||||
|
||||
export interface IAuthority {
|
||||
|
||||
@ -6,6 +6,12 @@ export const routes: Route[] = [{
|
||||
}, {
|
||||
name: 'profile',
|
||||
pattern: '/profile',
|
||||
}, {
|
||||
name: 'userList',
|
||||
pattern: '/users',
|
||||
}, {
|
||||
name: 'userRegistration',
|
||||
pattern: '/user-registration',
|
||||
}, {
|
||||
name: 'error',
|
||||
pattern: '/error',
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import {ViewMap} from "mobx-state-router";
|
||||
import Home from "../components/page/Home";
|
||||
import Error from "../components/page/Error";
|
||||
import Home from "../components/layout/Home";
|
||||
import Error from "../components/layout/Error";
|
||||
import UserProfilePage from "../components/user/UserProfilePage";
|
||||
import {UserList} from "../components/user/UserList";
|
||||
import {UserRegistration} from "../components/user/UserRegistration";
|
||||
|
||||
export const viewMap: ViewMap = {
|
||||
root: <Home/>,
|
||||
profile: <UserProfilePage/>,
|
||||
userList: <UserList/>,
|
||||
userRegistration: <UserRegistration/>,
|
||||
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) {
|
||||
this.router = router;
|
||||
console.debug('RouterService initialized');
|
||||
}
|
||||
|
||||
static redirect(state: string, options?: IRouterOptions) {
|
||||
|
||||
@ -12,6 +12,7 @@ export class MyRouterStore extends RouterStore {
|
||||
init() {
|
||||
const historyAdapter = new HistoryAdapter(this, browserHistory);
|
||||
historyAdapter.observeRouterStateChanges();
|
||||
console.debug('MyRouterStore initialized');
|
||||
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 {UserStore} from "./UserStore";
|
||||
import {PendingStore} from "./PendingStore";
|
||||
import {ThinkStore} from "./ThinkStore";
|
||||
import {NotificationStore} from "./NotificationStore";
|
||||
import {SysInfoStore} from "./SysInfoStore";
|
||||
|
||||
export class RootStore {
|
||||
pendingStore = new PendingStore(this);
|
||||
thinkStore = new ThinkStore(this);
|
||||
userStore = new UserStore(this);
|
||||
routerStore = new MyRouterStore(this);
|
||||
notificationStore = new NotificationStore(this);
|
||||
sysInfoStore = new SysInfoStore(this);
|
||||
|
||||
init() {
|
||||
this.userStore.init();
|
||||
this.thinkStore.init();
|
||||
this.routerStore.init();
|
||||
this.notificationStore.init();
|
||||
this.sysInfoStore.init();
|
||||
this.userStore.init();
|
||||
console.debug('RootStore initialized');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {RootStore} from "../store/RootStore";
|
||||
import {RootStore} from "./RootStore";
|
||||
import {ContextType, createContext} from "react";
|
||||
|
||||
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
|
||||
updateCurrentUser() {
|
||||
// todo: store token in localStorage
|
||||
this.rootStore.pendingStore.think('updateCurrentUser');
|
||||
updateCurrentUser(callback?: (user: IUser) => void) {
|
||||
this.rootStore.thinkStore.think('updateCurrentUser');
|
||||
get<IUser>('/user/current').then((response) => {
|
||||
runInAction(() => {
|
||||
this.user = response;
|
||||
@ -32,13 +31,21 @@ export class UserStore {
|
||||
}
|
||||
}).finally(() => {
|
||||
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() {
|
||||
this.updateCurrentUser();
|
||||
console.debug('UserStore initialized');
|
||||
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 {RootStore} from "../store/RootStore";
|
||||
import {RouterService} from "../services/RouterService";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||
import {library} from '@fortawesome/fontawesome-svg-core';
|
||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||
import {fab} from "@fortawesome/free-brands-svg-icons";
|
||||
import {far} from "@fortawesome/free-regular-svg-icons";
|
||||
import {NotificationService} from "../services/NotificationService";
|
||||
|
||||
const initMobX = () => {
|
||||
configure({enforceActions: 'observed'});
|
||||
console.debug('MobX initialized');
|
||||
}
|
||||
|
||||
const initFontAwesome = () => {
|
||||
library.add(fas);
|
||||
library.add(fab);
|
||||
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 = () => {
|
||||
initMobX();
|
||||
initFontAwesome();
|
||||
console.debug('Initializing app');
|
||||
console.debug('>>>>>>>>>>>>>>>>>>>>>>>>');
|
||||
|
||||
initLibs();
|
||||
|
||||
let rootStore = new RootStore().init();
|
||||
RouterService.init(rootStore.routerStore);
|
||||
|
||||
initServices(rootStore);
|
||||
|
||||
console.debug('<<<<<<<<<<<<<<<<<<<<<<<<');
|
||||
console.debug('App initialized');
|
||||
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,
|
||||
|
||||
/* lint */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictFunctionTypes": true,
|
||||
"alwaysStrict": true,
|
||||
"strictBindCallApply": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user