Implemented UserProfile.tsx, without editing
* added font awesome * replaced GitHub logo in Footer.tsx with one provided by FontAwesome * added loader, when userStore fetching data from server * allow circular dependencies, since this is no problem * fix default (e.g. prod) profile * fix a problem, when no authenticated person calls /api/v1/user/current endpoint
This commit is contained in:
parent
212249becd
commit
181dc824a1
@ -88,6 +88,16 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.14.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -11,8 +11,6 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
@Slf4j
|
||||
public class TdmsApplication {
|
||||
public static void main(String[] args) {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(TdmsApplication.class, args);
|
||||
String staticLocation = context.getEnvironment().getProperty("spring.web.resources.static-locations");
|
||||
log.info("Static location: {}", staticLocation);
|
||||
SpringApplication.run(TdmsApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,9 +3,14 @@ package ru.tubryansk.tdms.config;
|
||||
|
||||
import jakarta.servlet.http.HttpSessionEvent;
|
||||
import jakarta.servlet.http.HttpSessionListener;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
@ -26,24 +31,31 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfiguration {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager) throws Exception {
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
|
||||
AuthenticationManager authenticationManager,
|
||||
@Qualifier("corsConfig") CorsConfigurationSource cors) throws Exception {
|
||||
return httpSecurity
|
||||
.authorizeHttpRequests(this::configureHttpAuthorization)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(a -> a.configurationSource(corsConfiguration()))
|
||||
.cors(a -> a.configurationSource(cors))
|
||||
.authenticationManager(authenticationManager)
|
||||
.sessionManagement(this::configureSessionManagement)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfiguration() {
|
||||
@Profile("dev")
|
||||
@Qualifier("corsConfig")
|
||||
public CorsConfigurationSource corsConfigurationDev() {
|
||||
return request -> {
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.applyPermitDefaultValues();
|
||||
@ -54,15 +66,34 @@ public class SecurityConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("!dev")
|
||||
@Qualifier("corsConfig")
|
||||
public CorsConfigurationSource corsConfigurationProd(@Value("${application.domain}") String domain,
|
||||
@Value("${application.port}") String port,
|
||||
@Value("${application.protocol}") String protocol) {
|
||||
return request -> {
|
||||
String url = StringUtils.join(protocol, "://", domain, ":", port);
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setMaxAge(Duration.ofHours(1));
|
||||
corsConfiguration.addAllowedOrigin(url);
|
||||
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name()));
|
||||
// corsConfiguration.setAllowedHeaders();
|
||||
return corsConfiguration;
|
||||
};
|
||||
}
|
||||
|
||||
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
|
||||
/* API ROUTES */
|
||||
httpAuthorization.requestMatchers("/api/v1/diploma-topic/**").permitAll();
|
||||
httpAuthorization.requestMatchers("/api/v1/user/**").permitAll();
|
||||
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/student/current").permitAll();
|
||||
|
||||
httpAuthorization.requestMatchers("/api/**").denyAll();
|
||||
/* STATIC ROUTES */
|
||||
httpAuthorization.requestMatchers("/**").permitAll();
|
||||
/* OTHER */
|
||||
httpAuthorization.anyRequest().denyAll();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -76,18 +107,21 @@ public class SecurityConfiguration {
|
||||
return provider;
|
||||
}
|
||||
|
||||
private PasswordEncoder passwordEncoder() {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("dev")
|
||||
public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) {
|
||||
return new HttpSessionListener() {
|
||||
@Override
|
||||
public void sessionCreated(HttpSessionEvent se) {
|
||||
LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as izrailev_v_ya_1", se.getSession().getId());
|
||||
String username = "akulenko_mikhail";
|
||||
LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as {}", se.getSession().getId(), username);
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("izrailev_v_ya_1", "1");
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, "1");
|
||||
Authentication authenticated = authenticationManager.authenticate(authentication);
|
||||
context.setAuthentication(authenticated);
|
||||
SecurityContextHolder.setContext(context);
|
||||
|
||||
@ -1,33 +1,13 @@
|
||||
package ru.tubryansk.tdms.controller;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import ru.tubryansk.tdms.dto.DiplomaTopicDTO;
|
||||
import ru.tubryansk.tdms.service.DiplomaTopicService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/diploma-topic/")
|
||||
@Validated
|
||||
public class DiplomaTopicController {
|
||||
@Autowired
|
||||
private DiplomaTopicService diplomaTopicService;
|
||||
|
||||
@GetMapping("/get-all")
|
||||
public List<DiplomaTopicDTO> getAll() {
|
||||
return diplomaTopicService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/get-by-id/{id:[\\-+]?\\d+}")
|
||||
public DiplomaTopicDTO getById(@PathVariable @PositiveOrZero Integer id) {
|
||||
return diplomaTopicService.getById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package ru.tubryansk.tdms.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import ru.tubryansk.tdms.dto.StudentDTO;
|
||||
import ru.tubryansk.tdms.service.StudentService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/student/")
|
||||
public class StudentController {
|
||||
@Autowired
|
||||
private StudentService studentService;
|
||||
|
||||
@GetMapping("/current")
|
||||
public StudentDTO getCurrentStudent() {
|
||||
return studentService.getCallerStudent().map(StudentDTO::from).orElse(null);
|
||||
}
|
||||
}
|
||||
@ -2,31 +2,32 @@ package ru.tubryansk.tdms.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.tubryansk.tdms.dto.UserDTO;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
import ru.tubryansk.tdms.service.AuthenticationService;
|
||||
import ru.tubryansk.tdms.service.UserService;
|
||||
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/api/v1/user")
|
||||
@Slf4j
|
||||
public class UserController {
|
||||
@Autowired
|
||||
private AuthenticationService authenticationService;
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@GetMapping("/current")
|
||||
public UserDTO getCurrentUser() {
|
||||
User principal = userService.getCallerPrincipal();
|
||||
return principal != null ? UserDTO.from(principal, true) : UserDTO.fromUnauthenticated();
|
||||
return userService.getCallerUser().map(user -> UserDTO.from(user, true)).orElse(UserDTO.unauthenticated());
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public void logout() {
|
||||
userService.logout();
|
||||
authenticationService.logout();
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public void login(@RequestParam String username, @RequestParam String password) {
|
||||
authenticationService.login(username, password);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
package ru.tubryansk.tdms.dto;
|
||||
|
||||
|
||||
import lombok.Builder;
|
||||
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
||||
|
||||
|
||||
@Builder
|
||||
public record DiplomaTopicDTO(Integer id, String name) {
|
||||
public static DiplomaTopicDTO fromEntity(DiplomaTopic diplomaTopic) {
|
||||
return DiplomaTopicDTO.builder()
|
||||
.id(diplomaTopic.getId())
|
||||
.name(diplomaTopic.getName())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
13
server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java
Normal file
13
server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java
Normal file
@ -0,0 +1,13 @@
|
||||
package ru.tubryansk.tdms.dto;
|
||||
|
||||
import ru.tubryansk.tdms.entity.Group;
|
||||
|
||||
public record GroupDTO(String name, UserDTO principalUser) {
|
||||
|
||||
public static GroupDTO from(Group group) {
|
||||
return new GroupDTO(
|
||||
group.getName(),
|
||||
UserDTO.from(group.getPrincipalUser(), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,19 @@
|
||||
package ru.tubryansk.tdms.dto;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import ru.tubryansk.tdms.entity.Role;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class RoleDTO {
|
||||
private Integer id;
|
||||
private String name;
|
||||
public record RoleDTO(String name, String authority) {
|
||||
|
||||
public static RoleDTO from(Role role) {
|
||||
return new RoleDTO(role.getName(), role.getAuthority());
|
||||
}
|
||||
|
||||
public static List<RoleDTO> from(User user) {
|
||||
return user.getRoles().stream().map(RoleDTO::from).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,13 @@ package ru.tubryansk.tdms.dto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import ru.tubryansk.tdms.entity.Student;
|
||||
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class StudentDTO {
|
||||
private Integer id;
|
||||
private Boolean form;
|
||||
private Integer protectionOrder;
|
||||
private String magistracy;
|
||||
@ -23,8 +23,29 @@ public class StudentDTO {
|
||||
private String note;
|
||||
private Boolean recordBookReturned;
|
||||
private String work;
|
||||
private Integer userId;
|
||||
private Integer diplomaTopicId;
|
||||
private Integer mentorUserId;
|
||||
private Integer groupId;
|
||||
private UserDTO user;
|
||||
private String diplomaTopic;
|
||||
private UserDTO mentorUser;
|
||||
private GroupDTO group;
|
||||
|
||||
public static StudentDTO from(Student student) {
|
||||
return new StudentDTO(
|
||||
student.getForm(),
|
||||
student.getProtectionOrder(),
|
||||
student.getMagistracy(),
|
||||
student.getDigitalFormatPresent(),
|
||||
student.getMarkComment(),
|
||||
student.getMarkPractice(),
|
||||
student.getPredefenceComment(),
|
||||
student.getNormalControl(),
|
||||
student.getAntiPlagiarism(),
|
||||
student.getNote(),
|
||||
student.getRecordBookReturned(),
|
||||
student.getWork(),
|
||||
UserDTO.from(student.getUser(), true),
|
||||
student.getDiplomaTopic().getName(),
|
||||
UserDTO.from(student.getMentorUser(), true),
|
||||
GroupDTO.from(student.getGroup())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package ru.tubryansk.tdms.dto;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Builder;
|
||||
import ru.tubryansk.tdms.entity.Role;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
@ -10,18 +10,19 @@ import java.util.List;
|
||||
|
||||
|
||||
@Builder
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record UserDTO(
|
||||
boolean authenticated,
|
||||
String login,
|
||||
String password,
|
||||
String fullName,
|
||||
String email,
|
||||
String phoneNumber,
|
||||
String phone,
|
||||
ZonedDateTime createdAt,
|
||||
ZonedDateTime updatedAt,
|
||||
List<String> authorities) {
|
||||
List<RoleDTO> authorities) {
|
||||
|
||||
public static UserDTO fromUnauthenticated() {
|
||||
public static UserDTO unauthenticated() {
|
||||
return UserDTO.builder()
|
||||
.authenticated(false)
|
||||
.build();
|
||||
@ -34,10 +35,10 @@ public record UserDTO(
|
||||
.password(anonymize ? "" : user.getPassword())
|
||||
.fullName(user.getFullName())
|
||||
.email(user.getMail())
|
||||
.phoneNumber(user.getNumberPhone())
|
||||
.phone(user.getNumberPhone())
|
||||
.createdAt(user.getCreatedAt())
|
||||
.updatedAt(user.getUpdatedAt())
|
||||
.authorities(user.getRoles().stream().map(Role::getAuthority).toList())
|
||||
.authorities(RoleDTO.from(user))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package ru.tubryansk.tdms.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
|
||||
@Data
|
||||
@ -49,5 +51,4 @@ public class Student {
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id", nullable = false)
|
||||
private Group group;
|
||||
|
||||
}
|
||||
|
||||
@ -3,7 +3,11 @@ package ru.tubryansk.tdms.repository;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
||||
import ru.tubryansk.tdms.exception.NotFoundException;
|
||||
|
||||
@Repository
|
||||
public interface DiplomaTopicRepository extends JpaRepository<DiplomaTopic, Integer> {
|
||||
default DiplomaTopic findByIdThrow(Integer id) {
|
||||
return this.findById(id).orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package ru.tubryansk.tdms.repository;
|
||||
|
||||
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;
|
||||
|
||||
@Repository
|
||||
public interface StudentRepository extends JpaRepository<Student, Integer> {
|
||||
default Student findByIdThrow(Integer id) {
|
||||
return this.findById(id).orElseThrow(() -> new NotFoundException(Student.class, id));
|
||||
}
|
||||
|
||||
Optional<Student> findByUser(User user);
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package ru.tubryansk.tdms.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
|
||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||
|
||||
@Service
|
||||
public class AuthenticationService {
|
||||
@Autowired
|
||||
private HttpServletRequest request;
|
||||
@Autowired
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
public boolean authenticated() {
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication.isAuthenticated() && (authentication.getPrincipal() instanceof User);
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
HttpSession session = request.getSession(false);
|
||||
if(session != null) {
|
||||
session.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void login(String username, String password) {
|
||||
var context = SecurityContextHolder.createEmptyContext();
|
||||
var token = new UsernamePasswordAuthenticationToken(username, password);
|
||||
var authenticated = authenticationManager.authenticate(token);
|
||||
context.setAuthentication(authenticated);
|
||||
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,9 @@
|
||||
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.entity.DiplomaTopic;
|
||||
import ru.tubryansk.tdms.exception.NotFoundException;
|
||||
import ru.tubryansk.tdms.repository.DiplomaTopicRepository;
|
||||
import ru.tubryansk.tdms.dto.DiplomaTopicDTO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class DiplomaTopicService {
|
||||
@Autowired
|
||||
private DiplomaTopicRepository diplomaTopicRepository;
|
||||
|
||||
public List<DiplomaTopicDTO> getAll() {
|
||||
return diplomaTopicRepository.findAll()
|
||||
.stream()
|
||||
.map(DiplomaTopicDTO::fromEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public DiplomaTopicDTO getById(Integer id) {
|
||||
return DiplomaTopicDTO.fromEntity(diplomaTopicRepository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package ru.tubryansk.tdms.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.ContextStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LifeCycleService {
|
||||
@EventListener(ContextStartedEvent.class)
|
||||
public void onStartup(ContextStartedEvent event) {
|
||||
ApplicationContext applicationContext = event.getApplicationContext();
|
||||
log.info("Static files location: {}", applicationContext.getEnvironment().getProperty("spring.web.resources.static-locations"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
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.entity.DiplomaTopic;
|
||||
import ru.tubryansk.tdms.entity.Student;
|
||||
import ru.tubryansk.tdms.exception.AccessDeniedException;
|
||||
import ru.tubryansk.tdms.repository.DiplomaTopicRepository;
|
||||
import ru.tubryansk.tdms.repository.StudentRepository;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class StudentService {
|
||||
@Autowired
|
||||
private StudentRepository studentRepository;
|
||||
@Autowired
|
||||
private DiplomaTopicRepository diplomaTopicRepository;
|
||||
@Autowired
|
||||
private Optional<Student> student;
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/** @param studentToDiplomaTopic Map of @{@link Student} id and @{@link DiplomaTopic} id */
|
||||
public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) {
|
||||
studentToDiplomaTopic.forEach(this::changeDiplomaTopic);
|
||||
}
|
||||
|
||||
public void changeDiplomaTopic(Integer studentId, Integer diplomaTopicId) {
|
||||
Student student = studentRepository.findByIdThrow(studentId);
|
||||
DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
|
||||
student.setDiplomaTopic(diplomaTopic);
|
||||
}
|
||||
|
||||
public void changeCallerDiplomaTopic(Integer diplomaTopicId) {
|
||||
DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
|
||||
student.ifPresentOrElse(s -> s.setDiplomaTopic(diplomaTopic), () -> {throw new AccessDeniedException();});
|
||||
}
|
||||
|
||||
public Optional<Student> getCallerStudent() {
|
||||
return studentRepository.findByUser(userService.getCallerUser().orElse(null));
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,8 @@
|
||||
package ru.tubryansk.tdms.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
@ -13,7 +10,7 @@ import org.springframework.stereotype.Service;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
import ru.tubryansk.tdms.repository.UserRepository;
|
||||
|
||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@ -22,20 +19,7 @@ public class UserService implements UserDetailsService {
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Autowired
|
||||
private HttpServletRequest httpServletRequest;
|
||||
|
||||
public User getCallerPrincipal() {
|
||||
if(!authenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
|
||||
public boolean authenticated() {
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication.isAuthenticated() && !(authentication instanceof AnonymousAuthenticationToken);
|
||||
}
|
||||
private AuthenticationService authenticationService;
|
||||
|
||||
@Override
|
||||
public User loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
@ -43,11 +27,10 @@ public class UserService implements UserDetailsService {
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
HttpSession session = httpServletRequest.getSession(true);
|
||||
// if(session != null) {
|
||||
// session.invalidate();
|
||||
// }
|
||||
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, null);
|
||||
public Optional<User> getCallerUser() {
|
||||
if(authenticationService.authenticated()) {
|
||||
return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,15 @@ application:
|
||||
name: @name@
|
||||
version: @version@
|
||||
type: production
|
||||
port: 80
|
||||
port: 443
|
||||
domain: tdms.tu-bryansk.ru
|
||||
protocol: https
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: tdms
|
||||
name: ${application.name}
|
||||
main:
|
||||
allow-circular-references: true
|
||||
datasource:
|
||||
url: ${db.url}
|
||||
username: ${db.user}
|
||||
|
||||
69
web/package-lock.json
generated
69
web/package-lock.json
generated
@ -8,6 +8,11 @@
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"bootstrap": "^5.3.3",
|
||||
"mobx": "^6.13.1",
|
||||
@ -1733,6 +1738,70 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
|
||||
@ -3,10 +3,15 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build" : "webpack --mode production",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack-dev-server --mode development"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"bootstrap": "^5.3.3",
|
||||
"mobx": "^6.13.1",
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
import React from "react";
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import './index.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import {RouterContext, RouterView} from "mobx-state-router";
|
||||
import {initApp} from "./utils/init.ts";
|
||||
import {MyRouterStore} from "./store/MyRouterStore.ts";
|
||||
import { RootStoreContext } from "./store/RootStore.tsx";
|
||||
import {initApp} from "./utils/init";
|
||||
import {RootStoreContext} from './context/RootStoreContext';
|
||||
import {viewMap} from "./router/viewMap";
|
||||
|
||||
const rootStore = initApp();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<RootStoreContext.Provider value={rootStore}>
|
||||
<RouterContext.Provider value={rootStore.routerStore}>
|
||||
<RouterView viewMap={MyRouterStore.makeViewMap()} />
|
||||
</RouterContext.Provider>
|
||||
</RootStoreContext.Provider>
|
||||
</React.StrictMode>
|
||||
<RootStoreContext.Provider value={rootStore}>
|
||||
<RouterContext.Provider value={rootStore.routerStore}>
|
||||
<RouterView viewMap={viewMap}/>
|
||||
</RouterContext.Provider>
|
||||
</RootStoreContext.Provider>
|
||||
);
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import {Component, ReactNode} from "react";
|
||||
import Header from "./Header.tsx";
|
||||
import {Container} from "react-bootstrap";
|
||||
import Footer from "./Footer.tsx";
|
||||
|
||||
export abstract class DefaultPage extends Component {
|
||||
abstract get page(): ReactNode;
|
||||
// declare context: ContextType<typeof RootStoreContext>
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<Header/>
|
||||
<Container className={"mt-5 mb-5"}>
|
||||
{this.page}
|
||||
</Container>
|
||||
<Footer/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
|
||||
import {FC} from "react";
|
||||
import {RouterLink} from "mobx-state-router";
|
||||
import {useRootStore} from "../../store/RootStore.tsx";
|
||||
import {IAuthenticated} from "../../models/user.ts";
|
||||
import {observer} from "mobx-react";
|
||||
|
||||
export const Header: FC = observer(() => {
|
||||
const store = useRootStore();
|
||||
const user = store.userStore.user;
|
||||
|
||||
return <header>
|
||||
<Navbar className="bg-body-tertiary" fixed="top">
|
||||
<Container>
|
||||
<Navbar.Brand>
|
||||
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
||||
</Navbar.Brand>
|
||||
<Nav>
|
||||
<NavDropdown title="Группы">
|
||||
<NavDropdown.Item>Список</NavDropdown.Item>
|
||||
<NavDropdown.Item>Редактировать</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
|
||||
<Nav className="ms-auto">
|
||||
{
|
||||
user.authenticated &&
|
||||
<>
|
||||
<Navbar.Text>Пользователь:</Navbar.Text>
|
||||
<NavDropdown
|
||||
title={(user as IAuthenticated).fullName}>
|
||||
<NavDropdown.Item>Моя страница</NavDropdown.Item>
|
||||
<NavDropdown.Divider/>
|
||||
<NavDropdown.Item onClick={() => store.userStore.logout()}>Выйти</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
!user.authenticated &&
|
||||
<Nav.Link as={RouterLink} routeName='login'>Войти</Nav.Link>
|
||||
}
|
||||
</Nav>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</header>
|
||||
});
|
||||
|
||||
export default Header;
|
||||
@ -1,7 +0,0 @@
|
||||
import {DefaultPage} from "./DefaultPage.tsx";
|
||||
|
||||
export default class Root extends DefaultPage {
|
||||
get page() {
|
||||
return <h1>Home</h1>
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import {DefaultPage} from "./DefaultPage.tsx";
|
||||
|
||||
export default class UserProfile extends DefaultPage {
|
||||
get page() {
|
||||
return <h1>User Profile</h1>
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {DefaultPage} from "./DefaultPage.tsx";
|
||||
import {DefaultPage} from "./layout/DefaultPage";
|
||||
|
||||
export default class Error extends DefaultPage {
|
||||
get page() {
|
||||
11
web/src/components/page/Home.tsx
Normal file
11
web/src/components/page/Home.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import {DefaultPage} from "./layout/DefaultPage";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../context/RootStoreContext";
|
||||
|
||||
export default class Home extends DefaultPage {
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
|
||||
get page() {
|
||||
return <h1>Home</h1>
|
||||
}
|
||||
}
|
||||
177
web/src/components/page/UserProfile.tsx
Normal file
177
web/src/components/page/UserProfile.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import {DefaultPage} from "./layout/DefaultPage";
|
||||
import {Col, Form, Row} from "react-bootstrap";
|
||||
import {observer} from "mobx-react";
|
||||
import {RootStoreContext, type RootStoreContextType} from "../../context/RootStoreContext";
|
||||
import {IAuthenticated} from "../../models/user";
|
||||
import {Component} from "react";
|
||||
import {dateConverter} from "../../utils/converters";
|
||||
import {IStudent} from "../../models/student";
|
||||
import {makeObservable, observable} from "mobx";
|
||||
|
||||
@observer
|
||||
class UserInfo extends Component<{user: IAuthenticated}> {
|
||||
@observable
|
||||
user = this.props.user;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Row>
|
||||
<Col sm={6}>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>ФИО</Form.Label>
|
||||
<Form.Control type="text" value={this.user.fullName} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Имя пользователя</Form.Label>
|
||||
<Form.Control type="text" value={this.user.login} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Электронная почта</Form.Label>
|
||||
<Form.Control type="email" value={this.user.email} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Телефон</Form.Label>
|
||||
{/* todo: format phone */}
|
||||
<Form.Control type="text" value={this.user.phone} disabled={true}/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col sm={6}>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Роли</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={this.user.authorities?.map(a => a.name).join(', ')}
|
||||
disabled={true}/>
|
||||
</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.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Дата последней модификации</Form.Label>
|
||||
<Form.Control type="text" value={dateConverter(this.user.updatedAt)} disabled={true}/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class StudentInfo extends Component<{student: IStudent}> {
|
||||
@observable
|
||||
student = this.props.student;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
let student = this.student;
|
||||
|
||||
return (
|
||||
<Row className={"mt-4"}>
|
||||
<Col sm={6}>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Тема дипломной работы</Form.Label>
|
||||
<Form.Control type="text" value={student.diplomaTopic} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Очередь защиты</Form.Label>
|
||||
<Form.Control type="text" value={student.protectionOrder.toString()} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Презентация в электронном формате</Form.Label>
|
||||
<Form.Control type="text" value={student.digitalFormatPresent ? "Да" : "Нет"} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Оценка за комментарий</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.markComment.toString()} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Оценка за практику</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.markPractice.toString()} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Комментарий к предзащите</Form.Label>
|
||||
<Form.Control type="text" value={student.predefenceComment} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Форма контроля</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.normalControl} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Антиплагиат (процент
|
||||
уникальности)</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.antiPlagiarism.toString()} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Примечание</Form.Label>
|
||||
<Form.Control type="text" value={student.note} disabled={true}/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col sm={6}>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Группа</Form.Label>
|
||||
<Form.Control type="text" value={student.group.name} disabled={true}/>
|
||||
</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.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Форма обучения</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.form ? "Очная" : "Заочная"} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Научный руководитель</Form.Label>
|
||||
<Form.Control type="text" value={student.mentorUser.fullName} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Зачетная книжка сдана</Form.Label>
|
||||
<Form.Control type="text" value={student.recordBookReturned ? "Да" : "Нет"} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Работа</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.work} disabled={true}/>
|
||||
</Form.Group>
|
||||
<Form.Group className={"mt-2"}>
|
||||
<Form.Label column={"sm"}>Магистратура</Form.Label> {/* todo: обсудить с аналитиком */}
|
||||
<Form.Control type="text" value={student.magistracy} disabled={true}/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class UserProfile extends DefaultPage {
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
|
||||
get page() {
|
||||
let user = this.context.userStore.user;
|
||||
if (!user.authenticated) {
|
||||
// todo: implement login page with redirects
|
||||
this.context.routerStore.goTo('login', {redirect: 'profile'});
|
||||
}
|
||||
let student = this.context.userStore.student;
|
||||
|
||||
return <Form>
|
||||
{
|
||||
user.authenticated &&
|
||||
<UserInfo user={user}/>
|
||||
}
|
||||
{
|
||||
student && user.authenticated &&
|
||||
<StudentInfo student={student}/>
|
||||
}
|
||||
</Form>
|
||||
}
|
||||
}
|
||||
42
web/src/components/page/layout/DefaultPage.tsx
Normal file
42
web/src/components/page/layout/DefaultPage.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import {Component, ReactNode} from "react";
|
||||
import {Container} from "react-bootstrap";
|
||||
import Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {observer} from "mobx-react";
|
||||
|
||||
@observer
|
||||
class DefaultPage extends Component<any> {
|
||||
get page(): ReactNode {
|
||||
throw new Error('This is not abstract method, ' +
|
||||
'because mobx cant handle abstract methods. ' +
|
||||
'Please override this method in child class. ' +
|
||||
'Do not call it directly.');
|
||||
}
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
|
||||
render() {
|
||||
let isLoading = this.context.userStore.isLoading;
|
||||
|
||||
return <>
|
||||
<Header/>
|
||||
<Container className={"mt-5 mb-5"}>
|
||||
{
|
||||
isLoading &&
|
||||
<div id='fullscreen-loader'>
|
||||
<FontAwesomeIcon icon='gear' size="4x" spin/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!isLoading &&
|
||||
this.page
|
||||
}
|
||||
</Container>
|
||||
<Footer/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
export {DefaultPage};
|
||||
@ -1,5 +1,6 @@
|
||||
import {Container, Nav, Navbar} from "react-bootstrap";
|
||||
import {GitHubLogo} from "../../utils/svg.tsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
@ -9,7 +10,7 @@ const Footer = () => {
|
||||
<Navbar.Text>Thesis Defence Management System ©</Navbar.Text>
|
||||
<Nav>
|
||||
<Nav.Link href="https://github.com/Velixeor/Thesis-Defense-Management-System">
|
||||
<GitHubLogo width={32} height={32}/>
|
||||
<FontAwesomeIcon icon={findIconDefinition({iconName:'github', prefix:'fab'})} size="xl"/>
|
||||
</Nav.Link>
|
||||
</Nav>
|
||||
</Container>
|
||||
62
web/src/components/page/layout/Header.tsx
Normal file
62
web/src/components/page/layout/Header.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
|
||||
import {Component} from "react";
|
||||
import {RouterLink} from "mobx-state-router";
|
||||
import {IAuthenticated} from "../../../models/user";
|
||||
import {makeObservable} from "mobx";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
||||
import {observer} from "mobx-react";
|
||||
|
||||
@observer
|
||||
class Header extends Component {
|
||||
declare context: RootStoreContextType;
|
||||
static contextType = RootStoreContext;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const userStore = this.context.userStore;
|
||||
const routerStore = this.context.routerStore;
|
||||
const user = userStore.user;
|
||||
|
||||
return <header>
|
||||
<Navbar className="bg-body-tertiary" fixed="top">
|
||||
<Container>
|
||||
<Navbar.Brand>
|
||||
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
||||
</Navbar.Brand>
|
||||
<Nav>
|
||||
<NavDropdown title="Группы">
|
||||
<NavDropdown.Item>Список</NavDropdown.Item>
|
||||
<NavDropdown.Item>Редактировать</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
|
||||
<Nav className="ms-auto">
|
||||
{
|
||||
user.authenticated &&
|
||||
<>
|
||||
<Navbar.Text>Пользователь:</Navbar.Text>
|
||||
<NavDropdown
|
||||
title={(user as IAuthenticated).fullName}>
|
||||
<NavDropdown.Item onClick={() => {routerStore.goTo('profile')}}>Моя страница</NavDropdown.Item>
|
||||
<NavDropdown.Divider/>
|
||||
<NavDropdown.Item>Выйти</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
</>
|
||||
}
|
||||
|
||||
{
|
||||
!user.authenticated &&
|
||||
<Nav.Link as={RouterLink} routeName='login'>Войти</Nav.Link>
|
||||
}
|
||||
</Nav>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</header>
|
||||
}
|
||||
}
|
||||
|
||||
export default Header;
|
||||
5
web/src/context/RootStoreContext.ts
Normal file
5
web/src/context/RootStoreContext.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {RootStore} from "../store/RootStore";
|
||||
import {ContextType, createContext} from "react";
|
||||
|
||||
export const RootStoreContext = createContext<RootStore>(new RootStore());
|
||||
export type RootStoreContextType = ContextType<typeof RootStoreContext>;
|
||||
@ -65,3 +65,10 @@ body {
|
||||
footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
#fullscreen-loader {
|
||||
min-height: calc(100vh - (56px + 64px + 48px + 48px));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
10
web/src/models/role.ts
Normal file
10
web/src/models/role.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export enum Role {
|
||||
STUDENT = 'ROLE_STUDENT',
|
||||
TUTOR = 'ROLE_TUTOR',
|
||||
DIRECTOR = 'ROLE_DIRECTOR',
|
||||
}
|
||||
|
||||
export interface IAuthority {
|
||||
authority: Role;
|
||||
name: string;
|
||||
}
|
||||
25
web/src/models/student.ts
Normal file
25
web/src/models/student.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {IAuthenticated, IUser} from "./user";
|
||||
|
||||
export interface IGRoup {
|
||||
name: string;
|
||||
principalUser: IAuthenticated;
|
||||
}
|
||||
|
||||
export interface IStudent {
|
||||
form: boolean;
|
||||
protectionOrder: number;
|
||||
magistracy: string;
|
||||
digitalFormatPresent: boolean;
|
||||
markComment: number;
|
||||
markPractice: number;
|
||||
predefenceComment: string;
|
||||
normalControl: string;
|
||||
antiPlagiarism: number;
|
||||
note: string;
|
||||
recordBookReturned: boolean;
|
||||
work: string;
|
||||
user: IUser;
|
||||
diplomaTopic: string;
|
||||
mentorUser: IAuthenticated;
|
||||
group: IGRoup;
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import {IAuthority} from "./role";
|
||||
|
||||
export interface IAuthenticated {
|
||||
authenticated: true,
|
||||
login: string,
|
||||
@ -7,7 +9,7 @@ export interface IAuthenticated {
|
||||
phone: string,
|
||||
createdAt: string,
|
||||
updatedAt: string,
|
||||
authorities: string[],
|
||||
authorities: IAuthority[],
|
||||
}
|
||||
|
||||
export declare type IUser = {authenticated: false} | IAuthenticated;
|
||||
|
||||
12
web/src/router/routes.ts
Normal file
12
web/src/router/routes.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {Route} from "mobx-state-router";
|
||||
|
||||
export const routes: Route[] = [{
|
||||
name: 'root',
|
||||
pattern: '/',
|
||||
}, {
|
||||
name: 'profile',
|
||||
pattern: '/profile',
|
||||
}, {
|
||||
name: 'error',
|
||||
pattern: '/error',
|
||||
}];
|
||||
10
web/src/router/viewMap.tsx
Normal file
10
web/src/router/viewMap.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import {ViewMap} from "mobx-state-router";
|
||||
import Home from "../components/page/Home";
|
||||
import Error from "../components/page/Error";
|
||||
import UserProfile from "../components/page/UserProfile";
|
||||
|
||||
export const viewMap: ViewMap = {
|
||||
root: <Home/>,
|
||||
profile: <UserProfile/>,
|
||||
error: <Error/>,
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import UserProfile from "./components/Page/UserProfile.tsx";
|
||||
import React from "react";
|
||||
import Root from "./components/Page/Root.tsx";
|
||||
import Error from "./components/Page/Error.tsx";
|
||||
|
||||
interface Route {
|
||||
name: string;
|
||||
pattern: string;
|
||||
view: React.ReactElement;
|
||||
}
|
||||
|
||||
export const routes: Route[] = [{
|
||||
name: 'root',
|
||||
pattern: '/',
|
||||
view: <Root/>,
|
||||
}, {
|
||||
name: 'profile',
|
||||
pattern: '/profile',
|
||||
view: <UserProfile/>,
|
||||
}, {
|
||||
name: 'error',
|
||||
pattern: '/error',
|
||||
view: <Error/>,
|
||||
}];
|
||||
17
web/src/services/RouterService.ts
Normal file
17
web/src/services/RouterService.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {MyRouterStore} from "../store/MyRouterStore";
|
||||
|
||||
export interface IRouterOptions {
|
||||
redirect: string;
|
||||
}
|
||||
|
||||
export class RouterService {
|
||||
private static router: MyRouterStore;
|
||||
|
||||
static init(router: MyRouterStore) {
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
static redirect(state: string, options?: IRouterOptions) {
|
||||
this.router.goTo(state, options);
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,13 @@
|
||||
export default class UserService {
|
||||
import {Role} from "../models/role";
|
||||
import {IAuthenticated, IUser} from "../models/user";
|
||||
|
||||
export class UserService {
|
||||
static isUserInRole(user: IUser, role: Role): boolean {
|
||||
if (!user.authenticated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
user = user as IAuthenticated;
|
||||
return user.authorities.some(a => a.authority === role);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +1,17 @@
|
||||
import {browserHistory, createRouterState, HistoryAdapter, RouterStore} from "mobx-state-router";
|
||||
import {routes} from "../routes.tsx";
|
||||
import {RootStore} from "./RootStore.tsx";
|
||||
|
||||
import {RootStore} from "./RootStore";
|
||||
import {routes} from "../router/routes";
|
||||
|
||||
export class MyRouterStore extends RouterStore {
|
||||
constructor(rootStore: RootStore) {
|
||||
super(MyRouterStore.makeRoutesMap(),
|
||||
super(routes,
|
||||
createRouterState('error', {notFound: true}),
|
||||
{rootStore: rootStore});
|
||||
}
|
||||
|
||||
static makeViewMap() {
|
||||
return routes.reduce<any>((map, route) => {
|
||||
map[route.name] = route.view;
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static makeRoutesMap() {
|
||||
return routes.map(route => {
|
||||
return {...route, view: undefined};
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
const historyAdapter = new HistoryAdapter(this, browserHistory);
|
||||
historyAdapter.observeRouterStateChanges();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
13
web/src/store/RootStore.ts
Normal file
13
web/src/store/RootStore.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {MyRouterStore} from "./MyRouterStore";
|
||||
import {UserStore} from "./UserStore";
|
||||
|
||||
export class RootStore {
|
||||
userStore = new UserStore(this);
|
||||
routerStore = new MyRouterStore(this);
|
||||
|
||||
init() {
|
||||
this.userStore.init();
|
||||
this.routerStore.init();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import {UserStore} from "./UserStore.ts";
|
||||
import {MyRouterStore} from "./MyRouterStore.ts";
|
||||
import {createContext, useContext} from "react";
|
||||
|
||||
export class RootStore {
|
||||
userStore = new UserStore(this);
|
||||
routerStore = new MyRouterStore(this);
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.userStore.init();
|
||||
this.routerStore.init();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const RootStoreContext = createContext<RootStore | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function useRootStore(): RootStore {
|
||||
const rootStore = useContext(RootStoreContext);
|
||||
if (rootStore === undefined) {
|
||||
throw new Error('useRootStore must be used within a RootStoreProvider');
|
||||
}
|
||||
|
||||
return rootStore;
|
||||
}
|
||||
@ -1,30 +1,46 @@
|
||||
import {get, post} from "../utils/request.tsx";
|
||||
import {get} from "../utils/request";
|
||||
import {makeObservable, observable, runInAction} from "mobx";
|
||||
import {RootStore} from "./RootStore.ts";
|
||||
import {IUser} from "../models/user.ts";
|
||||
import {RootStore} from "./RootStore";
|
||||
import type {IUser} from "../models/user";
|
||||
import {IStudent} from "../models/student";
|
||||
import {Role} from "../models/role";
|
||||
|
||||
export class UserStore {
|
||||
rootStore: RootStore;
|
||||
@observable
|
||||
user: IUser = {authenticated: false};
|
||||
@observable
|
||||
student: IStudent | undefined;
|
||||
@observable
|
||||
isLoading: boolean = true;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
user: observable,
|
||||
});
|
||||
makeObservable(this);
|
||||
this.rootStore = rootStore;
|
||||
}
|
||||
|
||||
init() {
|
||||
fetchCurrentUserData() {
|
||||
// todo: store token in localStorage
|
||||
get<IUser>('/user/current').then((response) => {
|
||||
runInAction(() => {
|
||||
this.user = response;
|
||||
});
|
||||
if(response.authenticated && response.authorities.some(a => a.authority === Role.STUDENT)) {
|
||||
get<IStudent>('/student/current').then((student) => {
|
||||
runInAction(() => {
|
||||
this.student = student;
|
||||
});
|
||||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
post('/user/logout').then(() => {
|
||||
this.init();
|
||||
});
|
||||
init() {
|
||||
this.fetchCurrentUserData();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
3
web/src/utils/converters.ts
Normal file
3
web/src/utils/converters.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const dateConverter = (date: string) => {
|
||||
return new Date(date).toLocaleString();
|
||||
}
|
||||
@ -1,12 +1,25 @@
|
||||
import {configure} from "mobx";
|
||||
import {RootStore} from "../store/RootStore.tsx";
|
||||
import {RootStore} from "../store/RootStore";
|
||||
import {RouterService} from "../services/RouterService";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||
import {fab} from "@fortawesome/free-brands-svg-icons";
|
||||
import {far} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
|
||||
export const initMobX = () => {
|
||||
const initMobX = () => {
|
||||
configure({enforceActions: 'observed'});
|
||||
}
|
||||
|
||||
const initFontAwesome = () => {
|
||||
library.add(fas);
|
||||
library.add(fab);
|
||||
library.add(far);
|
||||
}
|
||||
|
||||
export const initApp = () => {
|
||||
initMobX();
|
||||
return new RootStore();
|
||||
initFontAwesome();
|
||||
let rootStore = new RootStore().init();
|
||||
RouterService.init(rootStore.routerStore);
|
||||
return rootStore;
|
||||
}
|
||||
@ -2,23 +2,23 @@ import axios, {AxiosRequestConfig} from "axios";
|
||||
|
||||
export const apiUrl = "http://localhost:8080/api/v1/";
|
||||
|
||||
export const get = async <T extends unknown> (url: string, data?: T) => {
|
||||
return await request({
|
||||
export const get = async <T,> (url: string, data?: any) => {
|
||||
return await request<T>({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
data: data,
|
||||
}) as T;
|
||||
});
|
||||
}
|
||||
|
||||
export const post = async <T extends unknown> (url: string, data?: T) => {
|
||||
return request({
|
||||
export const post = async <T,> (url: string, data?: any) => {
|
||||
return await request<T>({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export const request = async <T extends unknown> (config: AxiosRequestConfig<T>) => {
|
||||
export const request = async <T,> (config: AxiosRequestConfig<any>) => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
console.debug(`${config.method} ${config.url} request: ${config.method === 'GET' ? JSON.stringify(config.params) : JSON.stringify(config.data)}`);
|
||||
axios.request({...config, baseURL: apiUrl}).then((response) => {
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface Dimensions {
|
||||
width?: number,
|
||||
height?: number,
|
||||
}
|
||||
|
||||
export const GitHubLogo: React.FC<Dimensions> = ({width = 10, height = 10}: Dimensions) => {
|
||||
return (
|
||||
<svg viewBox="0, 0, 98, 96" width={width} height={height} xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill="#24292f"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@ -28,8 +28,16 @@ module.exports = {
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
},
|
||||
devServer: {
|
||||
client: {
|
||||
overlay: {
|
||||
errors: true,
|
||||
warnings: false,
|
||||
runtimeErrors: false,
|
||||
},
|
||||
},
|
||||
historyApiFallback: true,
|
||||
static: path.join(__dirname, "dist"),
|
||||
compress: false,
|
||||
compress: true,
|
||||
port: 8081,
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user