basic js: store/context, domain models, utils, router

This commit is contained in:
Maksim Skobaro 2024-09-30 02:08:07 +03:00 committed by Velixeor
parent a05e250888
commit 9191e6bf16
44 changed files with 852 additions and 171 deletions

View File

@ -1,5 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build &amp; Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot"> <configuration default="false" name="Build &amp; Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<module name="server" /> <module name="server" />
<option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" /> <option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" />
<method v="2"> <method v="2">

View File

@ -1,5 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clean, Build &amp; Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot"> <configuration default="false" name="Clean, Build &amp; Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<module name="server" /> <module name="server" />
<option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" /> <option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" />
<method v="2"> <method v="2">

View File

@ -1,5 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot"> <configuration default="false" name="Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<module name="server" /> <module name="server" />
<option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" /> <option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" />
<method v="2" /> <method v="2" />

6
mvnw.cmd vendored
View File

@ -32,7 +32,7 @@
@SET __MVNW_ERROR__= @SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% @SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath= @SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-PageContent -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
) )
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% @SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@ -51,7 +51,7 @@ if ($env:MVNW_VERBOSE -eq "true") {
} }
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl $distributionUrl = (Get-PageContent -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) { if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
} }
@ -121,7 +121,7 @@ if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file # If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum $distributionSha256Sum = (Get-PageContent -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) { if ($distributionSha256Sum) {
if ($USE_MVND) { if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."

12
pom.xml
View File

@ -3,6 +3,12 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<properties>
<name>TDMS</name>
<description>Thesis-Defense-Management-System</description>
<version>0.0.1</version>
</properties>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
@ -12,11 +18,11 @@
<groupId>ru.tubryansk</groupId> <groupId>ru.tubryansk</groupId>
<artifactId>tdms</artifactId> <artifactId>tdms</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>TDMS</name> <name>${name}</name>
<description>Thesis-Defense-Management-System</description> <description>${description}</description>
<modules> <modules>
<module>web</module> <module>web</module>

View File

@ -7,12 +7,12 @@
<parent> <parent>
<groupId>ru.tubryansk</groupId> <groupId>ru.tubryansk</groupId>
<artifactId>tdms</artifactId> <artifactId>tdms</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<groupId>ru.tubryansk.tdms</groupId> <groupId>ru.tubryansk.tdms</groupId>
<artifactId>server</artifactId> <artifactId>server</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
<name>TDMS :: SERVER</name> <name>TDMS :: SERVER</name>

View File

@ -1,15 +1,18 @@
package ru.tubryansk.tdms; package ru.tubryansk.tdms;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication @SpringBootApplication
@Slf4j
public class TdmsApplication { public class TdmsApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(TdmsApplication.class, args); ConfigurableApplicationContext context = SpringApplication.run(TdmsApplication.class, args);
String staticLocation = context.getEnvironment().getProperty("spring.web.resources.static-locations");
log.info("Static location: {}", staticLocation);
} }
} }

View File

@ -3,6 +3,7 @@ package ru.tubryansk.tdms.config;
import jakarta.servlet.http.HttpSessionEvent; import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener; import jakarta.servlet.http.HttpSessionListener;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
@ -18,12 +19,12 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
@ -31,19 +32,32 @@ import static org.springframework.security.web.context.HttpSessionSecurityContex
@Configuration @Configuration
public class SecurityConfiguration { public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager) throws Exception {
return httpSecurity return httpSecurity
.authorizeHttpRequests(this::configureHttpAuthorization) .authorizeHttpRequests(this::configureHttpAuthorization)
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable) .cors(a -> a.configurationSource(corsConfiguration()))
.authenticationManager(authenticationManager()) .authenticationManager(authenticationManager)
.sessionManagement(this::configureSessionManagement) .sessionManagement(this::configureSessionManagement)
.build(); .build();
} }
@Bean
public CorsConfigurationSource corsConfiguration() {
return request -> {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.applyPermitDefaultValues();
corsConfiguration.addAllowedMethod("DELETE");
corsConfiguration.addAllowedMethod("PUT");
corsConfiguration.addAllowedMethod("PATCH");
return corsConfiguration;
};
}
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) { private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
/* API ROUTES */ /* API ROUTES */
httpAuthorization.requestMatchers("/api/diploma-topic/**").permitAll(); httpAuthorization.requestMatchers("/api/v1/diploma-topic/**").permitAll();
httpAuthorization.requestMatchers("/api/v1/user/**").permitAll();
httpAuthorization.requestMatchers("/api/**").denyAll(); httpAuthorization.requestMatchers("/api/**").denyAll();
/* STATIC ROUTES */ /* STATIC ROUTES */
httpAuthorization.requestMatchers("/**").permitAll(); httpAuthorization.requestMatchers("/**").permitAll();
@ -52,38 +66,29 @@ public class SecurityConfiguration {
} }
@Bean @Bean
public AuthenticationManager authenticationManager() { public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
return new ProviderManager(authenticationProvider(userDetailsService));
return new ProviderManager(authenticationProvider());
} }
private AuthenticationProvider authenticationProvider() { private AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder()); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder());
provider.setUserDetailsService(userDetailsService()); provider.setUserDetailsService(userDetailsService);
return provider; return provider;
} }
private UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.builder()
.username("admin")
.password("{noop}admin")
.authorities("ROLE_STUDENT", "ROLE_TEACHER", "ROLE_CURATOR")
.build());
}
private PasswordEncoder passwordEncoder() { private PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); return PasswordEncoderFactories.createDelegatingPasswordEncoder();
} }
@Bean @Bean
// todo: remove when login/logout is implemented public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) {
public HttpSessionListener autoAuthenticateUnderAdmin() {
return new HttpSessionListener() { return new HttpSessionListener() {
@Override @Override
public void sessionCreated(HttpSessionEvent se) { public void sessionCreated(HttpSessionEvent se) {
LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as izrailev_v_ya_1", se.getSession().getId());
SecurityContext context = SecurityContextHolder.createEmptyContext(); SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("admin", "admin"); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("izrailev_v_ya_1", "1");
Authentication authenticated = authenticationManager().authenticate(authentication); Authentication authenticated = authenticationManager.authenticate(authentication);
context.setAuthentication(authenticated); context.setAuthentication(authenticated);
SecurityContextHolder.setContext(context); SecurityContextHolder.setContext(context);
se.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); se.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);

View File

@ -15,9 +15,9 @@ import java.util.List;
@RestController @RestController
@RequestMapping("/api/diploma-topic/") @RequestMapping("/api/v1/diploma-topic/")
@Validated @Validated
public class DiplomaTopicRestController { public class DiplomaTopicController {
@Autowired @Autowired
private DiplomaTopicService diplomaTopicService; private DiplomaTopicService diplomaTopicService;

View File

@ -0,0 +1,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 ru.tubryansk.tdms.dto.UserDTO;
import ru.tubryansk.tdms.entity.User;
import ru.tubryansk.tdms.service.UserService;
@RestController
@Validated
@RequestMapping("/api/v1/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/current")
public UserDTO getCurrentUser() {
User principal = userService.getCallerPrincipal();
return principal != null ? UserDTO.from(principal, true) : UserDTO.fromUnauthenticated();
}
@PostMapping("/logout")
public void logout() {
userService.logout();
}
}

View File

@ -1,26 +1,43 @@
package ru.tubryansk.tdms.dto; package ru.tubryansk.tdms.dto;
import jakarta.persistence.Column; import lombok.Builder;
import lombok.AllArgsConstructor; import ru.tubryansk.tdms.entity.Role;
import lombok.Data; import ru.tubryansk.tdms.entity.User;
import lombok.NoArgsConstructor;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
@Data @Builder
@AllArgsConstructor public record UserDTO(
@NoArgsConstructor boolean authenticated,
public class UserDTO { String login,
private Integer id; String password,
private String login; String fullName,
private String password; String email,
private String fullName; String phoneNumber,
private String mail; ZonedDateTime createdAt,
private String numberPhone; ZonedDateTime updatedAt,
private ZonedDateTime createAt; List<String> authorities) {
private ZonedDateTime updateAt;
List<Integer> roleId; public static UserDTO fromUnauthenticated() {
return UserDTO.builder()
.authenticated(false)
.build();
}
public static UserDTO from(User user, boolean anonymize) {
return UserDTO.builder()
.authenticated(true)
.login(user.getLogin())
.password(anonymize ? "" : user.getPassword())
.fullName(user.getFullName())
.email(user.getMail())
.phoneNumber(user.getNumberPhone())
.createdAt(user.getCreateAt())
.updatedAt(user.getUpdateAt())
.authorities(user.getRoles().stream().map(Role::getAuthority).toList())
.build();
}
} }

View File

@ -6,6 +6,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
@Entity @Entity
@ -14,11 +15,16 @@ import lombok.Setter;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table(name = "role", schema = "vkr") @Table(name = "role", schema = "vkr")
public class Role { public class Role implements GrantedAuthority {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; private Integer id;
@Column(name = "name", nullable = false) @Column(name = "name", nullable = false)
private String name; private String name;
@Override
public String getAuthority() {
return name;
}
} }

View File

@ -6,8 +6,12 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
@ -17,7 +21,7 @@ import java.util.List;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table(name = "user", schema = "vkr") @Table(name = "user", schema = "vkr")
public class User { public class User implements UserDetails {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@ -36,10 +40,19 @@ public class User {
private ZonedDateTime createAt; private ZonedDateTime createAt;
@Column(name = "update_at") @Column(name = "update_at")
private ZonedDateTime updateAt; private ZonedDateTime updateAt;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_role",schema = "vkr", @JoinTable(name = "user_role",schema = "vkr",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> userLoyaltyLevels; private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(Role::getAuthority).map(SimpleGrantedAuthority::new).toList();
}
@Override
public String getUsername() {
return login;
}
} }

View File

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

View File

@ -0,0 +1,53 @@
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;
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;
@Service
@Transactional
@Slf4j
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);
}
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findUserByLogin(username)
.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);
}
}

View File

@ -0,0 +1,53 @@
{
"properties": [
{
"name": "db.url",
"type": "java.lang.String",
"description": "Database url."
},
{
"name": "db.user",
"type": "java.lang.String",
"description": "Database user."
},
{
"name": "db.password",
"type": "java.lang.String",
"description": "Database password."
},
{
"name": "db.schema",
"type": "java.lang.String",
"description": "Database schema."
},
{
"name": "application.name",
"type": "java.lang.String",
"description": "Service name."
},
{
"name": "application.version",
"type": "java.lang.String",
"description": "Service version."
},
{
"name": "application.type",
"type": "java.lang.String",
"description": "Service build type."
},
{
"name": "application.domain",
"type": "java.lang.String",
"description": "Service domain."
},
{
"name": "application.port",
"type": "java.lang.Integer",
"description": "Service port."
},
{
"name": "application.protocol",
"type": "java.lang.String",
"description": "Service protocol."
}
] }

View File

@ -0,0 +1,17 @@
application:
type: development
port: 8080
domain: localhost
protocol: http
spring:
flyway:
out-of-order: true
web:
resources:
static-locations: file://${user.dir}/web/dist/
chain:
cache: false
compressed: false
server:
compression:
enabled: false

View File

@ -1,23 +1,36 @@
db:
url: jdbc:postgresql://localhost:5432/db
user: root
password: root
schema: vkr
application:
name: @name@
version: @version@
type: production
port: 80
domain: tdms.tu-bryansk.ru
protocol: https
spring: spring:
application: application:
name: tdms name: tdms
datasource: datasource:
url: jdbc:postgresql://localhost:5432/db url: ${db.url}
username: root username: ${db.user}
password: root password: ${db.password}
driver-class-name: org.postgresql.Driver hikari:
schema: ${db.schema}
jpa: jpa:
open-in-view: false open-in-view: false
show-sql: true
hibernate.ddl-auto: validate hibernate.ddl-auto: validate
database: postgresql
properties.hibernate:
pretty_print: true
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway: flyway:
user: root url: ${db.url}
password: root user: ${db.user}
schemas: vkr password: ${db.password}
schemas: ${db.schema}
locations: db.migration locations: db.migration
url: jdbc:postgresql://localhost:5432/db server:
port: ${application.port}
address: ${application.domain}
compression:
enabled: true

View File

@ -0,0 +1 @@
UPDATE vkr.user SET password = '{noop}1';

View File

@ -7,6 +7,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/Application.tsx"></script>
</body> </body>
</html> </html>

245
web/package-lock.json generated
View File

@ -9,9 +9,11 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-buddy/ide-toolbox": "^2.4.0", "@react-buddy/ide-toolbox": "^2.4.0",
"axios": "^1.7.7",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"mobx": "^6.13.1", "mobx": "^6.13.1",
"mobx-react": "^9.1.1", "mobx-react": "^9.1.1",
"mobx-state-router": "^6.0.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
@ -104,12 +106,12 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.25.0", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
"integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.25.0", "@babel/types": "^7.25.6",
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1" "jsesc": "^2.5.1"
@ -252,12 +254,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.25.3", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
"integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.25.2" "@babel/types": "^7.25.6"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -322,16 +324,16 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.25.3", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz",
"integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.24.7", "@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.25.0", "@babel/generator": "^7.25.6",
"@babel/parser": "^7.25.3", "@babel/parser": "^7.25.6",
"@babel/template": "^7.25.0", "@babel/template": "^7.25.0",
"@babel/types": "^7.25.2", "@babel/types": "^7.25.6",
"debug": "^4.3.1", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
}, },
@ -340,9 +342,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.25.2", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
"integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.24.8", "@babel/helper-string-parser": "^7.24.8",
@ -985,6 +987,14 @@
"react": "^17.0.0 || ^18.0.0" "react": "^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/@react-force/utils": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@react-force/utils/-/utils-2.3.0.tgz",
"integrity": "sha512-HdGqlVbNifHN0DL0kybtfuz3oXKk5B8nTkiZwMtMZGrYPOjw0WmHSLT0ptlyXkPUzIrJoRnazL//L3jHZghaCQ==",
"dependencies": {
"tslib": "^2.0.0"
}
},
"node_modules/@restart/hooks": { "node_modules/@restart/hooks": {
"version": "0.4.16", "version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
@ -1606,6 +1616,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1746,6 +1771,17 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true "dev": true
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1781,7 +1817,6 @@
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dev": true,
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -1794,12 +1829,28 @@
} }
} }
}, },
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true "dev": true
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -2264,6 +2315,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -2300,6 +2359,38 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true "dev": true
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2428,6 +2519,19 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@ -2676,6 +2780,25 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@ -2748,11 +2871,30 @@
} }
} }
}, },
"node_modules/mobx-state-router": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mobx-state-router/-/mobx-state-router-6.0.1.tgz",
"integrity": "sha512-tb2FCzb4Owxh6MQ+FkFC6ntJfM0TcOy2UG/3YBcRm/6hLhr42XG+ijOoQydf9C66DY7ihYVzf8LhSHvr8ie4fA==",
"dependencies": {
"@react-force/utils": "^2.3.0",
"debug": "^4.3.3",
"history": "^4.10.1",
"path-to-regexp": "^6.2.0",
"query-string": "^7.0.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"mobx": ">=6",
"mobx-react-lite": ">=3",
"react": ">=16"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
@ -2887,6 +3029,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
},
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@ -2973,6 +3120,11 @@
"react": ">=0.14.0" "react": ">=0.14.0"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -2982,6 +3134,23 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -3102,6 +3271,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@ -3245,6 +3419,22 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"engines": {
"node": ">=6"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@ -3287,6 +3477,16 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true "dev": true
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/to-fast-properties": { "node_modules/to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@ -3423,6 +3623,11 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.3.5", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",

View File

@ -10,13 +10,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@react-buddy/ide-toolbox": "^2.4.0",
"axios": "^1.7.7",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"mobx": "^6.13.1", "mobx": "^6.13.1",
"mobx-react": "^9.1.1", "mobx-react": "^9.1.1",
"mobx-state-router": "^6.0.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1", "react-dom": "^18.3.1"
"@react-buddy/ide-toolbox": "^2.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.3",

View File

@ -6,12 +6,12 @@
<parent> <parent>
<groupId>ru.tubryansk</groupId> <groupId>ru.tubryansk</groupId>
<artifactId>tdms</artifactId> <artifactId>tdms</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
</parent> </parent>
<groupId>ru.tubryansk.tdms</groupId> <groupId>ru.tubryansk.tdms</groupId>
<artifactId>web</artifactId> <artifactId>web</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>TDMS :: WEB</name> <name>TDMS :: WEB</name>

20
web/src/Application.tsx Normal file
View File

@ -0,0 +1,20 @@
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";
const rootStore = initApp();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RootStoreContext.Provider value={rootStore}>
<RouterContext.Provider value={rootStore.routerStore}>
<RouterView viewMap={MyRouterStore.makeViewMap()} />
</RouterContext.Provider>
</RootStoreContext.Provider>
</React.StrictMode>
);

View File

@ -1,60 +0,0 @@
import {GitHubLogo} from './Svgs.tsx';
import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
const Header = () =>
<header>
<Navbar className="bg-body-tertiary" fixed="top">
<Container>
<Navbar.Brand className="" href="#">TDMS</Navbar.Brand>
<Nav>
<NavDropdown title="Группы">
<NavDropdown.Item href="#">Список</NavDropdown.Item>
<NavDropdown.Item href="#">Редактировать</NavDropdown.Item>
</NavDropdown>
</Nav>
<Nav className="ms-auto">
<Navbar.Text>Пользователь:</Navbar.Text>
<NavDropdown title="Фамилия И. О." id="basic-nav-dropdown">
<NavDropdown.Item href="#">Моя страница</NavDropdown.Item>
<NavDropdown.Divider/>
<NavDropdown.Item href="#">Выйти</NavDropdown.Item>
</NavDropdown>
</Nav>
</Container>
</Navbar>
</header>
const Footer = () => {
return (
<footer>
<Navbar className="bg-body-tertiary">
<Container>
<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}/>
</Nav.Link>
</Nav>
</Container>
</Navbar>
</footer>
)
}
const Content = () =>
<article>
<div className="text-center mt-5">There you can see some dashboards in future :)</div>
</article>
function MainPage() {
return (
<>
<Header/>
<Content/>
<Footer/>
</>
)
}
export default MainPage;

View File

@ -0,0 +1,19 @@
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

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

View File

@ -0,0 +1,21 @@
import {Container, Nav, Navbar} from "react-bootstrap";
import {GitHubLogo} from "../../utils/svg.tsx";
const Footer = () => {
return (
<footer>
<Navbar className="bg-body-tertiary">
<Container>
<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}/>
</Nav.Link>
</Nav>
</Container>
</Navbar>
</footer>
)
}
export default Footer;

View File

@ -0,0 +1,49 @@
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

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

View File

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

View File

@ -1,5 +1,4 @@
/* Reset css */ /* Reset css */
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -1,12 +0,0 @@
import React from "react";
import ReactDOM from 'react-dom/client'
import MainPage from './components/MainPage.tsx'
import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MainPage/>
</React.StrictMode>,
)

13
web/src/models/user.ts Normal file
View File

@ -0,0 +1,13 @@
export interface IAuthenticated {
authenticated: true,
login: string,
password: string,
fullName: string,
email: string,
phone: string,
createdAt: string,
updatedAt: string,
authorities: string[],
}
export declare type IUser = {authenticated: false} | IAuthenticated;

24
web/src/routes.tsx Normal file
View File

@ -0,0 +1,24 @@
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,3 @@
export default class UserService {
}

View File

@ -0,0 +1,30 @@
import {browserHistory, createRouterState, HistoryAdapter, RouterStore} from "mobx-state-router";
import {routes} from "../routes.tsx";
import {RootStore} from "./RootStore.tsx";
export class MyRouterStore extends RouterStore {
constructor(rootStore: RootStore) {
super(MyRouterStore.makeRoutesMap(),
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();
}
}

View File

@ -0,0 +1,31 @@
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

@ -0,0 +1,30 @@
import {get, post} from "../utils/request.tsx";
import {makeObservable, observable, runInAction} from "mobx";
import {RootStore} from "./RootStore.ts";
import {IUser} from "../models/user.ts";
export class UserStore {
rootStore: RootStore;
user: IUser = {authenticated: false};
constructor(rootStore: RootStore) {
makeObservable(this, {
user: observable,
});
this.rootStore = rootStore;
}
init() {
get<IUser>('/user/current').then((response) => {
runInAction(() => {
this.user = response;
});
});
}
logout() {
post('/user/logout').then(() => {
this.init();
});
}
}

12
web/src/utils/init.ts Normal file
View File

@ -0,0 +1,12 @@
import {configure} from "mobx";
import {RootStore} from "../store/RootStore.tsx";
export const initMobX = () => {
configure({enforceActions: 'observed'});
}
export const initApp = () => {
initMobX();
return new RootStore();
}

32
web/src/utils/request.tsx Normal file
View File

@ -0,0 +1,32 @@
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({
url: url,
method: 'GET',
data: data,
}) as T;
}
export const post = async <T extends unknown> (url: string, data?: T) => {
return request({
url: url,
method: 'POST',
data: data,
});
}
export const request = async <T extends unknown> (config: AxiosRequestConfig<T>) => {
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) => {
console.debug(`${config.method} ${config.url} response: ${JSON.stringify(response.data)}`);
resolve(response.data);
}).catch((error) => {
console.error(`${config.method} ${config.url} error: ${error}`);
reject(error);
});
});
}

View File

@ -22,7 +22,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
}, },
"include": ["src"] "include": ["src"]
} }

View File

@ -2,5 +2,14 @@ import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
export default defineConfig({ export default defineConfig({
plugins: [react()], esbuild: {
tsconfigRaw: {
compilerOptions: {
experimentalDecorators: true,
},
},
},
plugins: [
react()
],
}) })