Improvements

This commit is contained in:
Maksim Skobaro 2025-02-15 02:32:20 +03:00
parent bc71a87414
commit d298a5f1f4
94 changed files with 1241 additions and 688 deletions

View File

@ -16,7 +16,7 @@
<relativePath/>
</parent>
<groupId>ru.tubryansk</groupId>
<groupId>ru.mskobaro</groupId>
<artifactId>tdms</artifactId>
<version>0.0.1</version>
<packaging>pom</packaging>

View File

@ -5,12 +5,12 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.tubryansk</groupId>
<groupId>ru.mskobaro</groupId>
<artifactId>tdms</artifactId>
<version>0.0.1</version>
</parent>
<groupId>ru.tubryansk.tdms</groupId>
<groupId>ru.mskobaro</groupId>
<artifactId>server</artifactId>
<version>0.0.1</version>

View File

@ -0,0 +1,21 @@
package ru.mskobaro.tdms;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
@SpringBootApplication
@Slf4j
public class TdmsApplication {
public static void main(String[] args) {
Thread.currentThread().setName("spring-bootstrapper");
ConfigurableApplicationContext ctx = SpringApplication.run(TdmsApplication.class, args);
Environment environment = ctx.getEnvironment();
log.info("Static files location: {}", environment.getProperty("spring.web.resources.static-locations"));
ctx.start();
}
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.entity;
package ru.mskobaro.tdms.domain.entity;
import jakarta.persistence.*;

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.entity;
package ru.mskobaro.tdms.domain.entity;
import jakarta.persistence.*;

View File

@ -1,11 +1,13 @@
package ru.tubryansk.tdms.entity;
package ru.mskobaro.tdms.domain.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
@ -13,6 +15,8 @@ import org.springframework.security.core.GrantedAuthority;
@Getter
@ToString
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "`role`")
public class Role implements GrantedAuthority {
@Id

View File

@ -1,10 +1,14 @@
package ru.tubryansk.tdms.entity;
package ru.mskobaro.tdms.domain.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.ZonedDateTime;
@Getter
@ -54,4 +58,11 @@ public class Student {
@ManyToOne
@JoinColumn(name = "group_id")
private Group group;
@Column(name = "created_at")
@CreationTimestamp
private ZonedDateTime createdAt;
@Column(name = "updated_at")
@UpdateTimestamp
private ZonedDateTime updatedAt;
}

View File

@ -1,7 +1,9 @@
package ru.tubryansk.tdms.entity;
package ru.mskobaro.tdms.domain.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@ -9,6 +11,8 @@ import java.time.ZonedDateTime;
import java.util.List;
@Getter
@Setter
@ToString
@Entity
@Table(name = "teacher")
public class Teacher {

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.entity;
package ru.mskobaro.tdms.domain.entity;
import jakarta.persistence.*;

View File

@ -0,0 +1,14 @@
package ru.mskobaro.tdms.domain.exception;
import ru.mskobaro.tdms.presentation.payload.ErrorDTO;
public class AccessDeniedException extends BusinessException {
public AccessDeniedException() {
super("Access denied");
}
@Override
public ErrorDTO.ErrorCode getErrorCode() {
return ErrorDTO.ErrorCode.ACCESS_DENIED;
}
}

View File

@ -0,0 +1,13 @@
package ru.mskobaro.tdms.domain.exception;
import ru.mskobaro.tdms.presentation.payload.ErrorDTO;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public ErrorDTO.ErrorCode getErrorCode() {
return ErrorDTO.ErrorCode.BUSINESS_ERROR;
}
}

View File

@ -0,0 +1,34 @@
package ru.mskobaro.tdms.domain.exception;
import ru.mskobaro.tdms.presentation.payload.ErrorDTO;
public class NotFoundException extends BusinessException {
public NotFoundException(Class<?> entityClass, Object id) {
super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден");
}
public NotFoundException(Class<?> entityClass) {
super(entityClass.getSimpleName() + " не найден");
}
public NotFoundException() {
super("Не найдено");
}
public static void throwIfNull(Object object) {
if (object == null) {
throw new NotFoundException();
}
}
public static void throwIfNull(Object object, Class<?> entityClass, Object id) {
if (object == null) {
throw new NotFoundException(entityClass, id);
}
}
@Override
public ErrorDTO.ErrorCode getErrorCode() {
return ErrorDTO.ErrorCode.NOT_FOUND;
}
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.service;
package ru.mskobaro.tdms.domain.service;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@ -9,9 +9,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
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;
@Service
@Slf4j
@ -21,11 +18,6 @@ public class AuthenticationService {
@Autowired
private AuthenticationManager authenticationManager;
public boolean authenticated() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.isAuthenticated() && (authentication.getPrincipal() instanceof User);
}
public void logout() {
log.info("Logging out user: {}", SecurityContextHolder.getContext().getAuthentication().getName());
HttpSession session = request.getSession(false);
@ -40,12 +32,9 @@ public class AuthenticationService {
public void login(String username, String password) {
log.info("Logging in user: {}, ip: {}", username, request.getRemoteAddr());
var context = SecurityContextHolder.createEmptyContext();
var token = new UsernamePasswordAuthenticationToken(username, password);
var authenticated = authenticationManager.authenticate(token);
context.setAuthentication(authenticated);
SecurityContextHolder.setContext(context);
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
SecurityContextHolder.getContext().setAuthentication(authenticated);
log.info("User {} logged in", username);
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.service;
package ru.mskobaro.tdms.domain.service;
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;

View File

@ -1,15 +1,15 @@
package ru.tubryansk.tdms.service;
package ru.mskobaro.tdms.domain.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.tubryansk.tdms.controller.payload.GroupDTO;
import ru.tubryansk.tdms.controller.payload.GroupEditDTO;
import ru.tubryansk.tdms.entity.Group;
import ru.tubryansk.tdms.entity.User;
import ru.tubryansk.tdms.entity.repository.GroupRepository;
import ru.tubryansk.tdms.exception.BusinessException;
import ru.mskobaro.tdms.domain.entity.Group;
import ru.mskobaro.tdms.domain.entity.User;
import ru.mskobaro.tdms.domain.exception.BusinessException;
import ru.mskobaro.tdms.integration.database.GroupRepository;
import ru.mskobaro.tdms.presentation.payload.GroupDTO;
import ru.mskobaro.tdms.presentation.payload.GroupEditDTO;
import java.util.Collection;
import java.util.List;
@ -21,12 +21,12 @@ public class GroupService {
@Autowired
private GroupRepository groupRepository;
@Autowired
private CallerService callerService;
private UserService userService;
public Collection<GroupDTO> getAllGroups() {
log.info("Getting all groups");
List<Group> groups = groupRepository.findAll();
User callerUser = callerService.getCallerUser().orElse(null);
User callerUser = userService.getCallerUser();
List<GroupDTO> result = groups.stream().map(group -> {
GroupDTO groupDTO = new GroupDTO();

View File

@ -0,0 +1,56 @@
package ru.mskobaro.tdms.domain.service;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.mskobaro.tdms.domain.entity.Role;
import ru.mskobaro.tdms.integration.database.RoleRepository;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
@Slf4j
public class RoleService {
@RequiredArgsConstructor
@Getter
public enum Authority {
ADMIN("ROLE_ADMINISTRATOR"),
COMM_MEMBER("ROLE_COMMISSION_MEMBER"),
TEACHER("ROLE_TEACHER"),
SECRETARY("ROLE_SECRETARY"),
STUDENT("ROLE_STUDENT"),
;
private final String authority;
public static Authority from(String authority) {
for (Authority value : values()) {
if (value.getAuthority().equals(authority)) {
return value;
}
}
throw new IllegalArgumentException("No such authority: " + authority);
}
}
public transient Map<String, Role> roles;
@Autowired
private RoleRepository roleRepository;
@PostConstruct
@Transactional
public void bootstrapRolesCache() {
roles = new ConcurrentHashMap<>();
roleRepository.findAll().forEach(role -> roles.put(role.getAuthority(), role));
log.info("Roles initialized: {}", roles);
}
public Role getRoleByAuthority(Authority authority) {
return roles.get(authority.getAuthority());
}
}

View File

@ -0,0 +1,57 @@
package ru.mskobaro.tdms.domain.service;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.mskobaro.tdms.domain.entity.Student;
import ru.mskobaro.tdms.domain.entity.User;
import ru.mskobaro.tdms.domain.exception.BusinessException;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
import ru.mskobaro.tdms.integration.database.StudentRepository;
import ru.mskobaro.tdms.integration.database.UserRepository;
import ru.mskobaro.tdms.presentation.payload.StudentDTO;
@Service
@Transactional
@Slf4j
public class StudentService {
@Autowired
private StudentRepository studentRepository;
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
public Student getCallerStudentThrow() {
userService.sureCallerInAnyRole(RoleService.Authority.STUDENT);
return studentRepository.findByUser(userService.getCallerUser());
}
public StudentDTO getCallerStudentDtoThrow() {
Student callerStudent = getCallerStudentThrow();
if (callerStudent == null) {
throw new BusinessException("Вызывающий пользователь является студентом, но ассоциированный с ним студент не найден");
}
return StudentDTO.from(callerStudent);
}
public StudentDTO getStudentByUserIdThrow(Long id) {
User callerUser = userService.getCallerUser();
if (callerUser == null) {
throw new NotFoundException();
}
if (callerUser.getId().equals(id)) {
return getCallerStudentDtoThrow();
} else if (userService.isCallerInRole(RoleService.Authority.ADMIN, RoleService.Authority.TEACHER, RoleService.Authority.SECRETARY)) {
User user = userRepository.findByIdThrow(id);
Student student = studentRepository.findByUser(user);
NotFoundException.throwIfNull(student, Student.class, user.getId());
return StudentDTO.from(student);
} else {
throw new NotFoundException(Student.class, id);
}
}
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.service;
package ru.mskobaro.tdms.domain.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

View File

@ -0,0 +1,181 @@
package ru.mskobaro.tdms.domain.service;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.mskobaro.tdms.domain.entity.*;
import ru.mskobaro.tdms.domain.exception.AccessDeniedException;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
import ru.mskobaro.tdms.integration.database.GroupRepository;
import ru.mskobaro.tdms.integration.database.StudentRepository;
import ru.mskobaro.tdms.integration.database.TeacherRepository;
import ru.mskobaro.tdms.integration.database.UserRepository;
import ru.mskobaro.tdms.presentation.payload.RegistrationDTO;
import ru.mskobaro.tdms.presentation.payload.UserDTO;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
@Slf4j
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;
@Autowired
private TeacherRepository teacherRepository;
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("Loading user with username: {}", username);
User user = userRepository.findUserByLogin(username).orElseThrow(
() -> new UsernameNotFoundException("User with login " + username + " not found"));
log.debug("User with login {} loaded", username);
return user;
}
public List<UserDTO> getAllUsers() {
log.debug("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 user: {}", registrationDTO);
User user = transientUser(registrationDTO);
fillRoles(user, registrationDTO);
userRepository.save(user);
if (userInAnyRole(user, RoleService.Authority.STUDENT)) {
sureCallerInAnyRole(RoleService.Authority.ADMIN, RoleService.Authority.SECRETARY);
Student student = transientStudent(registrationDTO.getStudentData());
student.setUser(user);
student = studentRepository.save(student);
log.info("New user is student: {}", student);
} else if (userInAnyRole(user, RoleService.Authority.TEACHER)) {
sureCallerInAnyRole(RoleService.Authority.ADMIN, RoleService.Authority.SECRETARY);
Teacher teacher = transientTeacher(registrationDTO.getTeacherData());
teacher.setUser(user);
teacher = teacherRepository.save(teacher);
log.info("New user is teacher: {}", teacher);
} else if (userInAnyRole(user, RoleService.Authority.ADMIN)) {
sureCallerInAnyRole(RoleService.Authority.ADMIN);
log.info("New user is administrator");
} else {
throw new UnsupportedOperationException("Role not supported: " + user.getAuthorities());
}
}
private Teacher transientTeacher(RegistrationDTO.TeacherRegistrationDTO teacherData) {
if (teacherData == null)
throw new NullPointerException("Teacher data is null");
if (teacherData.getCuratingGroups() == null)
teacherData.setCuratingGroups(List.of());
if (teacherData.getAdvisingStudents() == null)
teacherData.setAdvisingStudents(List.of());
Teacher teacher = new Teacher();
List<Group> groups = groupRepository.findAllById(teacherData.getCuratingGroups());
if (groups.size() != teacherData.getCuratingGroups().size()) {
throw new NotFoundException(Teacher.class);
}
List<Student> students = studentRepository.findAllById(teacherData.getAdvisingStudents());
if (students.size() != teacherData.getAdvisingStudents().size()) {
throw new NotFoundException(Student.class);
}
teacher.setCuratingGroups(groups);
teacher.setAdvisingStudents(students);
return teacher;
}
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)
throw new NullPointerException("Student data is null");
Student student = new Student();
if (studentData.getGroupId() != null) {
student.setGroup(groupRepository.findByIdThrow(studentData.getGroupId()));
}
return student;
}
private void fillRoles(User user, RegistrationDTO registrationDTO) {
RoleService.Authority accountType = registrationDTO.getAccountType();
Role role = roleService.getRoleByAuthority(accountType);
if (role == null) {
throw new IllegalArgumentException("Role not found for authority: " + accountType);
}
user.setRoles(List.of(role));
}
public boolean userInAnyRole(User user, RoleService.Authority... authority) {
if (user == null || authority == null) {
return false;
}
List<RoleService.Authority> toCheckAuthorities = List.of(authority);
return user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.map(RoleService.Authority::from)
.anyMatch(toCheckAuthorities::contains);
}
public User getCallerUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!(principal instanceof User)) {
return null;
}
return (User) principal;
}
public Optional<User> getCallerOptional() {
return Optional.ofNullable(getCallerUser());
}
public UserDTO getCallerUserDTO() {
return getCallerOptional().map(UserDTO::from).orElse(UserDTO.unauthenticated());
}
public boolean isCallerInRole(RoleService.Authority... authorities) {
return userInAnyRole(getCallerUser(), authorities);
}
public void sureCallerInAnyRole(RoleService.Authority... authority) {
if (!isCallerInRole(authority)) {
throw new AccessDeniedException();
}
}
}

View File

@ -0,0 +1,4 @@
package ru.mskobaro.tdms.integration.database;
public interface DefenceRepository {
}

View File

@ -1,9 +1,9 @@
package ru.tubryansk.tdms.entity.repository;
package ru.mskobaro.tdms.integration.database;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.DiplomaTopic;
import ru.tubryansk.tdms.exception.NotFoundException;
import ru.mskobaro.tdms.domain.entity.DiplomaTopic;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
@Repository
public interface DiplomaTopicRepository extends JpaRepository<DiplomaTopic, Integer> {

View File

@ -1,9 +1,9 @@
package ru.tubryansk.tdms.entity.repository;
package ru.mskobaro.tdms.integration.database;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Group;
import ru.tubryansk.tdms.exception.NotFoundException;
import ru.mskobaro.tdms.domain.entity.Group;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
@Repository
public interface GroupRepository extends JpaRepository<Group, Long> {

View File

@ -1,8 +1,8 @@
package ru.tubryansk.tdms.entity.repository;
package ru.mskobaro.tdms.integration.database;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Role;
import ru.mskobaro.tdms.domain.entity.Role;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {

View File

@ -1,12 +1,10 @@
package ru.tubryansk.tdms.entity.repository;
package ru.mskobaro.tdms.integration.database;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Student;
import ru.tubryansk.tdms.entity.User;
import ru.tubryansk.tdms.exception.NotFoundException;
import java.util.Optional;
import ru.mskobaro.tdms.domain.entity.Student;
import ru.mskobaro.tdms.domain.entity.User;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
@ -14,5 +12,5 @@ public interface StudentRepository extends JpaRepository<Student, Long> {
return this.findById(id).orElseThrow(() -> new NotFoundException(Student.class, id));
}
Optional<Student> findByUser(User user);
Student findByUser(User user);
}

View File

@ -1,9 +1,9 @@
package ru.tubryansk.tdms.entity.repository;
package ru.mskobaro.tdms.integration.database;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Teacher;
import ru.tubryansk.tdms.exception.NotFoundException;
import ru.mskobaro.tdms.domain.entity.Teacher;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
@Repository
public interface TeacherRepository extends JpaRepository<Teacher, Long> {

View File

@ -0,0 +1,16 @@
package ru.mskobaro.tdms.integration.database;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.mskobaro.tdms.domain.entity.User;
import ru.mskobaro.tdms.domain.exception.NotFoundException;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
default User findByIdThrow(Long id) {
return this.findById(id).orElseThrow(() -> new NotFoundException(User.class, id));
}
Optional<User> findUserByLogin(String login);
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.controller;
package ru.mskobaro.tdms.presentation.controller;
import org.springframework.validation.annotation.Validated;

View File

@ -1,12 +1,12 @@
package ru.tubryansk.tdms.controller;
package ru.mskobaro.tdms.presentation.controller;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.tubryansk.tdms.controller.payload.GroupCreateDTO;
import ru.tubryansk.tdms.controller.payload.GroupDTO;
import ru.tubryansk.tdms.controller.payload.GroupEditDTO;
import ru.tubryansk.tdms.service.GroupService;
import ru.mskobaro.tdms.domain.service.GroupService;
import ru.mskobaro.tdms.presentation.payload.GroupCreateDTO;
import ru.mskobaro.tdms.presentation.payload.GroupDTO;
import ru.mskobaro.tdms.presentation.payload.GroupEditDTO;
import java.util.Collection;

View File

@ -1,11 +1,12 @@
package ru.tubryansk.tdms.controller;
package ru.mskobaro.tdms.presentation.controller;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ru.tubryansk.tdms.controller.payload.StudentDTO;
import ru.tubryansk.tdms.service.StudentService;
import ru.mskobaro.tdms.domain.service.StudentService;
import ru.mskobaro.tdms.presentation.payload.StudentDTO;
@RestController
@RequestMapping("/api/v1/student/")
@ -15,6 +16,11 @@ public class StudentController {
@GetMapping("/current")
public StudentDTO getCurrentStudent() {
return studentService.getCallerStudentDTO();
return studentService.getCallerStudentDtoThrow();
}
@GetMapping("/by-user-id")
public StudentDTO getStudentByUserId(@RequestParam Long id) {
return studentService.getStudentByUserIdThrow(id);
}
}

View File

@ -1,10 +1,10 @@
package ru.tubryansk.tdms.controller;
package ru.mskobaro.tdms.presentation.controller;
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;
import ru.mskobaro.tdms.domain.service.SysInfoService;
@RestController
@RequestMapping("/api/v1/sysinfo")

View File

@ -1,15 +1,14 @@
package ru.tubryansk.tdms.controller;
package ru.mskobaro.tdms.presentation.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.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 ru.mskobaro.tdms.domain.service.AuthenticationService;
import ru.mskobaro.tdms.domain.service.UserService;
import ru.mskobaro.tdms.presentation.payload.LoginDTO;
import ru.mskobaro.tdms.presentation.payload.RegistrationDTO;
import ru.mskobaro.tdms.presentation.payload.UserDTO;
import java.util.List;
@ -20,13 +19,11 @@ public class UserController {
@Autowired
private AuthenticationService authenticationService;
@Autowired
private CallerService callerService;
@Autowired
private UserService userService;
@GetMapping("/current")
public UserDTO getCurrentUser() {
return callerService.getCallerUserDTO();
return userService.getCallerUserDTO();
}
@PostMapping("/logout")
@ -40,7 +37,7 @@ public class UserController {
}
@PostMapping("/register")
public void post(@RequestBody @Valid RegistrationDTO registrationDTO) {
public void register(@RequestBody @Valid RegistrationDTO registrationDTO) {
userService.registerUser(registrationDTO);
}

View File

@ -0,0 +1,73 @@
package ru.mskobaro.tdms.presentation.exception;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.security.core.AuthenticationException;
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.mskobaro.tdms.domain.exception.AccessDeniedException;
import ru.mskobaro.tdms.domain.exception.BusinessException;
import ru.mskobaro.tdms.presentation.payload.ErrorDTO;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static ru.mskobaro.tdms.presentation.payload.ErrorDTO.ErrorCode.*;
@RestControllerAdvice
@Slf4j
public class ApplicationExceptionHandler {
@ExceptionHandler(BindException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorDTO handleMethodArgumentNotValidException(BindException e) {
log.debug("Validation error: {}", e.getMessage());
String validationErrors = e.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("\n"));
return new ErrorDTO(validationErrors, VALIDATION_ERROR);
}
@ExceptionHandler(BusinessException.class)
public ErrorDTO handleBusinessException(BusinessException e, HttpServletResponse response) {
log.warn("Business error: {}", e.getMessage());
response.setStatus(e.getErrorCode().getHttpStatus().value());
return new ErrorDTO(e.getMessage(), e.getErrorCode());
}
@ExceptionHandler({org.springframework.security.access.AccessDeniedException.class, AccessDeniedException.class})
@ResponseStatus(FORBIDDEN)
public ErrorDTO handleAccessDeniedException(RuntimeException e) {
log.warn("Access denied: {}", e.getMessage());
return new ErrorDTO("Доступ запрещен", ACCESS_DENIED);
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(UNAUTHORIZED)
public ErrorDTO handleAuthenticationException(AuthenticationException e) {
log.warn("Authentication error: {}", e.getMessage());
return new ErrorDTO("Неверный логин или пароль", ACCESS_DENIED);
}
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(NOT_FOUND)
public ErrorDTO handleNoResourceFoundException(NoResourceFoundException e) {
UUID uuid = UUID.randomUUID();
String message = e.getMessage().substring(0, e.getMessage().length() - 1);
log.error("{} ({})", message, uuid);
return new ErrorDTO("Идентификатор ошибки: (" + uuid + ")\nРесурс не был наеден, обратитесь к администратору", ErrorDTO.ErrorCode.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(INTERNAL_SERVER_ERROR)
public ErrorDTO handleUnexpectedException(Exception e) {
UUID uuid = UUID.randomUUID();
log.error("Unexpected exception ({})", uuid, e);
return new ErrorDTO("Идентификатор ошибки: (" + uuid + ")\роизошла непредвиденная ошибка, обратитесь к администратору", INTERNAL_ERROR);
}
}

View File

@ -1,10 +1,10 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
public record ErrorResponse(String message, ErrorCode errorCode) {
public record ErrorDTO(String message, ErrorCode errorCode) {
@RequiredArgsConstructor
@Getter
public enum ErrorCode {

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import lombok.Getter;
import lombok.Setter;

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import jakarta.validation.constraints.*;
import lombok.Getter;

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;

View File

@ -1,10 +1,16 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import jakarta.validation.constraints.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.Length;
import ru.mskobaro.tdms.domain.service.RoleService.Authority;
import java.util.List;
@Getter
@ToString
public class RegistrationDTO {
@NotEmpty(message = "Логин не может быть пустым")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Логин должен содержать только латинские буквы, цифры и знак подчеркивания")
@ -24,11 +30,25 @@ public class RegistrationDTO {
@NotNull(message = "Номер телефона не может быть пустым")
@Pattern(regexp = "^\\+[1-9]\\d{6,14}$", message = "Номер телефона должен начинаться с '+' и содержать от 7 до 15 цифр")
private String numberPhone;
@NotNull(message = "Тип аккаунта не может быть пустым")
private Authority accountType;
private StudentRegistrationDTO studentData;
private TeacherRegistrationDTO teacherData;
@Getter
@ToString
public static class StudentRegistrationDTO {
@NotNull(message = "Группа не может быть пустой")
private Long groupId;
}
@Getter
@Setter
@ToString
public static class TeacherRegistrationDTO {
private List<Long> curatingGroups;
private List<Long> advisingStudents;
}
}

View File

@ -1,8 +1,8 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import ru.tubryansk.tdms.entity.Role;
import ru.tubryansk.tdms.entity.User;
import ru.mskobaro.tdms.domain.entity.Role;
import ru.mskobaro.tdms.domain.entity.User;
import java.util.List;

View File

@ -0,0 +1,47 @@
package ru.mskobaro.tdms.presentation.payload;
import lombok.Data;
import ru.mskobaro.tdms.domain.entity.Student;
@Data
public class StudentDTO {
private Long id;
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.setId(student.getId());
studentDTO.setForm(student.getForm());
studentDTO.setProtectionOrder(student.getProtectionOrder());
studentDTO.setMagistracy(student.getMagistracy());
studentDTO.setDigitalFormatPresent(student.getDigitalFormatPresent());
studentDTO.setMarkComment(student.getMarkComment());
studentDTO.setMarkPractice(student.getMarkPractice());
studentDTO.setPredefenceComment(student.getPredefenceComment());
studentDTO.setNormalControl(student.getNormalControl());
studentDTO.setAntiPlagiarism(student.getAntiPlagiarism());
studentDTO.setNote(student.getNote());
studentDTO.setRecordBookReturned(student.getRecordBookReturned());
studentDTO.setWork(student.getWork());
studentDTO.setUser(UserDTO.from(student.getUser()));
studentDTO.setDiplomaTopic(student.getDiplomaTopic().getName());
return studentDTO;
}
}

View File

@ -1,10 +1,10 @@
package ru.tubryansk.tdms.controller.payload;
package ru.mskobaro.tdms.presentation.payload;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import org.springframework.security.core.GrantedAuthority;
import ru.tubryansk.tdms.entity.User;
import ru.mskobaro.tdms.domain.entity.User;
import java.time.ZonedDateTime;
import java.util.List;
@ -13,6 +13,7 @@ import java.util.List;
@Builder
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record UserDTO(
Long id,
boolean authenticated,
String login,
String fullName,
@ -30,6 +31,7 @@ public record UserDTO(
public static UserDTO from(User user) {
return UserDTO.builder()
.id(user.getId())
.authenticated(true)
.login(user.getLogin())
.fullName(user.getFullName())

View File

@ -1,12 +1,13 @@
package ru.tubryansk.tdms.config;
package ru.mskobaro.tdms.system.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
@ -21,25 +22,38 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import ru.mskobaro.tdms.system.web.DevAuthenticationRequestFilter;
import ru.mskobaro.tdms.system.web.LoggingRequestFilter;
import java.time.Duration;
import java.util.List;
import static ru.mskobaro.tdms.domain.service.RoleService.Authority.*;
@Configuration
@Slf4j
public class SecurityConfiguration {
@Configuration
public class SecurityConfig {
@Autowired
private Environment environment;
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity httpSecurity,
AuthenticationManager authenticationManager,
@Qualifier("corsConfig") CorsConfigurationSource cors
) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager, CorsConfigurationSource cors) throws Exception {
if (environment.matchesProfiles("dev")) {
httpSecurity.addFilterAfter(new DevAuthenticationRequestFilter(), SecurityContextHolderFilter.class);
}
return httpSecurity
.addFilterAfter(new LoggingRequestFilter(), AnonymousAuthenticationFilter.class)
.authorizeHttpRequests(this::configureHttpAuthorization)
.csrf(AbstractHttpConfigurer::disable) /* todo: настроить csrf */
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.cors(a -> a.configurationSource(cors))
.authenticationManager(authenticationManager)
.sessionManagement(cfg -> {
@ -50,22 +64,19 @@ public class SecurityConfiguration {
}
@Bean
@Qualifier("corsConfig")
public CorsConfigurationSource corsConfigurationProd(
@Value("${application.domain}") String domain,
@Value("${application.port}") String port,
@Value("${application.protocol}") String protocol,
Environment environment
) {
@Primary
public CorsConfigurationSource corsConfigurationProd(@Value("${application.domain}") String domain, @Value("${application.port}") String port, @Value("${application.protocol}") String protocol) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.OPTIONS.name()));
corsConfiguration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(Duration.ofDays(1));
corsConfiguration.addAllowedOrigin(StringUtils.join(protocol, "://", domain, ":", port));
if (environment.matchesProfiles("dev")) {
corsConfiguration.addAllowedOrigin("http://localhost:8888");
}
log.info("CORS configuration: [headers: {}, methods: {}, origins: {}, credentials: {}, maxAge: {}]",
corsConfiguration.getAllowedHeaders(), corsConfiguration.getAllowedMethods(), corsConfiguration.getAllowedOrigins(),
corsConfiguration.getAllowCredentials(), corsConfiguration.getMaxAge());
@ -89,14 +100,16 @@ public class SecurityConfiguration {
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");
httpAuthorization.requestMatchers("/api/v1/user/get-all").hasAuthority(ADMIN.getAuthority());
httpAuthorization.requestMatchers("/api/v1/user/register").hasAuthority(ADMIN.getAuthority());
httpAuthorization.requestMatchers("/api/v1/user/validate-registration").hasAuthority(ADMIN.getAuthority());
/* StudentController */
httpAuthorization.requestMatchers("/api/v1/student/current").permitAll();
httpAuthorization.requestMatchers("api/v1/student/by-user-id").hasAnyAuthority(
SECRETARY.getAuthority(), ADMIN.getAuthority(), STUDENT.getAuthority(), TEACHER.getAuthority());
/* GroupController */
httpAuthorization.requestMatchers("/api/v1/group/get-all-groups").permitAll();
httpAuthorization.requestMatchers("/api/v1/group/create-group").hasAuthority("ROLE_ADMINISTRATOR");
httpAuthorization.requestMatchers("/api/v1/group/create-group").hasAuthority(ADMIN.getAuthority());
/* deny all other api requests */
httpAuthorization.requestMatchers("/api/**").denyAll();
/* since api already blocked, all other requests are static resources */

View File

@ -0,0 +1,46 @@
package ru.mskobaro.tdms.system.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.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.TransientSecurityContext;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.web.filter.OncePerRequestFilter;
import ru.mskobaro.tdms.domain.entity.Role;
import ru.mskobaro.tdms.domain.entity.User;
import java.io.IOException;
import java.util.List;
import static ru.mskobaro.tdms.domain.service.RoleService.Authority.ADMIN;
@Slf4j
public class DevAuthenticationRequestFilter extends OncePerRequestFilter {
public DevAuthenticationRequestFilter() {
log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!");
log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!");
log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!");
log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!");
log.warn("!!!ANY REQUEST WILL BE AUTHENTICATED AS DEV_ADMIN, IF YOU SEE THIS IN PRODUCTION, CHECK YOUR CONFIGURATION!!!");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Role role = new Role(-1L, "dev_admin_role", ADMIN.getAuthority());
User admin = new User();
admin.setRoles(List.of(role));
admin.setId(-1L);
admin.setLogin("dev_admin");
admin.setEmail("dev_admin@main.mail");
admin.setFullName("dev_admin");
admin.setNumberPhone("+79999999999");
admin.setPassword("{bcrypt}$2a$06$BHHQMjwQB2KI9sDdC9rRHOuYkTskjDt9WAyrscWP/Dcn7my3Jr77K");
var auth = new PreAuthenticatedAuthenticationToken(admin, null, admin.getAuthorities());
var context = new TransientSecurityContext(auth);
SecurityContextHolder.setContext(context);
filterChain.doFilter(request, response);
}
}

View File

@ -1,28 +1,29 @@
package ru.tubryansk.tdms.web;
package ru.mskobaro.tdms.system.web;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
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("Request received: {}. user: {}, session: {}, remote ip: {}",
request.getRequestURI(), request.getRemoteUser(), request.getSession().getId(), request.getRemoteAddr());
HttpSession session = request.getSession(false);
log.info("Request received: [{}] {} user: {}, session: {}, remote ip: {}",
request.getMethod(), request.getRequestURI(), request.getRemoteUser(),
session == null ? "no" : session.getId(), request.getRemoteAddr());
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
log.info("Response with {} status. duration: {} ms", response.getStatus(), duration);
log.info("Response with {} status duration: {} ms", response.getStatus(), duration);
}
}
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.web;
package ru.mskobaro.tdms.system.web;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
@ -8,6 +8,7 @@ import org.springframework.stereotype.Component;
@Component
@Slf4j
public class LoggingSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {

View File

@ -1,15 +0,0 @@
package ru.tubryansk.tdms;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@Slf4j
public class TdmsApplication {
public static void main(String[] args) {
SpringApplication.run(TdmsApplication.class, args).start();
}
}

View File

@ -1,50 +0,0 @@
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;
}
}

View File

@ -1,4 +0,0 @@
package ru.tubryansk.tdms.entity.repository;
public interface DefenceRepository {
}

View File

@ -1,10 +0,0 @@
package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.tubryansk.tdms.entity.User;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findUserByLogin(String login);
}

View File

@ -1,14 +0,0 @@
package ru.tubryansk.tdms.exception;
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
public class AccessDeniedException extends BusinessException {
public AccessDeniedException() {
super("Access denied");
}
@Override
public ErrorResponse.ErrorCode getErrorCode() {
return ErrorResponse.ErrorCode.ACCESS_DENIED;
}
}

View File

@ -1,13 +0,0 @@
package ru.tubryansk.tdms.exception;
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public ErrorResponse.ErrorCode getErrorCode() {
return ErrorResponse.ErrorCode.BUSINESS_ERROR;
}
}

View File

@ -1,67 +0,0 @@
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.security.core.AuthenticationException;
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.controller.payload.ErrorResponse;
import java.util.UUID;
import java.util.stream.Collectors;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(BindException e) {
log.debug("Validation error: {}", e.getMessage());
String validationErrors = e.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining("\n"));
return new ErrorResponse(validationErrors, ErrorResponse.ErrorCode.VALIDATION_ERROR);
}
@ExceptionHandler(BusinessException.class)
public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) {
log.info("Business error: {}", e.getMessage());
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.info("Access denied: {}", e.getMessage());
return new ErrorResponse("Доступ запрещен", ErrorResponse.ErrorCode.ACCESS_DENIED);
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleAuthenticationException(AuthenticationException e) {
log.info("Authentication error: {}", e.getMessage());
return new ErrorResponse("Неверный логин или пароль", ErrorResponse.ErrorCode.ACCESS_DENIED);
}
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) {
UUID uuid = UUID.randomUUID();
log.error("Resource not found ({})", uuid, e);
return new ErrorResponse("Идентификатор ошибки: (" + uuid + ")\nРесурс не был наеден, обратитесь к администратору", ErrorResponse.ErrorCode.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleUnexpectedException(Exception e) {
UUID uuid = UUID.randomUUID();
log.error("Unexpected exception ({})", uuid, e);
return new ErrorResponse("Идентификатор ошибки: (" + uuid + ")\роизошла непредвиденная ошибка, обратитесь к администратору", ErrorResponse.ErrorCode.INTERNAL_ERROR);
}
}

View File

@ -1,14 +0,0 @@
package ru.tubryansk.tdms.exception;
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
public class NotFoundException extends BusinessException {
public NotFoundException(Class<?> entityClass, Object id) {
super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден");
}
@Override
public ErrorResponse.ErrorCode getErrorCode() {
return ErrorResponse.ErrorCode.NOT_FOUND;
}
}

View File

@ -1,26 +0,0 @@
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;
@Service
public class CallerService {
@Autowired
private AuthenticationService authenticationService;
public Optional<User> getCallerUser() {
if(authenticationService.authenticated()) {
return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
return Optional.empty();
}
public UserDTO getCallerUserDTO() {
return getCallerUser().map(UserDTO::from).orElse(UserDTO.unauthenticated());
}
}

View File

@ -1,17 +0,0 @@
package ru.tubryansk.tdms.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class LifecycleService {
@EventListener(ContextStartedEvent.class)
public void onStartup(ContextStartedEvent event) {
Environment environment = event.getApplicationContext().getEnvironment();
log.info("Static files location: {}", environment.getProperty("spring.web.resources.static-locations"));
}
}

View File

@ -1,42 +0,0 @@
package ru.tubryansk.tdms.service;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
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() {
log.debug("Initializing roles");
roles = new ConcurrentHashMap<>();
roleRepository.findAll().forEach(role -> roles.put(role.getAuthority(), role));
log.info("Roles initialized: {}", roles);
}
public Role getRoleByAuthority(Authority authority) {
return roles.get(authority.name());
}
}

View File

@ -1,32 +0,0 @@
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.Student;
import ru.tubryansk.tdms.entity.repository.StudentRepository;
import java.util.Optional;
@Service
@Transactional
public class StudentService {
@Autowired
private StudentRepository studentRepository;
@Autowired
private CallerService callerService;
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);
}
}

View File

@ -1,98 +0,0 @@
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.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.entity.repository.GroupRepository;
import ru.tubryansk.tdms.entity.repository.StudentRepository;
import ru.tubryansk.tdms.entity.repository.UserRepository;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
@Slf4j
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 {
log.debug("Loading user with username: {}", username);
User user = userRepository.findUserByLogin(username).orElseThrow(
() -> new UsernameNotFoundException("User with login " + username + " not found"));
log.debug("User with login {} loaded", username);
return user;
}
public List<UserDTO> getAllUsers() {
log.debug("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.debug("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) {
log.debug("User is student, adding role ROLE_STUDENT");
roles.add(roleService.getRoleByAuthority(RoleService.Authority.ROLE_STUDENT));
}
user.setRoles(roles);
}
}

View File

@ -3,13 +3,3 @@ application:
port: 8080
domain: localhost
protocol: http
spring:
web:
resources:
static-locations: file:///${user.dir}/web/dist/
chain:
cache: false
compressed: false
server:
compression:
enabled: false

View File

@ -28,10 +28,20 @@ spring:
user: ${db.user}
password: ${db.password}
schemas: ${db.schema}
main:
banner-mode: off
banner:
location: banner.txt
web:
resources:
static-locations: classpath:/static/
# autoconfigure:
# exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
server:
port: ${application.port}
address: ${application.domain}
compression:
enabled: true
management:
endpoints:
web:
exposure:
exclude: "*"

View File

@ -0,0 +1,5 @@
__________ __ ________ ____ ____ ___
/_ __/ __ \/ |/ / ___/ _ __ / __ \ / __ \ < /
/ / / / / / /|_/ /\__ \ | | / / / / / / / / / / / /
/ / / /_/ / / / /___/ / | |/ /_/ /_/ /_/ /_/ /_ / /
/_/ /_____/_/ /_//____/ |___/(_)____/(_)____/(_)_/

View File

@ -2,9 +2,9 @@ create table student
(
id bigserial primary key,
user_id bigint not null,
diploma_topic_id bigint not null,
adviser_teacher_id bigint not null,
group_id bigint not null,
diploma_topic_id bigint,
adviser_teacher_id bigint,
group_id bigint,
form boolean,
protection_day int,

View File

@ -2,6 +2,11 @@ create table defence
(
id bigserial primary key,
defence_date timestamptz,
created_at timestamptz not null,
updated_at timestamptz
);
-- COMMENTS
comment on table defence is 'Таблица для хранения данных о защитах';
comment on column defence.defence_date is 'Дата защиты';

View File

@ -1,7 +1,7 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
@ -9,11 +9,11 @@
<file>logs/app.log</file>
<append>false</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="ru.tubryansk.tdms" level="debug" />
<logger name="ru.mskobaro.tdms" level="debug"/>
<root level="warn">
<appender-ref ref="CONSOLE" />

View File

@ -1,17 +0,0 @@
package ru.tubryansk.tdms;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@Import(TestcontainersConfiguration.class)
@SpringBootTest
class TdmsApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -1,13 +0,0 @@
package ru.tubryansk.tdms;
import org.springframework.boot.SpringApplication;
public class TestTdmsApplication {
public static void main(String[] args) {
SpringApplication.from(TdmsApplication::main).with(TestcontainersConfiguration.class).run(args);
}
}

View File

@ -1,20 +0,0 @@
package ru.tubryansk.tdms;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16.2-alpine3.19"));
}
}

View File

@ -4,12 +4,12 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.tubryansk</groupId>
<groupId>ru.mskobaro</groupId>
<artifactId>tdms</artifactId>
<version>0.0.1</version>
</parent>
<groupId>ru.tubryansk.tdms</groupId>
<groupId>ru.mskobaro.tdms</groupId>
<artifactId>web</artifactId>
<version>0.0.1</version>
<packaging>pom</packaging>

View File

@ -51,9 +51,14 @@ class NotificationPopup extends ComponentContext<{ notification: Notification, t
render() {
const hasTitle = !!this.props.notification.title && this.props.notification.title.length > 0;
const closeIcon = <span className={'ms-2'}><FontAwesomeIcon icon={'close'} onClick={this.close}/></span>;
const title = this.props.notification.title.split('\n').map((item, key) => <span key={key}>{item}<br/></span>);
const message = this.props.notification.message.split('\n').map((item, key) => <span key={key}>{item}<br/></span>);
const title = this.props.notification.title?.split('\n').map((item, key) =>
<span key={key}>{item}<br/></span>);
const message = this.props.notification.message?.split('\n').map((item, key) =>
<span key={key}>{item}<br/></span>);
const closeIcon = <span className={'ms-2'}>
<FontAwesomeIcon icon={'close'} onClick={this.close}/>
</span>;
return <Card className={`position-relative mt-3 opacity-75 ${this.cardClassName}`}>
{
@ -70,7 +75,7 @@ class NotificationPopup extends ComponentContext<{ notification: Notification, t
<CardBody>
<CardText className={'d-flex justify-content-between align-items-start'}>
<span>
{message}
{message ?? ''}
</span>
{
!hasTitle &&

View File

@ -0,0 +1,4 @@
._table-header:hover {
background: #f5f5f5;
}

View File

@ -7,6 +7,7 @@ import _ from "lodash";
import {ChangeEvent} from "react";
import {ModalState} from "../../utils/modalState";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import './DataTable.css';
export interface DataTableProps<T> {
tableDescriptor: TableDescriptor<T>;
@ -167,7 +168,7 @@ export class DataTable<R> extends ComponentContext<DataTableProps<R>> {
borderRight: lastColumn ? 'none' : '1px solid var(--bs-table-border-color)',
};
return <th key={column.key} style={style}>
return <th key={column.key} style={style} className={'_table-header'}>
<div className={'d-flex align-items-center justify-content-center position-relative user-select-none'} style={{cursor: "pointer"}}
onClick={() => runInAction(() => {
const other = this.descriptor.columns
@ -197,7 +198,6 @@ export class DataTable<R> extends ComponentContext<DataTableProps<R>> {
}
</tr>
</>
}
@computed

View File

@ -1,8 +1,8 @@
import React from "react";
import React, {ChangeEvent, Component} from "react";
import {ReactiveValue} from "../../../utils/reactive/reactiveValue";
import {observer} from "mobx-react";
import {action, makeObservable, observable} from "mobx";
import {Button, FloatingLabel, FormControl, FormText} from "react-bootstrap";
import {action, makeObservable, observable, runInAction} from "mobx";
import {Button, FloatingLabel, FormControl, FormSelect, FormText} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import './ReactiveControls.css';
@ -14,12 +14,8 @@ export interface ReactiveInputProps<T> {
validateless?: boolean;
}
export interface ReactiveSelectInputProps<T> extends ReactiveInputProps<T> {
possibleValues: { value: T, label: string }[];
}
@observer
export class StringInput extends React.Component<ReactiveInputProps<string>> {
export class StringInput extends Component<ReactiveInputProps<string>> {
constructor(props: any) {
super(props);
makeObservable(this);
@ -49,7 +45,7 @@ export class StringInput extends React.Component<ReactiveInputProps<string>> {
}
@observer
export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
export class PasswordInput extends Component<ReactiveInputProps<string>> {
@observable showPassword = false;
constructor(props: any) {
@ -76,9 +72,8 @@ export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
<div className={'position-relative d-flex justify-content-between align-items-center'} style={{minHeight: '58px'}}>
<FloatingLabel label={this.props.label} className={`${this.props.className} w-100 position-absolute`}>
<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}/>
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>
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"} className={'position-absolute rounded-pill'}
style={{width: '40px', height: '40px', left: '100%', transform: 'translateX(-120%)', padding: '0px'}}>
@ -90,14 +85,64 @@ export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
}
}
export interface ReactiveSelectInputSelect {
value: string;
label: string;
}
export interface ReactiveSelectInputProps extends ReactiveInputProps<ReactiveSelectInputSelect> {
possibleValues: ReactiveSelectInputSelect[];
unselectedLabel?: string;
initial?: ReactiveSelectInputSelect;
}
@observer
export class SelectInput<T> extends React.Component<ReactiveSelectInputProps<T>> {
constructor(props: ReactiveSelectInputProps<T>) {
export class SelectInput extends Component<ReactiveSelectInputProps> {
constructor(props: ReactiveSelectInputProps) {
super(props);
makeObservable(this);
runInAction(() => {
this.options = props.possibleValues;
this.value = props.value;
this.value.setField(this.props.label);
if (this.value.value === undefined && this.props.unselectedLabel !== undefined) {
this.options.unshift({value: '__unselected__', label: this.props.unselectedLabel});
this.value.setAuto(this.options[0]);
} else if (this.props.initial) {
this.value.set(this.props.initial);
} else {
this.value.set(this.options[0]);
}
});
}
@observable options: { value: string, label: string }[];
@observable value: ReactiveValue<ReactiveSelectInputSelect>;
@action.bound
onChange(event: ChangeEvent<HTMLSelectElement>) {
this.value.set(this.options.find(option => option.value === event.currentTarget.value));
}
render() {
return <div></div>;
const inputClassName = `${this.props.validateless ? '' : this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`;
return <div className={'mb-1 l-no-bg'}>
{
<FloatingLabel label={this.props.label} className={`${this.props.className} mt-0 mb-0`}>
<FormSelect disabled={this.props.disabled} value={this.value.value.value} onChange={this.onChange}
className={inputClassName}>
{
this.options.map(option =>
<option key={option.value} value={option.value}>
{option.label}
</option>)
}
</FormSelect>
</FloatingLabel>
}
<FormText children={this.props.value.firstError} className={'text-danger d-block mt-0 mb-0'}/>
</div>
}
}

View File

@ -15,7 +15,7 @@ export class Footer extends ComponentContext {
render() {
return <footer>
<Navbar className="bg-body-tertiary">
<Navbar className="bg-body-tertiary" style={{'minHeight': '50px'}}>
<Container>
<div>
<NavbarText>Thesis Defence Management System &mdash; </NavbarText>
@ -30,8 +30,8 @@ export class Footer extends ComponentContext {
</div>
<Nav>
<NavLink href="https://git.mskobaro.ru/mskobaro/TDMS">
<FontAwesomeIcon icon={findIconDefinition({iconName: 'github', prefix: 'fab'})} size="xl"/>
<NavLink href="https://git.mskobaro.ru/mskobaro/TDMS" className={'p-0'}>
<FontAwesomeIcon icon={findIconDefinition({iconName: 'git-alt', prefix: 'fab'})} size="xl"/>
</NavLink>
</Nav>
</Container>

View File

@ -4,7 +4,7 @@ import {action, computed, makeObservable, observable, reaction, runInAction} fro
import {Column, TableDescriptor} from "../../utils/tables";
import {get} from "../../utils/request";
import {DataTable} from "../custom/DataTable";
import {Group} from "../../models/group";
import {IGroup} from "../../models/IGroup";
import {Component} from "react";
import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap";
import {StringInput} from "../custom/controls/ReactiveControls";
@ -20,7 +20,7 @@ export class GroupListPage extends Page {
super(props);
makeObservable(this);
reaction(() => this.groups, () => {
this.tableDescriptor = new TableDescriptor<Group>(this.groupColumns, this.groups);
this.tableDescriptor = new TableDescriptor<IGroup>(this.groupColumns, this.groups);
});
}
@ -36,26 +36,26 @@ export class GroupListPage extends Page {
@observable filterModalState = new ModalState();
@observable editModalState = new ModalState();
@observable currentGroup: Group;
@observable currentGroup: IGroup;
@observable groups: Group[];
@observable tableDescriptor: TableDescriptor<Group>;
@observable groups: IGroup[];
@observable tableDescriptor: TableDescriptor<IGroup>;
@observable isAdministrator: boolean;
groupColumns = [
new Column<Group, string>('name', 'Название', x => x, (grp) => {
new Column<IGroup, string>('name', 'Название', x => x, (grp) => {
return this.isAdministrator && <FontAwesomeIcon style={{cursor: 'pointer'}} onClick={() => {
this.openEditModal(grp)
}} icon={'pen-to-square'}/>
}
),
new Column<Group, string>('curatorName', 'Куратор', (value: string) => value ?? 'Не назначен'),
new Column<IGroup, string>('curatorName', 'Куратор', (value: string) => value ?? 'Не назначен'),
];
@action.bound
requestGroups() {
this.thinkStore.think();
get<Group[]>('/group/get-all-groups').then(groups => {
get<IGroup[]>('/group/get-all-groups').then(groups => {
runInAction(() => {
this.groups = groups;
});
@ -65,7 +65,7 @@ export class GroupListPage extends Page {
}
@action.bound
openEditModal(group: Group) {
openEditModal(group: IGroup) {
runInAction(() => {
this.currentGroup = group;
this.editModalState.open();
@ -91,7 +91,7 @@ export class GroupListPage extends Page {
interface GroupListFilterProps {
modalState: ModalState;
filters: ((group: Group) => boolean)[];
filters: ((group: IGroup) => boolean)[];
}
@observer
@ -111,12 +111,12 @@ class GroupListFilterModal extends Component<GroupListFilterProps> {
@observable nameField = new ReactiveValue<string>().syncWithParam('name');
@observable curatorField = new ReactiveValue<string>().syncWithParam('curator');
@observable nameFilter = (group: Group) => {
@observable nameFilter = (group: IGroup) => {
if (!this.nameField.value) return true;
return group.name?.includes(this.nameField.value)
};
@observable curatorFilter = (group: Group) => {
@observable curatorFilter = (group: IGroup) => {
if (!this.curatorField.value) return true;
return group.curatorName?.includes(this.curatorField.value)
};
@ -146,7 +146,7 @@ class GroupListFilterModal extends Component<GroupListFilterProps> {
interface EditGroupModalProps {
modalState: ModalState;
group: Group
group: IGroup
}
@observer

View File

@ -0,0 +1,131 @@
import {ComponentContext} from "../../utils/ComponentContext";
import {IStudent} from "../../models/student";
import {observer} from "mobx-react";
import {computed, makeObservable, observable, reaction} from "mobx";
import {Col, Row} from "react-bootstrap";
import {StringInput} from "../custom/controls/ReactiveControls";
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
import _ from "lodash";
export interface StudentProfileProps {
student: IStudent;
viewMode: "VIEW" | "EDIT";
}
@observer
export class StudentProfile extends ComponentContext<StudentProfileProps> {
@observable student: IStudent = this.props.student;
@observable viewMode: "VIEW" | "EDIT" = this.props.viewMode;
@observable form = new ReactiveValue<string>().setAuto(this.student.form.toString());
@observable protectionOrder = new ReactiveValue<string>().setAuto(_.toString(this.student.protectionOrder));
@observable magistracy = new ReactiveValue<string>().setAuto(this.student.magistracy);
@observable digitalFormatPresent = new ReactiveValue<string>().setAuto(this.student.digitalFormatPresent.toString());
@observable markComment = new ReactiveValue<string>().setAuto(_.toString(this.student.markComment));
@observable markPractice = new ReactiveValue<string>().setAuto(_.toString(this.student.markPractice));
@observable predefenceComment = new ReactiveValue<string>().setAuto(this.student.predefenceComment);
@observable normalControl = new ReactiveValue<string>().setAuto(this.student.normalControl);
@observable antiPlagiarism = new ReactiveValue<string>().setAuto(_.toString(this.student.antiPlagiarism));
@observable note = new ReactiveValue<string>().setAuto(this.student.note);
@observable recordBookReturned = new ReactiveValue<string>().setAuto(this.student.recordBookReturned.toString());
@observable work = new ReactiveValue<string>().setAuto(this.student.work);
@observable diplomaTopic = new ReactiveValue<string>().setAuto(this.student.diplomaTopic);
@observable mentorUser = new ReactiveValue<string>().setAuto(this.student.mentorUser.fullName);
@observable group = new ReactiveValue<string>().setAuto(this.student.group.name);
constructor(props: any) {
super(props);
makeObservable(this);
reaction(() => this.props.viewMode, (viewMode) => {
this.viewMode = viewMode;
});
reaction(() => this.props.student, (student) => {
this.form.set(student.form.toString());
this.protectionOrder.set(_.toString(student.protectionOrder));
this.magistracy.set(student.magistracy);
this.digitalFormatPresent.set(student.digitalFormatPresent.toString());
this.markComment.set(_.toString(student.markComment));
this.markPractice.set(_.toString(student.markPractice));
this.predefenceComment.set(student.predefenceComment);
this.normalControl.set(student.normalControl);
this.antiPlagiarism.set(_.toString(student.antiPlagiarism));
this.note.set(student.note);
this.recordBookReturned.set(student.recordBookReturned.toString());
this.work.set(student.work);
this.diplomaTopic.set(student.diplomaTopic);
this.mentorUser.set(student.mentorUser.fullName);
this.group.set(student.group.name);
});
reaction(() => {
return {
form: this.form.value,
protectionOrder: this.protectionOrder.value,
magistracy: this.magistracy.value,
digitalFormatPresent: this.digitalFormatPresent.value,
markComment: this.markComment.value,
markPractice: this.markPractice.value,
predefenceComment: this.predefenceComment.value,
normalControl: this.normalControl.value,
antiPlagiarism: this.antiPlagiarism.value,
note: this.note.value,
recordBookReturned: this.recordBookReturned.value,
work: this.work.value,
diplomaTopic: this.diplomaTopic.value,
mentorUser: this.mentorUser.value,
group: this.group.value
}
}, () => {
this.student = {
...this.student,
form: this.form.value === "true",
protectionOrder: _.toNumber(this.protectionOrder.value),
magistracy: this.magistracy.value,
digitalFormatPresent: this.digitalFormatPresent.value === "true",
markComment: _.toNumber(this.markComment.value),
markPractice: _.toNumber(this.markPractice.value),
predefenceComment: this.predefenceComment.value,
normalControl: this.normalControl.value,
antiPlagiarism: _.toNumber(this.antiPlagiarism.value),
note: this.note.value,
recordBookReturned: this.recordBookReturned.value === "true",
work: this.work.value,
diplomaTopic: this.diplomaTopic.value,
group: {name: this.group.value}
}
});
}
@computed
get viewOnly() {
return this.viewMode === "VIEW";
}
render() {
return <div>
<Row>
<Col>
<StringInput value={this.form} label={'Форма обучения'} disabled={this.viewOnly}/>
<StringInput value={this.protectionOrder} label={'Порядок защиты'} disabled={this.viewOnly}/>
<StringInput value={this.magistracy} label={'Магистратура'} disabled={this.viewOnly}/>
<StringInput value={this.digitalFormatPresent} label={'Цифровой формат присутствует'} disabled={this.viewOnly}/>
<StringInput value={this.markComment} label={'Комментарий к оценке'} disabled={this.viewOnly}/>
<StringInput value={this.markPractice} label={'Оценка за практику'} disabled={this.viewOnly}/>
<StringInput value={this.predefenceComment} label={'Комментарий к предзащите'} disabled={this.viewOnly}/>
</Col>
<Col>
<StringInput value={this.normalControl} label={'Форма контроля'} disabled={this.viewOnly}/>
<StringInput value={this.antiPlagiarism} label={'Антиплагиат (процент уникальности)'} disabled={this.viewOnly}/>
<StringInput value={this.note} label={'Примечание'} disabled={this.viewOnly}/>
<StringInput value={this.recordBookReturned} label={'Зачетная книжка сдана'} disabled={this.viewOnly}/>
<StringInput value={this.work} label={'Работа'} disabled={this.viewOnly}/>
<StringInput value={this.diplomaTopic} label={'Тема диплома'} disabled={this.viewOnly}/>
<StringInput value={this.mentorUser} label={'Научный руководитель'} disabled={this.viewOnly}/>
<StringInput value={this.group} label={'Группа'} disabled={this.viewOnly}/>
</Col>
</Row>
</div>;
}
}

View File

@ -0,0 +1,57 @@
import {IStudent} from "../../models/student";
import {ComponentContext} from "../../utils/ComponentContext";
import {action, makeObservable, observable} from "mobx";
import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap";
import {StudentProfile} from "./StudentProfile";
import {ModalState} from "../../utils/modalState";
export interface StudentProfileProps {
modalState: ModalState;
fullName: string;
student: IStudent;
}
export class StudentProfileModal extends ComponentContext<StudentProfileProps> {
@observable modalState: ModalState = this.props.modalState;
@observable student: IStudent = this.props.student;
@observable viewMode: "VIEW" | "EDIT" = "VIEW";
constructor(props: any) {
super(props);
makeObservable(this);
}
@action.bound
onEdit() {
this.viewMode = "EDIT";
}
@action.bound
onSave() {
this.modalState.close();
this.viewMode = "VIEW";
this.notificationStore.warn("Не реализовано");
}
render() {
return <Modal show={this.modalState.isOpen}>
<ModalHeader>
<ModalTitle>Профиль студента: {this.props.fullName}</ModalTitle>
</ModalHeader>
<ModalBody>
<StudentProfile student={this.student} viewMode={this.viewMode}/>
</ModalBody>
<ModalFooter>
{
this.viewMode === "EDIT" &&
<Button variant={"primary"} onClick={this.onSave}>Сохранить</Button>
}
{
this.viewMode === "VIEW" &&
<Button variant={"secondary"} onClick={this.onEdit}>Редактировать</Button>
}
<Button variant={"outline-secondary"}>Закрыть</Button>
</ModalFooter>
</Modal>;
}
}

View File

@ -5,6 +5,11 @@ import {IAuthenticated} from "../../models/user";
import {DataTable} from "../custom/DataTable";
import {get} from "../../utils/request";
import {Column, TableDescriptor} from "../../utils/tables";
import {Authorities, getAuthorityByCode} from "../../models/authorities";
import {datetimeConverter} from "../../utils/converters";
import {StudentProfileModal} from "./StudentProfileModal";
import {ModalState} from "../../utils/modalState";
import {IStudent} from "../../models/student";
@observer
export class UserListPage extends Page {
@ -27,13 +32,37 @@ export class UserListPage extends Page {
@observable users?: IAuthenticated[];
@observable tableDescriptor?: TableDescriptor<IAuthenticated>;
@observable studentModalState = new ModalState();
@observable studentFullName: string = '';
@observable student: IStudent;
userColumns = [
new Column('login', 'Логин'),
new Column('fullName', 'Полное имя'),
new Column('email', 'Email'),
new Column('phone', 'Телефон'),
new Column('createdAt', 'Дата создания'),
new Column('updatedAt', 'Дата обновления', (value: string) => value ? value : 'Не обновлялось'),
new Column<IAuthenticated, string>('login', 'Логин'),
new Column<IAuthenticated, string>('fullName', 'Полное имя'),
new Column<IAuthenticated, string[]>('authorities', 'Роли', (value, user) => {
return value.map(getAuthorityByCode).map(authority => {
if (authority.code === Authorities.STUDENT.code) {
return <a href="#" onClick={() => {
console.log(user.id);
get<IStudent>('student/by-user-id', {id: user.id}).then((student) => {
runInAction(() => {
this.studentFullName = user.fullName;
this.student = student;
this.studentModalState.open();
});
});
}}>{authority.name}</a>;
} else {
return <span>{authority.name}</span>;
}
}).reduce((prev, curr) => <>
{prev}, {curr}
</>);
}),
new Column<IAuthenticated, string>('email', 'Email'),
new Column<IAuthenticated, string>('phone', 'Телефон'),
new Column<IAuthenticated, string>('createdAt', 'Дата создания', (value: string) => value ? datetimeConverter(value) : 'Не создавалось'),
new Column<IAuthenticated, string>('updatedAt', 'Дата обновления', (value: string) => value ? datetimeConverter(value) : 'Не обновлялось'),
];
@ -53,7 +82,14 @@ export class UserListPage extends Page {
return <>
{
this.tableDescriptor &&
<>
<DataTable tableDescriptor={this.tableDescriptor} name={'Пользователи'}/>
{
this.student &&
<StudentProfileModal modalState={this.studentModalState} fullName={this.studentFullName} student={this.student}/>
}
</>
}
</>
}

View File

@ -0,0 +1,14 @@
import {ComponentContext} from "../../utils/ComponentContext";
import {IUser} from "../../models/user";
import {IStudent} from "../../models/student";
import {ITeacher} from "../../models/teacher";
export interface UserProfileProps {
user: IUser;
student?: IStudent;
teacher?: ITeacher;
}
export class UserProfile extends ComponentContext {
}

View File

@ -6,7 +6,7 @@ import {action, makeObservable, observable, runInAction} from "mobx";
import {IAuthenticated} from "../../models/user";
import {ComponentContext} from "../../utils/ComponentContext";
import {getAuthorityByCode} from "../../models/authorities";
import {dateConverter} from "../../utils/converters";
import {datetimeConverter} from "../../utils/converters";
@observer
class UserInfo extends ComponentContext {
@ -57,11 +57,11 @@ class UserInfo extends ComponentContext {
</Form.Group>
<Form.Group className={"mt-2"}>
<Form.Label column={"sm"}>Дата создания</Form.Label>
<Form.Control type="text" value={dateConverter(this.user.createdAt)} disabled={true}/>
<Form.Control type="text" value={datetimeConverter(this.user.createdAt)} disabled={true}/>
</Form.Group>
<Form.Group className={"mt-2"}>
<Form.Label column={"sm"}>Дата последней модификации</Form.Label>
<Form.Control type="text" value={dateConverter(this.user.updatedAt)} disabled={true}/>
<Form.Control type="text" value={datetimeConverter(this.user.updatedAt)} disabled={true}/>
</Form.Group>
</Col>
</Row>
@ -136,7 +136,7 @@ class StudentInfo extends ComponentContext {
</Form.Group>
<Form.Group className={"mt-2"}>
<Form.Label column={"sm"}>Куратор</Form.Label>
<Form.Control type="text" value={student.group.principalUser.fullName} disabled={true}/>
<Form.Control type="text" value={student.group.curatorName} disabled={true}/>
</Form.Group>
<Form.Group className={"mt-2"}>
<Form.Label column={"sm"}>Форма обучения</Form.Label> {/* todo: обсудить с аналитиком */}

View File

@ -1,10 +1,10 @@
import {observer} from "mobx-react";
import {action, computed, makeObservable, observable} from "mobx";
import {action, makeObservable, observable} from "mobx";
import {Button, Col, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Row} from "react-bootstrap";
import {UserRegistrationDTO} from "../../models/registration";
import {post} from "../../utils/request";
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
import {PasswordInput, StringInput} from "../custom/controls/ReactiveControls";
import {PasswordInput, ReactiveSelectInputSelect, SelectInput, StringInput} from "../custom/controls/ReactiveControls";
import {
email,
emailChars,
@ -19,11 +19,13 @@ import {
passwordMaxLength,
phone,
phoneChars,
required
required,
selected
} from "../../utils/reactive/validators";
import {ComponentContext} from "../../utils/ComponentContext";
import {ModalState} from "../../utils/modalState";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Authorities} from "../../models/authorities";
export interface UserRegistrationModalProps {
modalState: ModalState;
@ -53,13 +55,33 @@ export class UserRegistrationModal extends ComponentContext<UserRegistrationModa
.addInputRestriction(phoneChars);
@observable userKindEnum = new ReactiveValue<string>().addValidator(required);
@observable accountType = new ReactiveValue<string>().addValidator(required).addValidator((value) => {
if (!['student', 'admin'].includes(value)) {
return 'Тип аккаунта должен быть "СТУДЕНТ" или "АДМИНИСТРАТОР"';
@observable accountType = new ReactiveValue<ReactiveSelectInputSelect>().addValidator(selected).addValidator((value) => {
if (value.value === Authorities.SECRETARY.code || value.value === Authorities.COMMISSION_MEMBER.code) {
return 'не реализовано'
}
});
@computed
@observable teacherGroup = new ReactiveValue<ReactiveSelectInputSelect>().addValidator(selected);
@observable teacherStudent = new ReactiveValue<ReactiveSelectInputSelect>().addValidator(selected);
@observable studentGroup = new ReactiveValue<ReactiveSelectInputSelect>().addValidator(selected);
possibleAccountTypes(): ReactiveSelectInputSelect[] {
const teacher = {value: Authorities.TEACHER.code, label: Authorities.TEACHER.name} as ReactiveSelectInputSelect;
const student = {value: Authorities.STUDENT.code, label: Authorities.STUDENT.name} as ReactiveSelectInputSelect;
const commissionMember = {value: Authorities.COMMISSION_MEMBER.code, label: Authorities.COMMISSION_MEMBER.name} as ReactiveSelectInputSelect;
const administrator = {value: Authorities.ADMINISTRATOR.code, label: Authorities.ADMINISTRATOR.name} as ReactiveSelectInputSelect;
const secretary = {value: Authorities.SECRETARY.code, label: Authorities.SECRETARY.name} as ReactiveSelectInputSelect;
if (this.userStore.isAdministrator) {
return [teacher, student, commissionMember, administrator, secretary];
} else if (this.userStore.isSecretary) {
return [teacher, student, commissionMember];
} else {
return [];
}
}
get formInvalid() {
return this.login.invalid || !this.login.touched
|| this.password.invalid || !this.password.touched
@ -82,7 +104,14 @@ export class UserRegistrationModal extends ComponentContext<UserRegistrationModa
fullName: this.fullName.value,
email: this.email.value,
numberPhone: this.numberPhone.value,
// studentData: { groupId: 1 }
accountType: this.accountType.value.value,
studentData: this.accountType.value.value === Authorities.STUDENT.code ? {
groupId: 1
} : undefined,
teacherData: this.accountType.value.value === Authorities.TEACHER.code ? {
curatingGroups: [],
advisingStudents: [],
} : undefined,
} as UserRegistrationDTO).then(() => {
this.notificationStore.success('Пользователь успешно зарегистрирован');
}).catch(() => {
@ -102,11 +131,11 @@ export class UserRegistrationModal extends ComponentContext<UserRegistrationModa
</ModalHeader>
{
thinking &&
<Modal.Body>
<ModalBody>
<div className={'text-center'}>
<FontAwesomeIcon icon={'gear'} spin size={'4x'}/>
</div>
</Modal.Body>
</ModalBody>
}
{
!thinking &&
@ -115,6 +144,7 @@ export class UserRegistrationModal extends ComponentContext<UserRegistrationModa
<Col>
<StringInput value={this.login} label={"Логин"}/>
<PasswordInput value={this.password} label={"Пароль"}/>
<SelectInput possibleValues={this.possibleAccountTypes()} value={this.accountType} label={"Тип аккаунта"} unselectedLabel={'Не выбрано'}/>
</Col>
<Col>
<StringInput value={this.fullName} label={"Полное имя"}/>
@ -122,6 +152,25 @@ export class UserRegistrationModal extends ComponentContext<UserRegistrationModa
<StringInput value={this.numberPhone} label={"Телефон"}/>
</Col>
</Row>
{
this.accountType.value?.value === Authorities.STUDENT.code &&
<Row>
<Col>
<SelectInput possibleValues={[{value: '1', label: '1'}]} value={this.studentGroup} label={"Группа"}/>
</Col>
</Row>
}
{
this.accountType.value?.value === Authorities.TEACHER.code &&
<Row>
<Col>
<SelectInput possibleValues={[{value: '1', label: '1'}]} value={this.teacherGroup} label={"Курируемые группы"}/>
<SelectInput possibleValues={[{value: '1', label: '1'}]} value={this.teacherStudent} label={"Консультируемые студенты"}/>
</Col>
</Row>
}
</ModalBody>
}
<ModalFooter>

View File

@ -1,4 +1,4 @@
export interface Group {
export interface IGroup {
name: string;
curatorName?: string;
iAmCurator?: boolean;

View File

@ -5,8 +5,14 @@ export interface UserRegistrationDTO {
email: string,
numberPhone: string,
studentData?: StudentRegistrationDTO
teacherData?: TeacherRegistrationDTO
}
export interface StudentRegistrationDTO {
groupId: number;
}
export interface TeacherRegistrationDTO {
curatingGroups: number[];
advisingStudents: number[];
}

View File

@ -1,11 +1,8 @@
import {IAuthenticated, IUser} from "./user";
export interface IGRoup {
name: string;
principalUser: IAuthenticated;
}
import {IGroup} from "./IGroup";
export interface IStudent {
id: number;
form: boolean;
protectionOrder: number;
magistracy: string;
@ -21,5 +18,5 @@ export interface IStudent {
user: IUser;
diplomaTopic: string;
mentorUser: IAuthenticated;
group: IGRoup;
group: IGroup;
}

10
web/src/models/teacher.ts Normal file
View File

@ -0,0 +1,10 @@
import {IGroup} from "./IGroup";
import {IStudent} from "./student";
export interface ITeacher {
id: number;
curatingGroups: IGroup[];
advisingStudents: IStudent[];
createdAt: string;
updatedAt: string;
}

View File

@ -1,4 +1,5 @@
export interface IAuthenticated {
id: number,
authenticated: true,
login: string,
fullName: string,

View File

@ -27,7 +27,7 @@ export class NotificationStore {
@observable warnings: Notification[] = [];
@observable infos: Notification[] = [];
@observable successes: Notification[] = [];
timeout: number = 5000;
timeout: number = 10000;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;

View File

@ -1,3 +1,11 @@
export const dateConverter = (date: string) => {
export const datetimeConverter = (date: string) => {
return new Date(date).toLocaleString();
}
export const dateConverter = (date: string) => {
return new Date(date).toLocaleDateString();
}
export const timeConverter = (date: string) => {
return new Date(date).toLocaleTimeString();
}

View File

@ -1,6 +1,19 @@
import {ReactiveSelectInputSelect} from "../../components/custom/controls/ReactiveControls";
export const required = (value: any, field = 'Поле') => {
if (!value || (typeof value === 'string' && value.trim().length === 0)) {
return `${field} обязательно для заполнения`;
const message = `${field} обязательно для заполнения`;
if (!value) {
return message;
} else if (typeof value === 'string' && (value.trim().length === 0)) {
return message;
} else if (Array.isArray(value) && value.length === 0) {
return message;
}
}
export const selected = (value: ReactiveSelectInputSelect, field = 'Поле') => {
if (!value.value || value.value === '__unselected__') {
return `${field} должно быть выбрано`;
}
}

View File

@ -7,14 +7,14 @@ 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);
params: data,
} as AxiosRequestConfig, 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);
} as AxiosRequestConfig, doReject, showError);
export const request = async <R>(config: AxiosRequestConfig<any>, doReject: boolean, showError: boolean) => {
return new Promise<R>((resolve, reject) => {

View File

@ -34,7 +34,7 @@ export class Column<R, C> {
constructor(
key: string, title: string,
format: (value: any) => string = value => value,
format: (value: C, data: R) => ReactNode = (value: C) => value ? _.toString(value) : 'Пусто',
suffix?: (data: R) => ReactNode,
sort: Sort<C> = new Sort<C>()
) {

View File

@ -4,11 +4,12 @@
"target": "ES5",
"module": "ES6",
"jsx": "react-jsx",
"sourceMap": false,
"sourceMap": true,
"useDefineForClassFields": true,
"moduleResolution": "Bundler",
"composite": true,
"resolveJsonModule": true,
"incremental": true,
/* enabling decorators */
"experimentalDecorators": true,

View File

@ -2,7 +2,6 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
app: path.join(__dirname, 'src', 'Application.tsx')
},
@ -11,21 +10,32 @@ module.exports = {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
rules: [{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: '/node_modules/'
},
{
exclude: /node_modules/
}, {
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}, {
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset/resource',
}, {
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
devtool: 'source-map',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
devServer: {
client: {