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>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -11,8 +11,6 @@ import org.springframework.context.ConfigurableApplicationContext;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class TdmsApplication {
|
public class TdmsApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
ConfigurableApplicationContext context = SpringApplication.run(TdmsApplication.class, args);
|
SpringApplication.run(TdmsApplication.class, args);
|
||||||
String staticLocation = context.getEnvironment().getProperty("spring.web.resources.static-locations");
|
|
||||||
log.info("Static location: {}", staticLocation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,14 @@ package ru.tubryansk.tdms.config;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.LoggerFactory;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
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.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
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;
|
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
@Bean
|
@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
|
return httpSecurity
|
||||||
.authorizeHttpRequests(this::configureHttpAuthorization)
|
.authorizeHttpRequests(this::configureHttpAuthorization)
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.cors(a -> a.configurationSource(corsConfiguration()))
|
.cors(a -> a.configurationSource(cors))
|
||||||
.authenticationManager(authenticationManager)
|
.authenticationManager(authenticationManager)
|
||||||
.sessionManagement(this::configureSessionManagement)
|
.sessionManagement(this::configureSessionManagement)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfiguration() {
|
@Profile("dev")
|
||||||
|
@Qualifier("corsConfig")
|
||||||
|
public CorsConfigurationSource corsConfigurationDev() {
|
||||||
return request -> {
|
return request -> {
|
||||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||||
corsConfiguration.applyPermitDefaultValues();
|
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) {
|
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
|
||||||
/* API ROUTES */
|
/* API ROUTES */
|
||||||
httpAuthorization.requestMatchers("/api/v1/diploma-topic/**").permitAll();
|
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
|
||||||
httpAuthorization.requestMatchers("/api/v1/user/**").permitAll();
|
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();
|
httpAuthorization.requestMatchers("/api/**").denyAll();
|
||||||
/* STATIC ROUTES */
|
/* STATIC ROUTES */
|
||||||
httpAuthorization.requestMatchers("/**").permitAll();
|
httpAuthorization.requestMatchers("/**").permitAll();
|
||||||
/* OTHER */
|
|
||||||
httpAuthorization.anyRequest().denyAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -76,18 +107,21 @@ public class SecurityConfiguration {
|
|||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PasswordEncoder passwordEncoder() {
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@Profile("dev")
|
||||||
public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) {
|
public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) {
|
||||||
return new HttpSessionListener() {
|
return new HttpSessionListener() {
|
||||||
@Override
|
@Override
|
||||||
public void sessionCreated(HttpSessionEvent se) {
|
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();
|
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("izrailev_v_ya_1", "1");
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, "1");
|
||||||
Authentication authenticated = authenticationManager.authenticate(authentication);
|
Authentication authenticated = authenticationManager.authenticate(authentication);
|
||||||
context.setAuthentication(authenticated);
|
context.setAuthentication(authenticated);
|
||||||
SecurityContextHolder.setContext(context);
|
SecurityContextHolder.setContext(context);
|
||||||
|
|||||||
@ -1,33 +1,13 @@
|
|||||||
package ru.tubryansk.tdms.controller;
|
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.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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import ru.tubryansk.tdms.dto.DiplomaTopicDTO;
|
|
||||||
import ru.tubryansk.tdms.service.DiplomaTopicService;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/diploma-topic/")
|
@RequestMapping("/api/v1/diploma-topic/")
|
||||||
@Validated
|
@Validated
|
||||||
public class DiplomaTopicController {
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.web.bind.annotation.*;
|
||||||
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 ru.tubryansk.tdms.dto.UserDTO;
|
import ru.tubryansk.tdms.dto.UserDTO;
|
||||||
import ru.tubryansk.tdms.entity.User;
|
import ru.tubryansk.tdms.service.AuthenticationService;
|
||||||
import ru.tubryansk.tdms.service.UserService;
|
import ru.tubryansk.tdms.service.UserService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Validated
|
|
||||||
@RequestMapping("/api/v1/user")
|
@RequestMapping("/api/v1/user")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationService authenticationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@GetMapping("/current")
|
@GetMapping("/current")
|
||||||
public UserDTO getCurrentUser() {
|
public UserDTO getCurrentUser() {
|
||||||
User principal = userService.getCallerPrincipal();
|
return userService.getCallerUser().map(user -> UserDTO.from(user, true)).orElse(UserDTO.unauthenticated());
|
||||||
return principal != null ? UserDTO.from(principal, true) : UserDTO.fromUnauthenticated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
public void 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;
|
package ru.tubryansk.tdms.dto;
|
||||||
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import ru.tubryansk.tdms.entity.Role;
|
||||||
import lombok.Data;
|
import ru.tubryansk.tdms.entity.User;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
@Data
|
public record RoleDTO(String name, String authority) {
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
public static RoleDTO from(Role role) {
|
||||||
public class RoleDTO {
|
return new RoleDTO(role.getName(), role.getAuthority());
|
||||||
private Integer id;
|
}
|
||||||
private String name;
|
|
||||||
|
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.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import ru.tubryansk.tdms.entity.Student;
|
||||||
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class StudentDTO {
|
public class StudentDTO {
|
||||||
private Integer id;
|
|
||||||
private Boolean form;
|
private Boolean form;
|
||||||
private Integer protectionOrder;
|
private Integer protectionOrder;
|
||||||
private String magistracy;
|
private String magistracy;
|
||||||
@ -23,8 +23,29 @@ public class StudentDTO {
|
|||||||
private String note;
|
private String note;
|
||||||
private Boolean recordBookReturned;
|
private Boolean recordBookReturned;
|
||||||
private String work;
|
private String work;
|
||||||
private Integer userId;
|
private UserDTO user;
|
||||||
private Integer diplomaTopicId;
|
private String diplomaTopic;
|
||||||
private Integer mentorUserId;
|
private UserDTO mentorUser;
|
||||||
private Integer groupId;
|
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;
|
package ru.tubryansk.tdms.dto;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import ru.tubryansk.tdms.entity.Role;
|
|
||||||
import ru.tubryansk.tdms.entity.User;
|
import ru.tubryansk.tdms.entity.User;
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@ -10,18 +10,19 @@ import java.util.List;
|
|||||||
|
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
public record UserDTO(
|
public record UserDTO(
|
||||||
boolean authenticated,
|
boolean authenticated,
|
||||||
String login,
|
String login,
|
||||||
String password,
|
String password,
|
||||||
String fullName,
|
String fullName,
|
||||||
String email,
|
String email,
|
||||||
String phoneNumber,
|
String phone,
|
||||||
ZonedDateTime createdAt,
|
ZonedDateTime createdAt,
|
||||||
ZonedDateTime updatedAt,
|
ZonedDateTime updatedAt,
|
||||||
List<String> authorities) {
|
List<RoleDTO> authorities) {
|
||||||
|
|
||||||
public static UserDTO fromUnauthenticated() {
|
public static UserDTO unauthenticated() {
|
||||||
return UserDTO.builder()
|
return UserDTO.builder()
|
||||||
.authenticated(false)
|
.authenticated(false)
|
||||||
.build();
|
.build();
|
||||||
@ -34,10 +35,10 @@ public record UserDTO(
|
|||||||
.password(anonymize ? "" : user.getPassword())
|
.password(anonymize ? "" : user.getPassword())
|
||||||
.fullName(user.getFullName())
|
.fullName(user.getFullName())
|
||||||
.email(user.getMail())
|
.email(user.getMail())
|
||||||
.phoneNumber(user.getNumberPhone())
|
.phone(user.getNumberPhone())
|
||||||
.createdAt(user.getCreatedAt())
|
.createdAt(user.getCreatedAt())
|
||||||
.updatedAt(user.getUpdatedAt())
|
.updatedAt(user.getUpdatedAt())
|
||||||
.authorities(user.getRoles().stream().map(Role::getAuthority).toList())
|
.authorities(RoleDTO.from(user))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package ru.tubryansk.tdms.entity;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
|
import org.springframework.web.context.annotation.SessionScope;
|
||||||
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ -49,5 +51,4 @@ public class Student {
|
|||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "group_id", nullable = false)
|
@JoinColumn(name = "group_id", nullable = false)
|
||||||
private Group group;
|
private Group group;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,11 @@ package ru.tubryansk.tdms.repository;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
import ru.tubryansk.tdms.entity.DiplomaTopic;
|
||||||
|
import ru.tubryansk.tdms.exception.NotFoundException;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface DiplomaTopicRepository extends JpaRepository<DiplomaTopic, Integer> {
|
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;
|
package ru.tubryansk.tdms.service;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
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
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
public class DiplomaTopicService {
|
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;
|
package ru.tubryansk.tdms.service;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
@ -13,7 +10,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import ru.tubryansk.tdms.entity.User;
|
import ru.tubryansk.tdms.entity.User;
|
||||||
import ru.tubryansk.tdms.repository.UserRepository;
|
import ru.tubryansk.tdms.repository.UserRepository;
|
||||||
|
|
||||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@ -22,20 +19,7 @@ public class UserService implements UserDetailsService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private HttpServletRequest httpServletRequest;
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User loadUserByUsername(String username) throws UsernameNotFoundException {
|
public User loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
@ -43,11 +27,10 @@ public class UserService implements UserDetailsService {
|
|||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logout() {
|
public Optional<User> getCallerUser() {
|
||||||
HttpSession session = httpServletRequest.getSession(true);
|
if(authenticationService.authenticated()) {
|
||||||
// if(session != null) {
|
return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
|
||||||
// session.invalidate();
|
}
|
||||||
// }
|
return Optional.empty();
|
||||||
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,15 @@ application:
|
|||||||
name: @name@
|
name: @name@
|
||||||
version: @version@
|
version: @version@
|
||||||
type: production
|
type: production
|
||||||
port: 80
|
port: 443
|
||||||
domain: tdms.tu-bryansk.ru
|
domain: tdms.tu-bryansk.ru
|
||||||
protocol: https
|
protocol: https
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: tdms
|
name: ${application.name}
|
||||||
|
main:
|
||||||
|
allow-circular-references: true
|
||||||
datasource:
|
datasource:
|
||||||
url: ${db.url}
|
url: ${db.url}
|
||||||
username: ${db.user}
|
username: ${db.user}
|
||||||
|
|||||||
69
web/package-lock.json
generated
69
web/package-lock.json
generated
@ -8,6 +8,11 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"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",
|
"axios": "^1.7.7",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"mobx": "^6.13.1",
|
"mobx": "^6.13.1",
|
||||||
@ -1733,6 +1738,70 @@
|
|||||||
"node": ">=10.0.0"
|
"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": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||||
|
|||||||
@ -3,10 +3,15 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build" : "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"dev": "webpack-dev-server --mode development"
|
"dev": "webpack-dev-server --mode development"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"axios": "^1.7.7",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"mobx": "^6.13.1",
|
"mobx": "^6.13.1",
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import {RouterContext, RouterView} from "mobx-state-router";
|
import {RouterContext, RouterView} from "mobx-state-router";
|
||||||
import {initApp} from "./utils/init.ts";
|
import {initApp} from "./utils/init";
|
||||||
import {MyRouterStore} from "./store/MyRouterStore.ts";
|
import {RootStoreContext} from './context/RootStoreContext';
|
||||||
import { RootStoreContext } from "./store/RootStore.tsx";
|
import {viewMap} from "./router/viewMap";
|
||||||
|
|
||||||
const rootStore = initApp();
|
const rootStore = initApp();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<RootStoreContext.Provider value={rootStore}>
|
||||||
<RootStoreContext.Provider value={rootStore}>
|
<RouterContext.Provider value={rootStore.routerStore}>
|
||||||
<RouterContext.Provider value={rootStore.routerStore}>
|
<RouterView viewMap={viewMap}/>
|
||||||
<RouterView viewMap={MyRouterStore.makeViewMap()} />
|
</RouterContext.Provider>
|
||||||
</RouterContext.Provider>
|
</RootStoreContext.Provider>
|
||||||
</RootStoreContext.Provider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 {
|
export default class Error extends DefaultPage {
|
||||||
get page() {
|
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 {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 = () => {
|
const Footer = () => {
|
||||||
return (
|
return (
|
||||||
@ -9,7 +10,7 @@ const Footer = () => {
|
|||||||
<Navbar.Text>Thesis Defence Management System ©</Navbar.Text>
|
<Navbar.Text>Thesis Defence Management System ©</Navbar.Text>
|
||||||
<Nav>
|
<Nav>
|
||||||
<Nav.Link href="https://github.com/Velixeor/Thesis-Defense-Management-System">
|
<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.Link>
|
||||||
</Nav>
|
</Nav>
|
||||||
</Container>
|
</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>;
|
||||||
@ -64,4 +64,11 @@ body {
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
margin-top: auto;
|
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 {
|
export interface IAuthenticated {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
login: string,
|
login: string,
|
||||||
@ -7,7 +9,7 @@ export interface IAuthenticated {
|
|||||||
phone: string,
|
phone: string,
|
||||||
createdAt: string,
|
createdAt: string,
|
||||||
updatedAt: string,
|
updatedAt: string,
|
||||||
authorities: string[],
|
authorities: IAuthority[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare type IUser = {authenticated: false} | IAuthenticated;
|
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 {browserHistory, createRouterState, HistoryAdapter, RouterStore} from "mobx-state-router";
|
||||||
import {routes} from "../routes.tsx";
|
import {RootStore} from "./RootStore";
|
||||||
import {RootStore} from "./RootStore.tsx";
|
import {routes} from "../router/routes";
|
||||||
|
|
||||||
|
|
||||||
export class MyRouterStore extends RouterStore {
|
export class MyRouterStore extends RouterStore {
|
||||||
constructor(rootStore: RootStore) {
|
constructor(rootStore: RootStore) {
|
||||||
super(MyRouterStore.makeRoutesMap(),
|
super(routes,
|
||||||
createRouterState('error', {notFound: true}),
|
createRouterState('error', {notFound: true}),
|
||||||
{rootStore: rootStore});
|
{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() {
|
init() {
|
||||||
const historyAdapter = new HistoryAdapter(this, browserHistory);
|
const historyAdapter = new HistoryAdapter(this, browserHistory);
|
||||||
historyAdapter.observeRouterStateChanges();
|
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 {makeObservable, observable, runInAction} from "mobx";
|
||||||
import {RootStore} from "./RootStore.ts";
|
import {RootStore} from "./RootStore";
|
||||||
import {IUser} from "../models/user.ts";
|
import type {IUser} from "../models/user";
|
||||||
|
import {IStudent} from "../models/student";
|
||||||
|
import {Role} from "../models/role";
|
||||||
|
|
||||||
export class UserStore {
|
export class UserStore {
|
||||||
rootStore: RootStore;
|
rootStore: RootStore;
|
||||||
|
@observable
|
||||||
user: IUser = {authenticated: false};
|
user: IUser = {authenticated: false};
|
||||||
|
@observable
|
||||||
|
student: IStudent | undefined;
|
||||||
|
@observable
|
||||||
|
isLoading: boolean = true;
|
||||||
|
|
||||||
constructor(rootStore: RootStore) {
|
constructor(rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this);
|
||||||
user: observable,
|
|
||||||
});
|
|
||||||
this.rootStore = rootStore;
|
this.rootStore = rootStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
fetchCurrentUserData() {
|
||||||
|
// todo: store token in localStorage
|
||||||
get<IUser>('/user/current').then((response) => {
|
get<IUser>('/user/current').then((response) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.user = response;
|
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() {
|
init() {
|
||||||
post('/user/logout').then(() => {
|
this.fetchCurrentUserData();
|
||||||
this.init();
|
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 {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";
|
||||||
|
|
||||||
|
const initMobX = () => {
|
||||||
export const initMobX = () => {
|
|
||||||
configure({enforceActions: 'observed'});
|
configure({enforceActions: 'observed'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initFontAwesome = () => {
|
||||||
|
library.add(fas);
|
||||||
|
library.add(fab);
|
||||||
|
library.add(far);
|
||||||
|
}
|
||||||
|
|
||||||
export const initApp = () => {
|
export const initApp = () => {
|
||||||
initMobX();
|
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 apiUrl = "http://localhost:8080/api/v1/";
|
||||||
|
|
||||||
export const get = async <T extends unknown> (url: string, data?: T) => {
|
export const get = async <T,> (url: string, data?: any) => {
|
||||||
return await request({
|
return await request<T>({
|
||||||
url: url,
|
url: url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
data: data,
|
data: data,
|
||||||
}) as T;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const post = async <T extends unknown> (url: string, data?: T) => {
|
export const post = async <T,> (url: string, data?: any) => {
|
||||||
return request({
|
return await request<T>({
|
||||||
url: url,
|
url: url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: data,
|
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) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
console.debug(`${config.method} ${config.url} request: ${config.method === 'GET' ? JSON.stringify(config.params) : JSON.stringify(config.data)}`);
|
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) => {
|
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')
|
path: path.resolve(__dirname, 'dist')
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
|
client: {
|
||||||
|
overlay: {
|
||||||
|
errors: true,
|
||||||
|
warnings: false,
|
||||||
|
runtimeErrors: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
historyApiFallback: true,
|
||||||
static: path.join(__dirname, "dist"),
|
static: path.join(__dirname, "dist"),
|
||||||
compress: false,
|
compress: true,
|
||||||
port: 8081,
|
port: 8081,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user