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:
Maksim Skobaro 2024-10-20 16:52:24 +03:00
parent 212249becd
commit 181dc824a1
52 changed files with 843 additions and 339 deletions

View File

@ -88,6 +88,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
</dependencies>
<build>

View File

@ -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);
}
}

View File

@ -3,9 +3,14 @@ package ru.tubryansk.tdms.config;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
@ -26,24 +31,31 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.time.Duration;
import java.util.List;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
AuthenticationManager authenticationManager,
@Qualifier("corsConfig") CorsConfigurationSource cors) throws Exception {
return httpSecurity
.authorizeHttpRequests(this::configureHttpAuthorization)
.csrf(AbstractHttpConfigurer::disable)
.cors(a -> a.configurationSource(corsConfiguration()))
.cors(a -> a.configurationSource(cors))
.authenticationManager(authenticationManager)
.sessionManagement(this::configureSessionManagement)
.build();
}
@Bean
public CorsConfigurationSource corsConfiguration() {
@Profile("dev")
@Qualifier("corsConfig")
public CorsConfigurationSource corsConfigurationDev() {
return request -> {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.applyPermitDefaultValues();
@ -54,15 +66,34 @@ public class SecurityConfiguration {
};
}
@Bean
@Profile("!dev")
@Qualifier("corsConfig")
public CorsConfigurationSource corsConfigurationProd(@Value("${application.domain}") String domain,
@Value("${application.port}") String port,
@Value("${application.protocol}") String protocol) {
return request -> {
String url = StringUtils.join(protocol, "://", domain, ":", port);
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setMaxAge(Duration.ofHours(1));
corsConfiguration.addAllowedOrigin(url);
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name()));
// corsConfiguration.setAllowedHeaders();
return corsConfiguration;
};
}
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
/* API ROUTES */
httpAuthorization.requestMatchers("/api/v1/diploma-topic/**").permitAll();
httpAuthorization.requestMatchers("/api/v1/user/**").permitAll();
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
httpAuthorization.requestMatchers("/api/v1/user/login").anonymous();
httpAuthorization.requestMatchers("/api/v1/user/current").permitAll();
httpAuthorization.requestMatchers("/api/v1/student/current").permitAll();
httpAuthorization.requestMatchers("/api/**").denyAll();
/* STATIC ROUTES */
httpAuthorization.requestMatchers("/**").permitAll();
/* OTHER */
httpAuthorization.anyRequest().denyAll();
}
@Bean
@ -76,18 +107,21 @@ public class SecurityConfiguration {
return provider;
}
private PasswordEncoder passwordEncoder() {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Profile("dev")
public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) {
return new HttpSessionListener() {
@Override
public void sessionCreated(HttpSessionEvent se) {
LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as izrailev_v_ya_1", se.getSession().getId());
String username = "akulenko_mikhail";
LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as {}", se.getSession().getId(), username);
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("izrailev_v_ya_1", "1");
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, "1");
Authentication authenticated = authenticationManager.authenticate(authentication);
context.setAuthentication(authenticated);
SecurityContextHolder.setContext(context);

View File

@ -1,33 +1,13 @@
package ru.tubryansk.tdms.controller;
import jakarta.validation.constraints.PositiveOrZero;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.tubryansk.tdms.dto.DiplomaTopicDTO;
import ru.tubryansk.tdms.service.DiplomaTopicService;
import java.util.List;
@RestController
@RequestMapping("/api/v1/diploma-topic/")
@Validated
public class DiplomaTopicController {
@Autowired
private DiplomaTopicService diplomaTopicService;
@GetMapping("/get-all")
public List<DiplomaTopicDTO> getAll() {
return diplomaTopicService.getAll();
}
@GetMapping("/get-by-id/{id:[\\-+]?\\d+}")
public DiplomaTopicDTO getById(@PathVariable @PositiveOrZero Integer id) {
return diplomaTopicService.getById(id);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View 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)
);
}
}

View File

@ -1,15 +1,19 @@
package ru.tubryansk.tdms.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import ru.tubryansk.tdms.entity.Role;
import ru.tubryansk.tdms.entity.User;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoleDTO {
private Integer id;
private String name;
public record RoleDTO(String name, String authority) {
public static RoleDTO from(Role role) {
return new RoleDTO(role.getName(), role.getAuthority());
}
public static List<RoleDTO> from(User user) {
return user.getRoles().stream().map(RoleDTO::from).toList();
}
}

View File

@ -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())
);
}
}

View File

@ -1,8 +1,8 @@
package ru.tubryansk.tdms.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import ru.tubryansk.tdms.entity.Role;
import ru.tubryansk.tdms.entity.User;
import java.time.ZonedDateTime;
@ -10,18 +10,19 @@ import java.util.List;
@Builder
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record UserDTO(
boolean authenticated,
String login,
String password,
String fullName,
String email,
String phoneNumber,
String phone,
ZonedDateTime createdAt,
ZonedDateTime updatedAt,
List<String> authorities) {
List<RoleDTO> authorities) {
public static UserDTO fromUnauthenticated() {
public static UserDTO unauthenticated() {
return UserDTO.builder()
.authenticated(false)
.build();
@ -34,10 +35,10 @@ public record UserDTO(
.password(anonymize ? "" : user.getPassword())
.fullName(user.getFullName())
.email(user.getMail())
.phoneNumber(user.getNumberPhone())
.phone(user.getNumberPhone())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.authorities(user.getRoles().stream().map(Role::getAuthority).toList())
.authorities(RoleDTO.from(user))
.build();
}
}

View File

@ -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;
}

View File

@ -3,7 +3,11 @@ package ru.tubryansk.tdms.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.DiplomaTopic;
import ru.tubryansk.tdms.exception.NotFoundException;
@Repository
public interface DiplomaTopicRepository extends JpaRepository<DiplomaTopic, Integer> {
default DiplomaTopic findByIdThrow(Integer id) {
return this.findById(id).orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id));
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -1,31 +1,9 @@
package ru.tubryansk.tdms.service;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.tubryansk.tdms.entity.DiplomaTopic;
import ru.tubryansk.tdms.exception.NotFoundException;
import ru.tubryansk.tdms.repository.DiplomaTopicRepository;
import ru.tubryansk.tdms.dto.DiplomaTopicDTO;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class DiplomaTopicService {
@Autowired
private DiplomaTopicRepository diplomaTopicRepository;
public List<DiplomaTopicDTO> getAll() {
return diplomaTopicRepository.findAll()
.stream()
.map(DiplomaTopicDTO::fromEntity)
.collect(Collectors.toList());
}
public DiplomaTopicDTO getById(Integer id) {
return DiplomaTopicDTO.fromEntity(diplomaTopicRepository.findById(id)
.orElseThrow(() -> new NotFoundException(DiplomaTopic.class, id)));
}
}

View File

@ -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"));
}
}

View File

@ -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));
}
}

View File

@ -1,11 +1,8 @@
package ru.tubryansk.tdms.service;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@ -13,7 +10,7 @@ import org.springframework.stereotype.Service;
import ru.tubryansk.tdms.entity.User;
import ru.tubryansk.tdms.repository.UserRepository;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
import java.util.Optional;
@Service
@Transactional
@ -22,20 +19,7 @@ public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private HttpServletRequest httpServletRequest;
public User getCallerPrincipal() {
if(!authenticated()) {
return null;
}
return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
public boolean authenticated() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.isAuthenticated() && !(authentication instanceof AnonymousAuthenticationToken);
}
private AuthenticationService authenticationService;
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
@ -43,11 +27,10 @@ public class UserService implements UserDetailsService {
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
public void logout() {
HttpSession session = httpServletRequest.getSession(true);
// if(session != null) {
// session.invalidate();
// }
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, null);
public Optional<User> getCallerUser() {
if(authenticationService.authenticated()) {
return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
return Optional.empty();
}
}

View File

@ -7,13 +7,15 @@ application:
name: @name@
version: @version@
type: production
port: 80
port: 443
domain: tdms.tu-bryansk.ru
protocol: https
spring:
application:
name: tdms
name: ${application.name}
main:
allow-circular-references: true
datasource:
url: ${db.url}
username: ${db.user}

69
web/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -1,20 +1,17 @@
import React from "react";
import ReactDOM from 'react-dom/client'
import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css';
import {RouterContext, RouterView} from "mobx-state-router";
import {initApp} from "./utils/init.ts";
import {MyRouterStore} from "./store/MyRouterStore.ts";
import { RootStoreContext } from "./store/RootStore.tsx";
import {initApp} from "./utils/init";
import {RootStoreContext} from './context/RootStoreContext';
import {viewMap} from "./router/viewMap";
const rootStore = initApp();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RootStoreContext.Provider value={rootStore}>
<RouterContext.Provider value={rootStore.routerStore}>
<RouterView viewMap={MyRouterStore.makeViewMap()} />
<RouterView viewMap={viewMap}/>
</RouterContext.Provider>
</RootStoreContext.Provider>
</React.StrictMode>
);

View File

@ -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/>
</>
}
}

View File

@ -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;

View File

@ -1,7 +0,0 @@
import {DefaultPage} from "./DefaultPage.tsx";
export default class Root extends DefaultPage {
get page() {
return <h1>Home</h1>
}
}

View File

@ -1,7 +0,0 @@
import {DefaultPage} from "./DefaultPage.tsx";
export default class UserProfile extends DefaultPage {
get page() {
return <h1>User Profile</h1>
}
}

View File

@ -1,4 +1,4 @@
import {DefaultPage} from "./DefaultPage.tsx";
import {DefaultPage} from "./layout/DefaultPage";
export default class Error extends DefaultPage {
get page() {

View 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>
}
}

View 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>
}
}

View 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};

View File

@ -1,5 +1,6 @@
import {Container, Nav, Navbar} from "react-bootstrap";
import {GitHubLogo} from "../../utils/svg.tsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
const Footer = () => {
return (
@ -9,7 +10,7 @@ const Footer = () => {
<Navbar.Text>Thesis Defence Management System &copy;</Navbar.Text>
<Nav>
<Nav.Link href="https://github.com/Velixeor/Thesis-Defense-Management-System">
<GitHubLogo width={32} height={32}/>
<FontAwesomeIcon icon={findIconDefinition({iconName:'github', prefix:'fab'})} size="xl"/>
</Nav.Link>
</Nav>
</Container>

View 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;

View 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>;

View File

@ -65,3 +65,10 @@ body {
footer {
margin-top: auto;
}
#fullscreen-loader {
min-height: calc(100vh - (56px + 64px + 48px + 48px));
display: flex;
justify-content: center;
align-items: center;
}

10
web/src/models/role.ts Normal file
View 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
View 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;
}

View File

@ -1,3 +1,5 @@
import {IAuthority} from "./role";
export interface IAuthenticated {
authenticated: true,
login: string,
@ -7,7 +9,7 @@ export interface IAuthenticated {
phone: string,
createdAt: string,
updatedAt: string,
authorities: string[],
authorities: IAuthority[],
}
export declare type IUser = {authenticated: false} | IAuthenticated;

12
web/src/router/routes.ts Normal file
View 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',
}];

View 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/>,
}

View File

@ -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/>,
}];

View 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);
}
}

View File

@ -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);
}
}

View File

@ -1,30 +1,17 @@
import {browserHistory, createRouterState, HistoryAdapter, RouterStore} from "mobx-state-router";
import {routes} from "../routes.tsx";
import {RootStore} from "./RootStore.tsx";
import {RootStore} from "./RootStore";
import {routes} from "../router/routes";
export class MyRouterStore extends RouterStore {
constructor(rootStore: RootStore) {
super(MyRouterStore.makeRoutesMap(),
super(routes,
createRouterState('error', {notFound: true}),
{rootStore: rootStore});
}
static makeViewMap() {
return routes.reduce<any>((map, route) => {
map[route.name] = route.view;
return map;
}, {});
}
static makeRoutesMap() {
return routes.map(route => {
return {...route, view: undefined};
});
}
init() {
const historyAdapter = new HistoryAdapter(this, browserHistory);
historyAdapter.observeRouterStateChanges();
return this;
}
}

View 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;
}
}

View File

@ -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;
}

View File

@ -1,30 +1,46 @@
import {get, post} from "../utils/request.tsx";
import {get} from "../utils/request";
import {makeObservable, observable, runInAction} from "mobx";
import {RootStore} from "./RootStore.ts";
import {IUser} from "../models/user.ts";
import {RootStore} from "./RootStore";
import type {IUser} from "../models/user";
import {IStudent} from "../models/student";
import {Role} from "../models/role";
export class UserStore {
rootStore: RootStore;
@observable
user: IUser = {authenticated: false};
@observable
student: IStudent | undefined;
@observable
isLoading: boolean = true;
constructor(rootStore: RootStore) {
makeObservable(this, {
user: observable,
});
makeObservable(this);
this.rootStore = rootStore;
}
init() {
fetchCurrentUserData() {
// todo: store token in localStorage
get<IUser>('/user/current').then((response) => {
runInAction(() => {
this.user = response;
});
if(response.authenticated && response.authorities.some(a => a.authority === Role.STUDENT)) {
get<IStudent>('/student/current').then((student) => {
runInAction(() => {
this.student = student;
});
});
}
}).finally(() => {
runInAction(() => {
this.isLoading = false;
});
});
}
logout() {
post('/user/logout').then(() => {
this.init();
});
init() {
this.fetchCurrentUserData();
return this;
}
}

View File

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

View File

@ -1,12 +1,25 @@
import {configure} from "mobx";
import {RootStore} from "../store/RootStore.tsx";
import {RootStore} from "../store/RootStore";
import {RouterService} from "../services/RouterService";
import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
import {fab} from "@fortawesome/free-brands-svg-icons";
import {far} from "@fortawesome/free-regular-svg-icons";
export const initMobX = () => {
const initMobX = () => {
configure({enforceActions: 'observed'});
}
const initFontAwesome = () => {
library.add(fas);
library.add(fab);
library.add(far);
}
export const initApp = () => {
initMobX();
return new RootStore();
initFontAwesome();
let rootStore = new RootStore().init();
RouterService.init(rootStore.routerStore);
return rootStore;
}

View File

@ -2,23 +2,23 @@ import axios, {AxiosRequestConfig} from "axios";
export const apiUrl = "http://localhost:8080/api/v1/";
export const get = async <T extends unknown> (url: string, data?: T) => {
return await request({
export const get = async <T,> (url: string, data?: any) => {
return await request<T>({
url: url,
method: 'GET',
data: data,
}) as T;
});
}
export const post = async <T extends unknown> (url: string, data?: T) => {
return request({
export const post = async <T,> (url: string, data?: any) => {
return await request<T>({
url: url,
method: 'POST',
data: data,
});
}
export const request = async <T extends unknown> (config: AxiosRequestConfig<T>) => {
export const request = async <T,> (config: AxiosRequestConfig<any>) => {
return new Promise<T>((resolve, reject) => {
console.debug(`${config.method} ${config.url} request: ${config.method === 'GET' ? JSON.stringify(config.params) : JSON.stringify(config.data)}`);
axios.request({...config, baseURL: apiUrl}).then((response) => {

View File

@ -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>
);
}

View File

@ -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: [