diff --git a/server/pom.xml b/server/pom.xml
index 15decd5..16f199e 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -88,6 +88,16 @@
org.springframework.boot
spring-boot-starter-actuator
+
+ org.apache.commons
+ commons-lang3
+ 3.14.0
+
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
diff --git a/server/src/main/java/ru/tubryansk/tdms/TdmsApplication.java b/server/src/main/java/ru/tubryansk/tdms/TdmsApplication.java
index 396b31a..597ecfe 100644
--- a/server/src/main/java/ru/tubryansk/tdms/TdmsApplication.java
+++ b/server/src/main/java/ru/tubryansk/tdms/TdmsApplication.java
@@ -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);
}
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java b/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java
index dd3e0b2..443463e 100644
--- a/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java
+++ b/server/src/main/java/ru/tubryansk/tdms/config/SecurityConfiguration.java
@@ -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.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);
diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicController.java b/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicController.java
index d0157f5..8f9a6ad 100644
--- a/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicController.java
+++ b/server/src/main/java/ru/tubryansk/tdms/controller/DiplomaTopicController.java
@@ -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 getAll() {
- return diplomaTopicService.getAll();
- }
-
- @GetMapping("/get-by-id/{id:[\\-+]?\\d+}")
- public DiplomaTopicDTO getById(@PathVariable @PositiveOrZero Integer id) {
- return diplomaTopicService.getById(id);
- }
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java b/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java
new file mode 100644
index 0000000..3bb6523
--- /dev/null
+++ b/server/src/main/java/ru/tubryansk/tdms/controller/StudentController.java
@@ -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);
+ }
+}
diff --git a/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java b/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java
index 66c83d2..cfcf057 100644
--- a/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java
+++ b/server/src/main/java/ru/tubryansk/tdms/controller/UserController.java
@@ -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);
}
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java
deleted file mode 100644
index 93e01bd..0000000
--- a/server/src/main/java/ru/tubryansk/tdms/dto/DiplomaTopicDTO.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java
new file mode 100644
index 0000000..649cf1d
--- /dev/null
+++ b/server/src/main/java/ru/tubryansk/tdms/dto/GroupDTO.java
@@ -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)
+ );
+ }
+}
diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java
index f34d5f9..dc9ebea 100644
--- a/server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java
+++ b/server/src/main/java/ru/tubryansk/tdms/dto/RoleDTO.java
@@ -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 from(User user) {
+ return user.getRoles().stream().map(RoleDTO::from).toList();
+ }
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java
index 2aad7fb..ddea302 100644
--- a/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java
+++ b/server/src/main/java/ru/tubryansk/tdms/dto/StudentDTO.java
@@ -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())
+ );
+ }
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java b/server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java
index f53401f..cb2f0b2 100644
--- a/server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java
+++ b/server/src/main/java/ru/tubryansk/tdms/dto/UserDTO.java
@@ -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 authorities) {
+ List 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();
}
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/entity/Student.java b/server/src/main/java/ru/tubryansk/tdms/entity/Student.java
index 38eb612..8c5ceef 100644
--- a/server/src/main/java/ru/tubryansk/tdms/entity/Student.java
+++ b/server/src/main/java/ru/tubryansk/tdms/entity/Student.java
@@ -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;
-
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java b/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java
index 49a1306..1ce61e6 100644
--- a/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java
+++ b/server/src/main/java/ru/tubryansk/tdms/repository/DiplomaTopicRepository.java
@@ -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 {
+ default DiplomaTopic findByIdThrow(Integer id) {
+ return this.findById(id).orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id));
+ }
}
\ No newline at end of file
diff --git a/server/src/main/java/ru/tubryansk/tdms/repository/StudentRepository.java b/server/src/main/java/ru/tubryansk/tdms/repository/StudentRepository.java
new file mode 100644
index 0000000..c41a842
--- /dev/null
+++ b/server/src/main/java/ru/tubryansk/tdms/repository/StudentRepository.java
@@ -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 {
+ default Student findByIdThrow(Integer id) {
+ return this.findById(id).orElseThrow(() -> new NotFoundException(Student.class, id));
+ }
+
+ Optional findByUser(User user);
+}
diff --git a/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java b/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java
new file mode 100644
index 0000000..6ba7be3
--- /dev/null
+++ b/server/src/main/java/ru/tubryansk/tdms/service/AuthenticationService.java
@@ -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);
+ }
+}
diff --git a/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java b/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java
index d43cb9e..bdf5deb 100644
--- a/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java
+++ b/server/src/main/java/ru/tubryansk/tdms/service/DiplomaTopicService.java
@@ -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 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)));
- }
}
diff --git a/server/src/main/java/ru/tubryansk/tdms/service/LifeCycleService.java b/server/src/main/java/ru/tubryansk/tdms/service/LifeCycleService.java
new file mode 100644
index 0000000..dc3e251
--- /dev/null
+++ b/server/src/main/java/ru/tubryansk/tdms/service/LifeCycleService.java
@@ -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"));
+ }
+}
\ No newline at end of file
diff --git a/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java b/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java
new file mode 100644
index 0000000..2b61f9a
--- /dev/null
+++ b/server/src/main/java/ru/tubryansk/tdms/service/StudentService.java
@@ -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;
+ @Autowired
+ private UserService userService;
+
+ /** @param studentToDiplomaTopic Map of @{@link Student} id and @{@link DiplomaTopic} id */
+ public void changeDiplomaTopic(Map 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 getCallerStudent() {
+ return studentRepository.findByUser(userService.getCallerUser().orElse(null));
+ }
+}
diff --git a/server/src/main/java/ru/tubryansk/tdms/service/UserService.java b/server/src/main/java/ru/tubryansk/tdms/service/UserService.java
index b782eb2..1c9f934 100644
--- a/server/src/main/java/ru/tubryansk/tdms/service/UserService.java
+++ b/server/src/main/java/ru/tubryansk/tdms/service/UserService.java
@@ -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 getCallerUser() {
+ if(authenticationService.authenticated()) {
+ return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
+ }
+ return Optional.empty();
}
}
diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml
index f74a198..2666244 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -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}
diff --git a/web/package-lock.json b/web/package-lock.json
index 10245d2..e1a0ae9 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -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",
diff --git a/web/package.json b/web/package.json
index db25241..1fb0428 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/src/Application.tsx b/web/src/Application.tsx
index 5b3bf91..b827e65 100644
--- a/web/src/Application.tsx
+++ b/web/src/Application.tsx
@@ -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(
-
-
-
-
-
-
-
+
+
+
+
+
);
diff --git a/web/src/components/Page/DefaultPage.tsx b/web/src/components/Page/DefaultPage.tsx
deleted file mode 100644
index 1188054..0000000
--- a/web/src/components/Page/DefaultPage.tsx
+++ /dev/null
@@ -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
-
- render() {
- return <>
-
-
- {this.page}
-
-
- >
- }
-}
diff --git a/web/src/components/Page/Header.tsx b/web/src/components/Page/Header.tsx
deleted file mode 100644
index 8471a56..0000000
--- a/web/src/components/Page/Header.tsx
+++ /dev/null
@@ -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
-
-
-
- TDMS
-
-
-
-
-
-
-
-});
-
-export default Header;
\ No newline at end of file
diff --git a/web/src/components/Page/Root.tsx b/web/src/components/Page/Root.tsx
deleted file mode 100644
index 600f315..0000000
--- a/web/src/components/Page/Root.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import {DefaultPage} from "./DefaultPage.tsx";
-
-export default class Root extends DefaultPage {
- get page() {
- return Home
- }
-}
\ No newline at end of file
diff --git a/web/src/components/Page/UserProfile.tsx b/web/src/components/Page/UserProfile.tsx
deleted file mode 100644
index f215714..0000000
--- a/web/src/components/Page/UserProfile.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import {DefaultPage} from "./DefaultPage.tsx";
-
-export default class UserProfile extends DefaultPage {
- get page() {
- return User Profile
- }
-}
diff --git a/web/src/components/Page/Error.tsx b/web/src/components/page/Error.tsx
similarity index 67%
rename from web/src/components/Page/Error.tsx
rename to web/src/components/page/Error.tsx
index 4471802..3990d54 100644
--- a/web/src/components/Page/Error.tsx
+++ b/web/src/components/page/Error.tsx
@@ -1,4 +1,4 @@
-import {DefaultPage} from "./DefaultPage.tsx";
+import {DefaultPage} from "./layout/DefaultPage";
export default class Error extends DefaultPage {
get page() {
diff --git a/web/src/components/page/Home.tsx b/web/src/components/page/Home.tsx
new file mode 100644
index 0000000..83d632e
--- /dev/null
+++ b/web/src/components/page/Home.tsx
@@ -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 Home
+ }
+}
diff --git a/web/src/components/page/UserProfile.tsx b/web/src/components/page/UserProfile.tsx
new file mode 100644
index 0000000..dc94dae
--- /dev/null
+++ b/web/src/components/page/UserProfile.tsx
@@ -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 (
+
+
+
+ ФИО
+
+
+
+ Имя пользователя
+
+
+
+ Электронная почта
+
+
+
+ Телефон
+ {/* todo: format phone */}
+
+
+
+
+
+ Роли
+ a.name).join(', ')}
+ disabled={true}/>
+
+
+ Дата создания
+
+
+
+ Дата последней модификации
+
+
+
+
+ )
+ }
+}
+
+@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 (
+
+
+
+ Тема дипломной работы
+
+
+
+ Очередь защиты
+
+
+
+ Презентация в электронном формате
+
+
+
+ Оценка за комментарий {/* todo: обсудить с аналитиком */}
+
+
+
+ Оценка за практику {/* todo: обсудить с аналитиком */}
+
+
+
+ Комментарий к предзащите
+
+
+
+ Форма контроля {/* todo: обсудить с аналитиком */}
+
+
+
+ Антиплагиат (процент
+ уникальности) {/* todo: обсудить с аналитиком */}
+
+
+
+ Примечание
+
+
+
+
+
+ Группа
+
+
+
+ Куратор
+
+
+
+ Форма обучения {/* todo: обсудить с аналитиком */}
+
+
+
+ Научный руководитель
+
+
+
+ Зачетная книжка сдана
+
+
+
+ Работа {/* todo: обсудить с аналитиком */}
+
+
+
+ Магистратура {/* todo: обсудить с аналитиком */}
+
+
+
+
+ );
+ }
+}
+
+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
+ }
+}
diff --git a/web/src/components/page/layout/DefaultPage.tsx b/web/src/components/page/layout/DefaultPage.tsx
new file mode 100644
index 0000000..f1a0c9f
--- /dev/null
+++ b/web/src/components/page/layout/DefaultPage.tsx
@@ -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 {
+ 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 <>
+
+
+ {
+ isLoading &&
+
+
+
+ }
+ {
+ !isLoading &&
+ this.page
+ }
+
+
+ >
+ }
+}
+
+export {DefaultPage};
\ No newline at end of file
diff --git a/web/src/components/Page/Footer.tsx b/web/src/components/page/layout/Footer.tsx
similarity index 69%
rename from web/src/components/Page/Footer.tsx
rename to web/src/components/page/layout/Footer.tsx
index ff4dfef..89d2569 100644
--- a/web/src/components/Page/Footer.tsx
+++ b/web/src/components/page/layout/Footer.tsx
@@ -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 = () => {
Thesis Defence Management System ©
diff --git a/web/src/components/page/layout/Header.tsx b/web/src/components/page/layout/Header.tsx
new file mode 100644
index 0000000..32a2101
--- /dev/null
+++ b/web/src/components/page/layout/Header.tsx
@@ -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
+
+
+
+ TDMS
+
+
+
+
+
+
+
+ }
+}
+
+export default Header;
\ No newline at end of file
diff --git a/web/src/context/RootStoreContext.ts b/web/src/context/RootStoreContext.ts
new file mode 100644
index 0000000..3344553
--- /dev/null
+++ b/web/src/context/RootStoreContext.ts
@@ -0,0 +1,5 @@
+import {RootStore} from "../store/RootStore";
+import {ContextType, createContext} from "react";
+
+export const RootStoreContext = createContext(new RootStore());
+export type RootStoreContextType = ContextType;
\ No newline at end of file
diff --git a/web/src/index.css b/web/src/index.css
index 114dedb..93c7567 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -64,4 +64,11 @@ body {
footer {
margin-top: auto;
-}
\ No newline at end of file
+}
+
+#fullscreen-loader {
+ min-height: calc(100vh - (56px + 64px + 48px + 48px));
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/web/src/models/role.ts b/web/src/models/role.ts
new file mode 100644
index 0000000..3c73756
--- /dev/null
+++ b/web/src/models/role.ts
@@ -0,0 +1,10 @@
+export enum Role {
+ STUDENT = 'ROLE_STUDENT',
+ TUTOR = 'ROLE_TUTOR',
+ DIRECTOR = 'ROLE_DIRECTOR',
+}
+
+export interface IAuthority {
+ authority: Role;
+ name: string;
+}
\ No newline at end of file
diff --git a/web/src/models/student.ts b/web/src/models/student.ts
new file mode 100644
index 0000000..edd6a6f
--- /dev/null
+++ b/web/src/models/student.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/web/src/models/user.ts b/web/src/models/user.ts
index 8a55367..fd1dc80 100644
--- a/web/src/models/user.ts
+++ b/web/src/models/user.ts
@@ -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;
diff --git a/web/src/router/routes.ts b/web/src/router/routes.ts
new file mode 100644
index 0000000..cfad712
--- /dev/null
+++ b/web/src/router/routes.ts
@@ -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',
+}];
\ No newline at end of file
diff --git a/web/src/router/viewMap.tsx b/web/src/router/viewMap.tsx
new file mode 100644
index 0000000..120efd9
--- /dev/null
+++ b/web/src/router/viewMap.tsx
@@ -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: ,
+ profile: ,
+ error: ,
+}
\ No newline at end of file
diff --git a/web/src/routes.tsx b/web/src/routes.tsx
deleted file mode 100644
index aa91c06..0000000
--- a/web/src/routes.tsx
+++ /dev/null
@@ -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: ,
-}, {
- name: 'profile',
- pattern: '/profile',
- view: ,
-}, {
- name: 'error',
- pattern: '/error',
- view: ,
-}];
\ No newline at end of file
diff --git a/web/src/services/RouterService.ts b/web/src/services/RouterService.ts
new file mode 100644
index 0000000..1eef29a
--- /dev/null
+++ b/web/src/services/RouterService.ts
@@ -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);
+ }
+}
diff --git a/web/src/services/UserService.ts b/web/src/services/UserService.ts
index e255901..552a20b 100644
--- a/web/src/services/UserService.ts
+++ b/web/src/services/UserService.ts
@@ -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);
+ }
}
diff --git a/web/src/store/MyRouterStore.ts b/web/src/store/MyRouterStore.ts
index 1e1d0a1..c3fd85b 100644
--- a/web/src/store/MyRouterStore.ts
+++ b/web/src/store/MyRouterStore.ts
@@ -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((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;
}
}
\ No newline at end of file
diff --git a/web/src/store/RootStore.ts b/web/src/store/RootStore.ts
new file mode 100644
index 0000000..d66b9a9
--- /dev/null
+++ b/web/src/store/RootStore.ts
@@ -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;
+ }
+}
diff --git a/web/src/store/RootStore.tsx b/web/src/store/RootStore.tsx
deleted file mode 100644
index 6b11989..0000000
--- a/web/src/store/RootStore.tsx
+++ /dev/null
@@ -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(
- undefined
-);
-
-export function useRootStore(): RootStore {
- const rootStore = useContext(RootStoreContext);
- if (rootStore === undefined) {
- throw new Error('useRootStore must be used within a RootStoreProvider');
- }
-
- return rootStore;
-}
diff --git a/web/src/store/UserStore.ts b/web/src/store/UserStore.ts
index bf19410..c7c7a68 100644
--- a/web/src/store/UserStore.ts
+++ b/web/src/store/UserStore.ts
@@ -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('/user/current').then((response) => {
runInAction(() => {
this.user = response;
});
+ if(response.authenticated && response.authorities.some(a => a.authority === Role.STUDENT)) {
+ get('/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;
}
}
\ No newline at end of file
diff --git a/web/src/utils/converters.ts b/web/src/utils/converters.ts
new file mode 100644
index 0000000..3152f1c
--- /dev/null
+++ b/web/src/utils/converters.ts
@@ -0,0 +1,3 @@
+export const dateConverter = (date: string) => {
+ return new Date(date).toLocaleString();
+}
\ No newline at end of file
diff --git a/web/src/utils/init.ts b/web/src/utils/init.ts
index 32694ea..3f03533 100644
--- a/web/src/utils/init.ts
+++ b/web/src/utils/init.ts
@@ -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();
-}
\ No newline at end of file
+ initFontAwesome();
+ let rootStore = new RootStore().init();
+ RouterService.init(rootStore.routerStore);
+ return rootStore;
+}
diff --git a/web/src/utils/request.tsx b/web/src/utils/request.tsx
index 1b7c01b..2f5201c 100644
--- a/web/src/utils/request.tsx
+++ b/web/src/utils/request.tsx
@@ -2,23 +2,23 @@ import axios, {AxiosRequestConfig} from "axios";
export const apiUrl = "http://localhost:8080/api/v1/";
-export const get = async (url: string, data?: T) => {
- return await request({
+export const get = async (url: string, data?: any) => {
+ return await request({
url: url,
method: 'GET',
data: data,
- }) as T;
+ });
}
-export const post = async (url: string, data?: T) => {
- return request({
+export const post = async (url: string, data?: any) => {
+ return await request({
url: url,
method: 'POST',
data: data,
});
}
-export const request = async (config: AxiosRequestConfig) => {
+export const request = async (config: AxiosRequestConfig) => {
return new Promise((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) => {
diff --git a/web/src/utils/svg.tsx b/web/src/utils/svg.tsx
deleted file mode 100644
index efa1956..0000000
--- a/web/src/utils/svg.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-
-interface Dimensions {
- width?: number,
- height?: number,
-}
-
-export const GitHubLogo: React.FC = ({width = 10, height = 10}: Dimensions) => {
- return (
-
- );
-}
\ No newline at end of file
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 5726e6d..c37fa02 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -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: [