Compare commits

...

3 Commits

Author SHA1 Message Date
Maksim Skobaro
c2d19a7724 improved, more featured, fixed
Exceptions and Errors are better
Files structure is better
New ComponentContext.ts
New DataTable.tsx tables.ts
Massive components refactoring
New Group.java
New LoggingRequestFilter.java LoggingSessionListener.java
New NotificationStore.ts SysInfoStore.ts
New reactiveValue.ts ReactiveControls.tsx
New dependencies
And much more
2025-02-07 07:05:15 +03:00
Maksim Skobaro
96ffb3ad41 implement login/quit, PendingStore.ts 2025-02-02 09:44:29 +03:00
Maksim Skobaro
f80db06adf refactoring 2025-02-02 09:43:34 +03:00
119 changed files with 2590 additions and 1276 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
/logs/app.log

View File

@ -1,19 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip

View File

@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build &amp; Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<module name="server" />
<option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" />
<method v="2">
<option name="Maven.BeforeRunTask" enabled="true" file="$PROJECT_DIR$/pom.xml" goal="package -Dmaven.test.skip=true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Start RDBMS" run_configuration_type="docker-deploy" />
</method>
</configuration>
</component>

View File

@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Clean, Build &amp; Run" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="dev" />
<module name="server" />
<option name="SPRING_BOOT_MAIN_CLASS" value="ru.tubryansk.tdms.TdmsApplication" />
<method v="2">
<option name="Maven.BeforeRunTask" enabled="true" file="$PROJECT_DIR$/pom.xml" goal="clean package -Dmaven.test.skip=true" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Start RDBMS" run_configuration_type="docker-deploy" />
</method>
</configuration>
</component>

View File

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

View File

@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start RDBMS" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="envFilePath" value="" />
<option name="sourceFilePath" value="server/docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@ -2,8 +2,8 @@ services:
db: db:
image: postgres:16.2-alpine3.19 image: postgres:16.2-alpine3.19
environment: environment:
- "POSTGRES_DB=db" - "POSTGRES_DB=tdms"
- "POSTGRES_PASSWORD=root" - "POSTGRES_PASSWORD=root"
- "POSTGRES_USER=root" - "POSTGRES_USER=root"
ports: ports:
- "5432:5432" - "5400:5432"

259
mvnw vendored
View File

@ -1,259 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

149
mvnw.cmd vendored
View File

@ -1,149 +0,0 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@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)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-PageContent -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-PageContent -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

3
server/.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
/logs/app.log

View File

@ -1,29 +1,22 @@
package ru.tubryansk.tdms.config; package ru.tubryansk.tdms.config;
import jakarta.servlet.http.HttpSessionEvent; import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.http.HttpSessionListener;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@ -34,104 +27,85 @@ import org.springframework.web.cors.CorsConfigurationSource;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
@Configuration @Configuration
@Slf4j
public class SecurityConfiguration { public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, public SecurityFilterChain securityFilterChain(
AuthenticationManager authenticationManager, HttpSecurity httpSecurity,
@Qualifier("corsConfig") CorsConfigurationSource cors) throws Exception { AuthenticationManager authenticationManager,
@Qualifier("corsConfig") CorsConfigurationSource cors
) throws Exception {
return httpSecurity return httpSecurity
.authorizeHttpRequests(this::configureHttpAuthorization) .authorizeHttpRequests(this::configureHttpAuthorization)
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) /* todo: настроить csrf */
.cors(a -> a.configurationSource(cors)) .cors(a -> a.configurationSource(cors))
.authenticationManager(authenticationManager) .authenticationManager(authenticationManager)
.sessionManagement(this::configureSessionManagement) .sessionManagement(cfg -> {
.build(); cfg.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
cfg.maximumSessions(1);
})
.build();
} }
@Bean @Bean
@Profile("dev")
@Qualifier("corsConfig") @Qualifier("corsConfig")
public CorsConfigurationSource corsConfigurationDev() { public CorsConfigurationSource corsConfigurationProd(
return request -> { @Value("${application.domain}") String domain,
CorsConfiguration corsConfiguration = new CorsConfiguration(); @Value("${application.port}") String port,
corsConfiguration.applyPermitDefaultValues(); @Value("${application.protocol}") String protocol,
corsConfiguration.addAllowedMethod("DELETE"); Environment environment
corsConfiguration.addAllowedMethod("PUT"); ) {
corsConfiguration.addAllowedMethod("PATCH");
return corsConfiguration;
};
}
@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 -> { return request -> {
String url = StringUtils.join(protocol, "://", domain, ":", port); String url = StringUtils.join(protocol, "://", domain, ":", port);
CorsConfiguration corsConfiguration = new CorsConfiguration(); CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setMaxAge(Duration.ofHours(1)); corsConfiguration.setMaxAge(Duration.ofDays(1));
corsConfiguration.addAllowedOrigin(url); corsConfiguration.addAllowedOrigin(url);
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name())); if (environment.matchesProfiles("dev")) {
// corsConfiguration.setAllowedHeaders(); corsConfiguration.addAllowedOrigin("http://localhost:8081");
}
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.OPTIONS.name()));
corsConfiguration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
corsConfiguration.setAllowCredentials(true);
return corsConfiguration; return corsConfiguration;
}; };
} }
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
/* API ROUTES */
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();
}
@Bean @Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) { public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
return new ProviderManager(authenticationProvider(userDetailsService)); return new ProviderManager(authenticationProvider(userDetailsService));
} }
private AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder());
provider.setUserDetailsService(userDetailsService);
return provider;
}
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); return PasswordEncoderFactories.createDelegatingPasswordEncoder();
} }
@Bean private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
@Profile("dev") /* SysInfoController */
public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) { httpAuthorization.requestMatchers("/api/v1/sysinfo/**").permitAll();
return new HttpSessionListener() { /* UserController */
@Override httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
public void sessionCreated(HttpSessionEvent se) { httpAuthorization.requestMatchers("/api/v1/user/login").anonymous();
String username = "akulenko_mikhail"; httpAuthorization.requestMatchers("/api/v1/user/current").permitAll();
LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as {}", se.getSession().getId(), username); httpAuthorization.requestMatchers("/api/v1/user/get-all").hasAuthority("ROLE_ADMINISTRATOR");
SecurityContext context = SecurityContextHolder.createEmptyContext(); httpAuthorization.requestMatchers("/api/v1/user/register").hasAuthority("ROLE_ADMINISTRATOR");
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, "1"); httpAuthorization.requestMatchers("/api/v1/user/validate-registration").hasAuthority("ROLE_ADMINISTRATOR");
Authentication authenticated = authenticationManager.authenticate(authentication); /* StudentController */
context.setAuthentication(authenticated); httpAuthorization.requestMatchers("/api/v1/student/current").permitAll();
SecurityContextHolder.setContext(context); /* GroupController */
se.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); httpAuthorization.requestMatchers("/api/v1/group/get-all").permitAll();
} /* deny all other api requests */
}; httpAuthorization.requestMatchers("/api/**").denyAll();
/* since api already blocked, all other requests are static resources */
httpAuthorization.requestMatchers("/**").permitAll();
} }
// todo: remove when login/logout is implemented, since we do not need automatically created session with no authentication private AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) {
private void configureSessionManagement(SessionManagementConfigurer<HttpSecurity> sessionManagement) { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(passwordEncoder());
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS); provider.setUserDetailsService(userDetailsService);
return provider;
} }
} }

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.RestController;
import ru.tubryansk.tdms.controller.payload.GroupDTO;
import ru.tubryansk.tdms.service.GroupService;
import java.util.Collection;
@RestController("/api/v1/group/")
public class GroupController {
@Autowired
private GroupService groupService;
@GetMapping("/get-all-groups")
public Collection<GroupDTO> getAllGroups() {
return groupService.getAllGroups();
}
}

View File

@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import ru.tubryansk.tdms.dto.StudentDTO; import ru.tubryansk.tdms.controller.payload.StudentDTO;
import ru.tubryansk.tdms.service.StudentService; import ru.tubryansk.tdms.service.StudentService;
@RestController @RestController
@ -15,6 +15,6 @@ public class StudentController {
@GetMapping("/current") @GetMapping("/current")
public StudentDTO getCurrentStudent() { public StudentDTO getCurrentStudent() {
return studentService.getCallerStudent().map(StudentDTO::from).orElse(null); return studentService.getCallerStudentDTO();
} }
} }

View File

@ -0,0 +1,21 @@
package ru.tubryansk.tdms.controller;
import lombok.SneakyThrows;
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.service.SysInfoService;
@RestController
@RequestMapping("/api/v1/sysinfo")
public class SysInfoController {
@Autowired
private SysInfoService sysInfoService;
@SneakyThrows
@GetMapping("/version")
public String getVersion() {
return sysInfoService.getVersion();
}
}

View File

@ -1,12 +1,18 @@
package ru.tubryansk.tdms.controller; package ru.tubryansk.tdms.controller;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ru.tubryansk.tdms.dto.UserDTO; import ru.tubryansk.tdms.controller.payload.LoginDTO;
import ru.tubryansk.tdms.controller.payload.RegistrationDTO;
import ru.tubryansk.tdms.controller.payload.UserDTO;
import ru.tubryansk.tdms.service.AuthenticationService; import ru.tubryansk.tdms.service.AuthenticationService;
import ru.tubryansk.tdms.service.CallerService;
import ru.tubryansk.tdms.service.UserService; import ru.tubryansk.tdms.service.UserService;
import java.util.List;
@RestController @RestController
@RequestMapping("/api/v1/user") @RequestMapping("/api/v1/user")
@Slf4j @Slf4j
@ -14,11 +20,13 @@ public class UserController {
@Autowired @Autowired
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
@Autowired @Autowired
private CallerService callerService;
@Autowired
private UserService userService; private UserService userService;
@GetMapping("/current") @GetMapping("/current")
public UserDTO getCurrentUser() { public UserDTO getCurrentUser() {
return userService.getCallerUser().map(user -> UserDTO.from(user, true)).orElse(UserDTO.unauthenticated()); return callerService.getCallerUserDTO();
} }
@PostMapping("/logout") @PostMapping("/logout")
@ -27,7 +35,17 @@ public class UserController {
} }
@PostMapping("/login") @PostMapping("/login")
public void login(@RequestParam String username, @RequestParam String password) { public void login(@RequestBody @Valid LoginDTO loginDTO) {
authenticationService.login(username, password); authenticationService.login(loginDTO.getUsername(), loginDTO.getPassword());
}
@PostMapping("/register")
public void post(@RequestBody @Valid RegistrationDTO registrationDTO) {
userService.registerUser(registrationDTO);
}
@GetMapping("/get-all")
public List<UserDTO> getAllUsers() {
return userService.getAllUsers();
} }
} }

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.dto; package ru.tubryansk.tdms.controller.payload;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -8,11 +8,11 @@ public record ErrorResponse(String message, ErrorCode errorCode) {
@RequiredArgsConstructor @RequiredArgsConstructor
@Getter @Getter
public enum ErrorCode { public enum ErrorCode {
BAD_REQUEST(HttpStatus.BAD_REQUEST), BUSINESS_ERROR(HttpStatus.BAD_REQUEST),
VALIDATION_ERROR(HttpStatus.BAD_REQUEST), VALIDATION_ERROR(HttpStatus.BAD_REQUEST),
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
NOT_FOUND(HttpStatus.NOT_FOUND), NOT_FOUND(HttpStatus.NOT_FOUND),
ACCESS_DENIED(HttpStatus.FORBIDDEN) ACCESS_DENIED(HttpStatus.FORBIDDEN),
; ;
private final HttpStatus httpStatus; private final HttpStatus httpStatus;

View File

@ -0,0 +1,14 @@
package ru.tubryansk.tdms.controller.payload;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class GroupDTO {
private String name;
private String principalName;
private Boolean isMePrincipal;
}

View File

@ -0,0 +1,12 @@
package ru.tubryansk.tdms.controller.payload;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
@Getter
public class LoginDTO {
@NotEmpty(message = "Логин не может быть пустым")
private String username;
@NotEmpty(message = "Пароль не может быть пустым")
private String password;
}

View File

@ -0,0 +1,34 @@
package ru.tubryansk.tdms.controller.payload;
import jakarta.validation.constraints.*;
import lombok.Getter;
import org.hibernate.validator.constraints.Length;
@Getter
public class RegistrationDTO {
@NotEmpty(message = "Логин не может быть пустым")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Логин должен содержать только латинские буквы, цифры и знак подчеркивания")
@Size(min = 5, message = "Логин должен содержать минимум 5 символов")
@Size(max = 32, message = "Логин должен содержать максимум 32 символов")
private String login;
@NotEmpty(message = "Пароль не может быть пустым")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$", message = "Пароль должен содержать хотя бы одну цифру, одну заглавную и одну строчную букву, минимум 8 символов")
private String password;
@NotEmpty(message = "Имя не может быть пустым")
@Length(min = 3, message = "Имя должно содержать минимум 3 символа")
@Pattern(regexp = "^[a-zA-Zа-яА-ЯёЁ\\s]+$", message = "Имя должно содержать только буквы английского или русского алфавита и пробелы")
private String fullName;
@NotNull(message = "Почта не может быть пустой")
@Email(message = "Почта должна быть валидным адресом электронной почты")
private String email;
@NotNull(message = "Номер телефона не может быть пустым")
@Pattern(regexp = "^\\+[1-9]\\d{6,14}$", message = "Номер телефона должен начинаться с '+' и содержать от 7 до 15 цифр")
private String numberPhone;
private StudentRegistrationDTO studentData;
@Getter
public static class StudentRegistrationDTO {
@NotNull(message = "Группа не может быть пустой")
private Long groupId;
}
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.dto; package ru.tubryansk.tdms.controller.payload;
import ru.tubryansk.tdms.entity.Role; import ru.tubryansk.tdms.entity.Role;

View File

@ -0,0 +1,50 @@
package ru.tubryansk.tdms.controller.payload;
import lombok.Data;
import ru.tubryansk.tdms.entity.Student;
@Data
public class StudentDTO {
// private Boolean form;
// private Integer protectionOrder;
// private String magistracy;
// private Boolean digitalFormatPresent;
// private Integer markComment;
// private Integer markPractice;
// private String predefenceComment;
// private String normalControl;
// private Integer antiPlagiarism;
// private String note;
// private Boolean recordBookReturned;
// private String work;
// private UserDTO user;
// private String diplomaTopic;
// private UserDTO mentorUser;
// private GroupDTO group;
public static StudentDTO from(Student student) {
StudentDTO studentDTO = new StudentDTO();
// studentDTO.setForm(student.getForm());
// return 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()),
// student.getDiplomaTopic().getName(),
// UserDTO.from(student.getMentorUser()),
// GroupDTO.from(student.getGroup())
// );
return studentDTO;
}
}

View File

@ -0,0 +1,42 @@
package ru.tubryansk.tdms.controller.payload;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import ru.tubryansk.tdms.entity.User;
import java.time.ZonedDateTime;
import java.util.List;
@Builder
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record UserDTO(
boolean authenticated,
String login,
String fullName,
String email,
String phone,
ZonedDateTime createdAt,
ZonedDateTime updatedAt,
List<RoleDTO> authorities) {
public static UserDTO unauthenticated() {
return UserDTO.builder()
.authenticated(false)
.build();
}
public static UserDTO from(User user) {
return UserDTO.builder()
.authenticated(true)
.login(user.getLogin())
.fullName(user.getFullName())
.email(user.getEmail())
.phone(user.getNumberPhone())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.authorities(RoleDTO.from(user))
.build();
}
}

View File

@ -1,13 +0,0 @@
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,51 +0,0 @@
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 Boolean form;
private Integer protectionOrder;
private String magistracy;
private Boolean digitalFormatPresent;
private Integer markComment;
private Integer markPractice;
private String predefenceComment;
private String normalControl;
private Integer antiPlagiarism;
private String note;
private Boolean recordBookReturned;
private String work;
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,44 +0,0 @@
package ru.tubryansk.tdms.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import ru.tubryansk.tdms.entity.User;
import java.time.ZonedDateTime;
import java.util.List;
@Builder
@JsonInclude(JsonInclude.Include.NON_ABSENT)
public record UserDTO(
boolean authenticated,
String login,
String password,
String fullName,
String email,
String phone,
ZonedDateTime createdAt,
ZonedDateTime updatedAt,
List<RoleDTO> authorities) {
public static UserDTO unauthenticated() {
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())
.phone(user.getNumberPhone())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.authorities(RoleDTO.from(user))
.build();
}
}

View File

@ -2,23 +2,22 @@ package ru.tubryansk.tdms.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
@Entity
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @ToString
@AllArgsConstructor @Entity
@Table(name = "diploma_topic", schema = "vkr") @Table(name = "diploma_topic")
public class DiplomaTopic { public class DiplomaTopic {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)//Макс из-за SEQUENCE прога лежит, если хочешь, можешь менять я не понял что не нравиться при сборке @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; private Long id;
@Column(name = "name", nullable = false) @Column(name = "name")
private String name; private String name;
} }

View File

@ -2,26 +2,24 @@ package ru.tubryansk.tdms.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
@Entity
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @ToString
@AllArgsConstructor @Entity
@Table(name = "group", schema = "vkr") @Table(name = "group")
public class Group { public class Group {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; private Long id;
@Column(name = "name", nullable = false) @Column(name = "name")
private String name; private String name;
@ManyToOne() @ManyToOne
@JoinColumn(name = "principal_user_id", nullable = false) @JoinColumn(name = "curator_user_id")
private User principalUser; private User groupCurator;
} }

View File

@ -1,28 +1,25 @@
package ru.tubryansk.tdms.entity; package ru.tubryansk.tdms.entity;
import jakarta.persistence.*; import jakarta.persistence.Column;
import lombok.AllArgsConstructor; import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.ToString;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@Entity
@Getter @Getter
@Setter @ToString
@NoArgsConstructor @Entity
@AllArgsConstructor @Table(name = "`role`")
@Table(name = "role", schema = "vkr")
public class Role implements GrantedAuthority { public class Role implements GrantedAuthority {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private Integer id; @Column(name = "name")
@Column(name = "name", nullable = false)
private String name; private String name;
@Column(name = "authority", nullable = false) @Column(name = "authority")
private String authority; private String authority;
} }

View File

@ -2,24 +2,26 @@ package ru.tubryansk.tdms.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Data; import lombok.Getter;
import org.springframework.context.annotation.Scope; import lombok.Setter;
import org.springframework.web.context.annotation.SessionScope; import lombok.ToString;
@Data @Getter
@Setter
@ToString
@Entity @Entity
@Table(name = "student", schema = "vkr") @Table(name = "student")
public class Student { public class Student {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; private Long id;
@Column(name = "form") @Column(name = "form")
private Boolean form; private Boolean form;
@Column(name = "protection_order" , nullable = false) @Column(name = "protection_order")
private Integer protectionOrder; private Integer protectionOrder;
@Column(name = "magistracy" ) @Column(name = "magistracy")
private String magistracy; private String magistracy;
@Column(name = "digital_format_present") @Column(name = "digital_format_present")
private Boolean digitalFormatPresent; private Boolean digitalFormatPresent;
@ -38,17 +40,17 @@ public class Student {
@Column(name = "record_book_returned") @Column(name = "record_book_returned")
private Boolean recordBookReturned; private Boolean recordBookReturned;
@Column(name = "work") @Column(name = "work")
private String work; private String work;
@OneToOne @OneToOne
@JoinColumn(name = "user_id", nullable = false) @JoinColumn(name = "user_id")
private User user; private User user;
@ManyToOne @ManyToOne
@JoinColumn(name = "diploma_topic_id", nullable = false) @JoinColumn(name = "diploma_topic_id")
private DiplomaTopic diplomaTopic; private DiplomaTopic diplomaTopic;
@ManyToOne @ManyToOne
@JoinColumn(name = "mentor_user_id", nullable = false) @JoinColumn(name = "mentor_user_id")
private User mentorUser; private User mentorUser;
@ManyToOne @ManyToOne
@JoinColumn(name = "group_id", nullable = false) @JoinColumn(name = "group_id")
private Group group; private Group group;
} }

View File

@ -2,10 +2,11 @@ package ru.tubryansk.tdms.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@ -15,40 +16,45 @@ import java.util.Collection;
import java.util.List; import java.util.List;
@Entity
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @ToString
@AllArgsConstructor @Entity
@Table(name = "user", schema = "vkr") @Table(name = "`user`")
public class User implements UserDetails { public class User implements UserDetails {
@Id @Id
@Column(name = "id") @Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; private Long id;
@Column(name = "login", nullable = false, unique = true) @Column(name = "login")
private String login; private String login;
@Column(name = "password", nullable = false) @Column(name = "password")
private String password; private String password;
@Column(name = "full_name", nullable = false) @Column(name = "full_name")
private String fullName; private String fullName;
@Column(name = "mail", nullable = false, unique = true) @Column(name = "email")
private String mail; private String email;
@Column(name = "number_phone", nullable = false, unique = true) @Column(name = "number_phone")
private String numberPhone; private String numberPhone;
@Column(name = "created_at", nullable = false) @Column(name = "created_at")
@CreationTimestamp
private ZonedDateTime createdAt; private ZonedDateTime createdAt;
@Column(name = "updated_at") @Column(name = "updated_at")
@UpdateTimestamp
private ZonedDateTime updatedAt; private ZonedDateTime updatedAt;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @ManyToMany
@JoinTable(name = "user_role",schema = "vkr", @JoinTable(
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), name = "user_role",
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> roles; private List<Role> roles;
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(Role::getAuthority).map(SimpleGrantedAuthority::new).toList(); return roles.stream()
.map(Role::getAuthority)
.map(SimpleGrantedAuthority::new)
.toList();
} }
@Override @Override

View File

@ -0,0 +1,4 @@
package ru.tubryansk.tdms.entity.repository;
public interface DefenceRepository {
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.repository; package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;

View File

@ -0,0 +1,13 @@
package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Group;
import ru.tubryansk.tdms.exception.NotFoundException;
@Repository
public interface GroupRepository extends JpaRepository<Group, Long> {
default Group findByIdThrow(Long id) {
return this.findById(id).orElseThrow(() -> new NotFoundException(Group.class, id));
}
}

View File

@ -0,0 +1,9 @@
package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.tubryansk.tdms.entity.Role;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
}

View File

@ -1,4 +1,4 @@
package ru.tubryansk.tdms.repository; package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;

View File

@ -1,7 +1,6 @@
package ru.tubryansk.tdms.repository; package ru.tubryansk.tdms.entity.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.core.userdetails.UserDetails;
import ru.tubryansk.tdms.entity.User; import ru.tubryansk.tdms.entity.User;
import java.util.Optional; import java.util.Optional;

View File

@ -1,6 +1,6 @@
package ru.tubryansk.tdms.exception; package ru.tubryansk.tdms.exception;
import ru.tubryansk.tdms.dto.ErrorResponse; import ru.tubryansk.tdms.controller.payload.ErrorResponse;
public class AccessDeniedException extends BusinessException { public class AccessDeniedException extends BusinessException {
public AccessDeniedException() { public AccessDeniedException() {

View File

@ -1,6 +1,6 @@
package ru.tubryansk.tdms.exception; package ru.tubryansk.tdms.exception;
import ru.tubryansk.tdms.dto.ErrorResponse; import ru.tubryansk.tdms.controller.payload.ErrorResponse;
public class BusinessException extends RuntimeException { public class BusinessException extends RuntimeException {
public BusinessException(String message) { public BusinessException(String message) {
@ -8,6 +8,6 @@ public class BusinessException extends RuntimeException {
} }
public ErrorResponse.ErrorCode getErrorCode() { public ErrorResponse.ErrorCode getErrorCode() {
return ErrorResponse.ErrorCode.INTERNAL_ERROR; return ErrorResponse.ErrorCode.BUSINESS_ERROR;
} }
} }

View File

@ -2,42 +2,55 @@ package ru.tubryansk.tdms.exception;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component; import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException;
import ru.tubryansk.tdms.dto.ErrorResponse; import ru.tubryansk.tdms.controller.payload.ErrorResponse;
import java.util.stream.Collectors;
@RestControllerAdvice @RestControllerAdvice
@Slf4j @Slf4j
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { public ErrorResponse handleMethodArgumentNotValidException(BindException e) {
// todo: make a better error message log.debug("Validation error: {}", e.getMessage());
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.VALIDATION_ERROR); String validationErrors = e.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return new ErrorResponse(validationErrors, ErrorResponse.ErrorCode.VALIDATION_ERROR);
} }
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) { public ErrorResponse handleBusinessException(BusinessException e, HttpServletResponse response) {
log.info("Business error", e);
response.setStatus(e.getErrorCode().getHttpStatus().value()); response.setStatus(e.getErrorCode().getHttpStatus().value());
return new ErrorResponse(e.getMessage(), e.getErrorCode()); return new ErrorResponse(e.getMessage(), e.getErrorCode());
} }
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorResponse handleAccessDeniedException(AccessDeniedException e) {
log.debug("Access denied", e);
return new ErrorResponse("", ErrorResponse.ErrorCode.ACCESS_DENIED);
}
@ExceptionHandler(NoResourceFoundException.class) @ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) @ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) { public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) {
// todo: make error page log.error("Resource not found", e);
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND); return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.NOT_FOUND);
} }
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleUnexpectedException(Exception e) { public ErrorResponse handleUnexpectedException(Exception e) {
// todo: make error page
log.error("Unexpected exception.", e); log.error("Unexpected exception.", e);
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR); return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR);
} }

View File

@ -1,10 +1,10 @@
package ru.tubryansk.tdms.exception; package ru.tubryansk.tdms.exception;
import ru.tubryansk.tdms.dto.ErrorResponse; import ru.tubryansk.tdms.controller.payload.ErrorResponse;
public class NotFoundException extends BusinessException { public class NotFoundException extends BusinessException {
public NotFoundException(Class<?> entityClass, Integer id) { public NotFoundException(Class<?> entityClass, Object id) {
super(entityClass.getSimpleName() + " with id " + id + " not found"); super(entityClass.getSimpleName() + " с идентификатором " + id + " не наеден");
} }
@Override @Override

View File

@ -2,16 +2,19 @@ package ru.tubryansk.tdms.service;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.tubryansk.tdms.entity.User; import ru.tubryansk.tdms.entity.User;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
@Service @Service
@Slf4j
public class AuthenticationService { public class AuthenticationService {
@Autowired @Autowired
private HttpServletRequest request; private HttpServletRequest request;
@ -30,11 +33,19 @@ public class AuthenticationService {
} }
} }
@Transactional
public void login(String username, String password) { public void login(String username, String password) {
var context = SecurityContextHolder.createEmptyContext(); try {
var token = new UsernamePasswordAuthenticationToken(username, password); var context = SecurityContextHolder.createEmptyContext();
var authenticated = authenticationManager.authenticate(token); var token = new UsernamePasswordAuthenticationToken(username, password);
context.setAuthentication(authenticated); var authenticated = authenticationManager.authenticate(token);
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); context.setAuthentication(authenticated);
SecurityContextHolder.setContext(context);
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
} catch (Exception e) {
log.error("Failed to log in user: {}", username, e);
throw e;
}
log.debug("User {} logged in", username);
} }
} }

View File

@ -0,0 +1,26 @@
package ru.tubryansk.tdms.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import ru.tubryansk.tdms.controller.payload.UserDTO;
import ru.tubryansk.tdms.entity.User;
import java.util.Optional;
@Service
public class CallerService {
@Autowired
private AuthenticationService authenticationService;
public Optional<User> getCallerUser() {
if(authenticationService.authenticated()) {
return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
return Optional.empty();
}
public UserDTO getCallerUserDTO() {
return getCallerUser().map(UserDTO::from).orElse(UserDTO.unauthenticated());
}
}

View File

@ -0,0 +1,15 @@
package ru.tubryansk.tdms.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.tubryansk.tdms.controller.payload.GroupDTO;
import java.util.Collection;
@Service
@Transactional
public class GroupService {
public Collection<GroupDTO> getAllGroups() {
return null;
}
}

View File

@ -0,0 +1,38 @@
package ru.tubryansk.tdms.service;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.tubryansk.tdms.entity.Role;
import ru.tubryansk.tdms.entity.repository.RoleRepository;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RoleService {
public enum Authority {
ROLE_ADMINISTRATOR,
ROLE_COMMISSION_MEMBER,
ROLE_TEACHER,
ROLE_SECRETARY,
ROLE_STUDENT,
}
public transient Map<String, Role> roles;
@Autowired
private RoleRepository roleRepository;
@PostConstruct
@Transactional
public void init() {
roles = new ConcurrentHashMap<>();
roleRepository.findAll().forEach(role -> roles.put(role.getAuthority(), role));
}
public Role getRoleByAuthority(Authority authority) {
return roles.get(authority.name());
}
}

View File

@ -3,11 +3,12 @@ package ru.tubryansk.tdms.service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.tubryansk.tdms.controller.payload.StudentDTO;
import ru.tubryansk.tdms.entity.DiplomaTopic; import ru.tubryansk.tdms.entity.DiplomaTopic;
import ru.tubryansk.tdms.entity.Student; import ru.tubryansk.tdms.entity.Student;
import ru.tubryansk.tdms.entity.repository.DiplomaTopicRepository;
import ru.tubryansk.tdms.entity.repository.StudentRepository;
import ru.tubryansk.tdms.exception.AccessDeniedException; 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.Map;
import java.util.Optional; import java.util.Optional;
@ -22,7 +23,7 @@ public class StudentService {
@Autowired @Autowired
private Optional<Student> student; private Optional<Student> student;
@Autowired @Autowired
private UserService userService; private CallerService callerService;
/** @param studentToDiplomaTopic Map of @{@link Student} id and @{@link DiplomaTopic} id */ /** @param studentToDiplomaTopic Map of @{@link Student} id and @{@link DiplomaTopic} id */
public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) { public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) {
@ -41,6 +42,15 @@ public class StudentService {
} }
public Optional<Student> getCallerStudent() { public Optional<Student> getCallerStudent() {
return studentRepository.findByUser(userService.getCallerUser().orElse(null)); return studentRepository.findByUser(callerService.getCallerUser().orElse(null));
}
public StudentDTO getCallerStudentDTO() {
Student callerStudent = getCallerStudent().orElse(null);
if (callerStudent == null) {
return null;
}
return StudentDTO.from(callerStudent);
} }
} }

View File

@ -0,0 +1,14 @@
package ru.tubryansk.tdms.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class SysInfoService {
@Value("${application.version}")
private String version;
public String getVersion() {
return version;
}
}

View File

@ -3,14 +3,21 @@ package ru.tubryansk.tdms.service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.tubryansk.tdms.controller.payload.RegistrationDTO;
import ru.tubryansk.tdms.controller.payload.UserDTO;
import ru.tubryansk.tdms.entity.Role;
import ru.tubryansk.tdms.entity.Student;
import ru.tubryansk.tdms.entity.User; import ru.tubryansk.tdms.entity.User;
import ru.tubryansk.tdms.repository.UserRepository; import ru.tubryansk.tdms.entity.repository.GroupRepository;
import ru.tubryansk.tdms.entity.repository.StudentRepository;
import ru.tubryansk.tdms.entity.repository.UserRepository;
import java.util.Optional; import java.util.ArrayList;
import java.util.List;
@Service @Service
@Transactional @Transactional
@ -19,18 +26,74 @@ public class UserService implements UserDetailsService {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@Autowired @Autowired
private AuthenticationService authenticationService; private GroupRepository groupRepository;
@Autowired
private StudentRepository studentRepository;
@Autowired
private RoleService roleService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override @Override
public User loadUserByUsername(String username) throws UsernameNotFoundException { public User loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findUserByLogin(username) log.info("Loading user with username: {}", username);
.orElseThrow(() -> new UsernameNotFoundException("User not found")); User user = userRepository.findUserByLogin(username).orElseThrow(() -> {
log.info("User with login {} not found", username);
return new UsernameNotFoundException("User with login " + username + " not found");
});
log.info("User with login {} loaded", username);
return user;
} }
public Optional<User> getCallerUser() { public List<UserDTO> getAllUsers() {
if(authenticationService.authenticated()) { log.info("Loading all users");
return Optional.of((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()); List<UserDTO> users = userRepository.findAll().stream()
.map(UserDTO::from)
.toList();
log.info("{} users loaded", users.size());
return users;
}
public void registerUser(RegistrationDTO registrationDTO) {
log.info("Registering new user with login: {}", registrationDTO.getLogin());
User user = transientUser(registrationDTO);
Student student = transientStudent(registrationDTO.getStudentData());
fillRoles(user, registrationDTO);
log.info("Saving new user: {}", user);
userRepository.save(user);
if (student != null) {
student.setUser(user);
log.info("User is student, saving student: {}", student);
studentRepository.save(student);
} }
return Optional.empty(); }
private User transientUser(RegistrationDTO registrationDTO) {
User user = new User();
user.setLogin(registrationDTO.getLogin());
user.setPassword(passwordEncoder.encode(registrationDTO.getPassword()));
user.setFullName(registrationDTO.getFullName());
user.setEmail(registrationDTO.getEmail());
user.setNumberPhone(registrationDTO.getNumberPhone());
return user;
}
private Student transientStudent(RegistrationDTO.StudentRegistrationDTO studentData) {
if (studentData == null) {
return null;
}
Student student = new Student();
student.setGroup(groupRepository.findByIdThrow(studentData.getGroupId()));
return student;
}
private void fillRoles(User user, RegistrationDTO registrationDTO) {
List<Role> roles = new ArrayList<>();
if (registrationDTO.getStudentData() != null) {
roles.add(roleService.getRoleByAuthority(RoleService.Authority.ROLE_STUDENT));
}
user.setRoles(roles);
} }
} }

View File

@ -0,0 +1,29 @@
package ru.tubryansk.tdms.web;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@Slf4j
public class LoggingRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
log.info("Making request: {}. user: {}, session: {}, remote ip: {}",
request.getRequestURI(), request.getRemoteUser(),
request.getSession().getId(), request.getRemoteAddr());
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
log.info("Request finished with {} status. duration: {} ms", response.getStatus(), duration);
}
}
}

View File

@ -0,0 +1,23 @@
package ru.tubryansk.tdms.web;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class LoggingSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
log.debug("Session created: {}, user {}",
se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
log.debug("Session destroyed: {}, user: {}",
se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
}
}

View File

@ -4,8 +4,6 @@ application:
domain: localhost domain: localhost
protocol: http protocol: http
spring: spring:
flyway:
out-of-order: true
web: web:
resources: resources:
static-locations: file:///${user.dir}/web/dist/ static-locations: file:///${user.dir}/web/dist/

View File

@ -1,21 +1,19 @@
db: db:
url: jdbc:postgresql://localhost:5432/db url: jdbc:postgresql://localhost:5400/tdms
schema: public
user: root user: root
password: root password: root
schema: vkr
application: application:
name: @name@ name: @name@
version: @version@ version: @version@
type: production type: production
port: 443 port: 80
domain: tdms.tu-bryansk.ru domain: tdms.tu-bryansk.ru
protocol: https protocol: http
spring: spring:
application: application:
name: ${application.name} name: ${application.name}
main:
allow-circular-references: true
datasource: datasource:
url: ${db.url} url: ${db.url}
username: ${db.user} username: ${db.user}
@ -30,7 +28,8 @@ spring:
user: ${db.user} user: ${db.user}
password: ${db.password} password: ${db.password}
schemas: ${db.schema} schemas: ${db.schema}
locations: db.migration main:
banner-mode: off
server: server:
port: ${application.port} port: ${application.port}
address: ${application.domain} address: ${application.domain}

View File

@ -1,6 +0,0 @@
create table vkr.role
(
id integer primary key generated by default as identity,
name text not null unique,
authority text not null unique
)

View File

@ -1,12 +0,0 @@
create table vkr.user
(
id integer primary key generated by default as identity,
login text not null unique,
password text not null,
full_name text not null,
mail text not null unique,
number_phone text not null unique,
created_at timestamp(6) with time zone not null,
updated_at timestamp(6) with time zone
)

View File

@ -1,9 +0,0 @@
create table vkr.user_role
(
user_id integer not null,
role_id integer not null,
foreign key (user_id) references vkr.user (id)
on delete cascade,
foreign key (role_id) references vkr.role (id)
on delete cascade
)

View File

@ -1,5 +0,0 @@
create table vkr.diploma_topic
(
id integer primary key generated by default as identity,
name text not null unique
)

View File

@ -1,9 +0,0 @@
create table vkr.group
(
id integer primary key generated by default as identity,
name text not null unique,
principal_user_id integer not null,
foreign key (principal_user_id) references vkr.user (id)
on delete cascade
)

View File

@ -1,24 +0,0 @@
create table vkr.student
(
id integer primary key generated by default as identity,
form boolean,
protection_order integer not null,
magistracy text,
digital_format_present boolean,
mark_comment integer,
mark_practice integer,
predefence_comment text,
normal_control text,
anti_plagiarism int,
note text,
record_book_returned boolean,
work text,
user_id integer not null,
diploma_topic_id integer not null,
mentor_user_id integer not null,
group_id integer not null,
foreign key (user_id) references vkr.user (id) on delete cascade,
foreign key (diploma_topic_id) references vkr.diploma_topic (id) on delete cascade,
foreign key (mentor_user_id) references vkr.user (id) on delete cascade,
foreign key (group_id) references vkr.group (id) on delete cascade
)

View File

@ -1,96 +0,0 @@
INSERT INTO vkr.user (login, password, full_name, mail, number_phone, created_at, updated_at)
VALUES ('akulenko_mikhail', 'password123', 'Акуленко Михаил Вячеславович', 'akulenko.mikhail@example.com',
'+79110000001', NOW(), NOW()),
('borovikov_artem', 'password123', 'Боровиков Артём Викторович', 'borovikov.artem@example.com', '+79110000002',
NOW(), NOW()),
('bykonya_alexey', 'password123', 'Быконя Алексей Николаевич', 'bykonya.alexey@example.com', '+79110000003',
NOW(), NOW()),
('ermakov_alexander', 'password123', 'Ермаков Александр Сергеевич', 'ermakov.alexander@example.com',
'+79110000004', NOW(), NOW()),
('zgursky_evgeny', 'password123', 'Згурский Евгений Олегович', 'zgursky.evgeny@example.com', '+79110000005',
NOW(), NOW()),
('ibishov_tural', 'password123', 'Ибишов Турал Садай оглы', 'ibishov.tural@example.com', '+79110000006', NOW(),
NOW()),
('ignatenko_vladimir', 'password123', 'Игнатенко Владимир Алексеевич', 'ignatenko.vladimir@example.com',
'+79110000007', NOW(), NOW()),
('lazukin_danila', 'password123', 'Лазукин Данила Дмитриевич', 'lazukin.danila@example.com', '+79110000008',
NOW(), NOW()),
('mitiaev_danila', 'password123', 'Митяев Данила Алексеевич', 'mitiaev.danila@example.com', '+79110000009',
NOW(), NOW()),
('neshkov_daniil', 'password123', 'Нешков Даниил Владимирович', 'neshkov.daniil@example.com', '+79110000010',
NOW(), NOW()),
('petrov_pavel', 'password123', 'Петров Павел Сергеевич', 'petrov.pavel@example.com', '+79110000011', NOW(),
NOW()),
('sazonov_andrey', 'password123', 'Сазонов Андрей Андреевич', 'sazonov.andrey@example.com', '+79110000012',
NOW(), NOW()),
('solokhin_maxim', 'password123', 'Солохин Максим Николаевич', 'solokhin.maxim@example.com', '+79110000013',
NOW(), NOW()),
('sochinsky_artem', 'password123', 'Сочинский Артем Александрович', 'sochinsky.artem@example.com',
'+79110000014', NOW(), NOW()),
('trisvyatsky_kirill', 'password123', 'Трисвятский Кирилл Андреевич', 'trisvyatsky.kirill@example.com',
'+79110000015', NOW(), NOW()),
('turov_alexander', 'password123', 'Туров Александр Сергеевич', 'turov.alexander@example.com', '+79110000016',
NOW(), NOW()),
('shevtsova_alexandra', 'password123', 'Шевцова Александра Валерьевна', 'shevtsova.alexandra@example.com',
'+79110000017', NOW(), NOW()),
('kibalyuk_artem', 'password123', 'Кибалюк Артем Сергеевич', 'kibalyuk.artem@example.com', '+79110000018', NOW(),
NOW()),
('shulindin_artem', 'password123', 'Шулындин Артём Андреевич', 'shulindin.artem@example.com', '+79110000019',
NOW(), NOW()),
('belyaev_egor', 'password123', 'Беляев Егор Андреевич', 'belyaev.egor@example.com', '+79110000020', NOW(),
NOW()),
('berezhnoy_igor', 'password123', 'Бережной Игорь Александрович', 'berezhnoy.igor@example.com', '+79110000021',
NOW(), NOW()),
('bogun_pavel', 'password123', 'Богун Павел Сергеевич', 'bogun.pavel@example.com', '+79110000022', NOW(), NOW()),
('vaseykin_nikita', 'password123', 'Васейкин Никита Павлович', 'vaseykin.nikita@example.com', '+79110000023',
NOW(), NOW()),
('gomonov_nikita', 'password123', 'Гомонов Никита Алексеевич', 'gomonov.nikita@example.com', '+79110000024',
NOW(), NOW()),
('druyan_oleg', 'password123', 'Друян Олег Викторович', 'druyan.oleg@example.com', '+79110000025', NOW(), NOW()),
('ivanov_kirill', 'password123', 'Иванов Кирилл Эдуардович', 'ivanov.kirill@example.com', '+79110000026', NOW(),
NOW()),
('ivanova_veronika', 'password123', 'Иванова Вероника Евгеньевна', 'ivanova.veronika@example.com',
'+79110000027', NOW(), NOW()),
('izotov_ivan', 'password123', 'Изотов Иван Алексеевич', 'izotov.ivan@example.com', '+79110000028', NOW(),
NOW()),
('isakov_zahar', 'password123', 'Исаков Захар Александрович', 'isakov.zahar@example.com', '+79110000029', NOW(),
NOW()),
('iskritsky_daniil', 'password123', 'Искрицкий Даниил Павлович', 'iskritsky.daniil@example.com', '+79110000030',
NOW(), NOW()),
('linko_daria', 'password123', 'Линько Дарья Андреевна', 'linko.daria@example.com', '+79110000031', NOW(),
NOW()),
('logutov_kirill', 'password123', 'Логутов Кирилл Александрович', 'logutov.kirill@example.com', '+79110000032',
NOW(), NOW()),
('nekrassov_sergey', 'password123', 'Некрасов Сергей Игоревич', 'nekrassov.sergey@example.com', '+79110000033',
NOW(), NOW()),
('sinyagin_ilya', 'password123', 'Синягин Илья Александрович', 'sinyagin.ilya@example.com', '+79110000034',
NOW(), NOW()),
('sopriko_daniil', 'password123', 'Соприко Даниил Сергеевич', 'sopriko.daniil@example.com', '+79110000035',
NOW(), NOW()),
('turovsky_ivan', 'password123', 'Туровский Иван Алексеевич', 'turovsky.ivan@example.com', '+79110000036', NOW(),
NOW()),
('frantsev_sergey', 'password123', 'Францев Сергей Дмитриевич', 'frantsev.sergey@example.com', '+79110000037',
NOW(), NOW()),
('chepurnoy_maxim', 'password123', 'Чепурной Максим Романович', 'chepurnoy.maxim@example.com', '+79110000038',
NOW(), NOW()),
('schemelinin_dmitry', 'password123', 'Щемелинин Дмитрий Михайлович', 'schemelinin.dmitry@example.com',
'+79110000039', NOW(), NOW()),
('bulatitsky_d_i_1', 'password123', 'Булатицкий Д. И.', 'bulatitsky.d.i.1@example.com', '+79110000040', NOW(),
NOW()),
('kopeliovich_d_i_1', 'password123', 'Копелиович Д. И.', 'kopeliovich.d.i.1@example.com', '+79110000041', NOW(),
NOW()),
('dergachev_k_v', 'password123', 'Дергачев К. В.', 'dergachev.k.v@example.com', '+79110000042', NOW(), NOW()),
('trubakov_e_o', 'password123', 'Трубаков Е. О.', 'trubakov.e.o@example.com', '+79110000043', NOW(), NOW()),
('radchenko_a_o', 'password123', 'Радченко А. О.', 'radchenko.a.o@example.com', '+79110000044', NOW(), NOW()),
('zimin_s_n_1', 'password123', 'Зимин С. Н.', 'zimin.s.n.1@example.com', '+79110000045', NOW(), NOW()),
('koptenok_e_v', 'password123', 'Коптенок Е. В.', 'koptenok.e.v@example.com', '+79110000046', NOW(), NOW()),
('mikhaleva_o_a', 'password123', 'Михалева О. А.', 'mikhaleva.o.a@example.com', '+79110000047', NOW(), NOW()),
('gulakov_k_v', 'password123', 'Гулаков К. В.', 'gulakov.k.v@example.com', '+79110000048', NOW(), NOW()),
('titarev_d_v', 'password123', 'Титарёв Д. В.', 'titarev.d.v@example.com', '+79110000049', NOW(), NOW()),
('izrailev_v_ya_1', 'password123', 'Израилев В. Я.', 'izrailev.v.ya.1@example.com', '+79110000050', NOW(),
NOW()),
('podvesovsky_a_g_1', 'password123', 'Подвесовский А. Г.', 'podvesovsky.a.g.1@example.com', '+79110000051',
NOW(), NOW()),
('trubakov_a_o', 'password123', 'Трубаков А. О.', 'trubakov.a.o@example.com', '+79110000059',
NOW(), NOW()),
('lageriev_d_g', 'password123', 'Лагерев Д. Г.', 'lageriev.d.g@example.com', '+79110000052', NOW(), NOW());

View File

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

View File

@ -1,2 +0,0 @@
INSERT INTO vkr.group (name ,principal_user_id)
VALUES('ИВТ-1',40),('ИВТ-2',40);

View File

@ -1,2 +0,0 @@
INSERT INTO vkr.role (name,authority)
VALUES('Руководитель','ROLE_DIRECTOR'),('Куратор','ROLE_TUTOR'),('Студент','ROLE_STUDENT');

View File

@ -1,73 +0,0 @@
INSERT INTO vkr.student (form,
protection_order,
magistracy,
digital_format_present,
mark_comment,
mark_practice,
predefence_comment,
normal_control,
anti_plagiarism,
note,
record_book_returned,
work,
user_id,
diploma_topic_id,
mentor_user_id,
group_id)
VALUES (true, 1100, 'ПРИ, ИВТ или другой вуз', true, 5, 5, 'ок', 'Подписано', 7862, 'Акт о внедрении', true, 'нет', 1,
1, 40, 1),
(true, 1110, 'Да, но не уверен, в БГТУ ли', true, 5, 5, 'ок', 'Подписано', 8196, 'Заявка, Акт о внедрении', true,
'ООО "ЦИРОБЗ"', 2, 2, 40, 1),
(true, 3500, 'Нет', true, 3, 3, 'Критически низкий уровень. Допущен под ответственность руководителя ВКР',
'Подписано', 7141, 'Иниц', true, 'нет', 3, 3, 41, 1),
(true, 1800, 'Да, но не уверен, в БГТУ ли', true, 4, 5, 'Усилить работу', 'Подписано', 5381, 'Иниц', true, 'нет',
4, 4, 42, 1),
(true, 2100, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 8146, 'Заявка, Акт о внедрении', true, 'нет', 5, 5, 43,
1),
(true, 1100, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 7965, 'Иниц', true, 'нет', 6, 6, 41, 1),
(true, 1200, 'нет', true, 5, 4, 'Усилить работу', 'Подписано', 8583, Null, true, 'Газ Энерго Комплект (ГЭК)', 7,
7, 44, 1),
(true, 1700, 'Нет', true, 5, 5, 'Критически низкий уровень. Допущен под ответственность руководителя ВКР',
'Подписано', 6647, Null, true, 'нет', 8, 8, 45, 1),
(true, 3300, 'ИВТ, ПРИ или другой вуз', true, 5, 5, 'Усилить работу', 'Подписано', 6741,
'Заявка, Акт о внедрении', true, 'нет', 9, 9, 46, 1),
(true, 1200, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7645, 'Заявка, Акт о внедрении', true, 'нет', 10, 10, 40, 1),
(true, 3700, 'нет', true, 3, 3, 'Критически низкий уровень. Допущен под ответственность руководителя ВКР', '1',
3093, 'Иниц', true, 'нет', 11, 11, 45, 1),
(true, 1900, 'Нет', true, 5, 5, 'ок', 'Подписано', 8175, 'Заявка, Акт о внедрении', true, 'нет', 12, 12, 42, 1),
(true, 3200, 'ИВТ', true, 5, 5, 'Усилить работу', 'Подписано', 7805, 'Акт', true, 'нет', 13, 13, 46, 1),
(true, 1200, 'ИВТ, ПРИ', true, 4, 4, 'ок', 'рек', 7590, 'Иниц', true, 'нет', 14, 14, 45, 1),
(true, 3900, 'нет', true, 5, 5, 'Усилить работу', 'Подписано', 6463, 'Заявка, Акт о внедрении', true, 'нет', 15,
15, 40, 1),
(true, 2200, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7441, Null, true, 'ООО "ЦИРОБЗ"', 16, 16, 44, 1),
(true, 1130, 'Нет', true, 5, 4, 'Критически низкий уровень. Допущен под ответственность руководителя ВКР',
'Подписано', 7319, 'Заявка, Акт о внедрении', true, 'нет', 17, 17, 40, 1),
(true, 2400, 'ИВТ', true, 4, 5, 'ок', 'Подписано', 6436, Null, true, 'нет', 18, 18, 45, 1),
(true, 1600, 'ИВТ', true, 5, 5, 'Усилить работу', 'Подписано', 6227, 'Исслед', true, 'АО "БЭМЗ"', 19, 19, 47, 1),
(true, 1400, 'Нет', true, 5, 5, 'ок', 'Подписано', 8935, 'Иниц', true, 'ООО "ЦИРОБЗ"', 20, 20, 44, 2),
(true, 2900, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7971, 'Заявка, Акт о внедрении', true, 'нет', 21, 21, 40, 2),
(true, 2600, 'ИВТ, ПРИ', true, 3, 3, 'ок', 'Подписано', 7284, Null, true, 'нет', 22, 22, 48, 2),
(true, 3100, 'ИВТ, ПРИ', true, 5, 5, 'Усилить работу', 'Подписано', 4966, 'Заявка, Акт о внедрении', true, 'нет',
23, 23, 45, 2),
(true, 3110, 'Нет', true, 5, 5, 'ок', 'Подписано', 7174, Null, true, 'нет', 24, 24, 40, 2),
(true, 3800, 'Нет', true, 5, 5, 'ок', 'Подписано', 7233, Null, true, 'нет', 25, 25, 45, 2),
(true, 3300, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7133, Null, true, 'нет', 26, 26, 43, 2),
(true, 3130, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 8083, 'Заявка, Акт о внедрении', true, 'нет', 27, 27, 49, 2),
(true, 3400, 'Нет', true, 5, 4, 'Критически низкий уровень. Допущен под ответственность руководителя ВКР',
'Подписано', 7968, 'Иниц', true, 'нет', 28, 28, 50, 2),
(true, 3120, 'ИВТ или ПРИ', true, 5, 5, 'ок', 'Подписано', 7940, 'Исслед', true, 'ООО "ЦИРОБЗ"', 29, 29, 40, 2),
(true, 2800, 'Нет (возможно)', true, 4, 4, 'Усилить работу', 'рек', 6775, 'Заявка, Акт о внедрении', true, 'нет',
30, 30, 40, 2),
(true, 2100, 'Нет (возможно)', true, 5, 5, 'ок', 'Подписано', 7637, 'Заявка, Акт о внедрении', true, 'нет', 31,
31, 40, 2),
(true, 2700, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 8544, 'Заявка, Акт о внедрении', true, 'нет', 32, 32, 45, 2),
(true, 2130, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 7166, 'Заявка, Акт о внедрении', true, 'нет', 33, 33, 51,
2),
(true, 2110, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 6075, 'Заявка, Акт о внедрении', true, 'нет', 34, 34, 52, 2),
(true, 3100, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7057, 'Заявка, Акт о внедрении', true, 'нет', 35, 35, 50, 2),
(true, 2120, 'В БГТУ на другой кафедре (38.04.01 Экономика или 27.04.05. Инноватика)', true, 5, 5, 'ок',
'Подписано', 7057, 'Заявка, Акт о внедрении', true, 'нет', 36, 36, 51, 2),
(true, 2500, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 6583, 'Заявка, Акт о внедрении', true, 'нет', 37, 37, 53, 2),
(true, 1300, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 8444, 'Заявка, Акт о внедрении', true, 'нет', 38, 38, 44,
2),
(true, 3600, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7631, 'Заявка, Акт о внедрении', true, 'нет', 39, 39, 45, 2);

View File

@ -1,55 +0,0 @@
INSERT INTO vkr.user_role (user_id, role_id)
VALUES (1, 3),
(2, 3),
(3, 3),
(4, 3),
(5, 3),
(6, 3),
(7, 3),
(8, 3),
(9, 3),
(10, 3),
(11, 3),
(12, 3),
(13, 3),
(14, 3),
(15, 3),
(16, 3),
(17, 3),
(18, 3),
(19, 3),
(20, 3),
(21, 3),
(22, 3),
(23, 3),
(24, 3),
(25, 3),
(26, 3),
(27, 3),
(28, 3),
(29, 3),
(30, 3),
(31, 3),
(32, 3),
(33, 3),
(34, 3),
(35, 3),
(36, 3),
(37, 3),
(38, 3),
(39, 3),
(40, 1),
(41, 1),
(42, 1),
(43, 1),
(44, 1),
(45, 1),
(46, 1),
(47, 1),
(48, 1),
(49, 1),
(50, 1),
(51, 1),
(52, 1),
(53, 1),
(40, 2);

View File

@ -0,0 +1,13 @@
create table role
(
id bigint primary key,
name text not null unique,
authority text not null unique
);
-- COMMENTS
comment on table role is 'Таблица ролей пользователей';
comment on column role.name is 'Человекочитаемое имя роли';
comment on column role.authority is 'Имя роли в системе';

View File

@ -0,0 +1,22 @@
create table "user"
(
id bigserial primary key,
login text not null unique,
password text not null,
full_name text not null,
email text not null unique,
number_phone text not null unique,
created_at timestamptz not null,
updated_at timestamptz
);
-- COMMENTS
comment on table "user" is 'Таблица пользователей';
comment on column "user".login is 'Логин пользователя';
comment on column "user".password is 'Пароль пользователя';
comment on column "user".full_name is 'Полное имя пользователя в формате Фамилия Имя Отчество';
comment on column "user".email is 'Почта пользователя';
comment on column "user".number_phone is 'Номер телефона пользователя';

View File

@ -0,0 +1,21 @@
create table user_role
(
id bigserial primary key,
user_id bigint not null,
role_id bigint not null
);
-- FOREIGN KEY
alter table user_role
add constraint fk_user_role_user_id
foreign key (user_id) references "user" (id);
alter table user_role
add constraint fk_user_role_role_id
foreign key (role_id) references role (id);
-- COMMENTS
comment on table user_role is 'Таблица связи пользователей и ролей';
comment on column user_role.user_id is 'Идентификатор пользователя';
comment on column user_role.role_id is 'Идентификатор роли';

View File

@ -0,0 +1,11 @@
create table diploma_topic
(
id bigserial primary key,
name text not null
);
-- COMMENTS
comment on table diploma_topic is 'Таблица тем дипломных работ';
comment on column diploma_topic.name is 'Название темы дипломной работы';

View File

@ -0,0 +1,22 @@
create table "group"
(
id bigserial primary key,
name text not null unique,
curator_user_id bigint,
created_at timestamptz not null,
updated_at timestamptz
);
-- FOREIGN KEY
alter table "group"
add constraint fk_group_curator_user_id
foreign key (curator_user_id) references "user" (id)
on delete set null on update cascade;
-- COMMENTS
comment on table "group" is 'Таблица групп студентов';
comment on column "group".name is 'Название группы';
comment on column "group".curator_user_id is 'Идентификатор куратора группы';

View File

@ -0,0 +1,65 @@
create table student
(
id bigserial primary key,
user_id bigint not null,
diploma_topic_id bigint not null,
mentor_user_id bigint not null,
group_id bigint not null,
form boolean,
protection_day int,
protection_order int,
magistracy text,
digital_format_present boolean,
mark_comment int,
mark_practice int,
predefence_comment text,
normal_control text,
anti_plagiarism int,
note text,
record_book_returned boolean,
work text,
created_at timestamptz not null,
updated_at timestamptz
);
-- FOREIGN KEY
alter table student
add constraint fk_student_user_id
foreign key (user_id) references "user" (id)
on delete cascade on update cascade;
alter table student
add constraint fk_student_diploma_topic_id
foreign key (diploma_topic_id) references diploma_topic (id)
on delete set null on update cascade;
alter table student
add constraint fk_student_mentor_user_id
foreign key (mentor_user_id) references "user" (id)
on delete set null on update cascade;
alter table student
add constraint fk_student_group_id
foreign key (group_id) references "group" (id)
on delete set null on update cascade;
-- COMMENTS
comment on table student is 'Таблица студентов';
comment on column student.user_id is 'Идентификатор пользователя';
comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы';
comment on column student.mentor_user_id is 'Идентификатор научного руководителя';
comment on column student.group_id is 'Идентификатор группы';
comment on column student.form is 'Форма обучения';
comment on column student.protection_day is 'День защиты';
comment on column student.protection_order is 'Порядок защиты';
comment on column student.magistracy is 'Магистратура';
comment on column student.digital_format_present is 'Предоставлен в электронном виде';
comment on column student.mark_comment is 'Комментарий к оценке';
comment on column student.mark_practice is 'Оценка практики';
comment on column student.predefence_comment is 'Комментарий к защите';
comment on column student.normal_control is 'Обычный контроль';
comment on column student.anti_plagiarism is 'Антиплагиат';
comment on column student.note is 'Примечание';
comment on column student.record_book_returned is 'Ведомость возвращена';
comment on column student.work is 'Работа';

View File

@ -0,0 +1,6 @@
insert into role (id, name, authority)
values (1, 'Преподаватель', 'ROLE_TEACHER'),
(2, 'Студент', 'ROLE_STUDENT'),
(3, 'Член комиссии ГЭК', 'ROLE_COMMISSION_MEMBER'),
(4, 'Администратор', 'ROLE_ADMINISTRATOR'),
(5, 'Секретарь', 'ROLE_SECRETARY');

View File

@ -0,0 +1,8 @@
insert into "user" (id, login, password, full_name, email, number_phone, created_at)
values (1, 'admin', '{noop}admin', 'Администратор', 'admin@tdms.tu-byransk.ru', '', now());
insert into user_role (id, user_id, role_id)
values (1, 1, 4);
select setval('user_id_seq', (select max(id) from "user"));
select setval('user_role_id_seq', (select max(id) from user_role));

View File

@ -0,0 +1,7 @@
create table defence
(
id bigserial primary key,
defence_date timestamptz,
created_at timestamptz not null,
updated_at timestamptz
);

View File

@ -1,4 +1,4 @@
INSERT INTO vkr.diploma_topic (name) INSERT INTO diploma_topic (name)
VALUES ('Мобильное приложение для заказа автозапчастей на платформе ABCP'), VALUES ('Мобильное приложение для заказа автозапчастей на платформе ABCP'),
('Подсистема уведомления пользователей для программного комплекса "РискПроф. Учебный центр"'), ('Подсистема уведомления пользователей для программного комплекса "РискПроф. Учебный центр"'),
('Веб-приложение "Таск-менеджер"'), ('Веб-приложение "Таск-менеджер"'),

View File

@ -0,0 +1,3 @@
INSERT INTO "group" (name, principal_user_id)
VALUES ('ИВТ-1', 40),
('ИВТ-2', 40);

View File

@ -0,0 +1,98 @@
do
$$
begin
INSERT INTO student (form,
protection_order,
magistracy,
digital_format_present,
mark_comment,
mark_practice,
predefence_comment,
normal_control,
anti_plagiarism,
note,
record_book_returned,
work,
user_id,
diploma_topic_id,
mentor_user_id,
group_id,
created_at)
VALUES (true, 1100, 'ПРИ, ИВТ или другой вуз', true, 5, 5, 'ок', 'Подписано', 7862, 'Акт о внедрении', true,
'нет', 1, 1, 40, 3, now()),
(true, 1110, 'Да, но не уверен, в БГТУ ли', true, 5, 5, 'ок', 'Подписано', 8196,
'Заявка, Акт о внедрении', true, 'ООО "ЦИРОБЗ"', 2, 2, 40, 3, now()),
(true, 3500, 'Нет', true, 3, 3,
'Критически низкий уровень. Допущен под ответственность руководителя ВКР', 'Подписано', 7141, 'Иниц',
true, 'нет', 3, 3, 41, 3, now()),
(true, 1800, 'Да, но не уверен, в БГТУ ли', true, 4, 5, 'Усилить работу', 'Подписано', 5381, 'Иниц',
true, 'нет', 4, 4, 42, 3, now()),
(true, 2100, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 8146, 'Заявка, Акт о внедрении', true, 'нет', 5,
5, 43, 3, now()),
(true, 1100, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 7965, 'Иниц', true, 'нет', 6, 6, 41, 3, now()),
(true, 1200, 'нет', true, 5, 4, 'Усилить работу', 'Подписано', 8583, Null, true,
'Газ Энерго Комплект (ГЭК)', 7, 7, 44, 3, now()),
(true, 1700, 'Нет', true, 5, 5,
'Критически низкий уровень. Допущен под ответственность руководителя ВКР', 'Подписано', 6647, Null,
true, 'нет', 8, 8, 45, 3, now()),
(true, 3300, 'ИВТ, ПРИ или другой вуз', true, 5, 5, 'Усилить работу', 'Подписано', 6741,
'Заявка, Акт о внедрении', true, 'нет', 9, 9, 46, 3, now()),
(true, 1200, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7645, 'Заявка, Акт о внедрении', true, 'нет', 10, 10,
40, 3, now()),
(true, 3700, 'нет', true, 3, 3,
'Критически низкий уровень. Допущен под ответственность руководителя ВКР', '1', 3093, 'Иниц', true,
'нет', 11, 11, 45, 3, now()),
(true, 1900, 'Нет', true, 5, 5, 'ок', 'Подписано', 8175, 'Заявка, Акт о внедрении', true, 'нет', 12, 12,
42, 3, now()),
(true, 3200, 'ИВТ', true, 5, 5, 'Усилить работу', 'Подписано', 7805, 'Акт', true, 'нет', 13, 13, 46, 3,
now()),
(true, 1200, 'ИВТ, ПРИ', true, 4, 4, 'ок', 'рек', 7590, 'Иниц', true, 'нет', 14, 14, 45, 3, now()),
(true, 3900, 'нет', true, 5, 5, 'Усилить работу', 'Подписано', 6463, 'Заявка, Акт о внедрении', true,
'нет', 15, 15, 40, 3, now()),
(true, 2200, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7441, Null, true, 'ООО "ЦИРОБЗ"', 16, 16, 44, 3,
now()),
(true, 1130, 'Нет', true, 5, 4,
'Критически низкий уровень. Допущен под ответственность руководителя ВКР', 'Подписано', 7319,
'Заявка, Акт о внедрении', true, 'нет', 17, 17, 40, 3, now()),
(true, 2400, 'ИВТ', true, 4, 5, 'ок', 'Подписано', 6436, Null, true, 'нет', 18, 18, 45, 3, now()),
(true, 1600, 'ИВТ', true, 5, 5, 'Усилить работу', 'Подписано', 6227, 'Исслед', true, 'АО "БЭМЗ"', 19, 19,
47, 3, now()),
(true, 1400, 'Нет', true, 5, 5, 'ок', 'Подписано', 8935, 'Иниц', true, 'ООО "ЦИРОБЗ"', 20, 20, 44, 4,
now()),
(true, 2900, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7971, 'Заявка, Акт о внедрении', true, 'нет', 21, 21,
40, 4, now()),
(true, 2600, 'ИВТ, ПРИ', true, 3, 3, 'ок', 'Подписано', 7284, Null, true, 'нет', 22, 22, 48, 4, now()),
(true, 3100, 'ИВТ, ПРИ', true, 5, 5, 'Усилить работу', 'Подписано', 4966, 'Заявка, Акт о внедрении',
true, 'нет', 23, 23, 45, 4, now()),
(true, 3110, 'Нет', true, 5, 5, 'ок', 'Подписано', 7174, Null, true, 'нет', 24, 24, 40, 4, now()),
(true, 3800, 'Нет', true, 5, 5, 'ок', 'Подписано', 7233, Null, true, 'нет', 25, 25, 45, 4, now()),
(true, 3300, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7133, Null, true, 'нет', 26, 26, 43, 4, now()),
(true, 3130, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 8083, 'Заявка, Акт о внедрении', true, 'нет', 27, 27,
49, 4, now()),
(true, 3400, 'Нет', true, 5, 4,
'Критически низкий уровень. Допущен под ответственность руководителя ВКР', 'Подписано', 7968, 'Иниц',
true, 'нет', 28, 28, 50, 4, now()),
(true, 3120, 'ИВТ или ПРИ', true, 5, 5, 'ок', 'Подписано', 7940, 'Исслед', true, 'ООО "ЦИРОБЗ"', 29, 29,
40, 4, now()),
(true, 2800, 'Нет (возможно)', true, 4, 4, 'Усилить работу', 'рек', 6775, 'Заявка, Акт о внедрении',
true, 'нет', 30, 30, 40, 4, now()),
(true, 2100, 'Нет (возможно)', true, 5, 5, 'ок', 'Подписано', 7637, 'Заявка, Акт о внедрении', true,
'нет', 31, 31, 40, 4, now()),
(true, 2700, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 8544, 'Заявка, Акт о внедрении', true, 'нет', 32, 32,
45, 4, now()),
(true, 2130, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 7166, 'Заявка, Акт о внедрении', true, 'нет', 33,
33, 51, 4, now()),
(true, 2110, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 6075, 'Заявка, Акт о внедрении', true, 'нет', 34, 34,
52, 4, now()),
(true, 3100, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7057, 'Заявка, Акт о внедрении', true, 'нет', 35, 35,
50, 4, now()),
(true, 2120, 'В БГТУ на другой кафедре (38.04.01 Экономика или 27.04.05. Инноватика)', true, 5, 5, 'ок',
'Подписано', 7057, 'Заявка, Акт о внедрении', true, 'нет', 36, 36, 51, 4, now()),
(true, 2500, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 6583, 'Заявка, Акт о внедрении', true, 'нет', 37, 37,
53, 4, now()),
(true, 1300, 'ИВТ, ПРИ', true, 5, 5, 'ок', 'Подписано', 8444, 'Заявка, Акт о внедрении', true, 'нет', 38,
38, 44, 4, now()),
(true, 3600, 'ИВТ', true, 5, 5, 'ок', 'Подписано', 7631, 'Заявка, Акт о внедрении', true, 'нет', 39, 39,
45, 4, now());
end
$$;

View File

@ -0,0 +1,82 @@
INSERT INTO "user" (login, password, full_name, mail, number_phone, created_at)
VALUES ('akulenko_mikhail', '{noop}1', 'Акуленко Михаил Вячеславович', 'akulenko.mikhail@example.com',
'+79110000001', NOW()),
('borovikov_artem', '{noop}1', 'Боровиков Артём Викторович', 'borovikov.artem@example.com', '+79110000002',
NOW()),
('bykonya_alexey', '{noop}1', 'Быконя Алексей Николаевич', 'bykonya.alexey@example.com', '+79110000003',
NOW()),
('ermakov_alexander', '{noop}1', 'Ермаков Александр Сергеевич', 'ermakov.alexander@example.com',
'+79110000004', NOW()),
('zgursky_evgeny', '{noop}1', 'Згурский Евгений Олегович', 'zgursky.evgeny@example.com', '+79110000005',
NOW()),
('ibishov_tural', '{noop}1', 'Ибишов Турал Садай оглы', 'ibishov.tural@example.com', '+79110000006', NOW()),
('ignatenko_vladimir', '{noop}1', 'Игнатенко Владимир Алексеевич', 'ignatenko.vladimir@example.com',
'+79110000007', NOW()),
('lazukin_danila', '{noop}1', 'Лазукин Данила Дмитриевич', 'lazukin.danila@example.com', '+79110000008',
NOW()),
('mitiaev_danila', '{noop}1', 'Митяев Данила Алексеевич', 'mitiaev.danila@example.com', '+79110000009',
NOW()),
('neshkov_daniil', '{noop}1', 'Нешков Даниил Владимирович', 'neshkov.daniil@example.com', '+79110000010',
NOW()),
('petrov_pavel', '{noop}1', 'Петров Павел Сергеевич', 'petrov.pavel@example.com', '+79110000011', NOW()),
('sazonov_andrey', '{noop}1', 'Сазонов Андрей Андреевич', 'sazonov.andrey@example.com', '+79110000012',
NOW()),
('solokhin_maxim', '{noop}1', 'Солохин Максим Николаевич', 'solokhin.maxim@example.com', '+79110000013',
NOW()),
('sochinsky_artem', '{noop}1', 'Сочинский Артем Александрович', 'sochinsky.artem@example.com',
'+79110000014', NOW()),
('trisvyatsky_kirill', '{noop}1', 'Трисвятский Кирилл Андреевич', 'trisvyatsky.kirill@example.com',
'+79110000015', NOW()),
('turov_alexander', '{noop}1', 'Туров Александр Сергеевич', 'turov.alexander@example.com', '+79110000016',
NOW()),
('shevtsova_alexandra', '{noop}1', 'Шевцова Александра Валерьевна', 'shevtsova.alexandra@example.com',
'+79110000017', NOW()),
('kibalyuk_artem', '{noop}1', 'Кибалюк Артем Сергеевич', 'kibalyuk.artem@example.com', '+79110000018', NOW()),
('shulindin_artem', '{noop}1', 'Шулындин Артём Андреевич', 'shulindin.artem@example.com', '+79110000019',
NOW()),
('belyaev_egor', '{noop}1', 'Беляев Егор Андреевич', 'belyaev.egor@example.com', '+79110000020', NOW()),
('berezhnoy_igor', '{noop}1', 'Бережной Игорь Александрович', 'berezhnoy.igor@example.com', '+79110000021',
NOW()),
('bogun_pavel', '{noop}1', 'Богун Павел Сергеевич', 'bogun.pavel@example.com', '+79110000022', NOW()),
('vaseykin_nikita', '{noop}1', 'Васейкин Никита Павлович', 'vaseykin.nikita@example.com', '+79110000023',
NOW()),
('gomonov_nikita', '{noop}1', 'Гомонов Никита Алексеевич', 'gomonov.nikita@example.com', '+79110000024',
NOW()),
('druyan_oleg', '{noop}1', 'Друян Олег Викторович', 'druyan.oleg@example.com', '+79110000025', NOW()),
('ivanov_kirill', '{noop}1', 'Иванов Кирилл Эдуардович', 'ivanov.kirill@example.com', '+79110000026', NOW()),
('ivanova_veronika', '{noop}1', 'Иванова Вероника Евгеньевна', 'ivanova.veronika@example.com',
'+79110000027', NOW()),
('izotov_ivan', '{noop}1', 'Изотов Иван Алексеевич', 'izotov.ivan@example.com', '+79110000028', NOW()),
('isakov_zahar', '{noop}1', 'Исаков Захар Александрович', 'isakov.zahar@example.com', '+79110000029', NOW()),
('iskritsky_daniil', '{noop}1', 'Искрицкий Даниил Павлович', 'iskritsky.daniil@example.com', '+79110000030',
NOW()),
('linko_daria', '{noop}1', 'Линько Дарья Андреевна', 'linko.daria@example.com', '+79110000031', NOW()),
('logutov_kirill', '{noop}1', 'Логутов Кирилл Александрович', 'logutov.kirill@example.com', '+79110000032',
NOW()),
('nekrassov_sergey', '{noop}1', 'Некрасов Сергей Игоревич', 'nekrassov.sergey@example.com', '+79110000033',
NOW()),
('sinyagin_ilya', '{noop}1', 'Синягин Илья Александрович', 'sinyagin.ilya@example.com', '+79110000034',
NOW()),
('sopriko_daniil', '{noop}1', 'Соприко Даниил Сергеевич', 'sopriko.daniil@example.com', '+79110000035',
NOW()),
('turovsky_ivan', '{noop}1', 'Туровский Иван Алексеевич', 'turovsky.ivan@example.com', '+79110000036', NOW()),
('frantsev_sergey', '{noop}1', 'Францев Сергей Дмитриевич', 'frantsev.sergey@example.com', '+79110000037',
NOW()),
('chepurnoy_maxim', '{noop}1', 'Чепурной Максим Романович', 'chepurnoy.maxim@example.com', '+79110000038',
NOW()),
('schemelinin_dmitry', '{noop}1', 'Щемелинин Дмитрий Михайлович', 'schemelinin.dmitry@example.com',
'+79110000039', NOW()),
('bulatitsky_d_i_1', '{noop}1', 'Булатицкий Д. И.', 'bulatitsky.d.i.1@example.com', '+79110000040', NOW()),
('kopeliovich_d_i_1', '{noop}1', 'Копелиович Д. И.', 'kopeliovich.d.i.1@example.com', '+79110000041', NOW()),
('dergachev_k_v', '{noop}1', 'Дергачев К. В.', 'dergachev.k.v@example.com', '+79110000042', NOW()),
('trubakov_e_o', '{noop}1', 'Трубаков Е. О.', 'trubakov.e.o@example.com', '+79110000043', NOW()),
('radchenko_a_o', '{noop}1', 'Радченко А. О.', 'radchenko.a.o@example.com', '+79110000044', NOW()),
('zimin_s_n_1', '{noop}1', 'Зимин С. Н.', 'zimin.s.n.1@example.com', '+79110000045', NOW()),
('koptenok_e_v', '{noop}1', 'Коптенок Е. В.', 'koptenok.e.v@example.com', '+79110000046', NOW()),
('mikhaleva_o_a', '{noop}1', 'Михалева О. А.', 'mikhaleva.o.a@example.com', '+79110000047', NOW()),
('gulakov_k_v', '{noop}1', 'Гулаков К. В.', 'gulakov.k.v@example.com', '+79110000048', NOW()),
('titarev_d_v', '{noop}1', 'Титарёв Д. В.', 'titarev.d.v@example.com', '+79110000049', NOW()),
('izrailev_v_ya_1', '{noop}1', 'Израилев В. Я.', 'izrailev.v.ya.1@example.com', '+79110000050', NOW()),
('podvesovsky_a_g_1', '{noop}1', 'Подвесовский А. Г.', 'podvesovsky.a.g.1@example.com', '+79110000051', NOW()),
('trubakov_a_o', '{noop}1', 'Трубаков А. О.', 'trubakov.a.o@example.com', '+79110000059', NOW()),
('lageriev_d_g', '{noop}1', 'Лагерев Д. Г.', 'lageriev.d.g@example.com', '+79110000052', NOW());

View File

@ -0,0 +1,58 @@
do
$$
declare
teacher_id bigint := 1;
student_id bigint := 2;
commission_member_id bigint := 3;
administrator_id bigint := 4;
secretary_id bigint := 5;
begin
INSERT INTO user_role (user_id, role_id)
VALUES (1, student_id),
(2, student_id),
(3, student_id),
(4, student_id),
(5, student_id),
(6, student_id),
(7, student_id),
(8, student_id),
(9, student_id),
(10, student_id),
(11, student_id),
(12, student_id),
(13, student_id),
(14, student_id),
(15, student_id),
(16, student_id),
(17, student_id),
(18, student_id),
(19, student_id),
(20, student_id),
(21, student_id),
(22, student_id),
(23, student_id),
(24, student_id),
(25, student_id),
(26, student_id),
(27, student_id),
(28, student_id),
(29, student_id),
(30, student_id),
(31, student_id),
(32, student_id),
(33, student_id),
(34, student_id),
(35, student_id),
(36, student_id),
(37, teacher_id),
(37, administrator_id),
(38, commission_member_id),
(39, teacher_id),
(40, teacher_id),
(41, teacher_id),
(42, teacher_id),
(43, teacher_id),
(44, secretary_id),
(45, secretary_id);
end
$$;

View File

@ -0,0 +1,22 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<append>false</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="ru.tubryansk.tdms" level="debug" />
<root level="warn">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>

51
web/package-lock.json generated
View File

@ -13,14 +13,17 @@
"@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@types/lodash": "^4.17.15",
"axios": "^1.7.7", "axios": "^1.7.7",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"lodash": "^4.17.21",
"mobx": "^6.13.1", "mobx": "^6.13.1",
"mobx-react": "^9.1.1", "mobx-react": "^9.1.1",
"mobx-state-router": "^6.0.1", "mobx-state-router": "^6.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"uuid": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-decorators": "^7.25.7", "@babel/plugin-proposal-decorators": "^7.25.7",
@ -2105,6 +2108,12 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true "dev": true
}, },
"node_modules/@types/lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
"license": "MIT"
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -3858,20 +3867,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -4591,7 +4586,7 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "license": "MIT"
}, },
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
@ -5974,6 +5969,16 @@
"websocket-driver": "^0.7.4" "websocket-driver": "^0.7.4"
} }
}, },
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -6454,12 +6459,16 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"dev": true, "funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/value-equal": { "node_modules/value-equal": {

View File

@ -12,14 +12,17 @@
"@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@types/lodash": "^4.17.15",
"axios": "^1.7.7", "axios": "^1.7.7",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"lodash": "^4.17.21",
"mobx": "^6.13.1", "mobx": "^6.13.1",
"mobx-react": "^9.1.1", "mobx-react": "^9.1.1",
"mobx-state-router": "^6.0.1", "mobx-state-router": "^6.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"uuid": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-decorators": "^7.25.7", "@babel/plugin-proposal-decorators": "^7.25.7",

View File

@ -3,7 +3,7 @@ import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import {RouterContext, RouterView} from "mobx-state-router"; import {RouterContext, RouterView} from "mobx-state-router";
import {initApp} from "./utils/init"; import {initApp} from "./utils/init";
import {RootStoreContext} from './context/RootStoreContext'; import {RootStoreContext} from './store/RootStoreContext';
import {viewMap} from "./router/viewMap"; import {viewMap} from "./router/viewMap";
const rootStore = initApp(); const rootStore = initApp();

View File

@ -0,0 +1,89 @@
import {ComponentContext} from "../utils/ComponentContext";
import {observer} from "mobx-react";
import {Notification, NotificationType} from "../store/NotificationStore";
import {Card, CardBody, CardHeader, CardText, CardTitle, Col, Row} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {action, makeObservable} from "mobx";
@observer
export class NotificationContainer extends ComponentContext {
forEachNotificationRender(notifications: Notification[], type: NotificationType) {
return notifications.map(notification => (
<NotificationPopup key={notification.uuid} notification={notification} type={type}/>
));
}
render() {
return <div style={{position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 1000}}>
{this.forEachNotificationRender(this.notificationStore.errors, NotificationType.ERROR)}
{this.forEachNotificationRender(this.notificationStore.successes, NotificationType.SUCCESS)}
{this.forEachNotificationRender(this.notificationStore.warnings, NotificationType.WARNING)}
{this.forEachNotificationRender(this.notificationStore.infos, NotificationType.INFO)}
</div>
}
}
@observer
class NotificationPopup extends ComponentContext<{ notification: Notification, type: NotificationType }> {
constructor(props: { notification: Notification, type: NotificationType }) {
super(props);
makeObservable(this);
}
@action.bound
close() {
this.notificationStore.close(this.props.notification.uuid);
}
get cardClassName() {
switch (this.props.type) {
case NotificationType.ERROR:
return 'text-bg-danger';
case NotificationType.WARNING:
return 'text-bg-warning';
case NotificationType.INFO:
return 'text-bg-info';
case NotificationType.SUCCESS:
return 'text-bg-success';
}
}
render() {
const hasTitle = !!this.props.notification.title && this.props.notification.title.length > 0;
const closeIcon = <FontAwesomeIcon icon={'close'} onClick={this.close}/>;
return <Card className={`position-relative mt-3 opacity-75 ${this.cardClassName}`}>
{
hasTitle &&
<CardHeader>
<CardTitle>
<Row>
<Col sm={11}>
{this.props.notification.title}
</Col>
<Col className={'text-end'}>
{closeIcon}
</Col>
</Row>
</CardTitle>
</CardHeader>
}
<CardBody>
<CardText>
<Row>
<Col sm={11}>
{this.props.notification.message}
</Col>
{
!hasTitle &&
<Col className={'text-end'}>
{closeIcon}
</Col>
}
</Row>
</CardText>
</CardBody>
</Card>
}
}

View File

@ -0,0 +1,147 @@
import {ComponentContext} from "../../utils/ComponentContext";
import {TableDescriptor} from "../../utils/tables";
import {observer} from "mobx-react";
import {action, makeObservable} from "mobx";
import {FormSelect, Pagination, Table} from "react-bootstrap";
export interface DataTableProps<T> {
tableDescriptor: TableDescriptor<T>;
}
@observer
export class DataTable<T> extends ComponentContext<DataTableProps<T>> {
constructor(props: DataTableProps<T>) {
super(props);
makeObservable(this);
}
header() {
return <tr>
{this.props.tableDescriptor.columns.map(column => <th className={'text-center'}
key={column.key}>{column.title}</th>)}
</tr>
}
body() {
const firstColumnKey = this.props.tableDescriptor.columns[0].key;
return this.props.tableDescriptor.data.map(row => {
const rowAny = row as any;
return <tr key={rowAny[firstColumnKey]}>
{
this.props.tableDescriptor.columns.map(column => {
return <td
className={'text-center'}
key={column.key}>
{column.format(rowAny[column.key])}
</td>
})
}
</tr>
});
}
isFirstPage() {
if (typeof this.props.tableDescriptor.page === 'undefined') {
return true;
}
return this.props.tableDescriptor.page === 0;
}
isLastPage() {
if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') {
return true;
}
return this.props.tableDescriptor.page === (this.props.tableDescriptor.data.length / this.props.tableDescriptor.pageSize);
}
@action.bound
goFirstPage() {
if (typeof this.props.tableDescriptor.page === 'undefined') {
return;
}
this.props.tableDescriptor.page = 0;
}
@action.bound
goLastPage() {
if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') {
return;
}
this.props.tableDescriptor.page = this.props.tableDescriptor.data.length / this.props.tableDescriptor.pageSize;
}
@action.bound
goNextPage() {
if (typeof this.props.tableDescriptor.page === 'undefined' || typeof this.props.tableDescriptor.pageSize === 'undefined') {
return;
}
this.props.tableDescriptor.page++;
}
@action.bound
goPrevPage() {
if (typeof this.props.tableDescriptor.page === 'undefined') {
return;
}
this.props.tableDescriptor.page--;
}
@action.bound
changePageSize(e: any) {
this.props.tableDescriptor.pageSize = parseInt(e.target.value);
}
footer() {
const table = this.props.tableDescriptor;
if (typeof table.page === 'undefined' || typeof table.pageSize === 'undefined') {
return null;
}
return <tr className={'text-center'}>
<td colSpan={table.columns.length}>
<div className={'d-flex justify-content-between'}>
<div/>
<Pagination className={'mb-0'}>
<Pagination.First onClick={this.goFirstPage} disabled={this.isFirstPage()}/>
<Pagination.Ellipsis disabled={this.isFirstPage()}/>
<Pagination.Prev onClick={this.goPrevPage} disabled={this.isFirstPage()}/>
<Pagination.Item active>{this.props.tableDescriptor.page}</Pagination.Item>
<Pagination.Next onClick={this.goNextPage} disabled={!this.isLastPage()}/>
<Pagination.Ellipsis disabled={!this.isLastPage()}/>
<Pagination.Last onClick={this.goLastPage} disabled={!this.isLastPage()}/>
</Pagination>
<FormSelect className={'w-auto'} onChange={this.changePageSize}>
<option>10</option>
<option>20</option>
<option>50</option>
<option>100</option>
</FormSelect>
</div>
</td>
</tr>
}
render() {
const table = this.props.tableDescriptor;
return <Table hover striped>
<thead>
{this.header()}
</thead>
<tbody>
{this.body()}
</tbody>
{
table.pageable &&
<tfoot>
{this.footer()}
</tfoot>
}
</Table>
}
}

View File

@ -0,0 +1,3 @@
.l-no-bg label::after {
background-color: rgba(0, 0, 0, 0) !important;
}

View File

@ -0,0 +1,116 @@
import React from "react";
import {ReactiveValue} from "../../../utils/reactive/reactiveValue";
import {observer} from "mobx-react";
import {action, makeObservable, observable} from "mobx";
import {Button, ButtonGroup, FloatingLabel, FormControl, FormText, ToggleButton} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import './ReactiveControls.css';
export interface ReactiveInputProps<T> {
value: ReactiveValue<T>;
label?: string;
disabled?: boolean;
className?: string;
}
@observer
export class StringInput extends React.Component<ReactiveInputProps<string>> {
constructor(props: any) {
super(props);
makeObservable(this);
if (this.props.value.value === undefined) {
this.props.value.setAuto('');
}
this.props.value.setField(this.props.label);
}
@action.bound
onChange(event: React.ChangeEvent<HTMLInputElement>) {
this.props.value.set(event.currentTarget.value);
}
render() {
return <div className={'mb-1 l-no-bg'}>
{/*todo: disable background-color for label*/}
<FloatingLabel label={this.props.label} className={`${this.props.className} mt-0 mb-0`}>
<FormControl type='text' placeholder={this.props.label} disabled={this.props.disabled}
onChange={this.onChange} value={this.props.value.value}
className={`${this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`}/>
</FloatingLabel>
<FormText children={this.props.value.firstError} className={`text-danger mt-0 mb-0 d-block`}/>
</div>
}
}
@observer
export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
@observable showPassword = false;
constructor(props: any) {
super(props);
makeObservable(this);
if (this.props.value.value === undefined) {
this.props.value.setAuto('');
}
this.props.value.setField(this.props.label);
}
@action.bound
onChange(event: React.ChangeEvent<HTMLInputElement>) {
this.props.value.set(event.currentTarget.value);
}
@action.bound
toggleShowPassword() {
this.showPassword = !this.showPassword;
}
render() {
return <div className={'mb-1 l-no-bg'}>
<div className={'d-flex justify-content-between align-items-center'}>
<FloatingLabel label={this.props.label} className={`${this.props.className} w-100`}>
<FormControl type={`${this.showPassword ? 'text' : 'password'}`} placeholder={this.props.label}
disabled={this.props.disabled}
className={`${this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`}
onChange={this.onChange} value={this.props.value.value}/>
</FloatingLabel>
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"}>
<FontAwesomeIcon icon={this.showPassword ? 'eye-slash' : 'eye'}/>
</Button>
</div>
<FormText children={this.props.value.firstError} className={'text-danger d-block mt-0 mb-0'}/>
</div>
}
}
@observer
export class SelectButtonInput extends React.Component<ReactiveInputProps<string>> {
constructor(props: any) {
super(props);
makeObservable(this);
if (this.props.value.value === undefined) {
this.props.value.setAuto('');
}
this.props.value.setField(this.props.label);
}
@action.bound
onChange(event: React.ChangeEvent<HTMLInputElement>) {
this.props.value.set(event.currentTarget.value);
}
render() {
return <>
<ButtonGroup className={'d-block l-no-bg'}>
<ToggleButton key={'admin'} value={'admin'} id={`radio-admin`} type="radio"
variant={'outline-primary'} children={'Администратор'}
checked={this.props.value.value === 'admin'} onChange={this.onChange}/>
<ToggleButton key={'student'} id={`radio-student`} type="radio" value={'student'}
variant={'outline-primary'}
checked={this.props.value.value === 'student'} onChange={this.onChange}
children={'Студент'}/>
</ButtonGroup>
<FormText children={this.props.value.firstError} className={'text-danger d-block'}/>
</>
}
}

View File

@ -1,42 +1,40 @@
import {Component, ReactNode} from "react"; import {ReactNode} from "react";
import {Container} from "react-bootstrap"; import {Container} from "react-bootstrap";
import Footer from "./Footer";
import Header from "./Header"; import Header from "./Header";
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {observer} from "mobx-react"; import {ComponentContext} from "../../utils/ComponentContext";
import {NotificationContainer} from "../NotificationContainer";
import {Footer} from "./Footer";
@observer export abstract class DefaultPage extends ComponentContext {
class DefaultPage extends Component<any> {
get page(): ReactNode { get page(): ReactNode {
throw new Error('This is not abstract method, ' + throw new Error('This is not abstract method, ' +
'because mobx cant handle abstract methods. ' + 'because mobx cant handle abstract methods. ' +
'Please override this method in child class. ' + 'Please override this method in child class. ' +
'Do not call it directly.'); 'Do not call it directly.');
} }
declare context: RootStoreContextType;
static contextType = RootStoreContext;
render() { render() {
let isLoading = this.context.userStore.isLoading; const thinking = this.thinkStore.isThinking();
return <> return <>
<Header/> <Header/>
<Container className={"mt-5 mb-5"}> <Container className={"mt-5 mb-5"}>
{ {
isLoading && thinking &&
<div id='fullscreen-loader'> <div id='fullscreen-loader'>
<FontAwesomeIcon icon='gear' size="4x" spin/> <FontAwesomeIcon icon='gear' size="4x" spin/>
</div> </div>
} }
{ {
!isLoading && !thinking &&
this.page <>
<NotificationContainer/>
{this.page}
</>
} }
</Container> </Container>
<Footer/> <Footer/>
</> </>
} }
} }
export {DefaultPage};

View File

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

View File

@ -0,0 +1,41 @@
import {ComponentContext} from "../../utils/ComponentContext";
import {observer} from "mobx-react";
import {makeObservable} from "mobx";
import {Container, Nav, Navbar, NavbarText, NavLink} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
@observer
export class Footer extends ComponentContext {
constructor(props: any) {
super(props);
makeObservable(this);
}
render() {
return <footer>
<Navbar className="bg-body-tertiary">
<Container>
<div>
<NavbarText>Thesis Defence Management System &mdash; </NavbarText>
{
this.thinkStore.isThinking('updateVersion') &&
<FontAwesomeIcon icon='gear' spin/>
}
{
!this.thinkStore.isThinking('updateVersion') &&
<NavbarText>{this.sysInfoStore.version}</NavbarText>
}
</div>
<Nav>
<NavLink href="https://git.mskobaro.ru/mskobaro/TDMS">
<FontAwesomeIcon icon={findIconDefinition({iconName: 'github', prefix: 'fab'})} size="xl"/>
</NavLink>
</Nav>
</Container>
</Navbar>
</footer>
}
}

View File

@ -0,0 +1,101 @@
import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
import {RouterLink} from "mobx-state-router";
import {IAuthenticated} from "../../models/user";
import {RootStoreContext, RootStoreContextType} from "../../store/RootStoreContext";
import {observer} from "mobx-react";
import {post} from "../../utils/request";
import {LoginModal} from "../user/LoginModal";
import {ModalState} from "../../utils/modalState";
import {action, makeObservable} from "mobx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {ComponentContext} from "../../utils/ComponentContext";
@observer
class Header extends ComponentContext {
loginModalState = new ModalState();
constructor(props: any) {
super(props);
makeObservable(this);
}
render() {
const userStore = this.context.userStore;
const user = userStore.user;
let thinking = this.thinkStore.isThinking('updateCurrentUser');
return <>
<header>
<Navbar className="bg-body-tertiary" fixed="top">
<Container>
<Navbar.Brand>
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
</Navbar.Brand>
<Nav>
{
user.authenticated && userStore.isAdministrator() &&
<NavDropdown title="Пользователи">
<NavDropdown.Item as={RouterLink} routeName={'userList'} children={'Список'}/>
<NavDropdown.Item as={RouterLink} routeName={'userRegistration'}
children={'Зарегистрировать'}/>
</NavDropdown>
}
</Nav>
<Nav className="ms-auto">
{
thinking &&
<FontAwesomeIcon icon='gear' spin/>
}
{
user.authenticated && !thinking &&
<AuthenticatedItems/>
}
{
!user.authenticated && !thinking &&
<>
<Nav.Link onClick={this.loginModalState.open}>Войти</Nav.Link>
</>
}
</Nav>
</Container>
</Navbar>
</header>
<LoginModal modalState={this.loginModalState}/>
</>
}
}
@observer
class AuthenticatedItems extends ComponentContext<any, any> {
declare context: RootStoreContextType;
static contextType = RootStoreContext;
constructor(props: any) {
super(props);
makeObservable(this);
}
@action.bound
logout() {
post('user/logout').then(() => this.context.userStore.updateCurrentUser());
}
render() {
const userStore = this.context.userStore;
const user = userStore.user;
return <>
<Navbar.Text>Пользователь:</Navbar.Text>
<NavDropdown
title={(user as IAuthenticated).fullName}>
<NavDropdown.Item as={RouterLink} routeName='profile'>Моя страница</NavDropdown.Item>
<NavDropdown.Divider/>
<NavDropdown.Item onClick={this.logout}>Выйти</NavDropdown.Item>
</NavDropdown>
</>
}
}
export default Header;

View File

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

View File

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

@ -1,22 +0,0 @@
import {Container, Nav, Navbar} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {findIconDefinition} from "@fortawesome/fontawesome-svg-core";
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">
<FontAwesomeIcon icon={findIconDefinition({iconName:'github', prefix:'fab'})} size="xl"/>
</Nav.Link>
</Nav>
</Container>
</Navbar>
</footer>
)
}
export default Footer;

View File

@ -1,62 +0,0 @@
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,142 @@
import {ChangeEvent} from "react";
import {Button, FormControl, FormGroup, FormLabel, FormText, Modal} from "react-bootstrap";
import {ModalState} from "../../utils/modalState";
import {observer} from "mobx-react";
import {action, computed, makeObservable, observable, reaction} from "mobx";
import {post} from "../../utils/request";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {ComponentContext} from "../../utils/ComponentContext";
interface LoginModalProps {
modalState: ModalState;
}
@observer
export class LoginModal extends ComponentContext<LoginModalProps> {
@observable login = '';
@observable loginError = '';
@observable password = '';
@observable passwordError = '';
constructor(props: LoginModalProps) {
super(props);
makeObservable(this);
reaction(() => this.login, this.validateLogin);
reaction(() => this.password, this.validatePassword);
}
@action.bound
validateLogin() {
if (!this.login) {
this.loginError = 'Имя пользователя не может быть пустым';
} else if (this.login.length < 5) {
this.loginError = 'Имя пользователя должно быть не менее 5 символов';
} else if (this.login.length > 50) {
this.loginError = 'Имя пользователя должно быть не более 50 символов';
} else if (!/^[a-zA-Z0-9_]+$/.test(this.login)) {
this.loginError = 'Имя пользователя должно содержать только латинские буквы, цифры и знак подчеркивания';
} else {
this.loginError = '';
}
}
@action.bound
validatePassword() {
if (!this.password) {
this.passwordError = 'Пароль не может быть пустым';
} else if (this.password.length < 5) {
this.passwordError = 'Пароль должен быть не менее 5 символов';
} else if (this.password.length > 32) {
this.passwordError = 'Пароль должен быть не более 32 символов';
} else if (!/^[a-zA-Z0-9!@#$%^&*()_+]+$/.test(this.password)) {
this.passwordError = 'Пароль должен содержать только латинские буквы, цифры и специальные символы';
} else {
this.passwordError = '';
}
}
@computed
get loginButtonDisabled() {
return !this.login || !this.password || !!this.loginError || !!this.passwordError;
}
@action.bound
onLoginInput(event: ChangeEvent<HTMLInputElement>) {
this.login = event.target.value;
}
@action.bound
onPasswordInput(event: ChangeEvent<HTMLInputElement>) {
this.password = event.target.value;
}
@action.bound
tryLogin() {
if (this.loginButtonDisabled)
return;
this.thinkStore.think('loginModal');
post('user/login', {
username: this.login,
password: this.password
}).then(() => {
this.userStore.updateCurrentUser((user) => {
if (user.authenticated) {
this.routerStore.goTo('profile').then();
this.notificationStore.success('Вы успешно вошли в систему, ' + user.fullName, 'Успешный вход');
} else {
this.routerStore.goTo('root').then();
this.notificationStore.error('Произошла ошибка при попытке входа в систему', 'Ошибка входа');
}
});
}).finally(() => {
this.props.modalState.close();
this.thinkStore.completeAll('loginModal');
});
}
render() {
const open = this.props.modalState.isOpen;
const thinking = this.thinkStore.isThinking('loginModal');
return <Modal show={open} centered>
<Modal.Header>
<Modal.Title>Вход</Modal.Title>
</Modal.Header>
{
thinking &&
<Modal.Body>
<FontAwesomeIcon icon={'gear'} spin/>
</Modal.Body>
}
{
!thinking &&
<>
<Modal.Body>
<FormGroup className={'mb-3'}>
<FormLabel>Имя пользователя</FormLabel>
<FormControl type="text" onChange={this.onLoginInput}/>
{
this.loginError &&
<FormText className={'text-danger'}>{this.loginError}</FormText>
}
</FormGroup>
<FormGroup className={'mb-3'}>
<FormLabel>Пароль</FormLabel>
<FormControl type="password" onChange={this.onPasswordInput}/>
{
this.passwordError &&
<FormText className={'text-danger'}>{this.passwordError}</FormText>
}
</FormGroup>
</Modal.Body>
<Modal.Footer>
<Button variant={'primary'} onClick={this.tryLogin}
disabled={this.loginButtonDisabled}>Войти</Button>
<Button variant={'secondary'} onClick={this.props.modalState.close}>Закрыть</Button>
</Modal.Footer>
</>
}
</Modal>
}
}

View File

@ -0,0 +1,60 @@
import {observer} from "mobx-react";
import {action, makeObservable, observable, reaction, runInAction} from "mobx";
import {DefaultPage} from "../layout/DefaultPage";
import {IAuthenticated} from "../../models/user";
import {DataTable} from "../custom/DataTable";
import {get} from "../../utils/request";
import {Column, TableDescriptor} from "../../utils/tables";
@observer
export class UserList extends DefaultPage {
constructor(props: {}) {
super(props);
makeObservable(this);
}
componentDidMount() {
this.requestUsers();
reaction(() => this.users, () => {
if (typeof this.users === 'undefined') {
return;
}
this.tableDescriptor = new TableDescriptor(this.userColumns, this.users);
}, {fireImmediately: true});
}
@observable users?: IAuthenticated[];
@observable tableDescriptor?: TableDescriptor<IAuthenticated>;
userColumns = [
new Column('login', 'Логин'),
new Column('fullName', 'Полное имя'),
new Column('email', 'Email'),
new Column('phone', 'Телефон'),
new Column('createdAt', 'Дата создания'),
new Column('updatedAt', 'Дата обновления', (value: string) => value ? value : 'Не обновлялось'),
];
@action.bound
requestUsers() {
this.thinkStore.think('userList');
get<IAuthenticated[]>('user/get-all').then((users) => {
runInAction(() => {
this.users = users;
});
}).finally(() => {
this.thinkStore.completeAll('userList');
});
}
get page() {
return <>
{
this.tableDescriptor &&
<DataTable tableDescriptor={this.tableDescriptor}/>
}
</>
}
}

View File

@ -1,7 +1,7 @@
import {DefaultPage} from "./layout/DefaultPage"; import {DefaultPage} from "../layout/DefaultPage";
import {Col, Form, Row} from "react-bootstrap"; import {Col, Form, Row} from "react-bootstrap";
import {observer} from "mobx-react"; import {observer} from "mobx-react";
import {RootStoreContext, type RootStoreContextType} from "../../context/RootStoreContext"; import {RootStoreContext, type RootStoreContextType} from "../../store/RootStoreContext";
import {IAuthenticated} from "../../models/user"; import {IAuthenticated} from "../../models/user";
import {Component} from "react"; import {Component} from "react";
import {dateConverter} from "../../utils/converters"; import {dateConverter} from "../../utils/converters";
@ -151,7 +151,7 @@ class StudentInfo extends Component<{student: IStudent}> {
} }
} }
export default class UserProfile extends DefaultPage { export default class UserProfilePage extends DefaultPage {
declare context: RootStoreContextType; declare context: RootStoreContextType;
static contextType = RootStoreContext; static contextType = RootStoreContext;

View File

@ -0,0 +1,87 @@
import {observer} from "mobx-react";
import {DefaultPage} from "../layout/DefaultPage";
import {action, computed, makeObservable, observable} from "mobx";
import {Button, Col, Form, Row} from "react-bootstrap";
import {UserRegistrationDTO} from "../../models/registration";
import {post} from "../../utils/request";
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
import {PasswordInput, SelectButtonInput, StringInput} from "../custom/controls/ReactiveControls";
import {
email,
loginChars,
loginLength,
nameChars,
nameLength,
passwordChars,
passwordLength,
phone,
required
} from "../../utils/reactive/validators";
@observer
export class UserRegistration extends DefaultPage {
constructor(props: any) {
super(props);
makeObservable(this);
}
@observable login = new ReactiveValue<string>().addValidator(required).addValidator(loginLength).addValidator(loginChars);
@observable password = new ReactiveValue<string>().addValidator(required).addValidator(passwordLength).addValidator(passwordChars);
@observable fullName = new ReactiveValue<string>().addValidator(required).addValidator(nameLength).addValidator(nameChars);
@observable email = new ReactiveValue<string>().addValidator(required).addValidator(email);
@observable numberPhone = new ReactiveValue<string>().addValidator(required).addValidator(phone).setAuto('+7');
@observable accountType = new ReactiveValue<string>().addValidator(required).addValidator((value) => {
if (!['student', 'admin'].includes(value)) {
return 'Тип аккаунта должен быть "СТУДЕНТ" или "АДМИНИСТРАТОР"';
}
});
@computed
get formInvalid() {
return this.login.invalid || !this.login.touched
|| this.password.invalid || !this.password.touched
|| this.fullName.invalid || !this.fullName.touched
|| this.email.invalid || !this.email.touched
|| this.numberPhone.invalid || !this.numberPhone.touched
|| this.accountType.invalid || !this.accountType.touched;
}
@action.bound
submit() {
post('user/register', {
login: this.login.value,
password: this.password.value,
fullName: this.fullName.value,
email: this.email.value,
numberPhone: this.numberPhone.value,
// studentData: { groupId: 1 }
} as UserRegistrationDTO).then(() => {
this.notificationStore.success('Пользователь успешно зарегистрирован');
}).catch(() => {
this.notificationStore.error('Ошибка регистрации пользователя');
});
}
get page() {
return <div className={'w-75 ms-auto me-auto'}>
<Form>
<Row>
<Col>
<StringInput value={this.login} label={"Логин"}/>
<PasswordInput value={this.password} label={"Пароль"}/>
</Col>
<Col>
<StringInput value={this.fullName} label={"Полное имя"}/>
<StringInput value={this.email} label={"Почта"}/>
<StringInput value={this.numberPhone} label={"Телефон"}/>
</Col>
</Row>
<SelectButtonInput value={this.accountType} label={'Тип аккаунта'}/>
<Button disabled={this.formInvalid} onClick={this.submit}>Зарегистрировать</Button>
</Form>
</div>
}
}

View File

@ -0,0 +1,12 @@
export interface UserRegistrationDTO {
login: string,
password: string,
fullName: string,
email: string,
numberPhone: string,
studentData?: StudentRegistrationDTO
}
export interface StudentRegistrationDTO {
groupId: number;
}

View File

@ -1,7 +1,7 @@
// todo: update
export enum Role { export enum Role {
STUDENT = 'ROLE_STUDENT', ADMINISTRATOR = "ROLE_ADMINISTRATOR",
TUTOR = 'ROLE_TUTOR', STUDENT = "ROLE_STUDENT",
DIRECTOR = 'ROLE_DIRECTOR',
} }
export interface IAuthority { export interface IAuthority {

View File

@ -6,6 +6,12 @@ export const routes: Route[] = [{
}, { }, {
name: 'profile', name: 'profile',
pattern: '/profile', pattern: '/profile',
}, {
name: 'userList',
pattern: '/users',
}, {
name: 'userRegistration',
pattern: '/user-registration',
}, { }, {
name: 'error', name: 'error',
pattern: '/error', pattern: '/error',

Some files were not shown because too many files have changed in this diff Show More