Compare commits
362 Commits
master
...
release/v1
Author | SHA1 | Date | |
---|---|---|---|
eb272baa38 | |||
a0ff624481 | |||
cd6f25deba | |||
0f47a98ad9 | |||
3279ef594b | |||
5bc729eb66 | |||
5317b7b563 | |||
665544236f | |||
f203ee71f0 | |||
d8dbf1562f | |||
dead9f89bb | |||
8c932cf0be | |||
80e74b34c1 | |||
b095ca9749 | |||
8fad070a9c | |||
6c20713d81 | |||
fc5ec1fd54 | |||
ed99fce9b8 | |||
2ccc476686 | |||
84d7b095f0 | |||
4605c81895 | |||
0788c36bd2 | |||
f5dbc46856 | |||
ebec0a2d2b | |||
4fc28378c5 | |||
98ee3c389c | |||
428c2dc3ba | |||
4970dd782a | |||
2e48b0067f | |||
71e8eca5f4 | |||
1f3aaca3cf | |||
b49df925d4 | |||
c9ebddf27a | |||
f5739647b2 | |||
26dbf608b9 | |||
2b89dd07a9 | |||
1c981fb7bf | |||
de5dc274d7 | |||
c5ecf00932 | |||
412751e30f | |||
076d6498a1 | |||
88d78dfab3 | |||
332e5a013b | |||
e8450400c7 | |||
65709e1f83 | |||
1e204c948c | |||
0ced152fc9 | |||
6f9bfd3880 | |||
ae0f437e2c | |||
592e8a1b42 | |||
a27549092b | |||
f27d07fb5a | |||
535bafa73a | |||
fba842acc3 | |||
31087a57c9 | |||
24c75e4306 | |||
dee89b278b | |||
565252382c | |||
80dc2e412c | |||
b1250616a7 | |||
c51a9cecc9 | |||
c189cc6955 | |||
c7b401eae7 | |||
837205f66e | |||
c6ca717b89 | |||
497b7f146b | |||
3326b17d74 | |||
80b46754ad | |||
3898463bc4 | |||
279ca9647b | |||
ab660f69c8 | |||
1c27bffa73 | |||
76fd1347ce | |||
820828276e | |||
ac4804e864 | |||
21055176ac | |||
f42caa3a45 | |||
57f4d1b822 | |||
cdb738ca42 | |||
d45c865f4e | |||
d87654a355 | |||
75c1aebea6 | |||
17e20fee2e | |||
e8ca2c42a6 | |||
9133b57a1b | |||
05ca45db49 | |||
7e2016080f | |||
fe24dfcd6a | |||
2041a187e7 | |||
098fca5df8 | |||
e0cff050de | |||
2f7c77e764 | |||
7e82d4a520 | |||
07edf0e5ad | |||
5b8d9e1f4a | |||
de2f909ed6 | |||
2e2cee2ca7 | |||
8c340e2a97 | |||
9abdb1ac43 | |||
42f0b8ee0e | |||
41b5bb571b | |||
2c112d00df | |||
f89136669d | |||
612efcb91c | |||
8d4c482bbd | |||
160c7505f0 | |||
c62ec33130 | |||
55371cb675 | |||
a36e0694ec | |||
1a0d539e76 | |||
79151e7da8 | |||
039d323643 | |||
a5f9e67647 | |||
21866d54cb | |||
eba11f515d | |||
70780d620a | |||
7d3952c373 | |||
0ecb796d54 | |||
2e389b252c | |||
984791f193 | |||
b640902777 | |||
4222e4702f | |||
993e66a084 | |||
6797adac4f | |||
8e58c83526 | |||
2e64caf6ea | |||
9c56fa582b | |||
b2a0a6dd7c | |||
78f589bb18 | |||
ca02509b97 | |||
f3a757d33d | |||
34addd930f | |||
054d319f7c | |||
2addd2aa78 | |||
bba9431733 | |||
6fb5a83183 | |||
ea538ac340 | |||
63216f3b66 | |||
e088374b14 | |||
ded577f40a | |||
32621515db | |||
fdf0ecc9ef | |||
5400e0c873 | |||
1fd6c8657a | |||
d09011d25a | |||
a902d9eb81 | |||
827cdaf9f9 | |||
d2ba2d982c | |||
79118c5283 | |||
8cd8277c22 | |||
ae46823685 | |||
7aa37618e0 | |||
04b6687181 | |||
7a741d7783 | |||
f17ee43805 | |||
b67f0a82ed | |||
815c860dc0 | |||
f0544ff42e | |||
c9c6a99fe9 | |||
aa1e1000fa | |||
a353b4c3f8 | |||
b81fe6d8c1 | |||
8a103831eb | |||
62ccf94222 | |||
271df127a6 | |||
7c79f7d840 | |||
b8728cd490 | |||
7db4dc2c86 | |||
348b78b84e | |||
4c93ed282d | |||
1bdf40f31f | |||
31c36443e1 | |||
43b6ab7934 | |||
f79c7c7db9 | |||
53a0439edb | |||
78a242f4c3 | |||
0a9c98cbf9 | |||
164d575d98 | |||
bf3a9d4b36 | |||
081c814036 | |||
f6f7ed6c86 | |||
d2ef99d0b2 | |||
38ec80a566 | |||
85802aa514 | |||
f2aa274d0a | |||
62a859b44c | |||
6f02021fe7 | |||
526bf5682b | |||
9287acf7d2 | |||
2efdc6dbfe | |||
25b6c7d691 | |||
61218c38a0 | |||
d84011cd71 | |||
4138c70007 | |||
9dd505a608 | |||
79fb05d428 | |||
81f2f995b0 | |||
f3063c5322 | |||
43011457d6 | |||
4240ad8110 | |||
a3a42dd5c2 | |||
b25be758ad | |||
7df4c8e4b6 | |||
f55d701ff3 | |||
d3a60d2a30 | |||
470031af39 | |||
916b3795ed | |||
f4ad1518ef | |||
ac7bbde75e | |||
47a57693f8 | |||
d05ba5349f | |||
5fde5bd396 | |||
8408b80c35 | |||
b14ae26a48 | |||
3c9694de08 | |||
e3db6b73e0 | |||
58ceca5313 | |||
f749ed42f5 | |||
6029ea3c2c | |||
656d7dca0b | |||
e3dd0a8419 | |||
3149f50586 | |||
e1123cf36b | |||
930edd4c2c | |||
7e283fe643 | |||
c427006283 | |||
e1ad287da1 | |||
f9750ef039 | |||
17961ccefc | |||
5d308f1a24 | |||
f5deeec6c9 | |||
29c9c10a53 | |||
c02240077f | |||
2d67a565ca | |||
ba8ccf8b7e | |||
e7ed69169c | |||
eefb049e0e | |||
07d7fec24f | |||
22793c7882 | |||
9bf9eabad7 | |||
966ab9bdda | |||
481839159c | |||
59785f600f | |||
fb6e119a34 | |||
35eb1eab39 | |||
08f13108d8 | |||
ae0b9daefa | |||
3f30b98cf9 | |||
af284e945f | |||
b62ddc9015 | |||
d1a806545d | |||
7b779463bb | |||
baedc667b7 | |||
0e9bb04b96 | |||
36a78a8284 | |||
7a1281692e | |||
fb736a1c34 | |||
d7299a1afd | |||
817f9d2308 | |||
99ecc4af5c | |||
202098b723 | |||
266e66a35c | |||
41689bca7f | |||
c06ed8b479 | |||
dfdc2ec109 | |||
6716ee9cf0 | |||
0d3461b769 | |||
22579038d3 | |||
d8f2f51cd7 | |||
ca18804a33 | |||
97d2fae79e | |||
628faf7e68 | |||
05dadff455 | |||
ed0143cda1 | |||
d02fb8becb | |||
5536162521 | |||
ee2351b5ed | |||
29485368f8 | |||
e3e7c4550f | |||
e3d74c373e | |||
59eccf8b93 | |||
477cec2f98 | |||
2b4065bbdf | |||
6396d8a318 | |||
84872a5d6d | |||
5cfc07ab7d | |||
14aedf9d46 | |||
aad3c74fba | |||
1caaa4759e | |||
1b8c82949a | |||
511c0db3ee | |||
2fe2dac76b | |||
ed823570f1 | |||
a00dc8ccd6 | |||
ce3bd23673 | |||
69a554ce76 | |||
70c9a6347e | |||
c8c9b7f456 | |||
851eccd876 | |||
93a1b6ea8e | |||
810c8ec5cf | |||
37eb798269 | |||
dfc9caf921 | |||
88b4eb594e | |||
2403cb35ec | |||
07e04b99c5 | |||
96a820dead | |||
a1a83ce2e6 | |||
64e8d1b8ce | |||
8852351ca6 | |||
fb6ca5df77 | |||
a4dfccbc22 | |||
335298fd91 | |||
7b584b2dc5 | |||
77136cc7ec | |||
bc99007d94 | |||
f25c815506 | |||
7a0a51f76a | |||
92504295f0 | |||
c5c4a2a8da | |||
806c3eeb17 | |||
3294325ad0 | |||
459d11dd6b | |||
2b02cfb616 | |||
98ebe5ffdb | |||
7b8bff8e54 | |||
f1ed45af96 | |||
d5b3859e86 | |||
788103c8a5 | |||
e6cc9437d5 | |||
8f334ae5c2 | |||
3c3bdc6155 | |||
e1c3165ad3 | |||
e6c62eae09 | |||
cea8a14f8b | |||
2d973eee3d | |||
66dc5e3e38 | |||
9493d4340f | |||
194dd1b729 | |||
e7c05b4a68 | |||
bd3a11486d | |||
28e862c670 | |||
9e4320f2d3 | |||
03ccec2119 | |||
96a120f017 | |||
def7111651 | |||
b4bbc413f2 | |||
6f5a84f645 | |||
f9a04ee84a | |||
84214e38cc | |||
40279ab2b8 | |||
cb55567519 | |||
8e628d17da | |||
9e9d4e06fd | |||
cfbd847d9a | |||
386272d493 | |||
924f97332f | |||
a389eb0a70 | |||
8028d40005 | |||
f7998a1798 | |||
9f1c3cd648 | |||
4eecc19f4f |
117
.env
Normal file
117
.env
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# The .env configuration file
|
||||||
|
# Please DO NOT share this file, it contains confidential data.
|
||||||
|
|
||||||
|
# All variables are specified according to this rule:
|
||||||
|
# DESCRIPTION - information about what the variable is responsible for
|
||||||
|
# TYPE - the type of the variable (string, boolean, etc.)
|
||||||
|
# Any additional information
|
||||||
|
# SOME_ENV_CODE=data - default data. If specified, then the variable is optional
|
||||||
|
|
||||||
|
# General
|
||||||
|
|
||||||
|
# The path to save the data
|
||||||
|
# string
|
||||||
|
# (optional)
|
||||||
|
# Saving logs (if the full path is not specified),
|
||||||
|
# databases (if Sqlite) and other data that should be saved in a place other than the place where the program is launched.
|
||||||
|
# REQUIRED if the application is inside the container
|
||||||
|
# If you want to change this value, you need to change the values in Settings.json and move the file itself to the desired location.
|
||||||
|
PATH_TO_SAVE=
|
||||||
|
|
||||||
|
# The actual sub path to the api
|
||||||
|
# string
|
||||||
|
# (optional)
|
||||||
|
ACTUAL_SUB_PATH=
|
||||||
|
|
||||||
|
# The sub path to the swagger
|
||||||
|
# string
|
||||||
|
# (optional)
|
||||||
|
SWAGGER_SUB_PATH=swagger
|
||||||
|
|
||||||
|
# Internal port configuration
|
||||||
|
# integer
|
||||||
|
# (optional)
|
||||||
|
# Specify the internal port on which the server will listen.
|
||||||
|
INTERNAL_PORT=8080
|
||||||
|
|
||||||
|
# Security
|
||||||
|
|
||||||
|
# JWT signature token
|
||||||
|
# string (UTF8)
|
||||||
|
# This token will be used to create and verify the signature of JWT tokens.
|
||||||
|
# The token must be equal to 64 characters
|
||||||
|
SECURITY_SIGNING_TOKEN=
|
||||||
|
|
||||||
|
# Token for JWT encryption
|
||||||
|
# string (UTF8)
|
||||||
|
# This token will be used to encrypt and decrypt JWT tokens.
|
||||||
|
# The token must be equal to 32 characters
|
||||||
|
SECURITY_ENCRYPTION_TOKEN=
|
||||||
|
|
||||||
|
# Time in minutes, which indicates after which time the Refresh Token will become invalid
|
||||||
|
# integer
|
||||||
|
# The token indicates how long after the user is inactive, he will need to log in again
|
||||||
|
SECURITY_LIFE_TIME_RT=1440
|
||||||
|
|
||||||
|
# The time in a minute, which indicates that this is exactly what it takes to become a non-state
|
||||||
|
# integer
|
||||||
|
# Do not specify a time that is too long or too short. Optimally 5 > x > 60
|
||||||
|
SECURITY_LIFE_TIME_JWT=15
|
||||||
|
|
||||||
|
# Time in minutes, which indicates after which time the token of the first factor will become invalid
|
||||||
|
# integer
|
||||||
|
# Do not specify a short time. The user must be able to log in using the second factor
|
||||||
|
SECURITY_LIFE_TIME_1_FA=15
|
||||||
|
|
||||||
|
# An identifier that points to the server that created the token
|
||||||
|
# string
|
||||||
|
SECURITY_JWT_ISSUER=
|
||||||
|
|
||||||
|
# ID of the audience for which the token is intended
|
||||||
|
# string
|
||||||
|
SECURITY_JWT_AUDIENCE=
|
||||||
|
|
||||||
|
### Hashing
|
||||||
|
|
||||||
|
# In order to set up hashing correctly, you need to start from the security requirements
|
||||||
|
# You can use the settings that were used in https://github.com/P-H-C/phc-winner-argon2
|
||||||
|
# These parameters have a STRONG impact on performance
|
||||||
|
# When testing the system, these values were used:
|
||||||
|
# 10 <= SECURITY_HASH_ITERATION <= 25 iterations
|
||||||
|
# 16384 <= SECURITY_HASH_MEMORY <= 32768 KB
|
||||||
|
# 4 <= SECURITY_HASH_PARALLELISM <= 8 lines
|
||||||
|
# If we take all the large values, it will take a little more than 1 second to get the hash. If this time is critical, reduce the parameters
|
||||||
|
|
||||||
|
# The number of iterations used to hash passwords in the Argon2 algorithm
|
||||||
|
# integer
|
||||||
|
# This parameter determines the number of iterations that the Argon2 algorithm goes through when hashing passwords.
|
||||||
|
# Increasing this value can improve security by increasing the time it takes to calculate the password hash.
|
||||||
|
# The average number of iterations to increase the security level should be set to at least 10.
|
||||||
|
SECURITY_HASH_ITERATION=
|
||||||
|
|
||||||
|
# The amount of memory used to hash passwords in the Argon2 algorithm
|
||||||
|
# integer
|
||||||
|
# 65536
|
||||||
|
# This parameter determines the number of kilobytes of memory that will be used for the password hashing process.
|
||||||
|
# Increasing this value may increase security, but it may also require more system resources.
|
||||||
|
SECURITY_HASH_MEMORY=
|
||||||
|
|
||||||
|
# Parallelism determines how many of the memory fragments divided into strips will be used to generate a hash
|
||||||
|
# integer
|
||||||
|
# This value affects the hash itself, but can be changed to achieve an ideal execution time, taking into account the processor and the number of cores.
|
||||||
|
SECURITY_HASH_PARALLELISM=
|
||||||
|
|
||||||
|
# The size of the output hash generated by the password hashing algorithm
|
||||||
|
# integer
|
||||||
|
SECURITY_HASH_SIZE=32
|
||||||
|
|
||||||
|
# Additional protection for Argon2
|
||||||
|
# string (BASE64)
|
||||||
|
# (optional)
|
||||||
|
# We recommend installing a token so that even if the data is compromised, an attacker cannot brute force a password without a token
|
||||||
|
SECURITY_HASH_TOKEN=
|
||||||
|
|
||||||
|
# The size of the salt used to hash passwords
|
||||||
|
# integer
|
||||||
|
# The salt is a random value added to the password before hashing to prevent the use of rainbow hash tables and other attacks.
|
||||||
|
SECURITY_SALT_SIZE=16
|
85
.gitea/workflows/deploy-stage.yaml
Normal file
85
.gitea/workflows/deploy-stage.yaml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
name: Build and Deploy Docker Container
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
[master, 'release/*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
docker build --build-arg NUGET_USERNAME=${{ secrets.NUGET_USERNAME }} --build-arg NUGET_PASSWORD=${{ secrets.NUGET_PASSWORD }} -t ${{ secrets.DOCKER_USERNAME }}/mirea-backend:latest .
|
||||||
|
docker push ${{ secrets.DOCKER_USERNAME }}/mirea-backend:latest
|
||||||
|
|
||||||
|
- name: Start ssh-agent
|
||||||
|
id: ssh-agent
|
||||||
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Deploy to Server
|
||||||
|
env:
|
||||||
|
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||||
|
SSH_USER: ${{ secrets.SSH_USER }}
|
||||||
|
DOCKER_IMAGE: ${{ secrets.DOCKER_USERNAME }}/mirea-backend:latest
|
||||||
|
PATH_TO_SAVE: /data
|
||||||
|
SECURITY_SIGNING_TOKEN: ${{ secrets.SECURITY_SIGNING_TOKEN }}
|
||||||
|
SECURITY_ENCRYPTION_TOKEN: ${{ secrets.SECURITY_ENCRYPTION_TOKEN }}
|
||||||
|
SECURITY_LIFE_TIME_RT: ${{ secrets.SECURITY_LIFE_TIME_RT }}
|
||||||
|
SECURITY_LIFE_TIME_JWT: ${{ secrets.SECURITY_LIFE_TIME_JWT }}
|
||||||
|
SECURITY_LIFE_TIME_1_FA: ${{ secrets.SECURITY_LIFE_TIME_1_FA }}
|
||||||
|
SECURITY_JWT_ISSUER: ${{ secrets.SECURITY_JWT_ISSUER }}
|
||||||
|
SECURITY_JWT_AUDIENCE: ${{ secrets.SECURITY_JWT_AUDIENCE }}
|
||||||
|
SECURITY_HASH_ITERATION: ${{ secrets.SECURITY_HASH_ITERATION }}
|
||||||
|
SECURITY_HASH_MEMORY: ${{ secrets.SECURITY_HASH_MEMORY }}
|
||||||
|
SECURITY_HASH_PARALLELISM: ${{ secrets.SECURITY_HASH_PARALLELISM }}
|
||||||
|
SECURITY_HASH_SIZE: ${{ secrets.SECURITY_HASH_SIZE }}
|
||||||
|
SECURITY_HASH_TOKEN: ${{ secrets.SECURITY_HASH_TOKEN }}
|
||||||
|
SECURITY_SALT_SIZE: ${{ secrets.SECURITY_SALT_SIZE }}
|
||||||
|
run: |
|
||||||
|
ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts
|
||||||
|
ssh $SSH_USER@$SSH_HOST "
|
||||||
|
docker pull $DOCKER_IMAGE &&
|
||||||
|
docker stop mirea-backend || true &&
|
||||||
|
docker rm mirea-backend || true &&
|
||||||
|
docker run -d --name mirea-backend -p 8085:8080 \
|
||||||
|
--restart=on-failure:10 \
|
||||||
|
-v mirea-data:/data \
|
||||||
|
-e PATH_TO_SAVE=$PATH_TO_SAVE \
|
||||||
|
-e SECURITY_SIGNING_TOKEN=$SECURITY_SIGNING_TOKEN \
|
||||||
|
-e SECURITY_ENCRYPTION_TOKEN=$SECURITY_ENCRYPTION_TOKEN \
|
||||||
|
-e SECURITY_LIFE_TIME_RT=$SECURITY_LIFE_TIME_RT \
|
||||||
|
-e SECURITY_LIFE_TIME_JWT=$SECURITY_LIFE_TIME_JWT \
|
||||||
|
-e SECURITY_LIFE_TIME_1_FA=$SECURITY_LIFE_TIME_1_FA \
|
||||||
|
-e SECURITY_JWT_ISSUER=$SECURITY_JWT_ISSUER \
|
||||||
|
-e SECURITY_JWT_AUDIENCE=$SECURITY_JWT_AUDIENCE \
|
||||||
|
-e SECURITY_HASH_ITERATION=$SECURITY_HASH_ITERATION \
|
||||||
|
-e SECURITY_HASH_MEMORY=$SECURITY_HASH_MEMORY \
|
||||||
|
-e SECURITY_HASH_PARALLELISM=$SECURITY_HASH_PARALLELISM \
|
||||||
|
-e SECURITY_HASH_SIZE=$SECURITY_HASH_SIZE \
|
||||||
|
-e SECURITY_HASH_TOKEN=$SECURITY_HASH_TOKEN \
|
||||||
|
-e SECURITY_SALT_SIZE=$SECURITY_SALT_SIZE \
|
||||||
|
-e ACTUAL_SUB_PATH=api \
|
||||||
|
-e SWAGGER_SUB_PATH=swagger \
|
||||||
|
-e TZ=Europe/Moscow \
|
||||||
|
$DOCKER_IMAGE
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Remove all keys from ssh-agent
|
||||||
|
run: ssh-add -D
|
29
.gitea/workflows/test.yaml
Normal file
29
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: .NET Test Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
[master, 'release/*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up .NET Core
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build the solution
|
||||||
|
run: dotnet build --configuration Release
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: dotnet test --configuration Release --no-build --no-restore --verbosity normal
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -361,3 +361,5 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/ApiDto/ApiDtoDocs.xml
|
||||||
|
/Endpoint/docs.xml
|
||||||
|
42
ApiDto/ApiDto.csproj
Normal file
42
ApiDto/ApiDto.csproj
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Company>Winsomnia</Company>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.0.3.0</FileVersion>
|
||||||
|
<AssemblyName>Mirea.Api.Dto</AssemblyName>
|
||||||
|
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||||
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
|
<DocumentationFile>ApiDtoDocs.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="ApiDtoDocs.xml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<CopyAllFilesToSingleFolderForPackageDependsOn>
|
||||||
|
CopyXmlDocuments;
|
||||||
|
$(CopyAllFilesToSingleFolderForPackageDependsOn);
|
||||||
|
</CopyAllFilesToSingleFolderForPackageDependsOn>
|
||||||
|
<CopyAllFilesToSingleFolderForMsdeployDependsOn>
|
||||||
|
CopyXmlDocuments;
|
||||||
|
$(CopyAllFilesToSingleFolderForMsdeployDependsOn);
|
||||||
|
</CopyAllFilesToSingleFolderForMsdeployDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Target Name="CopyXmlDocuments">
|
||||||
|
<ItemGroup>
|
||||||
|
<XmlDocuments Include="$(OutDir)*.xml" />
|
||||||
|
<FilesForPackagingFromProject Include="%(XmlDocuments.Identity)">
|
||||||
|
<DestinationRelativePath>bin\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
|
||||||
|
</FilesForPackagingFromProject>
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
12
ApiDto/Common/AuthRoles.cs
Normal file
12
ApiDto/Common/AuthRoles.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Mirea.Api.Dto.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An enumeration that indicates which role the user belongs to
|
||||||
|
/// </summary>
|
||||||
|
public enum AuthRoles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Administrator
|
||||||
|
/// </summary>
|
||||||
|
Admin
|
||||||
|
}
|
22
ApiDto/Common/PairPeriodTime.cs
Normal file
22
ApiDto/Common/PairPeriodTime.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pair of time periods.
|
||||||
|
/// </summary>
|
||||||
|
public class PairPeriodTime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the start time of the period.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public TimeOnly Start { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the end time of the period.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public TimeOnly End { get; set; }
|
||||||
|
}
|
26
ApiDto/Requests/Configuration/CacheRequest.cs
Normal file
26
ApiDto/Requests/Configuration/CacheRequest.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to configure cache settings.
|
||||||
|
/// </summary>
|
||||||
|
public class CacheRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the server address.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Server { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the port number.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password.
|
||||||
|
/// </summary>
|
||||||
|
public string? Password { get; set; }
|
||||||
|
}
|
44
ApiDto/Requests/Configuration/DatabaseRequest.cs
Normal file
44
ApiDto/Requests/Configuration/DatabaseRequest.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to configure the database connection settings.
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the server address.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Server { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the port number.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the database name.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Database { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the username.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether SSL is enabled.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public bool Ssl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password.
|
||||||
|
/// </summary>
|
||||||
|
public string? Password { get; set; }
|
||||||
|
}
|
45
ApiDto/Requests/Configuration/EmailRequest.cs
Normal file
45
ApiDto/Requests/Configuration/EmailRequest.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to configure email settings.
|
||||||
|
/// </summary>
|
||||||
|
public class EmailRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the server address.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Server { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the email address from which emails will be sent.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string From { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password for the email account.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the port number.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether SSL is enabled.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public bool Ssl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the username.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string User { get; set; }
|
||||||
|
}
|
25
ApiDto/Requests/Configuration/LoggingRequest.cs
Normal file
25
ApiDto/Requests/Configuration/LoggingRequest.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to configure logging settings.
|
||||||
|
/// </summary>
|
||||||
|
public class LoggingRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether logging to file is enabled.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public bool EnableLogToFile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the log file name.
|
||||||
|
/// </summary>
|
||||||
|
public string? LogFileName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the log file path.
|
||||||
|
/// </summary>
|
||||||
|
public string? LogFilePath { get; set; }
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to configure the schedule settings.
|
||||||
|
/// </summary>
|
||||||
|
public class ScheduleConfigurationRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the cron expression for updating the schedule.
|
||||||
|
/// </summary>
|
||||||
|
public string? CronUpdateSchedule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the start date of the term.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateOnly StartTerm { get; set; }
|
||||||
|
}
|
36
ApiDto/Requests/CreateUserRequest.cs
Normal file
36
ApiDto/Requests/CreateUserRequest.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for creating a user.
|
||||||
|
/// </summary>
|
||||||
|
public class CreateUserRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the email address of the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The email address is a required field.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the username of the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The username is a required field.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password of the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The password is a required field.
|
||||||
|
/// </remarks>
|
||||||
|
[Required]
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
21
ApiDto/Requests/LoginRequest.cs
Normal file
21
ApiDto/Requests/LoginRequest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to receive protected content
|
||||||
|
/// </summary>
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Login or Email to identify the client.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client's password.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
37
ApiDto/Requests/ScheduleRequest.cs
Normal file
37
ApiDto/Requests/ScheduleRequest.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
namespace Mirea.Api.Dto.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request object for retrieving schedules based on various filters.
|
||||||
|
/// </summary>
|
||||||
|
public class ScheduleRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an array of group IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This array can contain null values.</remarks>
|
||||||
|
public int[]? Groups { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to retrieve schedules for even weeks.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This property can contain null.</remarks>
|
||||||
|
public bool? IsEven { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an array of discipline IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This array can contain null values.</remarks>
|
||||||
|
public int[]? Disciplines { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an array of professor IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This array can contain null values.</remarks>
|
||||||
|
public int[]? Professors { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an array of lecture hall IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This array can contain null values.</remarks>
|
||||||
|
public int[]? LectureHalls { get; set; } = null;
|
||||||
|
}
|
17
ApiDto/Responses/AuthenticationStep.cs
Normal file
17
ApiDto/Responses/AuthenticationStep.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the steps required after a login attempt.
|
||||||
|
/// </summary>
|
||||||
|
public enum AuthenticationStep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No additional steps required; the user is successfully logged in.
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TOTP (Time-based One-Time Password) is required for additional verification.
|
||||||
|
/// </summary>
|
||||||
|
TotpRequired,
|
||||||
|
}
|
26
ApiDto/Responses/CampusBasicInfoResponse.cs
Normal file
26
ApiDto/Responses/CampusBasicInfoResponse.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents basic information about a campus.
|
||||||
|
/// </summary>
|
||||||
|
public class CampusBasicInfoResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the campus.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the code name of the campus.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string CodeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the full name of the campus (optional).
|
||||||
|
/// </summary>
|
||||||
|
public string? FullName { get; set; }
|
||||||
|
}
|
31
ApiDto/Responses/CampusDetailsResponse.cs
Normal file
31
ApiDto/Responses/CampusDetailsResponse.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents detailed information about a campus.
|
||||||
|
/// </summary>
|
||||||
|
public class CampusDetailsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the campus.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the code name of the campus.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string CodeName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the full name of the campus (optional).
|
||||||
|
/// </summary>
|
||||||
|
public string? FullName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the address of the campus (optional).
|
||||||
|
/// </summary>
|
||||||
|
public string? Address { get; set; }
|
||||||
|
}
|
21
ApiDto/Responses/DisciplineResponse.cs
Normal file
21
ApiDto/Responses/DisciplineResponse.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a discipline.
|
||||||
|
/// </summary>
|
||||||
|
public class DisciplineResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the discipline.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the discipline.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
}
|
22
ApiDto/Responses/ErrorResponse.cs
Normal file
22
ApiDto/Responses/ErrorResponse.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class for providing information about an error
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The text or translation code of the error. This field may not contain information in specific scenarios.
|
||||||
|
/// For example, it might be empty for HTTP 204 responses where no content is returned or if the validation texts have not been configured.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Error { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// In addition to returning the response code in the header, it is also duplicated in this field.
|
||||||
|
/// Represents the HTTP response code.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required int Code { get; set; }
|
||||||
|
}
|
21
ApiDto/Responses/FacultyResponse.cs
Normal file
21
ApiDto/Responses/FacultyResponse.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents basic information about a faculty.
|
||||||
|
/// </summary>
|
||||||
|
public class FacultyResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the faculty.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the faculty.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
}
|
37
ApiDto/Responses/GroupDetailsResponse.cs
Normal file
37
ApiDto/Responses/GroupDetailsResponse.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents detailed information about a group.
|
||||||
|
/// </summary>
|
||||||
|
public class GroupDetailsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the group.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the group.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the course number of the group.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int CourseNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the faculty to which the group belongs (optional).
|
||||||
|
/// </summary>
|
||||||
|
public int? FacultyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the faculty to which the group belongs (optional).
|
||||||
|
/// </summary>
|
||||||
|
public string? FacultyName { get; set; }
|
||||||
|
}
|
32
ApiDto/Responses/GroupResponse.cs
Normal file
32
ApiDto/Responses/GroupResponse.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents basic information about a group.
|
||||||
|
/// </summary>
|
||||||
|
public class GroupResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the group.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the group.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the course number of the group.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int CourseNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the faculty to which the group belongs (optional).
|
||||||
|
/// </summary>
|
||||||
|
public int? FacultyId { get; set; }
|
||||||
|
}
|
37
ApiDto/Responses/LectureHallDetailsResponse.cs
Normal file
37
ApiDto/Responses/LectureHallDetailsResponse.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the detailed response model for a lecture hall.
|
||||||
|
/// </summary>
|
||||||
|
public class LectureHallDetailsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the lecture hall.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the lecture hall.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the campus to which the lecture hall belongs.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int CampusId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the campus.
|
||||||
|
/// </summary>
|
||||||
|
public string? CampusName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the code of the campus.
|
||||||
|
/// </summary>
|
||||||
|
public string? CampusCode { get; set; }
|
||||||
|
}
|
27
ApiDto/Responses/LectureHallResponse.cs
Normal file
27
ApiDto/Responses/LectureHallResponse.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the response model for a lecture hall.
|
||||||
|
/// </summary>
|
||||||
|
public class LectureHallResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the lecture hall.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the lecture hall.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the campus to which the lecture hall belongs.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int CampusId { get; set; }
|
||||||
|
}
|
26
ApiDto/Responses/ProfessorResponse.cs
Normal file
26
ApiDto/Responses/ProfessorResponse.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a professor.
|
||||||
|
/// </summary>
|
||||||
|
public class ProfessorResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the professor.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the professor.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the alternate name of the professor (optional).
|
||||||
|
/// </summary>
|
||||||
|
public string? AltName { get; set; }
|
||||||
|
}
|
117
ApiDto/Responses/ScheduleResponse.cs
Normal file
117
ApiDto/Responses/ScheduleResponse.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a response object containing schedule information.
|
||||||
|
/// </summary>
|
||||||
|
public class ScheduleResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the day of the week for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DayOfWeek DayOfWeek { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the pair number for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int PairNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the pair is on an even week.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public bool IsEven { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the discipline for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Discipline { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the discipline for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required int DisciplineId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets exclude or include weeks for a specific discipline.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If is <see langword="true"/>, then the values in <see cref="Weeks"/> show the weeks when there will be no discipline.
|
||||||
|
/// <br/>
|
||||||
|
/// If is <see langword="false"/>, then the values in <see cref="Weeks"/> indicate the weeks during which a particular discipline will be studied.
|
||||||
|
/// <br/>
|
||||||
|
/// If is <see langword="null"/>, then there are no specific <see cref="Weeks"/>
|
||||||
|
/// </remarks>
|
||||||
|
///
|
||||||
|
public bool? IsExcludedWeeks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The week numbers required for the correct display of the schedule.
|
||||||
|
/// <br/>
|
||||||
|
/// Whether there will be <see cref="Discipline"/> during the week or not depends on the <see cref="IsExcludedWeeks"/> property.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To get the current week's number, use other queries.
|
||||||
|
/// </remarks>
|
||||||
|
public IEnumerable<int>? Weeks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the type of occupation for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required IEnumerable<string> TypeOfOccupations { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the group for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required string Group { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the ID of the group for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public required int GroupId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the names of the lecture halls for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<string?> LectureHalls { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the IDs of the lecture halls for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<int?> LectureHallsId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the names of the professors for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<string?> Professors { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the IDs of the professors for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<int?> ProfessorsId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the names of the campuses for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<string?> Campus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the IDs of the campuses for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<int?> CampusId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the links to online meetings for the schedule entry.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<string?> LinkToMeet { get; set; }
|
||||||
|
}
|
79
Backend.sln
79
Backend.sln
@ -3,18 +3,48 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.8.34330.188
|
VisualStudioVersion = 17.8.34330.188
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{C27FB5CD-6A70-4FB2-847A-847B34806902}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Endpoint", "Endpoint\Endpoint.csproj", "{F3A1D12E-F5B2-4339-9966-DBF869E78357}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Endpoint", "Endpoint\Endpoint.csproj", "{F3A1D12E-F5B2-4339-9966-DBF869E78357}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
.dockerignore = .dockerignore
|
.dockerignore = .dockerignore
|
||||||
|
.env = .env
|
||||||
.gitattributes = .gitattributes
|
.gitattributes = .gitattributes
|
||||||
.gitignore = .gitignore
|
.gitignore = .gitignore
|
||||||
|
.gitea\workflows\deploy-stage.yaml = .gitea\workflows\deploy-stage.yaml
|
||||||
Dockerfile = Dockerfile
|
Dockerfile = Dockerfile
|
||||||
LICENSE.txt = LICENSE.txt
|
LICENSE.txt = LICENSE.txt
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
|
.gitea\workflows\test.yaml = .gitea\workflows\test.yaml
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Security", "Security\Security.csproj", "{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SqlData", "SqlData", "{7E7A63CD-547B-4FB4-A383-EB75298020A1}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "SqlData\Domain\Domain.csproj", "{3BFD6180-7CA7-4E85-A379-225B872439A1}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "SqlData\Application\Application.csproj", "{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "SqlData\Persistence\Persistence.csproj", "{48C9998C-ECE2-407F-835F-1A7255A5C99E}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqliteMigrations", "SqlData\Migrations\SqliteMigrations\SqliteMigrations.csproj", "{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MysqlMigrations", "SqlData\Migrations\MysqlMigrations\MysqlMigrations.csproj", "{5861915B-9574-4D5D-872F-D54A09651697}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsqlMigrations", "SqlData\Migrations\PsqlMigrations\PsqlMigrations.csproj", "{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@ -23,18 +53,55 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{3BFD6180-7CA7-4E85-A379-225B872439A1} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||||
|
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||||
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||||
|
{79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
|
||||||
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
33
Dockerfile
33
Dockerfile
@ -1,25 +1,28 @@
|
|||||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
USER app
|
LABEL company="Winsomnia"
|
||||||
|
LABEL maintainer.name="Wesser" maintainer.email="support@winsomnia.net"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl --fail http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["Backend.csproj", "."]
|
|
||||||
RUN dotnet restore "./././Backend.csproj"
|
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/."
|
|
||||||
RUN dotnet build "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
ARG NUGET_USERNAME
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG NUGET_PASSWORD
|
||||||
RUN dotnet publish "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
ENV NUGET_USERNAME=$NUGET_USERNAME
|
||||||
|
ENV NUGET_PASSWORD=$NUGET_PASSWORD
|
||||||
|
|
||||||
|
RUN dotnet restore ./Backend.sln --configfile nuget.config
|
||||||
|
WORKDIR /app
|
||||||
|
WORKDIR /src
|
||||||
|
RUN dotnet publish ./Endpoint/Endpoint.csproj -c Release --self-contained false -p:PublishSingleFile=false -o /app
|
||||||
|
|
||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=build /app .
|
||||||
ENTRYPOINT ["dotnet", "Backend.dll"]
|
RUN find . -name "*.pdb" -type f -delete
|
||||||
|
ENTRYPOINT ["dotnet", "Mirea.Api.Endpoint.dll"]
|
@ -1,6 +0,0 @@
|
|||||||
@Backend_HostAddress = http://localhost:5269
|
|
||||||
|
|
||||||
GET {{Backend_HostAddress}}/weatherforecast/
|
|
||||||
Accept: application/json
|
|
||||||
|
|
||||||
###
|
|
@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
|
public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status400BadRequest);
|
26
Endpoint/Common/Attributes/CacheMaxAgeAttribute.cs
Normal file
26
Endpoint/Common/Attributes/CacheMaxAgeAttribute.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||||
|
public class CacheMaxAgeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public int MaxAge { get; }
|
||||||
|
|
||||||
|
public CacheMaxAgeAttribute(int days = 0, int hours = 0, int minutes = 0)
|
||||||
|
{
|
||||||
|
MaxAge = (int)new TimeSpan(days, hours, minutes, 0).TotalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CacheMaxAgeAttribute(int minutes) : this(0, 0, minutes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CacheMaxAgeAttribute(bool usingSetting = false)
|
||||||
|
{
|
||||||
|
if (usingSetting)
|
||||||
|
MaxAge = -1;
|
||||||
|
else
|
||||||
|
MaxAge = 0;
|
||||||
|
}
|
||||||
|
}
|
30
Endpoint/Common/Attributes/LocalhostAttribute.cs
Normal file
30
Endpoint/Common/Attributes/LocalhostAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
public class LocalhostAttribute : ActionFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnActionExecuting(ActionExecutingContext context)
|
||||||
|
{
|
||||||
|
var ip = context.HttpContext.Connection.RemoteIpAddress;
|
||||||
|
|
||||||
|
if (ip == null)
|
||||||
|
{
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isRunningInContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToLower() == "true";
|
||||||
|
|
||||||
|
if (IPAddress.IsLoopback(ip) || (isRunningInContainer && ip.ToString().StartsWith("172.")))
|
||||||
|
{
|
||||||
|
base.OnActionExecuting(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public class MaintenanceModeIgnoreAttribute : Attribute;
|
9
Endpoint/Common/Attributes/NotFoundResponseAttribute.cs
Normal file
9
Endpoint/Common/Attributes/NotFoundResponseAttribute.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
|
public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status404NotFound);
|
9
Endpoint/Common/Attributes/SwaggerDefaultAttribute.cs
Normal file
9
Endpoint/Common/Attributes/SwaggerDefaultAttribute.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)]
|
||||||
|
public class SwaggerDefaultAttribute(string value) : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; } = value;
|
||||||
|
}
|
28
Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
Normal file
28
Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class TokenAuthenticationAttribute : Attribute, IActionFilter
|
||||||
|
{
|
||||||
|
public const string AuthToken = "AuthToken";
|
||||||
|
public void OnActionExecuting(ActionExecutingContext context)
|
||||||
|
{
|
||||||
|
var setupToken = context.HttpContext.RequestServices.GetRequiredService<ISetupToken>();
|
||||||
|
if (!context.HttpContext.Request.Cookies.TryGetValue(AuthToken, out string? tokenFromCookie))
|
||||||
|
{
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setupToken.MatchToken(Convert.FromBase64String(tokenFromCookie))) return;
|
||||||
|
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnActionExecuted(ActionExecutedContext context) { }
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Exceptions;
|
||||||
|
|
||||||
|
public class ControllerArgumentException(string message) : Exception(message);
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
|
||||||
|
public interface IMaintenanceModeNotConfigureService
|
||||||
|
{
|
||||||
|
bool IsMaintenanceMode { get; }
|
||||||
|
|
||||||
|
void DisableMaintenanceMode();
|
||||||
|
}
|
10
Endpoint/Common/Interfaces/IMaintenanceModeService.cs
Normal file
10
Endpoint/Common/Interfaces/IMaintenanceModeService.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
|
||||||
|
public interface IMaintenanceModeService
|
||||||
|
{
|
||||||
|
bool IsMaintenanceMode { get; }
|
||||||
|
|
||||||
|
void EnableMaintenanceMode();
|
||||||
|
|
||||||
|
void DisableMaintenanceMode();
|
||||||
|
}
|
9
Endpoint/Common/Interfaces/ISetupToken.cs
Normal file
9
Endpoint/Common/Interfaces/ISetupToken.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
|
||||||
|
public interface ISetupToken
|
||||||
|
{
|
||||||
|
bool MatchToken(ReadOnlySpan<byte> token);
|
||||||
|
void SetToken(ReadOnlySpan<byte> token);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services;
|
||||||
|
|
||||||
|
public class MaintenanceModeNotConfigureService : IMaintenanceModeNotConfigureService
|
||||||
|
{
|
||||||
|
public bool IsMaintenanceMode { get; private set; } = true;
|
||||||
|
|
||||||
|
public void DisableMaintenanceMode() =>
|
||||||
|
IsMaintenanceMode = false;
|
||||||
|
}
|
14
Endpoint/Common/Services/MaintenanceModeService.cs
Normal file
14
Endpoint/Common/Services/MaintenanceModeService.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services;
|
||||||
|
|
||||||
|
public class MaintenanceModeService : IMaintenanceModeService
|
||||||
|
{
|
||||||
|
public bool IsMaintenanceMode { get; private set; }
|
||||||
|
|
||||||
|
public void EnableMaintenanceMode() =>
|
||||||
|
IsMaintenanceMode = true;
|
||||||
|
|
||||||
|
public void DisableMaintenanceMode() =>
|
||||||
|
IsMaintenanceMode = false;
|
||||||
|
}
|
13
Endpoint/Common/Services/PairPeriodTimeConverter.cs
Normal file
13
Endpoint/Common/Services/PairPeriodTimeConverter.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services;
|
||||||
|
|
||||||
|
public static class PairPeriodTimeConverter
|
||||||
|
{
|
||||||
|
public static Dictionary<int, Dto.Common.PairPeriodTime> ConvertToDto(this IDictionary<int, ScheduleSettings.PairPeriodTime> pairPeriod) =>
|
||||||
|
pairPeriod.ToDictionary(kvp => kvp.Key, kvp => new Dto.Common.PairPeriodTime { Start = kvp.Value.Start, End = kvp.Value.End });
|
||||||
|
|
||||||
|
public static Dictionary<int, ScheduleSettings.PairPeriodTime> ConvertFromDto(this IDictionary<int, Dto.Common.PairPeriodTime> pairPeriod) => pairPeriod.ToDictionary(kvp => kvp.Key, kvp => new ScheduleSettings.PairPeriodTime(kvp.Value.Start, kvp.Value.End));
|
||||||
|
}
|
12
Endpoint/Common/Services/PathBuilder.cs
Normal file
12
Endpoint/Common/Services/PathBuilder.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services;
|
||||||
|
|
||||||
|
public static class PathBuilder
|
||||||
|
{
|
||||||
|
public static bool IsDefaultPath => Environment.GetEnvironmentVariable("PATH_TO_SAVE") == null;
|
||||||
|
public static string PathToSave => Environment.GetEnvironmentVariable("PATH_TO_SAVE") ?? Directory.GetCurrentDirectory();
|
||||||
|
public static string Combine(params string[] paths) => Path.Combine([.. paths.Prepend(PathToSave)]);
|
||||||
|
}
|
15
Endpoint/Common/Services/ScheduleSyncManager.cs
Normal file
15
Endpoint/Common/Services/ScheduleSyncManager.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services;
|
||||||
|
|
||||||
|
public static class ScheduleSyncManager
|
||||||
|
{
|
||||||
|
public static event Action? OnUpdateIntervalRequested;
|
||||||
|
public static event Action? OnForceSyncRequested;
|
||||||
|
|
||||||
|
public static void RequestIntervalUpdate() =>
|
||||||
|
OnUpdateIntervalRequested?.Invoke();
|
||||||
|
|
||||||
|
public static void RequestForceSync() =>
|
||||||
|
OnForceSyncRequested?.Invoke();
|
||||||
|
}
|
63
Endpoint/Common/Services/Security/DistributedCacheService.cs
Normal file
63
Endpoint/Common/Services/Security/DistributedCacheService.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services.Security;
|
||||||
|
|
||||||
|
public class DistributedCacheService(IDistributedCache cache) : ICacheService
|
||||||
|
{
|
||||||
|
public async Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var options = new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow,
|
||||||
|
SlidingExpiration = slidingExpiration
|
||||||
|
};
|
||||||
|
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime))
|
||||||
|
{
|
||||||
|
await cache.SetStringAsync(key, value?.ToString() ?? string.Empty, options, cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializedValue = value as byte[] ?? JsonSerializer.SerializeToUtf8Bytes(value);
|
||||||
|
await cache.SetAsync(key, serializedValue, options, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
|
||||||
|
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime))
|
||||||
|
{
|
||||||
|
var primitiveValue = await cache.GetStringAsync(key, cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(primitiveValue))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
if (type == typeof(string))
|
||||||
|
return (T?)(object?)primitiveValue;
|
||||||
|
|
||||||
|
var tryParseMethod = type.GetMethod("TryParse", [typeof(string), type.MakeByRefType()])
|
||||||
|
?? throw new NotSupportedException($"Type {type.Name} does not support TryParse.");
|
||||||
|
|
||||||
|
var parameters = new[] { primitiveValue, Activator.CreateInstance(type) };
|
||||||
|
var success = (bool)tryParseMethod.Invoke(null, parameters)!;
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
return (T)parameters[1]!;
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedValue = await cache.GetAsync(key, cancellationToken);
|
||||||
|
return cachedValue == null ? default : JsonSerializer.Deserialize<T>(cachedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string key, CancellationToken cancellationToken = default) =>
|
||||||
|
cache.RemoveAsync(key, cancellationToken);
|
||||||
|
}
|
82
Endpoint/Common/Services/Security/JwtTokenService.cs
Normal file
82
Endpoint/Common/Services/Security/JwtTokenService.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services.Security;
|
||||||
|
|
||||||
|
public class JwtTokenService : IAccessToken
|
||||||
|
{
|
||||||
|
public required string Issuer { private get; init; }
|
||||||
|
public required string Audience { private get; init; }
|
||||||
|
public TimeSpan Lifetime { private get; init; }
|
||||||
|
|
||||||
|
public ReadOnlyMemory<byte> EncryptionKey { private get; init; }
|
||||||
|
public ReadOnlyMemory<byte> SigningKey { private get; init; }
|
||||||
|
|
||||||
|
public (string Token, DateTime ExpireIn) GenerateToken(string userId)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var signingKey = new SymmetricSecurityKey(SigningKey.ToArray());
|
||||||
|
var encryptionKey = new SymmetricSecurityKey(EncryptionKey.ToArray());
|
||||||
|
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512);
|
||||||
|
|
||||||
|
var expires = DateTime.UtcNow.Add(Lifetime);
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Issuer = Issuer,
|
||||||
|
Audience = Audience,
|
||||||
|
Expires = expires,
|
||||||
|
SigningCredentials = signingCredentials,
|
||||||
|
Subject = new ClaimsIdentity(
|
||||||
|
[
|
||||||
|
new Claim(ClaimTypes.Name, userId),
|
||||||
|
// todo: get role by userId
|
||||||
|
new Claim(ClaimTypes.Role, "")
|
||||||
|
]),
|
||||||
|
EncryptingCredentials = new EncryptingCredentials(encryptionKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512)
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
return (tokenHandler.WriteToken(token), expires);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset GetExpireDateTime(string token)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var signingKey = new SymmetricSecurityKey(SigningKey.ToArray());
|
||||||
|
var encryptionKey = new SymmetricSecurityKey(EncryptionKey.ToArray());
|
||||||
|
|
||||||
|
var tokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidIssuer = Issuer,
|
||||||
|
ValidAudience = Audience,
|
||||||
|
IssuerSigningKey = signingKey,
|
||||||
|
TokenDecryptionKey = encryptionKey,
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidateLifetime = false
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var claimsPrincipal = tokenHandler.ValidateToken(token, tokenValidationParameters, out _);
|
||||||
|
|
||||||
|
var expClaim = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "exp");
|
||||||
|
|
||||||
|
if (expClaim != null && long.TryParse(expClaim.Value, out var expUnix))
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(expUnix);
|
||||||
|
}
|
||||||
|
catch (SecurityTokenException)
|
||||||
|
{
|
||||||
|
return DateTimeOffset.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeOffset.MinValue;
|
||||||
|
}
|
||||||
|
}
|
62
Endpoint/Common/Services/Security/MemoryCacheService.cs
Normal file
62
Endpoint/Common/Services/Security/MemoryCacheService.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services.Security;
|
||||||
|
|
||||||
|
public class MemoryCacheService(IMemoryCache cache) : ICacheService
|
||||||
|
{
|
||||||
|
public Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow,
|
||||||
|
SlidingExpiration = slidingExpiration
|
||||||
|
};
|
||||||
|
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime))
|
||||||
|
{
|
||||||
|
cache.Set(key, value?.ToString() ?? string.Empty, options);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.Set(key, value as byte[] ?? JsonSerializer.SerializeToUtf8Bytes(value), options);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
|
||||||
|
if (!type.IsPrimitive && type != typeof(string) && type != typeof(DateTime))
|
||||||
|
return Task.FromResult(
|
||||||
|
cache.TryGetValue(key, out byte[]? value) ? JsonSerializer.Deserialize<T>(value) : default
|
||||||
|
);
|
||||||
|
|
||||||
|
var primitiveValue = cache.Get(key);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(primitiveValue?.ToString()))
|
||||||
|
return Task.FromResult<T?>(default);
|
||||||
|
|
||||||
|
if (type == typeof(string))
|
||||||
|
return Task.FromResult((T?)primitiveValue);
|
||||||
|
|
||||||
|
var tryParseMethod = type.GetMethod("TryParse", [typeof(string), type.MakeByRefType()])
|
||||||
|
?? throw new NotSupportedException($"Type {type.Name} does not support TryParse.");
|
||||||
|
|
||||||
|
var parameters = new[] { primitiveValue, Activator.CreateInstance(type) };
|
||||||
|
var success = (bool)tryParseMethod.Invoke(null, parameters)!;
|
||||||
|
|
||||||
|
return success ? Task.FromResult((T?)parameters[1]) : Task.FromResult<T?>(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cache.Remove(key);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services.Security;
|
||||||
|
|
||||||
|
public class MemoryRevokedTokenService(IMemoryCache cache) : IRevokedToken
|
||||||
|
{
|
||||||
|
public Task AddTokenToRevokedAsync(string token, DateTimeOffset expiresIn)
|
||||||
|
{
|
||||||
|
cache.Set(token, true, expiresIn);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> IsTokenRevokedAsync(string token) => Task.FromResult(cache.TryGetValue(token, out _));
|
||||||
|
}
|
46
Endpoint/Common/Services/UrlHelper.cs
Normal file
46
Endpoint/Common/Services/UrlHelper.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Common.Services;
|
||||||
|
|
||||||
|
public static class UrlHelper
|
||||||
|
{
|
||||||
|
public static string GetCurrentDomain(this HttpContext context) =>
|
||||||
|
context.Request.Headers["X-Forwarded-Host"].FirstOrDefault() ?? context.Request.Host.Host;
|
||||||
|
|
||||||
|
private static string CreateSubPath(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
return "/";
|
||||||
|
|
||||||
|
return "/" + path.Trim('/') + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSubPath => CreateSubPath(Environment.GetEnvironmentVariable("ACTUAL_SUB_PATH"));
|
||||||
|
|
||||||
|
public static string GetSubPathWithoutFirstApiName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var path = GetSubPath;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path) || path == "/")
|
||||||
|
return CreateSubPath(null);
|
||||||
|
|
||||||
|
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
for (int i = 0; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
if (!parts[i].Equals("api", StringComparison.CurrentCultureIgnoreCase)) continue;
|
||||||
|
|
||||||
|
parts = parts.Take(i).Concat(parts.Skip(i + 1)).ToArray();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateSubPath(string.Join("/", parts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSubPathSwagger => CreateSubPath(Environment.GetEnvironmentVariable("SWAGGER_SUB_PATH"));
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
using Cronos;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Sync;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks;
|
||||||
|
|
||||||
|
public class ScheduleSyncService : IHostedService, IDisposable
|
||||||
|
{
|
||||||
|
private Timer? _timer;
|
||||||
|
private readonly IOptionsMonitor<GeneralConfig> _generalConfigMonitor;
|
||||||
|
private readonly ILogger<ScheduleSyncService> _logger;
|
||||||
|
private CancellationTokenSource _cancellationTokenSource = new();
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public ScheduleSyncService(IOptionsMonitor<GeneralConfig> generalConfigMonitor, ILogger<ScheduleSyncService> logger, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_generalConfigMonitor = generalConfigMonitor;
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
|
||||||
|
ScheduleSyncManager.OnForceSyncRequested += OnForceSyncRequested;
|
||||||
|
ScheduleSyncManager.OnUpdateIntervalRequested += OnUpdateIntervalRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnForceSyncRequested()
|
||||||
|
{
|
||||||
|
StopAsync(default).ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
ExecuteTask(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUpdateIntervalRequested()
|
||||||
|
{
|
||||||
|
StopAsync(default).ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
StartAsync(default);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScheduleNextRun()
|
||||||
|
{
|
||||||
|
var cronExpression = _generalConfigMonitor.CurrentValue.ScheduleSettings?.CronUpdateSchedule;
|
||||||
|
if (string.IsNullOrEmpty(cronExpression))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Cron expression is not set. The scheduled task will not run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextRunTime = CronExpression.Parse(cronExpression).GetNextOccurrence(DateTimeOffset.Now, TimeZoneInfo.Local);
|
||||||
|
|
||||||
|
if (!nextRunTime.HasValue)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No next run time found. The task will not be scheduled. Timezone: {TimeZone}", TimeZoneInfo.Local.DisplayName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Next task run in {Time}", nextRunTime.Value.ToString("G"));
|
||||||
|
|
||||||
|
var delay = (nextRunTime.Value - DateTimeOffset.Now).TotalMilliseconds;
|
||||||
|
|
||||||
|
// The chance is small, but it's better to check
|
||||||
|
if (delay <= 0)
|
||||||
|
delay = 1;
|
||||||
|
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_timer = new Timer(ExecuteTask, null, (int)delay, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ExecuteTask(object? state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var syncService = ActivatorUtilities.GetServiceOrCreateInstance<ScheduleSynchronizer>(scope.ServiceProvider);
|
||||||
|
await syncService.StartSync(_cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurred during schedule synchronization.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ScheduleNextRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ScheduleNextRun();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_cancellationTokenSource.Cancel();
|
||||||
|
_timer?.Change(Timeout.Infinite, 0);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
StopAsync(default).GetAwaiter().GetResult();
|
||||||
|
_timer?.Dispose();
|
||||||
|
ScheduleSyncManager.OnForceSyncRequested -= OnForceSyncRequested;
|
||||||
|
ScheduleSyncManager.OnUpdateIntervalRequested -= OnUpdateIntervalRequested;
|
||||||
|
_cancellationTokenSource.Dispose();
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
using Cronos;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
|
||||||
|
public class CacheMaxAgeMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
if (!context.Response.StatusCode.ToString().StartsWith('2'))
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint = context.GetEndpoint();
|
||||||
|
|
||||||
|
var actionDescriptor = endpoint?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
|
||||||
|
|
||||||
|
if (actionDescriptor == null)
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var controllerType = actionDescriptor.ControllerTypeInfo;
|
||||||
|
var methodInfo = actionDescriptor.MethodInfo;
|
||||||
|
|
||||||
|
var maxAgeAttribute = methodInfo.GetCustomAttribute<CacheMaxAgeAttribute>() ?? controllerType.GetCustomAttribute<CacheMaxAgeAttribute>();
|
||||||
|
|
||||||
|
if (maxAgeAttribute == null)
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (maxAgeAttribute.MaxAge)
|
||||||
|
{
|
||||||
|
case < 0:
|
||||||
|
{
|
||||||
|
DateTime? nextDate;
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
using (var scope = serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var updateCronString = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<GeneralConfig>>().Value.ScheduleSettings?.CronUpdateSchedule;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(updateCronString) ||
|
||||||
|
!CronExpression.TryParse(updateCronString, CronFormat.Standard, out var updateCron))
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDate = updateCron.GetNextOccurrence(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextDate.HasValue)
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.Headers.CacheControl = "max-age=" + (int)(nextDate.Value - now).TotalSeconds;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case > 0:
|
||||||
|
context.Response.Headers.CacheControl = "max-age=" + maxAgeAttribute.MaxAge;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Mirea.Api.Security.Common;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
|
||||||
|
public class CookieAuthorizationMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.Request.Cookies.ContainsKey(CookieNames.AccessToken))
|
||||||
|
context.Request.Headers.Authorization = "Bearer " + context.Request.Cookies[CookieNames.AccessToken];
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Mirea.Api.DataAccess.Application.Common.Exceptions;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Exceptions;
|
||||||
|
using System;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
|
||||||
|
public class CustomExceptionHandlerMiddleware(RequestDelegate next, ILogger<CustomExceptionHandlerMiddleware> logger)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
await HandleExceptionAsync(context, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||||
|
{
|
||||||
|
var code = StatusCodes.Status500InternalServerError;
|
||||||
|
var result = string.Empty;
|
||||||
|
switch (exception)
|
||||||
|
{
|
||||||
|
case ValidationException validationException:
|
||||||
|
code = StatusCodes.Status400BadRequest;
|
||||||
|
result = JsonSerializer.Serialize(new ErrorResponse()
|
||||||
|
{
|
||||||
|
Error = validationException.Message,
|
||||||
|
Code = code
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case NotFoundException:
|
||||||
|
code = StatusCodes.Status404NotFound;
|
||||||
|
break;
|
||||||
|
case ControllerArgumentException:
|
||||||
|
code = StatusCodes.Status400BadRequest;
|
||||||
|
break;
|
||||||
|
case SecurityException:
|
||||||
|
code = StatusCodes.Status401Unauthorized;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.StatusCode = code;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(result))
|
||||||
|
return context.Response.WriteAsync(result);
|
||||||
|
|
||||||
|
string error;
|
||||||
|
if (code == StatusCodes.Status500InternalServerError)
|
||||||
|
{
|
||||||
|
error = "Internal Server Error";
|
||||||
|
logger.LogError(exception, "Internal server error when processing the request");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
error = exception.Message;
|
||||||
|
|
||||||
|
result = JsonSerializer.Serialize(new ErrorResponse()
|
||||||
|
{
|
||||||
|
Error = error,
|
||||||
|
Code = code
|
||||||
|
});
|
||||||
|
|
||||||
|
return context.Response.WriteAsync(result);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
|
||||||
|
public class JwtRevocationMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context, IRevokedToken revokedTokenStore)
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.ContainsKey("Authorization"))
|
||||||
|
{
|
||||||
|
var token = context.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||||
|
if (await revokedTokenStore.IsTokenRevokedAsync(token))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
|
||||||
|
public class MaintenanceModeMiddleware(RequestDelegate next, IMaintenanceModeService maintenanceModeService, IMaintenanceModeNotConfigureService maintenanceModeNotConfigureService)
|
||||||
|
{
|
||||||
|
private static bool IsIgnoreMaintenanceMode(HttpContext context)
|
||||||
|
{
|
||||||
|
var endpoint = context.GetEndpoint();
|
||||||
|
return endpoint?.Metadata.GetMetadata<MaintenanceModeIgnoreAttribute>() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
if (!maintenanceModeService.IsMaintenanceMode && !maintenanceModeNotConfigureService.IsMaintenanceMode || IsIgnoreMaintenanceMode(context))
|
||||||
|
await next(context);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||||
|
context.Response.ContentType = "plain/text";
|
||||||
|
|
||||||
|
string error;
|
||||||
|
if (maintenanceModeService.IsMaintenanceMode)
|
||||||
|
{
|
||||||
|
context.Response.Headers.RetryAfter = "600";
|
||||||
|
error = "The service is currently undergoing maintenance. Please try again later.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
error =
|
||||||
|
"The service is currently not configured. Go to the setup page if you are an administrator or try again later.";
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class ApiVersioningConfiguration
|
||||||
|
{
|
||||||
|
public static IApiVersioningBuilder AddCustomApiVersioning(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services.AddApiVersioning(options =>
|
||||||
|
{
|
||||||
|
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||||
|
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||||
|
options.ReportApiVersions = true;
|
||||||
|
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||||
|
}).AddApiExplorer(options =>
|
||||||
|
{
|
||||||
|
options.GroupNameFormat = "'v'VVV";
|
||||||
|
options.SubstituteApiVersionInUrl = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
Normal file
26
Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class CacheConfiguration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddCustomRedis(this IServiceCollection services, IConfiguration configuration, IHealthChecksBuilder? healthChecksBuilder = null)
|
||||||
|
{
|
||||||
|
var cache = configuration.Get<GeneralConfig>()?.CacheSettings;
|
||||||
|
if (cache?.TypeDatabase != CacheSettings.CacheEnum.Redis)
|
||||||
|
return services;
|
||||||
|
|
||||||
|
services.AddStackExchangeRedisCache(options =>
|
||||||
|
{
|
||||||
|
options.Configuration = cache.ConnectionString;
|
||||||
|
options.InstanceName = "mirea_";
|
||||||
|
});
|
||||||
|
|
||||||
|
healthChecksBuilder?.AddRedis(cache.ConnectionString!, name: "Redis");
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class EnvironmentConfiguration
|
||||||
|
{
|
||||||
|
private static Dictionary<string, string> LoadEnvironment(string envFile)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> environment = [];
|
||||||
|
|
||||||
|
if (!File.Exists(envFile)) return environment;
|
||||||
|
|
||||||
|
foreach (var line in File.ReadAllLines(envFile))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(line)) continue;
|
||||||
|
|
||||||
|
var commentIndex = line.IndexOf('#', StringComparison.Ordinal);
|
||||||
|
|
||||||
|
string arg = line;
|
||||||
|
|
||||||
|
if (commentIndex != -1)
|
||||||
|
arg = arg.Remove(commentIndex, arg.Length - commentIndex);
|
||||||
|
|
||||||
|
var parts = arg.Split(
|
||||||
|
'=',
|
||||||
|
StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (parts.Length > 2)
|
||||||
|
parts = [parts[0], string.Join("=", parts[1..])];
|
||||||
|
|
||||||
|
if (parts.Length != 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
environment.Add(parts[0].Trim(), parts[1].Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IConfigurationRoot GetEnvironment()
|
||||||
|
{
|
||||||
|
var variablesFromFile = LoadEnvironment(".env");
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
LoadEnvironment(".env.develop").ToList().ForEach(x => variablesFromFile.Add(x.Key, x.Value));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var environmentVariables = Environment.GetEnvironmentVariables()
|
||||||
|
.OfType<DictionaryEntry>()
|
||||||
|
.ToDictionary(
|
||||||
|
entry => entry.Key.ToString() ?? string.Empty,
|
||||||
|
entry => entry.Value?.ToString() ?? string.Empty
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(environmentVariables!)
|
||||||
|
.AddInMemoryCollection(variablesFromFile!);
|
||||||
|
|
||||||
|
if (variablesFromFile.TryGetValue("PATH_TO_SAVE", out var pathToSave))
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("PATH_TO_SAVE", pathToSave);
|
||||||
|
if (!Directory.Exists(pathToSave))
|
||||||
|
Directory.CreateDirectory(pathToSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variablesFromFile.TryGetValue("ACTUAL_SUB_PATH", out var actualSubPath))
|
||||||
|
Environment.SetEnvironmentVariable("ACTUAL_SUB_PATH", actualSubPath);
|
||||||
|
|
||||||
|
if (variablesFromFile.TryGetValue("SWAGGER_SUB_PATH", out var swaggerSubPath))
|
||||||
|
Environment.SetEnvironmentVariable("SWAGGER_SUB_PATH", swaggerSubPath);
|
||||||
|
|
||||||
|
return result.Build();
|
||||||
|
}
|
||||||
|
}
|
65
Endpoint/Configuration/Core/Startup/JwtConfiguration.cs
Normal file
65
Endpoint/Configuration/Core/Startup/JwtConfiguration.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services.Security;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class JwtConfiguration
|
||||||
|
{
|
||||||
|
public static AuthenticationBuilder AddJwtToken(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var lifeTimeJwt = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_JWT"]!));
|
||||||
|
|
||||||
|
var jwtDecrypt = Encoding.UTF8.GetBytes(configuration["SECURITY_ENCRYPTION_TOKEN"] ?? string.Empty);
|
||||||
|
|
||||||
|
if (jwtDecrypt.Length != 32)
|
||||||
|
throw new InvalidOperationException("The secret token \"SECURITY_ENCRYPTION_TOKEN\" cannot be less than 32 characters long. Now the size is equal is " + jwtDecrypt.Length);
|
||||||
|
|
||||||
|
var jwtKey = Encoding.UTF8.GetBytes(configuration["SECURITY_SIGNING_TOKEN"] ?? string.Empty);
|
||||||
|
|
||||||
|
if (jwtKey.Length != 64)
|
||||||
|
throw new InvalidOperationException("The signature token \"SECURITY_SIGNING_TOKEN\" cannot be less than 64 characters. Now the size is " + jwtKey.Length);
|
||||||
|
|
||||||
|
var jwtIssuer = configuration["SECURITY_JWT_ISSUER"];
|
||||||
|
var jwtAudience = configuration["SECURITY_JWT_AUDIENCE"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(jwtAudience) || string.IsNullOrEmpty(jwtIssuer))
|
||||||
|
throw new InvalidOperationException("The \"SECURITY_JWT_ISSUER\" and \"SECURITY_JWT_AUDIENCE\" are not specified");
|
||||||
|
|
||||||
|
services.AddSingleton<IAccessToken, JwtTokenService>(_ => new JwtTokenService
|
||||||
|
{
|
||||||
|
Audience = jwtAudience,
|
||||||
|
Issuer = jwtIssuer,
|
||||||
|
Lifetime = lifeTimeJwt,
|
||||||
|
EncryptionKey = jwtDecrypt,
|
||||||
|
SigningKey = jwtKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
}).AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = jwtIssuer,
|
||||||
|
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = jwtAudience,
|
||||||
|
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
|
||||||
|
TokenDecryptionKey = new SymmetricSecurityKey(jwtDecrypt)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
83
Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs
Normal file
83
Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Filters;
|
||||||
|
using Serilog.Formatting.Compact;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class LoggerConfiguration
|
||||||
|
{
|
||||||
|
public static IHostBuilder AddCustomSerilog(this IHostBuilder hostBuilder)
|
||||||
|
{
|
||||||
|
return hostBuilder.UseSerilog((context, _, configuration) =>
|
||||||
|
{
|
||||||
|
var generalConfig = context.Configuration.Get<GeneralConfig>()?.LogSettings;
|
||||||
|
configuration
|
||||||
|
.MinimumLevel.Debug()
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.WriteTo.Console(
|
||||||
|
outputTemplate:
|
||||||
|
"[{Level:u3}] [{Timestamp:dd.MM.yyyy HH:mm:ss}] {Message:lj}{NewLine}{Exception}");
|
||||||
|
|
||||||
|
if (generalConfig?.EnableLogToFile == true)
|
||||||
|
{
|
||||||
|
generalConfig.LogFilePath = PathBuilder.Combine(generalConfig.LogFilePath ?? string.Empty);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(generalConfig.LogFilePath) && Directory.Exists(generalConfig.LogFilePath))
|
||||||
|
Directory.CreateDirectory(generalConfig.LogFilePath);
|
||||||
|
|
||||||
|
configuration.WriteTo.File(
|
||||||
|
new CompactJsonFormatter(),
|
||||||
|
PathBuilder.Combine(
|
||||||
|
generalConfig.LogFilePath!,
|
||||||
|
generalConfig.LogFileName + ".json"
|
||||||
|
),
|
||||||
|
LogEventLevel.Debug,
|
||||||
|
rollingInterval: RollingInterval.Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning);
|
||||||
|
|
||||||
|
configuration.Filter.ByExcluding(Matching.WithProperty<string>("SourceContext", sc =>
|
||||||
|
sc.Contains("Microsoft.EntityFrameworkCore.Database.Command")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IApplicationBuilder UseCustomSerilog(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app.UseSerilogRequestLogging(options =>
|
||||||
|
{
|
||||||
|
options.MessageTemplate = "[{RequestMethod}] {RequestPath} [Client {RemoteIPAddress}] [{StatusCode}] in {Elapsed:0.0000} ms";
|
||||||
|
|
||||||
|
options.GetLevel = (httpContext, elapsed, ex) =>
|
||||||
|
{
|
||||||
|
if (httpContext.Request.Path.StartsWithSegments("/health"))
|
||||||
|
return LogEventLevel.Verbose;
|
||||||
|
|
||||||
|
return elapsed >= 2500 || ex != null
|
||||||
|
? LogEventLevel.Warning
|
||||||
|
: elapsed >= 1000
|
||||||
|
? LogEventLevel.Information
|
||||||
|
: LogEventLevel.Debug;
|
||||||
|
};
|
||||||
|
|
||||||
|
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||||||
|
{
|
||||||
|
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
|
||||||
|
diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
|
||||||
|
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
|
||||||
|
diagnosticContext.Set("RemoteIPAddress", httpContext.Connection.RemoteIpAddress?.ToString());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
Normal file
26
Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services.Security;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
using Mirea.Api.Security;
|
||||||
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class SecureConfiguration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddSecurity(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddSecurityServices(configuration);
|
||||||
|
|
||||||
|
services.AddSingleton<IRevokedToken, MemoryRevokedTokenService>();
|
||||||
|
|
||||||
|
if (configuration.Get<GeneralConfig>()?.CacheSettings?.TypeDatabase == CacheSettings.CacheEnum.Redis)
|
||||||
|
services.AddSingleton<ICacheService, DistributedCacheService>();
|
||||||
|
else
|
||||||
|
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
74
Endpoint/Configuration/Core/Startup/SwaggerConfiguration.cs
Normal file
74
Endpoint/Configuration/Core/Startup/SwaggerConfiguration.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
using Asp.Versioning.ApiExplorer;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
|
||||||
|
public static class SwaggerConfiguration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddCustomSwagger(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SchemaFilter<SwaggerExampleFilter>();
|
||||||
|
options.OperationFilter<SwaggerDefaultValues>();
|
||||||
|
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Keep the JWT token in the field (Bearer token)",
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.ApiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (File.Exists(Path.Combine(basePath, "docs.xml")))
|
||||||
|
options.IncludeXmlComments(Path.Combine(basePath, "docs.xml"));
|
||||||
|
|
||||||
|
if (File.Exists(Path.Combine(basePath, "ApiDtoDocs.xml")))
|
||||||
|
options.IncludeXmlComments(Path.Combine(basePath, "ApiDtoDocs.xml"));
|
||||||
|
});
|
||||||
|
|
||||||
|
return services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app, IServiceProvider services)
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
return app.UseSwaggerUI(options =>
|
||||||
|
{
|
||||||
|
options.InjectStylesheet($"{UrlHelper.GetSubPath}css/swagger/SwaggerDark.css");
|
||||||
|
var provider = services.GetService<IApiVersionDescriptionProvider>();
|
||||||
|
|
||||||
|
foreach (var description in provider!.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
var url = $"/swagger/{description.GroupName}/swagger.json";
|
||||||
|
var name = description.GroupName.ToUpperInvariant();
|
||||||
|
options.SwaggerEndpoint(url, name);
|
||||||
|
options.RoutePrefix = UrlHelper.GetSubPathSwagger.Trim('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
5
Endpoint/Configuration/ISaveSettings.cs
Normal file
5
Endpoint/Configuration/ISaveSettings.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Mirea.Api.Endpoint.Configuration;
|
||||||
|
public interface ISaveSettings
|
||||||
|
{
|
||||||
|
void SaveSetting();
|
||||||
|
}
|
27
Endpoint/Configuration/Model/Admin.cs
Normal file
27
Endpoint/Configuration/Model/Admin.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
|
||||||
|
public class Admin : ISaveSettings
|
||||||
|
{
|
||||||
|
[JsonIgnore] private const string FileName = "admin.json";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public static string FilePath => PathBuilder.Combine(FileName);
|
||||||
|
|
||||||
|
public required string Username { get; set; }
|
||||||
|
public required string Email { get; set; }
|
||||||
|
public required string PasswordHash { get; set; }
|
||||||
|
public required string Salt { get; set; }
|
||||||
|
public SecondFactor SecondFactor { get; set; } = SecondFactor.None;
|
||||||
|
public string? Secret { get; set; }
|
||||||
|
|
||||||
|
public void SaveSetting()
|
||||||
|
{
|
||||||
|
File.WriteAllText(FilePath, JsonSerializer.Serialize(this));
|
||||||
|
}
|
||||||
|
}
|
33
Endpoint/Configuration/Model/GeneralConfig.cs
Normal file
33
Endpoint/Configuration/Model/GeneralConfig.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
|
||||||
|
public class GeneralConfig : ISaveSettings
|
||||||
|
{
|
||||||
|
[JsonIgnore] private const string FileName = "Settings.json";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public static string FilePath => PathBuilder.Combine(FileName);
|
||||||
|
|
||||||
|
public DbSettings? DbSettings { get; set; }
|
||||||
|
public CacheSettings? CacheSettings { get; set; }
|
||||||
|
public ScheduleSettings? ScheduleSettings { get; set; }
|
||||||
|
public EmailSettings? EmailSettings { get; set; }
|
||||||
|
public LogSettings? LogSettings { get; set; }
|
||||||
|
public string? SecretForwardToken { get; set; }
|
||||||
|
|
||||||
|
public void SaveSetting()
|
||||||
|
{
|
||||||
|
File.WriteAllText(
|
||||||
|
FilePath,
|
||||||
|
JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
|
[RequiredSettings]
|
||||||
|
public class CacheSettings : IIsConfigured
|
||||||
|
{
|
||||||
|
public enum CacheEnum
|
||||||
|
{
|
||||||
|
Memcached,
|
||||||
|
Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
public CacheEnum TypeDatabase { get; set; }
|
||||||
|
public string? ConnectionString { get; set; }
|
||||||
|
|
||||||
|
public bool IsConfigured()
|
||||||
|
{
|
||||||
|
return TypeDatabase == CacheEnum.Memcached ||
|
||||||
|
!string.IsNullOrEmpty(ConnectionString);
|
||||||
|
}
|
||||||
|
}
|
33
Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
Normal file
33
Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Mirea.Api.DataAccess.Persistence.Common;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
|
[RequiredSettings]
|
||||||
|
public class DbSettings : IIsConfigured
|
||||||
|
{
|
||||||
|
public enum DatabaseEnum
|
||||||
|
{
|
||||||
|
Mysql,
|
||||||
|
Sqlite,
|
||||||
|
PostgresSql
|
||||||
|
}
|
||||||
|
public DatabaseEnum TypeDatabase { get; set; }
|
||||||
|
public required string ConnectionStringSql { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public DatabaseProvider DatabaseProvider =>
|
||||||
|
TypeDatabase switch
|
||||||
|
{
|
||||||
|
DatabaseEnum.PostgresSql => DatabaseProvider.Postgresql,
|
||||||
|
DatabaseEnum.Mysql => DatabaseProvider.Mysql,
|
||||||
|
DatabaseEnum.Sqlite => DatabaseProvider.Sqlite,
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool IsConfigured() =>
|
||||||
|
!string.IsNullOrEmpty(ConnectionStringSql);
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
|
public class EmailSettings : IIsConfigured
|
||||||
|
{
|
||||||
|
public string? Server { get; set; }
|
||||||
|
public string? User { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string? From { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public bool? Ssl { get; set; }
|
||||||
|
|
||||||
|
public bool IsConfigured()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(Server) &&
|
||||||
|
!string.IsNullOrEmpty(User) &&
|
||||||
|
!string.IsNullOrEmpty(Password) &&
|
||||||
|
!string.IsNullOrEmpty(From) &&
|
||||||
|
Port.HasValue &&
|
||||||
|
Ssl.HasValue;
|
||||||
|
}
|
||||||
|
}
|
19
Endpoint/Configuration/Model/GeneralSettings/LogSettings.cs
Normal file
19
Endpoint/Configuration/Model/GeneralSettings/LogSettings.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
|
[RequiredSettings]
|
||||||
|
public class LogSettings : IIsConfigured
|
||||||
|
{
|
||||||
|
public bool EnableLogToFile { get; set; }
|
||||||
|
public string? LogFilePath { get; set; }
|
||||||
|
public string? LogFileName { get; set; }
|
||||||
|
|
||||||
|
public bool IsConfigured()
|
||||||
|
{
|
||||||
|
return !EnableLogToFile ||
|
||||||
|
!string.IsNullOrEmpty(LogFilePath) &&
|
||||||
|
!string.IsNullOrEmpty(LogFileName);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
|
[RequiredSettings]
|
||||||
|
public class ScheduleSettings : IIsConfigured
|
||||||
|
{
|
||||||
|
public struct PairPeriodTime
|
||||||
|
{
|
||||||
|
public TimeOnly Start { get; set; }
|
||||||
|
public TimeOnly End { get; set; }
|
||||||
|
|
||||||
|
public PairPeriodTime(TimeOnly t1, TimeOnly t2)
|
||||||
|
{
|
||||||
|
if (t1 > t2)
|
||||||
|
{
|
||||||
|
Start = t2;
|
||||||
|
End = t1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Start = t1;
|
||||||
|
End = t2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairPeriodTime(Dto.Common.PairPeriodTime time) : this(time.Start, time.End) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public required string CronUpdateSchedule { get; set; }
|
||||||
|
public DateOnly StartTerm { get; set; }
|
||||||
|
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
|
||||||
|
|
||||||
|
public bool IsConfigured()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(CronUpdateSchedule) &&
|
||||||
|
StartTerm != default &&
|
||||||
|
PairPeriod.Count != 0 &&
|
||||||
|
PairPeriod.Any();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
using Asp.Versioning.ApiExplorer;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||||
|
|
||||||
|
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
||||||
|
{
|
||||||
|
public void Configure(SwaggerGenOptions options)
|
||||||
|
{
|
||||||
|
foreach (var description in provider.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
|
||||||
|
{
|
||||||
|
var info = new OpenApiInfo()
|
||||||
|
{
|
||||||
|
Title = "MIREA Schedule Web API",
|
||||||
|
Version = description.ApiVersion.ToString(),
|
||||||
|
Description = "This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.",
|
||||||
|
Contact = new OpenApiContact { Name = "Author name", Email = "support@winsomnia.net" },
|
||||||
|
License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (description.IsDeprecated)
|
||||||
|
info.Description += " This API version has been deprecated.";
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||||
|
|
||||||
|
public class SwaggerDefaultValues : IOperationFilter
|
||||||
|
{
|
||||||
|
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||||
|
{
|
||||||
|
var apiDescription = context.ApiDescription;
|
||||||
|
operation.Deprecated |= apiDescription.IsDeprecated();
|
||||||
|
|
||||||
|
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
|
||||||
|
{
|
||||||
|
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
|
||||||
|
var response = operation.Responses[responseKey];
|
||||||
|
|
||||||
|
foreach (var contentType in response.Content.Keys)
|
||||||
|
{
|
||||||
|
if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
|
||||||
|
{
|
||||||
|
response.Content.Remove(contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.Parameters == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parameter in operation.Parameters)
|
||||||
|
{
|
||||||
|
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
|
||||||
|
|
||||||
|
parameter.Description ??= description.ModelMetadata.Description;
|
||||||
|
|
||||||
|
if (parameter.Schema.Default == null &&
|
||||||
|
description.DefaultValue != null &&
|
||||||
|
description.DefaultValue is not DBNull &&
|
||||||
|
description.ModelMetadata is ModelMetadata modelMetadata)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType);
|
||||||
|
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
parameter.Required |= description.IsRequired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||||
|
|
||||||
|
public class SwaggerExampleFilter : ISchemaFilter
|
||||||
|
{
|
||||||
|
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||||
|
{
|
||||||
|
var att = context.ParameterInfo?.GetCustomAttribute<SwaggerDefaultAttribute>();
|
||||||
|
if (att != null)
|
||||||
|
schema.Example = new Microsoft.OpenApi.Any.OpenApiString(att.Value);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public class RequiredSettingsAttribute : Attribute;
|
||||||
|
|
||||||
|
// todo: only with IIsConfigured. If possible add Roslyn Analyzer later
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
|
public interface IIsConfigured
|
||||||
|
{
|
||||||
|
bool IsConfigured();
|
||||||
|
}
|
28
Endpoint/Configuration/Validation/SetupTokenService.cs
Normal file
28
Endpoint/Configuration/Validation/SetupTokenService.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Validation;
|
||||||
|
|
||||||
|
public class SetupTokenService : ISetupToken
|
||||||
|
{
|
||||||
|
public ReadOnlyMemory<byte>? Token { get; private set; }
|
||||||
|
|
||||||
|
public bool MatchToken(ReadOnlySpan<byte> token)
|
||||||
|
{
|
||||||
|
if (Token == null || token.Length != Token.Value.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var token2 = Token.Value.Span;
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
|
for (int i = 0; i < Token.Value.Length; i++)
|
||||||
|
result |= token2[i] ^ token[i];
|
||||||
|
|
||||||
|
return result == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetToken(ReadOnlySpan<byte> token)
|
||||||
|
{
|
||||||
|
Token = token.ToArray();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Configuration.Validation.Validators;
|
||||||
|
|
||||||
|
public class SettingsRequiredValidator
|
||||||
|
{
|
||||||
|
private readonly GeneralConfig _generalConfig;
|
||||||
|
|
||||||
|
public SettingsRequiredValidator(IOptionsSnapshot<GeneralConfig> configuration) =>
|
||||||
|
_generalConfig = configuration.Value;
|
||||||
|
|
||||||
|
public SettingsRequiredValidator(GeneralConfig configuration) =>
|
||||||
|
_generalConfig = configuration;
|
||||||
|
|
||||||
|
public bool AreSettingsValid()
|
||||||
|
{
|
||||||
|
foreach (var property in _generalConfig
|
||||||
|
.GetType()
|
||||||
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
if (!Attribute.IsDefined(property.PropertyType, typeof(RequiredSettingsAttribute))) continue;
|
||||||
|
|
||||||
|
var value = property.GetValue(_generalConfig);
|
||||||
|
if (value == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var isConfigured = value as IIsConfigured;
|
||||||
|
if (!isConfigured!.IsConfigured())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
8
Endpoint/Controllers/BaseController.cs
Normal file
8
Endpoint/Controllers/BaseController.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers;
|
||||||
|
|
||||||
|
[Produces("application/json")]
|
||||||
|
[Route("api/v{version:apiVersion}/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class BaseController : ControllerBase;
|
360
Endpoint/Controllers/Configuration/SetupController.cs
Normal file
360
Endpoint/Controllers/Configuration/SetupController.cs
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using Cronos;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Mirea.Api.Dto.Requests;
|
||||||
|
using Mirea.Api.Dto.Requests.Configuration;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Common.Exceptions;
|
||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Validators;
|
||||||
|
using Mirea.Api.Security.Services;
|
||||||
|
using MySqlConnector;
|
||||||
|
using Npgsql;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Mail;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.Configuration;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[MaintenanceModeIgnore]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
public class SetupController(
|
||||||
|
ISetupToken setupToken,
|
||||||
|
IMaintenanceModeNotConfigureService notConfigureService,
|
||||||
|
IMemoryCache cache,
|
||||||
|
PasswordHashService passwordHashService) : BaseController
|
||||||
|
{
|
||||||
|
private const string CacheGeneralKey = "config_general";
|
||||||
|
private const string CacheAdminKey = "config_admin";
|
||||||
|
|
||||||
|
private GeneralConfig GeneralConfig
|
||||||
|
{
|
||||||
|
get => cache.Get<GeneralConfig>(CacheGeneralKey) ?? new GeneralConfig();
|
||||||
|
set => cache.Set(CacheGeneralKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("GenerateToken")]
|
||||||
|
[Localhost]
|
||||||
|
public ActionResult<string> GenerateToken()
|
||||||
|
{
|
||||||
|
if (!notConfigureService.IsMaintenanceMode)
|
||||||
|
throw new ControllerArgumentException(
|
||||||
|
"The token cannot be generated because the server has been configured. " +
|
||||||
|
$"If you need to restart the configuration, then delete the \"{GeneralConfig.FilePath}\" file and restart the application.");
|
||||||
|
|
||||||
|
var token = new byte[32];
|
||||||
|
RandomNumberGenerator.Create().GetBytes(token);
|
||||||
|
setupToken.SetToken(token);
|
||||||
|
|
||||||
|
return Ok(Convert.ToBase64String(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("IsConfigured")]
|
||||||
|
public ActionResult<bool> IsConfigured() =>
|
||||||
|
!notConfigureService.IsMaintenanceMode;
|
||||||
|
|
||||||
|
[HttpGet("CheckToken")]
|
||||||
|
public ActionResult<bool> CheckToken([FromQuery] string token)
|
||||||
|
{
|
||||||
|
if (!setupToken.MatchToken(Convert.FromBase64String(token)))
|
||||||
|
return Unauthorized("The token is not valid");
|
||||||
|
|
||||||
|
Response.Cookies.Append(TokenAuthenticationAttribute.AuthToken, token, new CookieOptions
|
||||||
|
{
|
||||||
|
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api",
|
||||||
|
Domain = HttpContext.GetCurrentDomain(),
|
||||||
|
HttpOnly = true,
|
||||||
|
#if !DEBUG
|
||||||
|
Secure = true
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult<bool> SetDatabase<TConnection, TException>(string connectionString, DbSettings.DatabaseEnum databaseType)
|
||||||
|
where TConnection : class, IDbConnection, new()
|
||||||
|
where TException : Exception
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var connection = new TConnection())
|
||||||
|
{
|
||||||
|
connection.ConnectionString = connectionString;
|
||||||
|
connection.Open();
|
||||||
|
connection.Close();
|
||||||
|
|
||||||
|
if (connection is SqliteConnection)
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
}
|
||||||
|
|
||||||
|
var general = GeneralConfig;
|
||||||
|
general.DbSettings = new DbSettings
|
||||||
|
{
|
||||||
|
ConnectionStringSql = connectionString,
|
||||||
|
TypeDatabase = databaseType
|
||||||
|
};
|
||||||
|
GeneralConfig = general;
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
catch (TException ex)
|
||||||
|
{
|
||||||
|
throw new ControllerArgumentException($"Error when connecting: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetPsql")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetPsql([FromBody] DatabaseRequest request)
|
||||||
|
{
|
||||||
|
string connectionString = $"Host={request.Server}:{request.Port};Username={request.User};Database={request.Database}";
|
||||||
|
if (request.Password != null)
|
||||||
|
connectionString += $";Password={request.Password}";
|
||||||
|
if (request.Ssl)
|
||||||
|
connectionString += ";SSL Mode=Require;";
|
||||||
|
|
||||||
|
return SetDatabase<NpgsqlConnection, NpgsqlException>(connectionString, DbSettings.DatabaseEnum.PostgresSql);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetMysql")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetMysql([FromBody] DatabaseRequest request)
|
||||||
|
{
|
||||||
|
string connectionString = $"Server={request.Server}:{request.Port};Uid={request.User};Database={request.Database};";
|
||||||
|
if (request.Password != null)
|
||||||
|
connectionString += $"Pwd={request.Password};";
|
||||||
|
if (request.Ssl)
|
||||||
|
connectionString += "SslMode=Require;";
|
||||||
|
|
||||||
|
return SetDatabase<MySqlConnection, MySqlException>(connectionString, DbSettings.DatabaseEnum.Mysql);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetSqlite")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
public ActionResult<bool> SetSqlite([FromQuery] string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path)) path = "database";
|
||||||
|
|
||||||
|
path = PathBuilder.Combine(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
else
|
||||||
|
Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
||||||
|
}
|
||||||
|
else if (Directory.GetDirectories(path).Length != 0 ||
|
||||||
|
!Directory.GetFiles(path).Select(x => string.Equals(Path.GetFileName(x), "database.db3")).All(x => x))
|
||||||
|
{
|
||||||
|
throw new ControllerArgumentException("Such a folder exists. Enter a different name");
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath = Path.Combine(path, "database.db3");
|
||||||
|
var connectionString = $"Data Source={filePath}";
|
||||||
|
|
||||||
|
var result = SetDatabase<SqliteConnection, SqliteException>(connectionString, DbSettings.DatabaseEnum.Sqlite);
|
||||||
|
|
||||||
|
foreach (var file in Directory.GetFiles(path))
|
||||||
|
System.IO.File.Delete(file);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetRedis")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetRedis([FromBody] CacheRequest request)
|
||||||
|
{
|
||||||
|
string connectionString = $"{request.Server}:{request.Port},ssl=false";
|
||||||
|
if (request.Password != null)
|
||||||
|
connectionString += $",password={request.Password}";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var redis = ConnectionMultiplexer.Connect(connectionString);
|
||||||
|
redis.Close();
|
||||||
|
|
||||||
|
var general = GeneralConfig;
|
||||||
|
general.CacheSettings = new CacheSettings
|
||||||
|
{
|
||||||
|
ConnectionString = connectionString,
|
||||||
|
TypeDatabase = CacheSettings.CacheEnum.Redis
|
||||||
|
};
|
||||||
|
GeneralConfig = general;
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new ControllerArgumentException("Error when connecting to Redis: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetMemcached")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetMemcached()
|
||||||
|
{
|
||||||
|
var general = GeneralConfig;
|
||||||
|
general.CacheSettings = new CacheSettings
|
||||||
|
{
|
||||||
|
ConnectionString = null,
|
||||||
|
TypeDatabase = CacheSettings.CacheEnum.Memcached
|
||||||
|
};
|
||||||
|
GeneralConfig = general;
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("CreateAdmin")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<string> CreateAdmin([FromBody] CreateUserRequest user)
|
||||||
|
{
|
||||||
|
if (!PasswordHashService.HasPasswordInPolicySecurity(user.Password))
|
||||||
|
throw new ControllerArgumentException("The password must be at least 8 characters long and contain at least one uppercase letter and one special character.");
|
||||||
|
|
||||||
|
if (!MailAddress.TryCreate(user.Email, out _))
|
||||||
|
throw new ControllerArgumentException("The email address is incorrect.");
|
||||||
|
|
||||||
|
var (salt, hash) = passwordHashService.HashPassword(user.Password);
|
||||||
|
|
||||||
|
var admin = new Admin
|
||||||
|
{
|
||||||
|
Username = user.Username,
|
||||||
|
Email = user.Email,
|
||||||
|
PasswordHash = hash,
|
||||||
|
Salt = salt
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.Set(CacheAdminKey, admin);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetLogging")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetLogging([FromBody] LoggingRequest? request = null)
|
||||||
|
{
|
||||||
|
var settings = (request == null) switch
|
||||||
|
{
|
||||||
|
true => new LogSettings
|
||||||
|
{
|
||||||
|
EnableLogToFile = true
|
||||||
|
},
|
||||||
|
false => new LogSettings
|
||||||
|
{
|
||||||
|
EnableLogToFile = request.EnableLogToFile,
|
||||||
|
LogFileName = request.LogFileName,
|
||||||
|
LogFilePath = request.LogFilePath
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.EnableLogToFile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(settings.LogFileName))
|
||||||
|
settings.LogFileName = "log-";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(settings.LogFilePath))
|
||||||
|
settings.LogFilePath = OperatingSystem.IsWindows() || PathBuilder.IsDefaultPath ?
|
||||||
|
PathBuilder.Combine("logs") :
|
||||||
|
"/var/log/mirea";
|
||||||
|
}
|
||||||
|
|
||||||
|
var general = GeneralConfig;
|
||||||
|
general.LogSettings = settings;
|
||||||
|
GeneralConfig = general;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetEmail")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetEmail([FromBody] EmailRequest? request = null)
|
||||||
|
{
|
||||||
|
var settings = (request == null) switch
|
||||||
|
{
|
||||||
|
true => new EmailSettings(),
|
||||||
|
false => new EmailSettings
|
||||||
|
{
|
||||||
|
Server = request.Server,
|
||||||
|
From = request.From,
|
||||||
|
Password = request.Password,
|
||||||
|
Port = request.Port,
|
||||||
|
Ssl = request.Ssl,
|
||||||
|
User = request.User
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var general = GeneralConfig;
|
||||||
|
general.EmailSettings = settings;
|
||||||
|
GeneralConfig = general;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("SetSchedule")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> SetSchedule([FromBody] ScheduleConfigurationRequest request)
|
||||||
|
{
|
||||||
|
var general = GeneralConfig;
|
||||||
|
general.ScheduleSettings = new ScheduleSettings
|
||||||
|
{
|
||||||
|
// every 6 hours
|
||||||
|
CronUpdateSchedule = request.CronUpdateSchedule ?? "0 */6 * * *",
|
||||||
|
StartTerm = request.StartTerm,
|
||||||
|
PairPeriod = new Dictionary<int, ScheduleSettings.PairPeriodTime>
|
||||||
|
{
|
||||||
|
{1, new ScheduleSettings.PairPeriodTime(new TimeOnly(9, 0, 0), new TimeOnly(10, 30, 0))},
|
||||||
|
{2, new ScheduleSettings.PairPeriodTime(new TimeOnly(10, 40, 0), new TimeOnly(12, 10, 0))},
|
||||||
|
{3, new ScheduleSettings.PairPeriodTime(new TimeOnly(12, 40, 0), new TimeOnly(14, 10, 0))},
|
||||||
|
{4, new ScheduleSettings.PairPeriodTime(new TimeOnly(14, 20, 0), new TimeOnly(15, 50, 0))},
|
||||||
|
{5, new ScheduleSettings.PairPeriodTime(new TimeOnly(16, 20, 0), new TimeOnly(17, 50, 0))},
|
||||||
|
{6, new ScheduleSettings.PairPeriodTime(new TimeOnly(18, 0, 0), new TimeOnly(19, 30, 0))},
|
||||||
|
{7, new ScheduleSettings.PairPeriodTime(new TimeOnly(19, 40, 0), new TimeOnly(21, 10, 0))},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!CronExpression.TryParse(general.ScheduleSettings.CronUpdateSchedule, CronFormat.Standard, out _))
|
||||||
|
throw new ControllerArgumentException("The Cron task could not be parsed. Check the format of the entered data.");
|
||||||
|
|
||||||
|
GeneralConfig = general;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("Submit")]
|
||||||
|
[TokenAuthentication]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<bool> Submit()
|
||||||
|
{
|
||||||
|
if (!new SettingsRequiredValidator(GeneralConfig).AreSettingsValid())
|
||||||
|
throw new ControllerArgumentException("The necessary data has not been configured.");
|
||||||
|
|
||||||
|
if (!cache.TryGetValue(CacheAdminKey, out Admin? admin) || admin == null)
|
||||||
|
throw new ControllerArgumentException("The administrator's data was not set.");
|
||||||
|
|
||||||
|
admin.SaveSetting();
|
||||||
|
GeneralConfig.SaveSetting();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
125
Endpoint/Controllers/V1/AuthController.cs
Normal file
125
Endpoint/Controllers/V1/AuthController.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.Dto.Common;
|
||||||
|
using Mirea.Api.Dto.Requests;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Common.Exceptions;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Security.Common.Domain;
|
||||||
|
using Mirea.Api.Security.Services;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, PasswordHashService passwordService) : BaseController
|
||||||
|
{
|
||||||
|
private CookieOptionsParameters GetCookieParams() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Domain = HttpContext.GetCurrentDomain(),
|
||||||
|
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
|
||||||
|
};
|
||||||
|
|
||||||
|
[HttpPost("Login")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<AuthenticationStep>> Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
var userEntity = user.Value;
|
||||||
|
|
||||||
|
if (!userEntity.Username.Equals(request.Username, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!userEntity.Email.Equals(request.Username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return BadRequest("Invalid username/email or password");
|
||||||
|
|
||||||
|
var tokenResult = await auth.LoginAsync(
|
||||||
|
GetCookieParams(),
|
||||||
|
new User
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Username = userEntity.Username,
|
||||||
|
Email = userEntity.Email,
|
||||||
|
PasswordHash = userEntity.PasswordHash,
|
||||||
|
Salt = userEntity.Salt,
|
||||||
|
SecondFactor = userEntity.SecondFactor,
|
||||||
|
SecondFactorToken = userEntity.Secret
|
||||||
|
},
|
||||||
|
HttpContext, request.Password);
|
||||||
|
|
||||||
|
return Ok(tokenResult ? AuthenticationStep.None : AuthenticationStep.TotpRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("Login")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<AuthenticationStep>> Login([FromQuery] string code)
|
||||||
|
{
|
||||||
|
var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, code);
|
||||||
|
return Ok(tokenResult ? AuthenticationStep.None : AuthenticationStep.TotpRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the authentication token using the existing refresh token.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>User's AuthRoles.</returns>
|
||||||
|
[HttpGet("ReLogin")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
public async Task<ActionResult<AuthRoles>> ReLogin()
|
||||||
|
{
|
||||||
|
await auth.RefreshTokenAsync(GetCookieParams(), HttpContext);
|
||||||
|
return Ok(AuthRoles.Admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs the user out by clearing the refresh token and performing any necessary cleanup.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An Ok response if the logout was successful.</returns>
|
||||||
|
[HttpGet("Logout")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> Logout()
|
||||||
|
{
|
||||||
|
await auth.LogoutAsync(GetCookieParams(), HttpContext);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the role of the authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The role of the authenticated user.</returns>
|
||||||
|
[HttpGet("GetRole")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[Authorize]
|
||||||
|
public ActionResult<AuthRoles> GetRole() => Ok(AuthRoles.Admin);
|
||||||
|
|
||||||
|
[HttpPost("RenewPassword")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
[Localhost]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public ActionResult<string> RenewPassword([FromBody] string? password = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
password = string.Empty;
|
||||||
|
else if (!PasswordHashService.HasPasswordInPolicySecurity(password))
|
||||||
|
throw new ControllerArgumentException("The password must be at least 8 characters long and contain at least one uppercase letter and one special character.");
|
||||||
|
|
||||||
|
while (!PasswordHashService.HasPasswordInPolicySecurity(password))
|
||||||
|
password = GeneratorKey.GenerateAlphaNumeric(16, includes: "!@#%^");
|
||||||
|
|
||||||
|
var (salt, hash) = passwordService.HashPassword(password);
|
||||||
|
|
||||||
|
var admin = user.Value;
|
||||||
|
|
||||||
|
admin.Salt = salt;
|
||||||
|
admin.PasswordHash = hash;
|
||||||
|
admin.SaveSetting();
|
||||||
|
|
||||||
|
return Ok(password);
|
||||||
|
}
|
||||||
|
}
|
60
Endpoint/Controllers/V1/CampusController.cs
Normal file
60
Endpoint/Controllers/V1/CampusController.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Campus.Queries.GetCampusBasicInfoList;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Campus.Queries.GetCampusDetails;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class CampusController(IMediator mediator) : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets basic information about campuses.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Basic information about campuses.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<List<CampusBasicInfoResponse>>> Get()
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetCampusBasicInfoListQuery());
|
||||||
|
|
||||||
|
return Ok(result.Campuses
|
||||||
|
.Select(c => new CampusBasicInfoResponse()
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
CodeName = c.CodeName,
|
||||||
|
FullName = c.FullName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets details of a specific campus by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Campus ID.</param>
|
||||||
|
/// <returns>Details of the specified campus.</returns>
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<CampusDetailsResponse>> GetDetails(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetCampusDetailsQuery()
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new CampusDetailsResponse()
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
CodeName = result.CodeName,
|
||||||
|
FullName = result.FullName,
|
||||||
|
Address = result.Address
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
64
Endpoint/Controllers/V1/DisciplineController.cs
Normal file
64
Endpoint/Controllers/V1/DisciplineController.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Discipline.Queries.GetDisciplineDetails;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Discipline.Queries.GetDisciplineList;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class DisciplineController(IMediator mediator) : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a paginated list of disciplines.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">Page number. Start from 0.</param>
|
||||||
|
/// <param name="pageSize">Number of items per page.</param>
|
||||||
|
/// <returns>Paginated list of disciplines.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<List<DisciplineResponse>>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetDisciplineListQuery()
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(result.Disciplines
|
||||||
|
.Select(d => new DisciplineResponse()
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
Name = d.Name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets details of a specific discipline by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Discipline ID.</param>
|
||||||
|
/// <returns>Details of the specified discipline.</returns>
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<DisciplineResponse>> GetDetails(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetDisciplineInfoQuery()
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new DisciplineResponse()
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Name = result.Name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
41
Endpoint/Controllers/V1/FacultyController.cs
Normal file
41
Endpoint/Controllers/V1/FacultyController.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Faculty.Queries.GetFacultyList;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class FacultyController(IMediator mediator) : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a paginated list of faculties.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">Page number. Start from 0.</param>
|
||||||
|
/// <param name="pageSize">Number of items per page.</param>
|
||||||
|
/// <returns>Paginated list of faculties.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<List<FacultyResponse>>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetFacultyListQuery()
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(result.Faculties
|
||||||
|
.Select(f => new FacultyResponse()
|
||||||
|
{
|
||||||
|
Id = f.Id,
|
||||||
|
Name = f.Name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
Endpoint/Controllers/V1/GroupController.cs
Normal file
106
Endpoint/Controllers/V1/GroupController.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Group.Queries.GetGroupDetails;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Group.Queries.GetGroupList;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class GroupController(IMediator mediator) : BaseController
|
||||||
|
{
|
||||||
|
private static int GetCourseNumber(string groupName)
|
||||||
|
{
|
||||||
|
var current = DateTime.Now;
|
||||||
|
if (!int.TryParse(groupName[2..], out var yearOfGroup)
|
||||||
|
&& !int.TryParse(groupName.Split('-')[^1][..2], out yearOfGroup))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// Convert a two-digit year to a four-digit one
|
||||||
|
yearOfGroup += current.Year / 100 * 100;
|
||||||
|
|
||||||
|
return current.Year - yearOfGroup + (current.Month < 8 ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a list of groups.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">The page number for pagination (optional).</param>
|
||||||
|
/// <param name="pageSize">The page size for pagination (optional).</param>
|
||||||
|
/// <returns>A list of groups.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<List<GroupResponse>>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetGroupListQuery()
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(result.Groups
|
||||||
|
.Select(g => new GroupResponse()
|
||||||
|
{
|
||||||
|
Id = g.Id,
|
||||||
|
Name = g.Name,
|
||||||
|
FacultyId = g.FacultyId,
|
||||||
|
CourseNumber = GetCourseNumber(g.Name)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves detailed information about a specific group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the group to retrieve.</param>
|
||||||
|
/// <returns>Detailed information about the group.</returns>
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<GroupDetailsResponse>> GetDetails(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetGroupInfoQuery()
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new GroupDetailsResponse()
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Name = result.Name,
|
||||||
|
FacultyId = result.FacultyId,
|
||||||
|
FacultyName = result.Faculty,
|
||||||
|
CourseNumber = GetCourseNumber(result.Name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a list of groups by faculty ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the faculty.</param>
|
||||||
|
/// <returns>A list of groups belonging to the specified faculty.</returns>
|
||||||
|
[HttpGet("GetByFaculty/{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<GroupResponse>>> GetByFaculty(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetGroupListQuery());
|
||||||
|
|
||||||
|
return Ok(result.Groups
|
||||||
|
.Where(g => g.FacultyId == id)
|
||||||
|
.Select(g => new GroupResponse()
|
||||||
|
{
|
||||||
|
Id = g.Id,
|
||||||
|
Name = g.Name,
|
||||||
|
CourseNumber = GetCourseNumber(g.Name),
|
||||||
|
FacultyId = g.FacultyId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
167
Endpoint/Controllers/V1/ImportController.cs
Normal file
167
Endpoint/Controllers/V1/ImportController.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Schedule.Queries.GetScheduleList;
|
||||||
|
using Mirea.Api.Dto.Requests;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using OfficeOpenXml;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
public class ImportController(IMediator mediator, IOptionsSnapshot<GeneralConfig> config) : BaseController
|
||||||
|
{
|
||||||
|
// todo: transfer data to storage
|
||||||
|
private static string GetFaculty(char c) =>
|
||||||
|
c switch
|
||||||
|
{
|
||||||
|
'У' => "ИТУ",
|
||||||
|
'Б' => "ИКБ",
|
||||||
|
'Х' => "ИТХТ",
|
||||||
|
'Э' => "ИПТИП",
|
||||||
|
'Т' => "ИПТИП",
|
||||||
|
'Р' => "ИРИ",
|
||||||
|
'К' => "ИИИ",
|
||||||
|
'И' => "ИИТ",
|
||||||
|
'П' => "ИИТ",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an Excel file based on a schedule filter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request object containing filter criteria.</param>
|
||||||
|
/// <returns>Excel file</returns>
|
||||||
|
[HttpPost("ImportToExcel")]
|
||||||
|
[Produces("application/vnd.ms-excel")]
|
||||||
|
public async Task<IActionResult> ImportToExcel([FromBody] ScheduleRequest request)
|
||||||
|
{
|
||||||
|
var result = (await mediator.Send(new GetScheduleListQuery
|
||||||
|
{
|
||||||
|
IsEven = request.IsEven,
|
||||||
|
DisciplineIds = request.Disciplines,
|
||||||
|
GroupIds = request.Groups,
|
||||||
|
LectureHallIds = request.LectureHalls,
|
||||||
|
ProfessorIds = request.Professors
|
||||||
|
})).Schedules;
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
return NoContent();
|
||||||
|
|
||||||
|
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
|
||||||
|
using var package = new ExcelPackage();
|
||||||
|
var worksheet = package.Workbook.Worksheets.Add("Расписание");
|
||||||
|
|
||||||
|
int row = 1;
|
||||||
|
int col = 1;
|
||||||
|
|
||||||
|
worksheet.Cells[row, col++].Value = "День";
|
||||||
|
worksheet.Cells[row, col++].Value = "Пара";
|
||||||
|
worksheet.Cells[row, col++].Value = "Неделя";
|
||||||
|
worksheet.Cells[row, col++].Value = "Время";
|
||||||
|
worksheet.Cells[row, col++].Value = "Группа";
|
||||||
|
worksheet.Cells[row, col++].Value = "Институт";
|
||||||
|
worksheet.Cells[row, col++].Value = "Курс";
|
||||||
|
worksheet.Cells[row, col++].Value = "Дисциплина";
|
||||||
|
worksheet.Cells[row, col++].Value = "Преподаватель";
|
||||||
|
worksheet.Cells[row, col++].Value = "Вид";
|
||||||
|
worksheet.Cells[row, col++].Value = "Кампус";
|
||||||
|
worksheet.Cells[row, col].Value = "Ауд.";
|
||||||
|
|
||||||
|
row++;
|
||||||
|
col = 1;
|
||||||
|
|
||||||
|
var pairsDictionary = config.Value.ScheduleSettings!.PairPeriod;
|
||||||
|
|
||||||
|
var ruCulture = new CultureInfo("ru-RU");
|
||||||
|
|
||||||
|
foreach (var dto in result.GroupBy(s => new
|
||||||
|
{
|
||||||
|
s.DayOfWeek,
|
||||||
|
s.PairNumber,
|
||||||
|
s.IsEven,
|
||||||
|
s.DisciplineId,
|
||||||
|
TypeOfOccupations = string.Join(',', s.TypeOfOccupations.OrderBy(x => x)),
|
||||||
|
LectureHalls = string.Join(',', s.LectureHalls.OrderBy(x => x)),
|
||||||
|
Campus = string.Join(',', s.Campus.OrderBy(x => x)),
|
||||||
|
Professors = string.Join(',', s.Professors.OrderBy(x => x))
|
||||||
|
})
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
g.Key.DayOfWeek,
|
||||||
|
g.Key.PairNumber,
|
||||||
|
g.Key.IsEven,
|
||||||
|
g.First().Discipline,
|
||||||
|
g.First().LectureHalls,
|
||||||
|
g.First().Campus,
|
||||||
|
g.First().Professors,
|
||||||
|
Groups = string.Join('\n', g.Select(x => x.Group)),
|
||||||
|
|
||||||
|
IsExclude = g.First().IsExcludedWeeks,
|
||||||
|
g.First().TypeOfOccupations,
|
||||||
|
g.First().Weeks
|
||||||
|
})
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
// День
|
||||||
|
worksheet.Cells[row, col++].Value =
|
||||||
|
$"{(int)dto.DayOfWeek} [{ruCulture.DateTimeFormat.GetAbbreviatedDayName(dto.DayOfWeek).ToUpper()}]";
|
||||||
|
|
||||||
|
// Пара
|
||||||
|
worksheet.Cells[row, col++].Value = dto.PairNumber + " п";
|
||||||
|
|
||||||
|
// Неделя
|
||||||
|
worksheet.Cells[row, col++].Value = $"[{(dto.IsEven ? 2 : 1)}] {(dto.IsEven ? "Четная" : "Нечетная")}";
|
||||||
|
|
||||||
|
// Время
|
||||||
|
worksheet.Cells[row, col++].Value = pairsDictionary[dto.PairNumber].Start.ToString(ruCulture);
|
||||||
|
|
||||||
|
// Группа
|
||||||
|
worksheet.Cells[row, col].Style.WrapText = true;
|
||||||
|
worksheet.Cells[row, col++].Value = dto.Groups;
|
||||||
|
|
||||||
|
var groupTemplate = dto.Groups.Split('\n')[0];
|
||||||
|
|
||||||
|
// Институт
|
||||||
|
worksheet.Cells[row, col++].Value = GetFaculty(groupTemplate[0]);
|
||||||
|
// Курс
|
||||||
|
worksheet.Cells[row, col++].Value = groupTemplate[2] == 'М' ?
|
||||||
|
'М' :
|
||||||
|
(24 - int.Parse(groupTemplate.Split(' ')[0].Split('-').TakeLast(1).ElementAt(0)) + 1).ToString();
|
||||||
|
|
||||||
|
var disciplineAdditional = string.Empty;
|
||||||
|
|
||||||
|
if (dto.IsExclude.HasValue && dto.Weeks != null && dto.Weeks.Any())
|
||||||
|
disciplineAdditional += $"{(dto.IsExclude.Value ? "Кр. " : "")}{string.Join(", ", dto.Weeks.OrderBy(x => x))} н. ";
|
||||||
|
|
||||||
|
// Дисциплина
|
||||||
|
worksheet.Cells[row, col++].Value = disciplineAdditional + dto.Discipline;
|
||||||
|
|
||||||
|
// Преподаватель
|
||||||
|
worksheet.Cells[row, col++].Value = dto.Professors;
|
||||||
|
// Вид
|
||||||
|
worksheet.Cells[row, col++].Value = dto.TypeOfOccupations.FirstOrDefault();
|
||||||
|
// Кампус
|
||||||
|
worksheet.Cells[row, col++].Value = dto.Campus.FirstOrDefault()?.Replace("С-20", "С20").Replace("В-78", "В78");
|
||||||
|
// Ауд.
|
||||||
|
worksheet.Cells[row, col].Value = dto.LectureHalls;
|
||||||
|
|
||||||
|
col = 1;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
worksheet.Cells[1, 1, 1, 12].AutoFilter = true;
|
||||||
|
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
|
||||||
|
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
await package.SaveAsAsync(stream);
|
||||||
|
stream.Position = 0;
|
||||||
|
return File(stream, "application/vnd.ms-excel", "data.xlsx");
|
||||||
|
}
|
||||||
|
}
|
82
Endpoint/Controllers/V1/LectureHallController.cs
Normal file
82
Endpoint/Controllers/V1/LectureHallController.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.LectureHall.Queries.GetLectureHallDetails;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.LectureHall.Queries.GetLectureHallList;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class LectureHallController(IMediator mediator) : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a list of all lecture halls.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of lecture halls.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<List<LectureHallResponse>>> Get()
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetLectureHallListQuery());
|
||||||
|
|
||||||
|
return Ok(result.LectureHalls
|
||||||
|
.Select(l => new LectureHallResponse()
|
||||||
|
{
|
||||||
|
Id = l.Id,
|
||||||
|
Name = l.Name,
|
||||||
|
CampusId = l.CampusId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves details of a specific lecture hall by its ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the lecture hall to retrieve.</param>
|
||||||
|
/// <returns>The details of the specified lecture hall.</returns>
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<LectureHallDetailsResponse>> GetDetails(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetLectureHallInfoQuery()
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new LectureHallDetailsResponse()
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Name = result.Name,
|
||||||
|
CampusId = result.CampusId,
|
||||||
|
CampusCode = result.CampusCode,
|
||||||
|
CampusName = result.CampusName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a list of lecture halls by campus ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the campus.</param>
|
||||||
|
/// <returns>A list of lecture halls in the specified campus.</returns>
|
||||||
|
[HttpGet("GetByCampus/{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<LectureHallResponse>>> GetByCampus(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetLectureHallListQuery());
|
||||||
|
|
||||||
|
return Ok(result.LectureHalls.Where(l => l.CampusId == id)
|
||||||
|
.Select(l => new LectureHallResponse()
|
||||||
|
{
|
||||||
|
Id = l.Id,
|
||||||
|
Name = l.Name,
|
||||||
|
CampusId = l.CampusId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
98
Endpoint/Controllers/V1/ProfessorController.cs
Normal file
98
Endpoint/Controllers/V1/ProfessorController.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Professor.Queries.GetProfessorDetails;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Professor.Queries.GetProfessorDetailsBySearch;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Professor.Queries.GetProfessorList;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class ProfessorController(IMediator mediator) : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a list of professors.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">The page number for pagination (optional).</param>
|
||||||
|
/// <param name="pageSize">The page size for pagination (optional).</param>
|
||||||
|
/// <returns>A list of professors.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<List<ProfessorResponse>>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetProfessorListQuery()
|
||||||
|
{
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(result.Professors
|
||||||
|
.Select(p => new ProfessorResponse()
|
||||||
|
{
|
||||||
|
Id = p.Id,
|
||||||
|
Name = p.Name,
|
||||||
|
AltName = p.AltName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves detailed information about a specific professor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the professor to retrieve.</param>
|
||||||
|
/// <returns>Detailed information about the professor.</returns>
|
||||||
|
[HttpGet("{id:int}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<ProfessorResponse>> GetDetails(int id)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new GetProfessorInfoQuery()
|
||||||
|
{
|
||||||
|
Id = id
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new ProfessorResponse()
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Name = result.Name,
|
||||||
|
AltName = result.AltName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves detailed information about professors based on their name.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method searches for professors whose name matches the provided search term.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="name">The name of the professor to search for. Must be at least 4 characters long.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A list of <see cref="ProfessorResponse"/> objects containing the details of the matching professors.
|
||||||
|
/// </returns>
|
||||||
|
[HttpGet("{name:required}")]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<ProfessorResponse>>> GetDetails(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name) || name.Length < 4)
|
||||||
|
return BadRequest($"The minimum number of characters is 4 (current: {name.Length}).");
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetProfessorInfoSearchQuery()
|
||||||
|
{
|
||||||
|
Name = name
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(result.Details.Select(x => new ProfessorResponse()
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Name = x.Name,
|
||||||
|
AltName = x.AltName
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
206
Endpoint/Controllers/V1/ScheduleController.cs
Normal file
206
Endpoint/Controllers/V1/ScheduleController.cs
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.DataAccess.Application.Cqrs.Schedule.Queries.GetScheduleList;
|
||||||
|
using Mirea.Api.Dto.Common;
|
||||||
|
using Mirea.Api.Dto.Requests;
|
||||||
|
using Mirea.Api.Dto.Responses;
|
||||||
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[CacheMaxAge(true)]
|
||||||
|
public class ScheduleController(IMediator mediator, IOptionsSnapshot<GeneralConfig> config) : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the start term for the schedule.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The start term as a <see cref="DateOnly"/> value.</returns>
|
||||||
|
[CacheMaxAge(1, 0)]
|
||||||
|
[HttpGet("StartTerm")]
|
||||||
|
public ActionResult<DateOnly> GetStartTerm() => config.Value.ScheduleSettings!.StartTerm;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the pair periods.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A dictionary of pair periods, where the key is an integer identifier and the value is a <see cref="PairPeriodTime"/> object.</returns>
|
||||||
|
[CacheMaxAge(1, 0)]
|
||||||
|
[HttpGet("PairPeriod")]
|
||||||
|
public ActionResult<Dictionary<int, PairPeriodTime>> GetPairPeriod() => config.Value.ScheduleSettings!.PairPeriod.ConvertToDto();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves schedules based on various filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request object containing filter criteria.</param>
|
||||||
|
/// <returns>A list of schedules matching the filter criteria.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[BadRequestResponse]
|
||||||
|
public async Task<ActionResult<List<ScheduleResponse>>> Get([FromBody] ScheduleRequest request)
|
||||||
|
{
|
||||||
|
if ((request.Groups == null || request.Groups.Length == 0) &&
|
||||||
|
(request.Disciplines == null || request.Disciplines.Length == 0) &&
|
||||||
|
(request.Professors == null || request.Professors.Length == 0) &&
|
||||||
|
(request.LectureHalls == null || request.LectureHalls.Length == 0))
|
||||||
|
{
|
||||||
|
return BadRequest(new ErrorResponse()
|
||||||
|
{
|
||||||
|
Error = "At least one of the arguments must be selected."
|
||||||
|
+ (request.IsEven.HasValue
|
||||||
|
? $" \"{nameof(request.IsEven)}\" is not a strong argument"
|
||||||
|
: string.Empty),
|
||||||
|
Code = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = (await mediator.Send(new GetScheduleListQuery
|
||||||
|
{
|
||||||
|
IsEven = request.IsEven,
|
||||||
|
DisciplineIds = request.Disciplines,
|
||||||
|
GroupIds = request.Groups,
|
||||||
|
LectureHallIds = request.LectureHalls,
|
||||||
|
ProfessorIds = request.Professors
|
||||||
|
})).Schedules;
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
NoContent();
|
||||||
|
|
||||||
|
return Ok(result.Select(s => new ScheduleResponse
|
||||||
|
{
|
||||||
|
DayOfWeek = s.DayOfWeek,
|
||||||
|
PairNumber = s.PairNumber,
|
||||||
|
IsEven = s.IsEven,
|
||||||
|
Discipline = s.Discipline,
|
||||||
|
DisciplineId = s.DisciplineId,
|
||||||
|
IsExcludedWeeks = s.IsExcludedWeeks,
|
||||||
|
Weeks = s.Weeks,
|
||||||
|
TypeOfOccupations = s.TypeOfOccupations,
|
||||||
|
Group = s.Group,
|
||||||
|
GroupId = s.GroupId,
|
||||||
|
LectureHalls = s.LectureHalls,
|
||||||
|
LectureHallsId = s.LectureHallsId,
|
||||||
|
Professors = s.Professors,
|
||||||
|
ProfessorsId = s.ProfessorsId,
|
||||||
|
Campus = s.Campus,
|
||||||
|
CampusId = s.CampusId,
|
||||||
|
LinkToMeet = s.LinkToMeet
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves schedules for a specific group based on various filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the group.</param>
|
||||||
|
/// <param name="isEven">A value indicating whether to retrieve schedules for even weeks.</param>
|
||||||
|
/// <param name="disciplines">An array of discipline IDs.</param>
|
||||||
|
/// <param name="professors">An array of professor IDs.</param>
|
||||||
|
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||||
|
/// <returns>A response containing schedules for the specified group.</returns>
|
||||||
|
[HttpGet("GetByGroup/{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<ScheduleResponse>>> GetByGroup(int id,
|
||||||
|
[FromQuery] bool? isEven = null,
|
||||||
|
[FromQuery] int[]? disciplines = null,
|
||||||
|
[FromQuery] int[]? professors = null,
|
||||||
|
[FromQuery] int[]? lectureHalls = null) =>
|
||||||
|
await Get(new ScheduleRequest
|
||||||
|
{
|
||||||
|
Disciplines = disciplines,
|
||||||
|
IsEven = isEven,
|
||||||
|
Groups = [id],
|
||||||
|
Professors = professors,
|
||||||
|
LectureHalls = lectureHalls
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves schedules for a specific professor based on various filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the professor.</param>
|
||||||
|
/// <param name="isEven">A value indicating whether to retrieve schedules for even weeks.</param>
|
||||||
|
/// <param name="disciplines">An array of discipline IDs.</param>
|
||||||
|
/// <param name="groups">An array of group IDs.</param>
|
||||||
|
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||||
|
/// <returns>A response containing schedules for the specified professor.</returns>
|
||||||
|
[HttpGet("GetByProfessor/{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<ScheduleResponse>>> GetByProfessor(int id,
|
||||||
|
[FromQuery] bool? isEven = null,
|
||||||
|
[FromQuery] int[]? disciplines = null,
|
||||||
|
[FromQuery] int[]? groups = null,
|
||||||
|
[FromQuery] int[]? lectureHalls = null) =>
|
||||||
|
await Get(new ScheduleRequest
|
||||||
|
{
|
||||||
|
Disciplines = disciplines,
|
||||||
|
IsEven = isEven,
|
||||||
|
Groups = groups,
|
||||||
|
Professors = [id],
|
||||||
|
LectureHalls = lectureHalls
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves schedules for a specific lecture hall based on various filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the lecture hall.</param>
|
||||||
|
/// <param name="isEven">A value indicating whether to retrieve schedules for even weeks.</param>
|
||||||
|
/// <param name="disciplines">An array of discipline IDs.</param>
|
||||||
|
/// <param name="professors">An array of professor IDs.</param>
|
||||||
|
/// <param name="groups">An array of group IDs.</param>
|
||||||
|
/// <returns>A response containing schedules for the specified lecture hall.</returns>
|
||||||
|
[HttpGet("GetByLectureHall/{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<ScheduleResponse>>> GetByLectureHall(int id,
|
||||||
|
[FromQuery] bool? isEven = null,
|
||||||
|
[FromQuery] int[]? disciplines = null,
|
||||||
|
[FromQuery] int[]? groups = null,
|
||||||
|
[FromQuery] int[]? professors = null) =>
|
||||||
|
await Get(new ScheduleRequest
|
||||||
|
{
|
||||||
|
Disciplines = disciplines,
|
||||||
|
IsEven = isEven,
|
||||||
|
Groups = groups,
|
||||||
|
Professors = professors,
|
||||||
|
LectureHalls = [id]
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves schedules for a specific discipline based on various filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the discipline.</param>
|
||||||
|
/// <param name="isEven">A value indicating whether to retrieve schedules for even weeks.</param>
|
||||||
|
/// <param name="groups">An array of group IDs.</param>
|
||||||
|
/// <param name="professors">An array of professor IDs.</param>
|
||||||
|
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||||
|
/// <returns>A response containing schedules for the specified discipline.</returns>
|
||||||
|
[HttpGet("GetByDiscipline/{id:int}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[BadRequestResponse]
|
||||||
|
[NotFoundResponse]
|
||||||
|
public async Task<ActionResult<List<ScheduleResponse>>> GetByDiscipline(int id,
|
||||||
|
[FromQuery] bool? isEven = null,
|
||||||
|
[FromQuery] int[]? groups = null,
|
||||||
|
[FromQuery] int[]? professors = null,
|
||||||
|
[FromQuery] int[]? lectureHalls = null) =>
|
||||||
|
await Get(new ScheduleRequest
|
||||||
|
{
|
||||||
|
Disciplines = [id],
|
||||||
|
IsEven = isEven,
|
||||||
|
Groups = groups,
|
||||||
|
Professors = professors,
|
||||||
|
LectureHalls = lectureHalls
|
||||||
|
});
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("[controller]")]
|
|
||||||
public class WeatherForecastController : ControllerBase
|
|
||||||
{
|
|
||||||
private static readonly string[] Summaries = new[]
|
|
||||||
{
|
|
||||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly ILogger<WeatherForecastController> _logger;
|
|
||||||
|
|
||||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
|
||||||
public IEnumerable<WeatherForecast> Get()
|
|
||||||
{
|
|
||||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
|
||||||
{
|
|
||||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
|
||||||
TemperatureC = Random.Shared.Next(-20, 55),
|
|
||||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,21 +5,68 @@
|
|||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Company>Winsomnia</Company>
|
<Company>Winsomnia</Company>
|
||||||
<Version>1.0.0-a0</Version>
|
<Version>1.0-rc4</Version>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.0.2.4</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.0.2.4</FileVersion>
|
||||||
<AssemblyName>Mirea.Api.Endpoint</AssemblyName>
|
<AssemblyName>Mirea.Api.Endpoint</AssemblyName>
|
||||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
<UserSecretsId>65cea060-88bf-4e35-9cfb-18fc996a8f05</UserSecretsId>
|
<UserSecretsId>65cea060-88bf-4e35-9cfb-18fc996a8f05</UserSecretsId>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<DockerfileContext>.</DockerfileContext>
|
<DockerfileContext>.</DockerfileContext>
|
||||||
|
<SignAssembly>False</SignAssembly>
|
||||||
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
|
<DocumentationFile>docs.xml</DocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
|
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="8.0.1" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.System" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Cronos" Version="0.8.4" />
|
||||||
|
<PackageReference Include="EPPlus" Version="7.4.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.11.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="8.0.10">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.1.2" />
|
||||||
|
<PackageReference Include="Mirea.Tools.Schedule.WebParser" Version="1.0.4" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
|
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||||
|
<PackageReference Include="System.CodeDom" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Composition" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Composition.TypedParts" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.2" />
|
||||||
|
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
|
||||||
|
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Z.EntityFramework.Extensions.EFCore" Version="8.103.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ApiDto\ApiDto.csproj" />
|
||||||
|
<ProjectReference Include="..\SqlData\Domain\Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\SqlData\Persistence\Persistence.csproj" />
|
||||||
|
<ProjectReference Include="..\Security\Security.csproj" />
|
||||||
|
<ProjectReference Include="..\SqlData\Migrations\PsqlMigrations\PsqlMigrations.csproj" />
|
||||||
|
<ProjectReference Include="..\SqlData\Migrations\SqliteMigrations\SqliteMigrations.csproj" />
|
||||||
|
<ProjectReference Include="..\SqlData\Migrations\MysqlMigrations\MysqlMigrations.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -1,35 +1,157 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.DataAccess.Application;
|
||||||
|
using Mirea.Api.DataAccess.Persistence;
|
||||||
|
using Mirea.Api.DataAccess.Persistence.Common;
|
||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using Mirea.Api.Endpoint.Common.Services;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Validators;
|
||||||
|
using Mirea.Api.Security.Services;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint;
|
namespace Mirea.Api.Endpoint;
|
||||||
|
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
|
public static IServiceCollection AddDatabase(IServiceCollection services, IConfiguration configuration, IHealthChecksBuilder? healthCheckBuilder = null)
|
||||||
|
{
|
||||||
|
var dbSettings = configuration.Get<GeneralConfig>()?.DbSettings;
|
||||||
|
services.AddApplication();
|
||||||
|
services.AddPersistence(
|
||||||
|
dbSettings?.DatabaseProvider ?? DatabaseProvider.Sqlite,
|
||||||
|
dbSettings?.ConnectionStringSql ?? string.Empty);
|
||||||
|
|
||||||
|
healthCheckBuilder?.AddDatabaseHealthCheck(
|
||||||
|
dbSettings?.DatabaseProvider ?? DatabaseProvider.Sqlite,
|
||||||
|
dbSettings?.ConnectionStringSql ?? string.Empty);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
|
||||||
|
|
||||||
// Add services to the container.
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddConfiguration(EnvironmentConfiguration.GetEnvironment());
|
||||||
|
|
||||||
|
var healthCheckBuilder = builder.Services.AddHealthChecks();
|
||||||
|
builder.Configuration.AddJsonFile(GeneralConfig.FilePath, optional: true, reloadOnChange: true);
|
||||||
|
builder.Services.Configure<GeneralConfig>(builder.Configuration);
|
||||||
|
healthCheckBuilder.AddFile(x => x.AddFile(GeneralConfig.FilePath), name: nameof(GeneralConfig));
|
||||||
|
|
||||||
|
builder.Configuration.AddJsonFile(Admin.FilePath, optional: true, reloadOnChange: true);
|
||||||
|
builder.Services.Configure<Admin>(builder.Configuration);
|
||||||
|
healthCheckBuilder.AddFile(x => x.AddFile(Admin.FilePath), name: nameof(Admin));
|
||||||
|
|
||||||
|
builder.Host.AddCustomSerilog();
|
||||||
|
AddDatabase(builder.Services, builder.Configuration, healthCheckBuilder);
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddSingleton<IMaintenanceModeNotConfigureService, MaintenanceModeNotConfigureService>();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSingleton<IMaintenanceModeService, MaintenanceModeService>();
|
||||||
|
builder.Services.AddSingleton<ISetupToken, SetupTokenService>();
|
||||||
|
|
||||||
|
builder.Services.AddHostedService<ScheduleSyncService>();
|
||||||
|
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddCustomRedis(builder.Configuration, healthCheckBuilder);
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyMethod();
|
||||||
|
policy.AllowAnyHeader();
|
||||||
|
policy.AllowCredentials();
|
||||||
|
#if DEBUG
|
||||||
|
policy.WithOrigins("http://localhost:4200");
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.ListenAnyIP(
|
||||||
|
int.Parse(builder.Configuration.GetValue<string>("INTERNAL_PORT") ?? "8080"));
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
var secretForward = builder.Configuration.Get<GeneralConfig>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretForward!.SecretForwardToken))
|
||||||
|
{
|
||||||
|
secretForward.SecretForwardToken = GeneratorKey.GenerateAlphaNumeric(16);
|
||||||
|
secretForward.SaveSetting();
|
||||||
|
Console.WriteLine($"For the reverse proxy server to work correctly, use the header: '{secretForward.SecretForwardToken}-X-Forwarded-For'");
|
||||||
|
}
|
||||||
|
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
options.ForwardedForHeaderName = secretForward.SecretForwardToken + "-X-Forwarded-For";
|
||||||
|
options.KnownNetworks.Clear();
|
||||||
|
options.KnownProxies.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddCustomApiVersioning();
|
||||||
|
builder.Services.AddCustomSwagger();
|
||||||
|
|
||||||
|
builder.Services.AddJwtToken(builder.Configuration);
|
||||||
|
builder.Services.AddSecurity(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddDataProtection()
|
||||||
|
.PersistKeysToFileSystem(new DirectoryInfo(PathBuilder.Combine("DataProtection")));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
app.UseForwardedHeaders();
|
||||||
if (app.Environment.IsDevelopment())
|
app.UseStaticFiles(UrlHelper.GetSubPath.TrimEnd('/'));
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
app.UseCustomSerilog();
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
var serviceProvider = scope.ServiceProvider;
|
||||||
app.UseSwaggerUI();
|
|
||||||
|
var optionsSnapshot = serviceProvider.GetRequiredService<IOptionsSnapshot<GeneralConfig>>();
|
||||||
|
var settingsValidator = new SettingsRequiredValidator(optionsSnapshot);
|
||||||
|
var isDoneConfig = settingsValidator.AreSettingsValid();
|
||||||
|
|
||||||
|
if (isDoneConfig)
|
||||||
|
{
|
||||||
|
var uberDbContext = serviceProvider.GetRequiredService<UberDbContext>();
|
||||||
|
var maintenanceModeService = serviceProvider.GetRequiredService<IMaintenanceModeNotConfigureService>();
|
||||||
|
|
||||||
|
maintenanceModeService.DisableMaintenanceMode();
|
||||||
|
DbInitializer.Initialize(uberDbContext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.UseCustomSwagger(app.Services);
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseMiddleware<CustomExceptionHandlerMiddleware>();
|
||||||
|
app.UseMiddleware<MaintenanceModeMiddleware>();
|
||||||
|
app.UseMiddleware<CookieAuthorizationMiddleware>();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.UseMiddleware<JwtRevocationMiddleware>();
|
||||||
|
app.UseMiddleware<CacheMaxAgeMiddleware>();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
42
Endpoint/Sync/Common/DataRepository.cs
Normal file
42
Endpoint/Sync/Common/DataRepository.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Sync.Common;
|
||||||
|
|
||||||
|
internal class DataRepository<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly ConcurrentBag<T> _data = [];
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public IEnumerable<T> GetAll() => _data.ToList();
|
||||||
|
|
||||||
|
public DataRepository(List<T> data)
|
||||||
|
{
|
||||||
|
foreach (var d in data)
|
||||||
|
_data.Add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Get(Func<T, bool> predicate)
|
||||||
|
{
|
||||||
|
var entity = _data.FirstOrDefault(predicate);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Create(Func<T> createEntity)
|
||||||
|
{
|
||||||
|
var entity = createEntity();
|
||||||
|
_data.Add(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetOrCreate(Func<T, bool> predicate, Func<T> createEntity)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var entity = Get(predicate);
|
||||||
|
return entity ?? Create(createEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
289
Endpoint/Sync/ScheduleSynchronizer.cs
Normal file
289
Endpoint/Sync/ScheduleSynchronizer.cs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Mirea.Api.DataAccess.Domain.Schedule;
|
||||||
|
using Mirea.Api.DataAccess.Persistence;
|
||||||
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
|
using Mirea.Api.Endpoint.Sync.Common;
|
||||||
|
using Mirea.Tools.Schedule.WebParser;
|
||||||
|
using Mirea.Tools.Schedule.WebParser.Common.Domain;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Group = Mirea.Api.DataAccess.Domain.Schedule.Group;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Sync;
|
||||||
|
|
||||||
|
internal partial class ScheduleSynchronizer(UberDbContext dbContext, IOptionsSnapshot<GeneralConfig> config, ILogger<ScheduleSynchronizer> logger, IMaintenanceModeService maintenanceMode)
|
||||||
|
{
|
||||||
|
private readonly DataRepository<Campus> _campuses = new([.. dbContext.Campuses]);
|
||||||
|
private readonly DataRepository<Discipline> _disciplines = new([.. dbContext.Disciplines]);
|
||||||
|
private readonly DataRepository<Faculty> _faculties = new([.. dbContext.Faculties]);
|
||||||
|
private readonly DataRepository<Group> _groups = new([.. dbContext.Groups]);
|
||||||
|
private readonly DataRepository<LectureHall> _lectureHalls = new([.. dbContext.LectureHalls]);
|
||||||
|
private readonly DataRepository<Lesson> _lessons = new([]);
|
||||||
|
private readonly DataRepository<LessonAssociation> _lessonAssociation = new([]);
|
||||||
|
private readonly DataRepository<Professor> _professors = new([.. dbContext.Professors]);
|
||||||
|
private readonly DataRepository<TypeOfOccupation> _typeOfOccupations = new([.. dbContext.TypeOfOccupations]);
|
||||||
|
private readonly DataRepository<SpecificWeek> _specificWeeks = new([]);
|
||||||
|
|
||||||
|
// todo: transfer data to storage
|
||||||
|
private static string GetFaculty(char c) =>
|
||||||
|
c switch
|
||||||
|
{
|
||||||
|
'У' => "ИТУ",
|
||||||
|
'Б' => "ИКБ",
|
||||||
|
'Х' => "ИТХТ",
|
||||||
|
'Э' => "ИПТИП",
|
||||||
|
'Т' => "ИПТИП",
|
||||||
|
'Р' => "ИРИ",
|
||||||
|
'К' => "ИИИ",
|
||||||
|
'И' => "ИИТ",
|
||||||
|
'П' => "ИИТ",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void ParallelSync(GroupResult groupInfo)
|
||||||
|
{
|
||||||
|
var facultyName = GetFaculty(groupInfo.Group[0]);
|
||||||
|
|
||||||
|
var faculty = _faculties.GetOrCreate(
|
||||||
|
f => f.Name.Equals(facultyName, StringComparison.OrdinalIgnoreCase),
|
||||||
|
() => new Faculty
|
||||||
|
{
|
||||||
|
Name = facultyName
|
||||||
|
});
|
||||||
|
|
||||||
|
var groupName = OnlyGroupName().Match(groupInfo.Group.ToUpper()).Value;
|
||||||
|
|
||||||
|
var group = _groups.GetOrCreate(
|
||||||
|
g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase),
|
||||||
|
() => new Group
|
||||||
|
{
|
||||||
|
Name = groupName,
|
||||||
|
Faculty = faculty
|
||||||
|
});
|
||||||
|
|
||||||
|
var typeOfOccupation = _typeOfOccupations.GetOrCreate(
|
||||||
|
t => t.ShortName.Equals(groupInfo.TypeOfOccupation.Trim(), StringComparison.OrdinalIgnoreCase),
|
||||||
|
() => new TypeOfOccupation
|
||||||
|
{
|
||||||
|
ShortName = groupInfo.TypeOfOccupation.ToUpper()
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Professor>? professor = [];
|
||||||
|
if (groupInfo.Professor != null)
|
||||||
|
{
|
||||||
|
foreach (var prof in groupInfo.Professor)
|
||||||
|
{
|
||||||
|
var professorParts = prof.Split(' ').ToList();
|
||||||
|
|
||||||
|
string? altName = null;
|
||||||
|
|
||||||
|
if (professorParts is { Count: >= 2 })
|
||||||
|
{
|
||||||
|
altName = professorParts.ElementAtOrDefault(0);
|
||||||
|
|
||||||
|
if (professorParts.ElementAtOrDefault(1) != null)
|
||||||
|
altName += $" {professorParts.ElementAtOrDefault(1)?[0]}.";
|
||||||
|
|
||||||
|
if (professorParts.ElementAtOrDefault(2) != null)
|
||||||
|
altName += $"{professorParts.ElementAtOrDefault(2)?[0]}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(altName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var profDb = _professors.GetOrCreate(x =>
|
||||||
|
(x.AltName == null || x.AltName.Equals(prof, StringComparison.OrdinalIgnoreCase)) &&
|
||||||
|
x.Name.Equals(altName, StringComparison.OrdinalIgnoreCase),
|
||||||
|
() => new Professor
|
||||||
|
{
|
||||||
|
AltName = prof,
|
||||||
|
Name = altName
|
||||||
|
});
|
||||||
|
|
||||||
|
professor.Add(profDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
professor = null;
|
||||||
|
|
||||||
|
List<LectureHall>? hall = null;
|
||||||
|
List<Campus>? campuses;
|
||||||
|
if (groupInfo.Campuses != null && groupInfo.Campuses.Length != 0)
|
||||||
|
{
|
||||||
|
hall = [];
|
||||||
|
campuses = [];
|
||||||
|
for (int i = 0; i < groupInfo.Campuses.Length; i++)
|
||||||
|
{
|
||||||
|
var campus = groupInfo.Campuses[i];
|
||||||
|
campuses.Add(_campuses.GetOrCreate(
|
||||||
|
c => c.CodeName.Equals(campus, StringComparison.OrdinalIgnoreCase),
|
||||||
|
() => new Campus
|
||||||
|
{
|
||||||
|
CodeName = campus.ToUpper()
|
||||||
|
}));
|
||||||
|
if (groupInfo.LectureHalls == null || groupInfo.LectureHalls.Length <= i)
|
||||||
|
continue;
|
||||||
|
var lectureHall = groupInfo.LectureHalls[i];
|
||||||
|
|
||||||
|
hall.Add(_lectureHalls.GetOrCreate(l =>
|
||||||
|
l.Name.Equals(lectureHall, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(l.Campus?.CodeName, campuses[^1].CodeName, StringComparison.CurrentCultureIgnoreCase),
|
||||||
|
() => new LectureHall
|
||||||
|
{
|
||||||
|
Name = lectureHall,
|
||||||
|
Campus = campuses[^1]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var discipline = _disciplines.GetOrCreate(
|
||||||
|
d => d.Name.Equals(groupInfo.Discipline, StringComparison.OrdinalIgnoreCase),
|
||||||
|
() => new Discipline
|
||||||
|
{
|
||||||
|
Name = groupInfo.Discipline
|
||||||
|
});
|
||||||
|
|
||||||
|
var lesson = _lessons.GetOrCreate(l =>
|
||||||
|
l.IsEven == groupInfo.IsEven &&
|
||||||
|
l.DayOfWeek == groupInfo.Day &&
|
||||||
|
l.PairNumber == groupInfo.Pair &&
|
||||||
|
l.Discipline?.Name == discipline.Name &&
|
||||||
|
l.Group?.Name == group.Name,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var lesson = new Lesson
|
||||||
|
{
|
||||||
|
IsEven = groupInfo.IsEven,
|
||||||
|
DayOfWeek = groupInfo.Day,
|
||||||
|
PairNumber = groupInfo.Pair,
|
||||||
|
Discipline = discipline,
|
||||||
|
Group = group,
|
||||||
|
IsExcludedWeeks = groupInfo.IsExclude
|
||||||
|
};
|
||||||
|
|
||||||
|
if (groupInfo.SpecialWeek == null)
|
||||||
|
return lesson;
|
||||||
|
|
||||||
|
foreach (var week in groupInfo.SpecialWeek)
|
||||||
|
_specificWeeks.Create(() => new SpecificWeek
|
||||||
|
{
|
||||||
|
Lesson = lesson,
|
||||||
|
WeekNumber = week
|
||||||
|
});
|
||||||
|
|
||||||
|
return lesson;
|
||||||
|
});
|
||||||
|
|
||||||
|
int maxValue = int.Max(int.Max(professor?.Count ?? -1, hall?.Count ?? -1), 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < maxValue; i++)
|
||||||
|
{
|
||||||
|
var prof = professor?.ElementAtOrDefault(i);
|
||||||
|
var lectureHall = hall?.ElementAtOrDefault(i);
|
||||||
|
_lessonAssociation.Create(() => new LessonAssociation
|
||||||
|
{
|
||||||
|
Professor = prof,
|
||||||
|
Lesson = lesson,
|
||||||
|
LectureHall = lectureHall,
|
||||||
|
TypeOfOccupation = typeOfOccupation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveChanges(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
foreach (var group in _groups.GetAll())
|
||||||
|
{
|
||||||
|
var existingGroup = await dbContext.Groups.FirstOrDefaultAsync(g => g.Id == group.Id, cancellationToken);
|
||||||
|
if (existingGroup != null)
|
||||||
|
dbContext.Remove(existingGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.Disciplines.BulkSynchronizeAsync(_disciplines.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
|
await dbContext.Professors.BulkSynchronizeAsync(_professors.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
|
await dbContext.TypeOfOccupations.BulkSynchronizeAsync(_typeOfOccupations.GetAll(), bulkOperation => bulkOperation.BatchSize = 100, cancellationToken);
|
||||||
|
await dbContext.Faculties.BulkSynchronizeAsync(_faculties.GetAll(), bulkOperation => bulkOperation.BatchSize = 100, cancellationToken);
|
||||||
|
await dbContext.Campuses.BulkSynchronizeAsync(_campuses.GetAll(), bulkOperation => bulkOperation.BatchSize = 10, cancellationToken);
|
||||||
|
await dbContext.LectureHalls.BulkSynchronizeAsync(_lectureHalls.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
|
await dbContext.Groups.BulkSynchronizeAsync(_groups.GetAll(), bulkOperation => bulkOperation.BatchSize = 100, cancellationToken);
|
||||||
|
await dbContext.Lessons.BulkSynchronizeAsync(_lessons.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
|
await dbContext.SpecificWeeks.BulkSynchronizeAsync(_specificWeeks.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
|
await dbContext.LessonAssociations.BulkSynchronizeAsync(_lessonAssociation.GetAll(), bulkOperation => bulkOperation.BatchSize = 1000, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartSync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var pairPeriods = config.Value.ScheduleSettings?.PairPeriod;
|
||||||
|
var startTerm = config.Value.ScheduleSettings?.StartTerm;
|
||||||
|
|
||||||
|
if (pairPeriods == null || startTerm == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("It is not possible to synchronize the schedule due to the fact that the {Arg1} or {Arg2} variable is not initialized.", nameof(pairPeriods), nameof(startTerm));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stopwatch watch = new();
|
||||||
|
watch.Start();
|
||||||
|
|
||||||
|
var parser = new Parser
|
||||||
|
{
|
||||||
|
Pairs = pairPeriods
|
||||||
|
.ToDictionary(x => x.Key,
|
||||||
|
x => (x.Value.Start, x.Value.End)),
|
||||||
|
TermStart = startTerm.Value.ToDateTime(new TimeOnly(0, 0, 0))
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogDebug("Start parsing schedule");
|
||||||
|
var data = await parser.ParseAsync(cancellationToken);
|
||||||
|
|
||||||
|
watch.Stop();
|
||||||
|
var parsingTime = watch.ElapsedMilliseconds;
|
||||||
|
watch.Restart();
|
||||||
|
|
||||||
|
ParallelOptions options = new()
|
||||||
|
{
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
MaxDegreeOfParallelism = Environment.ProcessorCount
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.LogDebug("Start mapping parsed data");
|
||||||
|
Parallel.ForEach(data, options, ParallelSync);
|
||||||
|
|
||||||
|
watch.Stop();
|
||||||
|
var mappingTime = watch.ElapsedMilliseconds;
|
||||||
|
watch.Restart();
|
||||||
|
|
||||||
|
maintenanceMode.EnableMaintenanceMode();
|
||||||
|
|
||||||
|
logger.LogDebug("Start saving changing");
|
||||||
|
await SaveChanges(cancellationToken);
|
||||||
|
|
||||||
|
maintenanceMode.DisableMaintenanceMode();
|
||||||
|
|
||||||
|
watch.Stop();
|
||||||
|
|
||||||
|
logger.LogInformation("Parsing time: {ParsingTime}ms Mapping time: {MappingTime}ms Saving time: {SavingTime}ms Total time: {TotalTime}ms",
|
||||||
|
parsingTime, mappingTime, watch.ElapsedMilliseconds, parsingTime + mappingTime + watch.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred during synchronization.");
|
||||||
|
maintenanceMode.DisableMaintenanceMode();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\w{4}-\d{2}-\d{2}(?=\s?\d?\s?[Пп]/?[Гг]\s?\d?)?")]
|
||||||
|
private static partial Regex OnlyGroupName();
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint;
|
|
||||||
|
|
||||||
public class WeatherForecast
|
|
||||||
{
|
|
||||||
public DateOnly Date { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureC { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
|
|
||||||
public string? Summary { get; set; }
|
|
||||||
}
|
|
1
Endpoint/wwwroot/css/swagger/SwaggerDark.css
Normal file
1
Endpoint/wwwroot/css/swagger/SwaggerDark.css
Normal file
File diff suppressed because one or more lines are too long
205
README.md
205
README.md
@ -1 +1,204 @@
|
|||||||
# Backend
|
# MIREA schedule by Winsomnia
|
||||||
|
|
||||||
|
[![NET Release](https://img.shields.io/badge/v8.0-8?style=flat-square&label=.NET&labelColor=512BD4&color=606060)](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||||
|
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
This project is a backend part of an application developed on ASP.NET , which provides an API for getting schedule data.
|
||||||
|
|
||||||
|
The main task is to provide convenient and flexible tools for accessing the schedule in various ways.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The purpose of this project is to provide convenient and flexible tools for obtaining schedule data.
|
||||||
|
|
||||||
|
In a situation where existing resources provide limited functionality or an inconvenient interface, this project aims to provide users with a simple and effective tool for accessing information about class schedules.
|
||||||
|
|
||||||
|
Developing your own API and using your own tools for downloading and processing data allows you to ensure the reliability, flexibility and extensibility of the application functionality.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
1. **Flexible API**: The API provides a variety of methods for accessing schedule data. Unlike competitors that provide a limited set of endpoints, this application provides a wider range of functionality, allowing you to get data about groups, campuses, faculties, classrooms and teachers. You can get all the data at once or select specific IDs with the details that are needed.
|
||||||
|
2. **Database Providers**: The application provides the capability of various database providers.
|
||||||
|
3. **Using self-written packages**: The project uses two proprietary NuGet packages. One of them is designed for parsing schedules, and the other is for downloading Excel spreadsheets from external sites.
|
||||||
|
|
||||||
|
## Project status
|
||||||
|
|
||||||
|
The project is under development. Further development will be aimed at expanding the functionality and improving the user experience.
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
This table provides information about the environment variables that are used in the application. These variables are stored in the [.env](.env) file.
|
||||||
|
|
||||||
|
In addition to these variables, you also need to fill in a file with settings in json format. The web application provided by this project already has everything necessary to configure the file in the Client-Server communication format via the controller. If you need to get the configuration file otherwise, then you need to refer to the classes that provide configuration-related variables.
|
||||||
|
|
||||||
|
Please note that the application will not work correctly if you do not fill in the required variables.
|
||||||
|
|
||||||
|
| Variable | Default | Description | Required |
|
||||||
|
|---------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
|
||||||
|
| PATH_TO_SAVE | Current folder | The path to save the data. Saving logs (if the full path is not specified), databases (if Sqlite), and other data that should be saved in a place other than where the program is launched. | ✔ |
|
||||||
|
| SECURITY_SIGNING_TOKEN | ❌ | JWT signature token. This token will be used to create and verify the signature of JWT tokens. The token must be equal to 64 characters. | ✔ |
|
||||||
|
| SECURITY_ENCRYPTION_TOKEN | ❌ | Token for JWT encryption. This token will be used to encrypt and decrypt JWT tokens. The token must be equal to 32 characters. | ✔ |
|
||||||
|
| SECURITY_LIFE_TIME_RT | 1440 | Time in minutes after which the Refresh Token will become invalid. | ❌ |
|
||||||
|
| SECURITY_LIFE_TIME_JWT | 15 | Time in minutes after which the JWT token will become invalid. | ❌ |
|
||||||
|
| SECURITY_LIFE_TIME_1_FA | 15 | Time in minutes after which the token of the first factor will become invalid. | ❌ |
|
||||||
|
| SECURITY_JWT_ISSUER | ❌ | An identifier that points to the server that created the token. | ✔ |
|
||||||
|
| SECURITY_JWT_AUDIENCE | ❌ | ID of the audience for which the token is intended. | ✔ |
|
||||||
|
| SECURITY_HASH_ITERATION | ❌ | The number of iterations used to hash passwords in the Argon2 algorithm. | ✔ |
|
||||||
|
| SECURITY_HASH_MEMORY | ❌ | The amount of memory used to hash passwords in the Argon2 algorithm. | ✔ |
|
||||||
|
| SECURITY_HASH_PARALLELISM | ❌ | Parallelism determines how many of the memory fragments divided into strips will be used to generate a hash. | ✔ |
|
||||||
|
| SECURITY_HASH_SIZE | 32 | The size of the output hash generated by the password hashing algorithm. | ❌ |
|
||||||
|
| SECURITY_HASH_TOKEN | ❌ | Additional protection for Argon2. We recommend installing a token so that even if the data is compromised, an attacker cannot brute force a password without a token. | ❌ |
|
||||||
|
| SECURITY_SALT_SIZE | 16 | The size of the salt used to hash passwords. The salt is a random value added to the password before hashing to prevent the use of rainbow hash tables and other attacks. | ❌ |
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
If you want to make a fork of this project or place the Backend application on your hosting yourself, then follow the instructions below.
|
||||||
|
|
||||||
|
1. [Docker Installation](#docker-installation)
|
||||||
|
2. [Docker Self Build](#docker-self-build)
|
||||||
|
3. [Manual Installation](#manual-installation)
|
||||||
|
4. [Self Build](#self-build)
|
||||||
|
|
||||||
|
## Docker Installation
|
||||||
|
|
||||||
|
**Requirements**
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
To launch the application, pull out the application image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull winsomnia/mirea-backend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you need to fill in the required fields inside.env and pass it when the container is started:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name mirea-backend -p 8080 \
|
||||||
|
--restart=on-failure:10 \
|
||||||
|
-v mirea-data:/data \
|
||||||
|
-e PATH_TO_SAVE=/data \
|
||||||
|
-e .env \
|
||||||
|
winsomnia/mirea-backend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the `--name` option, you can specify your container name, for example: `--name mirea`.
|
||||||
|
|
||||||
|
With the `-p` option, you can specify the port you need: `-p 80:8080`.
|
||||||
|
|
||||||
|
It is necessary to tell the application exactly where to save the data so that it does not disappear when the container is deleted.
|
||||||
|
|
||||||
|
To do this, replace the `-v` option, where you need to specify the path to the data on the host first, and then using `:` specify the path inside the container. `-v /nas/mirea/backend:/myfolder`.
|
||||||
|
|
||||||
|
At the same time, do not forget to replace inside [.env](.env) `PATH_TO_SAVE` with what you specify in the `-v` option. In our case, it will be `PATH_TO_SAVE=/myfolder`.
|
||||||
|
|
||||||
|
That's it, the container is running!
|
||||||
|
|
||||||
|
## Docker Self Build
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
To build your own application image, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t my-name/mirea-backend:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `-t` indicates the name and version of the image. You can specify their `your-name/image-name:version`.
|
||||||
|
|
||||||
|
Now the image is ready. To launch the container, refer to [Docker Installation](#docker-installation), do not forget to specify the name of the image that you have built.
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
**Requirements**
|
||||||
|
|
||||||
|
- ASP.NET Core runtime 8.0
|
||||||
|
|
||||||
|
To install using a pre-built application, follow these steps:
|
||||||
|
|
||||||
|
1. [Install ASP.NET](#install-aspnet)
|
||||||
|
2. [Download Package](#download-package)
|
||||||
|
3. [Run](#run)
|
||||||
|
|
||||||
|
### Install ASP.NET
|
||||||
|
|
||||||
|
Installation ASP.NET it depends on the specific platform.
|
||||||
|
Go to [Microsoft website](https://dotnet.microsoft.com/download/dotnet/8.0 ) and find your platform. Follow the installation instructions.
|
||||||
|
|
||||||
|
### Download Package
|
||||||
|
|
||||||
|
The latest versions of the packages can be found in [releases](https://git.winsomnia.net/Winsomnia/MireaBackend/releases ). If there is no build for your platform, go [to the Self Build section](#self-build).
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
Go to the directory with the application.
|
||||||
|
|
||||||
|
Don't forget to set up [required configurations](#environment-variables) for the application to work.
|
||||||
|
|
||||||
|
Run the program.
|
||||||
|
|
||||||
|
`Debian/Ubuntu`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet Mirea.Api.Endpoint.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self Build
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
|
||||||
|
- ASP.NET Core runtime 8.0
|
||||||
|
- .NET 8.0 sdk (for build)
|
||||||
|
- git
|
||||||
|
|
||||||
|
To build your own version of the program, follow these steps:
|
||||||
|
|
||||||
|
1. [Install .NET SDK](#install-net-sdk)
|
||||||
|
2. [Clone The Repository](#clone-the-repository)
|
||||||
|
3. [Build Self Release](#build-self-release)
|
||||||
|
|
||||||
|
### Install NET SDK
|
||||||
|
|
||||||
|
Installation.The NET SDK depends on the specific platform.
|
||||||
|
Go to [Microsoft website](https://dotnet.microsoft.com/download/dotnet/8.0 ) and find your platform. Follow the installation instructions.
|
||||||
|
|
||||||
|
### Clone The Repository
|
||||||
|
|
||||||
|
Install git in advance or clone the repository in another way.
|
||||||
|
|
||||||
|
> ⚠ It is advisable to clone the `master` branch, the rest of the branches may work unstable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.winsomnia.net/Winsomnia/MireaBackend.git \
|
||||||
|
cd MireaBackend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Self Release
|
||||||
|
|
||||||
|
Go to the project folder. Restore the dependencies using the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's move on to the assembly.
|
||||||
|
|
||||||
|
Variables:
|
||||||
|
|
||||||
|
- `<platform>` — Platform for which the build will be performed.
|
||||||
|
- `<arch>` — System architecture. Example: x86 x64.
|
||||||
|
- `<output directory>` — The directory where the assembly will be saved.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet publish "./Endpoint/Endpoint.csproj" -c Release -r <platform>-<arch> -framework net8.0 -o <output directory> /p:SelfContained=false /p:UseAppHost=false
|
||||||
|
```
|
||||||
|
|
||||||
|
The release is now in the directory you specified. To run it, look at the [startup instructions](#run).
|
||||||
|
|
||||||
|
# Contribution and Feedback
|
||||||
|
|
||||||
|
You can contribute to the project by creating pull requests. Any feedback is welcome to improve the project.
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](LICENSE.txt).
|
||||||
|
8
Security/Common/CookieNames.cs
Normal file
8
Security/Common/CookieNames.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Mirea.Api.Security.Common;
|
||||||
|
|
||||||
|
public class CookieNames
|
||||||
|
{
|
||||||
|
public const string AccessToken = "access_token";
|
||||||
|
public const string RefreshToken = "refresh_token";
|
||||||
|
public const string FingerprintToken = "fingerprint";
|
||||||
|
}
|
27
Security/Common/Domain/AuthToken.cs
Normal file
27
Security/Common/Domain/AuthToken.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Security.Common.Domain;
|
||||||
|
|
||||||
|
internal class AuthToken
|
||||||
|
{
|
||||||
|
public AuthToken(RequestContextInfo context)
|
||||||
|
{
|
||||||
|
UserAgent = context.UserAgent;
|
||||||
|
Ip = context.Ip;
|
||||||
|
Fingerprint = context.Fingerprint;
|
||||||
|
RefreshToken = context.RefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthToken()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserAgent { get; set; } = null!;
|
||||||
|
public string Ip { get; set; } = null!;
|
||||||
|
public string Fingerprint { get; set; } = null!;
|
||||||
|
public string RefreshToken { get; set; } = null!;
|
||||||
|
|
||||||
|
public required string UserId { get; set; }
|
||||||
|
public required string AccessToken { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user