Compare commits

..

403 Commits

Author SHA1 Message Date
46bbc34956 refactor: fix error WHITESPACE and FINALNEWLINE 2025-02-11 17:13:13 +03:00
047ccfa754 fix: correct calculate next occurrence 2025-02-11 16:35:56 +03:00
b0d9a67c1c refacotr: clean code 2025-02-11 15:36:55 +03:00
3eb043b24c build: fix code style with CRLF 2025-02-11 15:29:43 +03:00
4cd476764d build: fix secrets 2025-02-11 15:25:47 +03:00
90b4662dda Release 1.0.0 2025-02-11 15:16:51 +03:00
e7edc79ebc build: instead build run analyze 2025-02-11 15:04:38 +03:00
aabeed0aa5 feat: add backend version to swagger 2025-02-10 16:07:51 +03:00
e79ec360ea Merge branch 'release/v1.0.0' of https://git.winsomnia.net/Winsomnia/MireaBackend into release/v1.0.0 2025-02-06 16:29:48 +03:00
31c1d2804d fix: hotfix calculate next run time 2025-02-06 16:27:20 +03:00
ea4c8b61e0 refactor: use thread pool instead task 2025-02-03 11:25:39 +03:00
b40e394bcf fix: System.ObjectDisposedException for db context into sync secrvice 2025-02-03 10:55:47 +03:00
885b937b0b feat: add parsing from files 2025-02-03 03:44:40 +03:00
dc08285ec8 feat: clear old records 2025-02-02 20:31:52 +03:00
b3a0964aac fix: correct filter data 2025-02-02 20:28:04 +03:00
7d6b21c5bb fix: move from body to query 2025-02-02 04:51:09 +03:00
93912caf01 fix: return correct value 2025-02-02 04:50:54 +03:00
c725cfed32 refactor: increase max value 2025-02-02 04:50:35 +03:00
7c7707b1e2 fix: if delay more than int set max of int 2025-02-02 04:50:04 +03:00
1687e9d89b fix: continue if in filter exist value 2025-02-02 04:49:25 +03:00
8d1b709b43 feat: add start term update and cron schedule update 2025-02-02 03:39:30 +03:00
ce6b0f2673 feat: add cron skipping date 2025-02-02 03:30:52 +03:00
16afc0bc69 feat: show enum name instead value 2025-02-02 03:29:19 +03:00
c9bc6a3565 refactor: remove "swagger" in class name 2025-02-02 03:28:24 +03:00
ad8f356fc1 fix: get non negative number 2025-02-02 01:57:08 +03:00
dda0a29300 refactor: subscribe to onChange instead of waiting for the event to be received from the manager 2025-02-01 21:23:51 +03:00
369901db78 fix: set long, because the value may be greater than int 2025-02-01 21:19:56 +03:00
a67b72b7fb refactor: rename cancellation to cancellationToken 2025-02-01 21:18:56 +03:00
2453b2bd51 build: upgrade ref 2025-02-01 20:47:25 +03:00
5870eef552 feat: add a tag schema to combine similar controllers. 2025-02-01 20:47:08 +03:00
52de98969d refactor: remove unused brackets 2025-02-01 20:45:08 +03:00
bc86e077bd refactor: move to SetupConfiguration namespace 2025-02-01 19:39:02 +03:00
03b6560bc4 feat: add lesson type controller 2025-02-01 17:08:00 +03:00
5bcb7bfbc1 feat: allow filter by lesson type 2025-02-01 17:06:02 +03:00
38fba5556f feat: add filter by type of occupation (lesson type) 2025-02-01 16:46:20 +03:00
fd26178a24 build: update ref 2025-01-24 17:12:39 +03:00
7eb307b65e fix: return empty string if null 2025-01-24 17:10:46 +03:00
56c7196100 refactor: change const name to class with name 2025-01-24 17:10:18 +03:00
92081156cf fix: save token after update 2024-12-28 08:34:19 +03:00
6358410f18 sec: to establish the ownership of the token for the first one who received it 2024-12-28 08:30:56 +03:00
e79ddf220f sec: set the absolute time of the token 2024-12-28 08:29:31 +03:00
c3c9844e2f refactor: improve logging 2024-12-28 08:29:06 +03:00
206720cd63 fix: add force select account 2024-12-28 08:16:00 +03:00
d9f4176aca fix: return message if 401 2024-12-28 08:15:43 +03:00
1de344ac25 refactor: to enable oauth during registration, use the appropriate controller. 2024-12-28 07:46:06 +03:00
61a11ea223 fix: return exception message if controller exception 2024-12-28 06:47:21 +03:00
07111b9b61 sec: do not return the error text to the user 2024-12-26 16:40:30 +03:00
538f1d67c8 fix: change the link to the error type 2024-12-26 16:39:29 +03:00
233458ed89 refactor: add standard traceId 2024-12-26 16:38:53 +03:00
7f87b4d856 style: add space between point and provider 2024-12-26 16:38:13 +03:00
0c6d1c9bfb refactor: compact two factor auth 2024-12-26 16:16:33 +03:00
516ba5bb8e feat: add a token handler 2024-12-26 16:14:55 +03:00
9d5007ef3a refactor: add user converter 2024-12-26 16:14:28 +03:00
c75ac60b0b sec: add verification for OAuth authorization 2024-12-26 15:47:38 +03:00
5b7412f20f feat: return the provider 2024-12-26 15:46:55 +03:00
c4a4478b8c refactor: standardize the order of arguments 2024-12-26 15:46:30 +03:00
05166188be feat: add a method for getting info about a token 2024-12-26 14:32:28 +03:00
157708d00f feat: store the result at each stage 2024-12-26 14:18:12 +03:00
36026b3afb refactor: distribute the domain folder 2024-12-26 13:38:43 +03:00
43edab2912 sec: return the token instead of performing actions with the user 2024-12-26 08:51:22 +03:00
dcdd43469b feat: add payload model 2024-12-26 08:48:17 +03:00
17fd260068 sec: add payload 2024-12-26 08:47:56 +03:00
97187a8e45 sec: save readonly byte array instead string 2024-12-26 08:44:05 +03:00
cfe08dcf9b refactor: get interface instead array 2024-12-26 08:42:24 +03:00
ae4d2073c4 feat: add a 200 result schema 2024-12-25 07:22:07 +03:00
269d976ad4 refactor: move arguments to a new line 2024-12-25 05:54:27 +03:00
5fa545e981 fix: trim the variable to avoid the effects of whitespace characters 2024-12-25 05:53:59 +03:00
2ab5dea8ba feat: add a change to the User Agent and Ip address in case of a mismatch 2024-12-25 05:52:39 +03:00
5e65aded79 refactor: instead of Reason, add explicit arguments 2024-12-25 05:51:54 +03:00
dfac9ddca8 sec: add failed attempts for 2FA 2024-12-25 05:49:13 +03:00
c66f3355ec feat: add logging for empty secret 2024-12-25 05:48:22 +03:00
c12323dc29 refactor: rename methods to match the context 2024-12-25 05:47:51 +03:00
71c31c0bbb refactor: separate the method of counting failed attempts 2024-12-25 05:46:27 +03:00
8c51ba83a4 fix: add trim for email and username 2024-12-25 05:44:37 +03:00
9ff0f51e19 refactor: add data annotations 2024-12-25 05:44:15 +03:00
408a95e4b3 refactor: add .editorconfig and refactor code 2024-12-25 05:43:30 +03:00
2a33ecbf07 build: fix deploy script
Signed-off-by: Polianin Nikita <wesser@noreply.git.winsomnia.net>
2024-12-23 16:54:05 +03:00
97e50b5331 fix: set minimum level for authorization and authentication to warning
Signed-off-by: Polianin Nikita <wesser@noreply.git.winsomnia.net>
2024-12-23 10:53:32 +03:00
d505041c72 fix: escape data for state 2024-12-23 08:10:19 +03:00
5ff8744a55 feat: improve logging 2024-12-23 07:48:28 +03:00
053f01eec1 fix: hotfix getting current port 2024-12-23 06:56:01 +03:00
e8e94e45a5 fix: add missing using 2024-12-23 06:32:08 +03:00
55562a9f00 feat: add default response type 2024-12-23 06:29:29 +03:00
57b9819d13 feat: add maintenance ignore 2024-12-23 06:29:00 +03:00
78254ed23d fix: change same name 2024-12-23 06:28:28 +03:00
202d20bb25 build: add providers 2024-12-23 06:28:06 +03:00
3e05863aea refactor: clean code 2024-12-22 07:25:41 +03:00
b82fbc491f fix: add missing using 2024-12-22 07:21:51 +03:00
85722f8552 feat: add integration with seq 2024-12-22 07:13:59 +03:00
9231c4d5ca refactor: remove unused code 2024-12-22 06:39:12 +03:00
7b94f9cc1f refactor: for maintenance mode, return the standard error 2024-12-22 05:51:11 +03:00
7bafbb95c4 build: update ref 2024-12-22 05:27:07 +03:00
544ad6e791 feat: add an annotation to the data 2024-12-22 05:26:10 +03:00
e4b942d062 refactor: to return the result according to the RFC 7807 standard and add a traceId 2024-12-22 05:25:19 +03:00
f2e79e51f2 sec: transfer user verification to the appropriate service 2024-12-22 05:13:46 +03:00
5cc54eac44 feat: add a method for getting data from OAuth 2024-12-18 07:40:07 +03:00
e9ff1cabe8 feat: add a return of the data that has been configured 2024-12-18 07:39:17 +03:00
fd578aa61e fix: add condition for token 2024-12-18 07:32:00 +03:00
cff42d0a31 feat: add password policy to general config 2024-12-18 07:31:16 +03:00
8250957b85 refactor: use another origin 2024-12-18 07:30:47 +03:00
39208037f0 fix: add method for ignoring mode attribute 2024-12-18 07:30:23 +03:00
5e072d88c2 fix: converter dto 2024-12-18 07:29:58 +03:00
25eddbe776 feat: add generator totp qr-code 2024-12-18 07:29:31 +03:00
74ba4e901a fix: replace localhost to 127.0.0.1 2024-12-18 07:29:05 +03:00
e760ddae0a feat: give the user the ability to make a password policy 2024-12-18 07:27:57 +03:00
598ebabc5c sec: use HMAC to encrypt state 2024-12-18 07:24:33 +03:00
08aeb7ea3c sec: get links to the backend to initiate the receipt of provider data 2024-12-18 07:23:23 +03:00
182235c4cd feat: add generator base32 for totp 2024-12-18 07:14:04 +03:00
5437623a20 build: update ref 2024-12-18 07:13:27 +03:00
2c09122971 fix: add cookie expire time 2024-11-04 03:15:13 +03:00
503f5792fb docs: add comment 2024-11-04 03:14:42 +03:00
95627003e5 refactor: change the error 2024-11-04 03:14:17 +03:00
a96073d44d feat: add available providers list 2024-11-04 02:59:51 +03:00
5f36e0f75b docs: update 2024-11-04 02:39:45 +03:00
e977de3e4f feat: add authorize in OAuth 2024-11-04 02:39:10 +03:00
65d928ec2d fix: remove authorize 2024-11-04 02:36:22 +03:00
713bbfa16f feat: add calculate correct api url 2024-11-04 02:35:43 +03:00
6b5eda7756 fix: remove the latest api 2024-11-04 02:34:50 +03:00
dbd9e1a070 refactor: change Name to NameIdentifier 2024-11-04 02:33:56 +03:00
0dda336de1 fix: logout for all users to delete cookies 2024-11-04 02:32:13 +03:00
727f5c276e refactor: move files 2024-11-02 23:34:23 +03:00
db70e4dd96 refactor: change log text 2024-11-02 22:10:46 +03:00
6831d9c708 fix: return bool instead 2024-11-02 22:09:40 +03:00
1b24954c3e refactor: change int to string for Id 2024-11-02 20:21:46 +03:00
c5ba1cfcca refactor: transfer two factor method to security 2024-11-02 01:09:15 +03:00
3811d879ab refactor: return next step from security 2024-11-02 01:06:58 +03:00
61dc0a8bc4 feat: add converter for two factor 2024-11-02 01:05:24 +03:00
b3b00aa9e1 refator: move converter to MapperDto 2024-11-02 00:59:37 +03:00
6c9af942f4 refactor: change token to instance token 2024-11-02 00:51:27 +03:00
23f74b3bdf refactor: change name enums 2024-11-02 00:50:10 +03:00
eb272baa38 fix: change cookie name 2024-10-31 04:23:43 +03:00
a0ff624481 fix: add forgotten changes 2024-10-31 04:12:22 +03:00
cd6f25deba refactor: transfer logic
All logic related to token manipulation has been transferred to the AuthService. Also added TOTP 2FA and rethought the logic of logging into the application
2024-10-31 04:12:02 +03:00
0f47a98ad9 feat: return security exception 2024-10-31 04:07:35 +03:00
3279ef594b fix: change current culture to russian for import 2024-10-31 04:06:58 +03:00
5bc729eb66 fix: add an implementation for saving primitive data 2024-10-31 04:05:40 +03:00
5317b7b563 feat: add import to excel
Made at the request of the customer
2024-10-27 08:25:46 +03:00
665544236f build: add timezone 2024-10-27 07:34:26 +03:00
f203ee71f0 fix: add an additional condition 2024-10-27 07:14:30 +03:00
d8dbf1562f refactor: clean code 2024-10-27 06:51:05 +03:00
dead9f89bb feat: remove unused ref campus 2024-10-27 06:50:47 +03:00
8c932cf0be docs: update 2024-10-27 06:09:35 +03:00
80e74b34c1 feat: add background task 2024-10-27 05:42:50 +03:00
b095ca9749 feat: add sync and mapper schedule 2024-10-27 05:41:49 +03:00
8fad070a9c refactor: error logging 2024-10-27 04:36:20 +03:00
6c20713d81 feat: add docker as localhost 2024-10-27 04:09:31 +03:00
fc5ec1fd54 fix: log exception 2024-10-27 03:02:25 +03:00
ed99fce9b8 build: fix unhealth
Signed-off-by: Polianin Nikita <wesser@noreply.git.winsomnia.net>
2024-10-25 09:38:28 +03:00
2ccc476686 feat: add search professors by name 2024-10-25 04:44:18 +03:00
84d7b095f0 fix: return altName 2024-10-25 04:43:30 +03:00
4605c81895 docs: add some specification 2024-10-25 04:43:18 +03:00
0788c36bd2 style: remove "id" from text 2024-10-25 04:42:27 +03:00
f5dbc46856 build: add healthcheck for docker 2024-10-25 02:37:28 +03:00
ebec0a2d2b fix: remove request to /health from log 2024-10-25 02:37:08 +03:00
4fc28378c5 feat: add healthcheck for main project 2024-10-25 02:36:39 +03:00
98ee3c389c feat: add healthcheck for databases 2024-10-25 02:35:36 +03:00
428c2dc3ba refactor: return the modified interfaces for further modification 2024-10-25 02:22:42 +03:00
4970dd782a build: update ref 2024-10-25 01:54:56 +03:00
2e48b0067f build: update ref 2024-10-09 03:08:15 +03:00
71e8eca5f4 refactor: remove unused param 2024-10-09 03:02:35 +03:00
1f3aaca3cf sec: move token from responce to cookie 2024-10-09 03:00:26 +03:00
b49df925d4 fix: doesn't set expire time for fingerpring 2024-10-09 02:58:52 +03:00
c9ebddf27a fix: remove cache for method 2024-10-09 02:58:09 +03:00
f5739647b2 refactor: remove default produce 200 code 2024-10-07 02:45:38 +03:00
26dbf608b9 refactor: sync namespace 2024-10-07 02:25:36 +03:00
2b89dd07a9 feat: output a token when generating a token for the header 2024-10-07 02:16:20 +03:00
1c981fb7bf refactor: code restructuring 2024-10-07 02:13:35 +03:00
de5dc274d7 build: add cors for debugging frontend 2024-10-07 01:42:00 +03:00
c5ecf00932 revert: remove produced 2024-10-07 01:20:10 +03:00
412751e30f build: update ref 2024-09-30 01:10:37 +03:00
076d6498a1 fix: set to correct produces 2024-09-18 06:05:40 +03:00
88d78dfab3 build: update ref 2024-09-18 06:00:07 +03:00
332e5a013b fix: add forgotten changes 2024-09-08 03:24:32 +03:00
e8450400c7 build: update ref 2024-09-07 04:56:41 +03:00
65709e1f83 refactor: move files to another namespace 2024-09-07 04:28:07 +03:00
1e204c948c refactor: set cookie name to attribute 2024-09-07 04:19:51 +03:00
0ced152fc9 fix: remove database when check connect to sqlite 2024-09-07 04:19:05 +03:00
6f9bfd3880 fix: set correct password condition 2024-09-07 04:18:04 +03:00
ae0f437e2c fix: remove Regex 2024-08-27 22:58:05 +03:00
592e8a1b42 feat: add renew password 2024-08-27 22:52:07 +03:00
a27549092b refactor: move checking password 2024-08-27 22:51:14 +03:00
f27d07fb5a build: upgrade ref 2024-08-27 22:50:21 +03:00
535bafa73a fix: set 8-th mounth instead 9-th 2024-08-27 21:35:35 +03:00
fba842acc3 feat: add a cache with a short lifetime 2024-08-24 04:30:31 +03:00
31087a57c9 feat: add cache for api 2024-08-24 02:27:05 +03:00
24c75e4306 refator: set fingerprint expire instead session mode 2024-08-24 02:26:11 +03:00
dee89b278b refactor: set HttpOnly for debug mode too 2024-08-24 02:25:29 +03:00
565252382c feat: add cache control in response 2024-08-12 21:54:05 +03:00
80dc2e412c refactor: change Invoke to async 2024-08-12 21:36:07 +03:00
b1250616a7 refactor: use this in static method 2024-08-10 23:11:43 +03:00
c51a9cecc9 fix: storing data protection keys 2024-08-10 23:03:28 +03:00
c189cc6955 docs: add readme file 2024-08-10 22:34:52 +03:00
c7b401eae7 fix: save log with PathBuilder 2024-08-10 22:04:11 +03:00
837205f66e build: update ref 2024-07-24 21:50:04 +03:00
c6ca717b89 feat: add new endpoint 2024-07-08 01:56:14 +03:00
497b7f146b fix: add use forwarded headers and clear known 2024-07-08 00:00:32 +03:00
3326b17d74 fix: try disable origins 2024-07-07 23:20:54 +03:00
80b46754ad feat: add new generator key 2024-07-05 23:14:45 +03:00
3898463bc4 build: set SWAGGER_SUB_PATH 2024-07-05 02:50:20 +03:00
279ca9647b fix: path to wwwroot 2024-07-05 02:50:02 +03:00
ab660f69c8 fix: add listen any ip instead localhost 2024-07-05 02:37:12 +03:00
1c27bffa73 build: remove pdb files 2024-07-05 02:28:18 +03:00
76fd1347ce build: add ACTUAL_SUB_PATH 2024-07-05 01:59:54 +03:00
820828276e fix: get sub url without first "api" 2024-07-05 01:59:36 +03:00
ac4804e864 fix: get host name without port 2024-07-05 01:58:14 +03:00
21055176ac fix: check file exist 2024-07-05 01:57:34 +03:00
f42caa3a45 feat: add sub path for actual url 2024-07-05 01:35:19 +03:00
57f4d1b822 fix: add test env to variablesFromFile 2024-07-05 01:22:21 +03:00
cdb738ca42 fix: set default port 8080 2024-07-05 01:11:24 +03:00
d45c865f4e feat: add listen port from env 2024-07-05 00:44:55 +03:00
d87654a355 ref: update 2024-07-04 23:57:44 +03:00
75c1aebea6 refactor: change namespace 2024-07-04 23:54:17 +03:00
17e20fee2e refactor: Admin 2024-07-04 23:52:25 +03:00
e8ca2c42a6 sec: add random scret forward token for set ip if app under proxy 2024-07-04 23:46:43 +03:00
9133b57a1b refactor: GeneralConfig 2024-07-04 23:45:33 +03:00
05ca45db49 perf: precompile regex 2024-07-04 23:39:12 +03:00
7e2016080f style: compact css 2024-07-04 23:37:16 +03:00
fe24dfcd6a perf: return Dictionary instead interface 2024-07-04 23:31:05 +03:00
2041a187e7 fix: create directory if not exist 2024-07-04 22:40:59 +03:00
098fca5df8 build: fix copy docs
Signed-off-by: Polianin Nikita <wesser@noreply.git.winsomnia.net>
2024-07-03 11:04:34 +03:00
e0cff050de build: fix endpoint name 2024-07-03 10:10:58 +03:00
2f7c77e764 fix: try get value
Signed-off-by: Polianin Nikita <wesser@noreply.git.winsomnia.net>
2024-07-03 10:09:50 +03:00
7e82d4a520 build: fix name of program 2024-07-03 01:09:06 +03:00
07edf0e5ad build: fix restart policy 2024-07-03 00:15:41 +03:00
5b8d9e1f4a build: add secret env 2024-07-03 00:05:01 +03:00
de2f909ed6 build: try create image 2024-07-02 23:42:29 +03:00
2e2cee2ca7 build: change image name 2024-07-02 23:33:22 +03:00
8c340e2a97 build: add deploy to server 2024-07-02 23:24:28 +03:00
9abdb1ac43 build: create dockerfile 2024-07-02 23:24:00 +03:00
42f0b8ee0e Merge pull request 'Add authentication methods to access protected resources' () from feat/auth into release/v1.0.0
Reviewed-on: 
2024-06-28 23:14:17 +03:00
41b5bb571b docs: add xml comments 2024-06-28 22:55:18 +03:00
2c112d00df fix: add Admin model to configuration 2024-06-28 22:52:58 +03:00
f89136669d fix: change RT in cache after generation 2024-06-28 22:52:05 +03:00
612efcb91c fix: exception if value is null 2024-06-28 01:49:45 +03:00
8d4c482bbd fix: set correct cache key 2024-06-27 23:37:40 +03:00
160c7505f0 feat: add controller for authentication 2024-06-21 21:52:21 +03:00
c62ec33130 feat: add user roles 2024-06-21 21:51:23 +03:00
55371cb675 feat: add request to log in 2024-06-21 21:51:06 +03:00
a36e0694ec feat: add token response 2024-06-21 21:50:42 +03:00
1a0d539e76 fix: if cache get bytes then skip serealize 2024-06-21 21:44:34 +03:00
79151e7da8 feat: add bearer auth to swagger 2024-06-21 21:43:40 +03:00
039d323643 fix: add refresh expire date 2024-06-21 21:36:11 +03:00
a5f9e67647 feat: add middleware for revocated tokens 2024-06-15 21:53:00 +03:00
21866d54cb fix: add private to get EncryptKey 2024-06-13 00:42:41 +03:00
eba11f515d fix: singleton added in JwtTokenService 2024-06-13 00:42:15 +03:00
70780d620a fix: error CS0246 2024-06-10 22:15:15 +03:00
7d3952c373 build: upgrade reference 2024-06-10 22:05:18 +03:00
0ecb796d54 fix: set default settings for logging if null or empty 2024-06-10 22:04:29 +03:00
2e389b252c refactor: use default pairPeriod 2024-06-10 22:03:48 +03:00
984791f193 style: change template for log message 2024-06-10 22:02:29 +03:00
b640902777 feat: add dark theme for swagger 2024-06-10 22:01:58 +03:00
4222e4702f refactor: provide single response for schedule 2024-06-10 21:59:34 +03:00
993e66a084 fix: add JsonIgnore to calculated property 2024-06-01 11:11:21 +03:00
6797adac4f fix: get GeneralConfig 2024-06-01 11:10:42 +03:00
8e58c83526 build: ignore files 2024-06-01 11:01:08 +03:00
2e64caf6ea refactor: code cleaning 2024-06-01 10:59:23 +03:00
9c56fa582b build: upgrade reference 2024-06-01 10:58:43 +03:00
b2a0a6dd7c feat: add default value attribute 2024-06-01 10:58:11 +03:00
78f589bb18 refactor: distribute configurations by classes 2024-06-01 10:57:52 +03:00
ca02509b97 build: add needed reference 2024-06-01 10:55:43 +03:00
f3a757d33d feat: add redis 2024-06-01 10:53:21 +03:00
34addd930f feat: add serilog for logging 2024-06-01 10:50:38 +03:00
054d319f7c Add an Administrator ()
Reviewed-on: 
2024-06-01 08:27:01 +03:00
2addd2aa78 feat: use Admin model 2024-06-01 08:20:27 +03:00
bba9431733 feat: add data checks 2024-06-01 08:19:26 +03:00
6fb5a83183 feat: add model to endpoint 2024-06-01 08:18:27 +03:00
ea538ac340 Add Application configuration ()
Reviewed-on: 
2024-06-01 07:35:29 +03:00
63216f3b66 feat: comment this for show controller in swagger 2024-06-01 07:33:08 +03:00
e088374b14 feat: add request for create user 2024-06-01 07:31:14 +03:00
ded577f40a feat: add path depending on OS 2024-06-01 07:27:18 +03:00
32621515db fix: default value is null for optional body 2024-06-01 07:26:22 +03:00
fdf0ecc9ef feat: add is default path 2024-06-01 07:25:51 +03:00
5400e0c873 feat: create submit configuration 2024-06-01 06:29:16 +03:00
1fd6c8657a fix: change connction string for mysql 2024-06-01 06:28:13 +03:00
d09011d25a feat: add create admin 2024-06-01 06:27:49 +03:00
a902d9eb81 Use the configuration depending on the selected database provider ()
Reviewed-on: 
2024-06-01 05:45:17 +03:00
827cdaf9f9 refactor: change create database to migrate 2024-06-01 05:43:00 +03:00
d2ba2d982c feat: add migrations 2024-06-01 05:42:23 +03:00
79118c5283 build: add ref to projects for migrations 2024-06-01 05:42:05 +03:00
8cd8277c22 feat: add assembly for migration 2024-06-01 05:40:48 +03:00
ae46823685 build: upgrade dependencies 2024-06-01 05:40:09 +03:00
7aa37618e0 feat: add postgresql configurations 2024-06-01 05:39:25 +03:00
04b6687181 feat: add mysql configurations 2024-06-01 05:39:02 +03:00
7a741d7783 test: remove testing 2024-05-31 01:37:03 +03:00
f17ee43805 test: change docker to apt 2024-05-31 01:25:22 +03:00
b67f0a82ed test: fix port 2024-05-31 01:16:13 +03:00
815c860dc0 fix: error CS0103 2024-05-30 23:58:24 +03:00
f0544ff42e fix: error CS0103 2024-05-30 23:30:11 +03:00
c9c6a99fe9 test: trying to set up tests to test application in different databases 2024-05-30 21:45:45 +03:00
aa1e1000fa fix: set new path to projects 2024-05-30 21:45:27 +03:00
a353b4c3f8 test: upgrade test 2024-05-30 21:19:54 +03:00
b81fe6d8c1 fix: rename table 2024-05-30 20:29:11 +03:00
8a103831eb refactor: send provider 2024-05-30 20:26:42 +03:00
62ccf94222 feat: add converter DatabaseEnum to DatabaseProvider 2024-05-30 20:25:21 +03:00
271df127a6 feat: add DbContext for UberDbContext 2024-05-30 20:20:20 +03:00
7c79f7d840 feat: add wrap for generic configuration 2024-05-30 20:19:58 +03:00
b8728cd490 feat: add factory DbContext for configuration by provider 2024-05-30 20:17:36 +03:00
7db4dc2c86 feat: add factory for DbContext 2024-05-30 20:15:40 +03:00
348b78b84e feat: add resolver for getting configuration Type 2024-05-30 20:14:46 +03:00
4c93ed282d refactor: move default configuration to sqlite 2024-05-30 20:14:15 +03:00
1bdf40f31f build: move projects 2024-05-30 20:13:06 +03:00
31c36443e1 refactor: use wrap DbContext for UberDbContext 2024-05-30 20:12:36 +03:00
43b6ab7934 feat: add sealed class for Mark configuration namespace 2024-05-30 20:12:04 +03:00
f79c7c7db9 refactor: use wrap DbContext 2024-05-30 20:11:18 +03:00
53a0439edb feat: add wrap DbContext for OnModelCreating 2024-05-30 20:10:13 +03:00
78a242f4c3 feat: add providers database for presistence 2024-05-30 20:07:59 +03:00
0a9c98cbf9 refactor: change GetService to GetRequiredService 2024-05-30 20:07:20 +03:00
164d575d98 refactor: move database-related projects to a separate folder 2024-05-29 08:08:58 +03:00
bf3a9d4b36 refactor: move database-related projects to separate folder 2024-05-29 07:49:42 +03:00
081c814036 feat: return the schedule-related settings 2024-05-29 07:38:32 +03:00
f6f7ed6c86 Add hashing and other security features ()
Reviewed-on: 
2024-05-29 06:42:46 +03:00
d2ef99d0b2 feat: add security configure 2024-05-29 06:42:14 +03:00
38ec80a566 feat: add configuration for jwt token 2024-05-29 06:30:01 +03:00
85802aa514 feat: add jwt token service 2024-05-29 06:28:42 +03:00
f2aa274d0a build: add jwt ref 2024-05-29 06:28:21 +03:00
62a859b44c style: clean code 2024-05-29 06:11:29 +03:00
6f02021fe7 feat: add revoked token service 2024-05-29 06:11:18 +03:00
526bf5682b build: add security ref 2024-05-29 06:08:41 +03:00
9287acf7d2 feat: add cache implementations depending on the type 2024-05-29 06:08:14 +03:00
2efdc6dbfe feat: add auth service to DI 2024-05-29 06:04:09 +03:00
25b6c7d691 feat: add method if there is no pre-auth token 2024-05-29 06:00:15 +03:00
61218c38a0 feat: add logout 2024-05-29 05:56:27 +03:00
d84011cd71 feat: add refresh token 2024-05-29 05:55:57 +03:00
4138c70007 feat: add wrap for revoke access token 2024-05-29 05:55:31 +03:00
9dd505a608 feat: add auth token response 2024-05-29 05:55:13 +03:00
79fb05d428 feat: add token revocation 2024-05-29 05:54:45 +03:00
81f2f995b0 feat: add generate auth token 2024-05-29 05:51:32 +03:00
f3063c5322 feat: add generate access token 2024-05-29 05:51:03 +03:00
43011457d6 feat: add wrap for save to cache 2024-05-29 05:50:47 +03:00
4240ad8110 feat: add auth key for cache 2024-05-29 05:36:26 +03:00
a3a42dd5c2 feat: add generate refresh token 2024-05-29 05:35:44 +03:00
b25be758ad feat: add auth token 2024-05-29 05:33:55 +03:00
7df4c8e4b6 feat: add auth service 2024-05-29 05:32:22 +03:00
f55d701ff3 feat: add sliding expiration for cache 2024-05-29 05:30:00 +03:00
d3a60d2a30 feat: add interface for gen access token 2024-05-29 05:29:25 +03:00
470031af39 feat: add match token 2024-05-29 05:27:49 +03:00
916b3795ed feat: add ip to struct 2024-05-29 05:27:27 +03:00
f4ad1518ef style: rename variables 2024-05-29 04:58:21 +03:00
ac7bbde75e fix: add key for save pre auth token 2024-05-29 04:57:44 +03:00
47a57693f8 sec: complicate the token 2024-05-29 04:55:34 +03:00
d05ba5349f refactor: isolate key generation 2024-05-29 04:48:37 +03:00
5fde5bd396 build: add security to sln 2024-05-29 04:34:39 +03:00
8408b80c35 feat: add pre-auth to DI 2024-05-29 04:34:00 +03:00
b14ae26a48 feat: add pre-auth service 2024-05-29 04:31:47 +03:00
3c9694de08 feat: add request for get token 2024-05-29 04:31:19 +03:00
e3db6b73e0 feat: add pre-auth response 2024-05-29 04:30:55 +03:00
58ceca5313 feat: add pre-auth token structure 2024-05-29 04:30:32 +03:00
f749ed42f5 feat: add interface for save to cache 2024-05-29 04:29:50 +03:00
6029ea3c2c refactor: move hashing to services 2024-05-29 04:11:04 +03:00
656d7dca0b feat: add DI 2024-05-29 04:09:10 +03:00
e3dd0a8419 build: add ref for DI 2024-05-29 04:08:51 +03:00
3149f50586 refactor: move class to correct namespace 2024-05-29 04:05:18 +03:00
e1123cf36b feat: add password hashing 2024-05-29 04:04:02 +03:00
930edd4c2c build: add ref 2024-05-29 04:03:47 +03:00
7e283fe643 feat: add security layer 2024-05-29 04:03:20 +03:00
c427006283 docs: add env data 2024-05-29 03:52:31 +03:00
e1ad287da1 build: add missing reference 2024-05-29 03:47:55 +03:00
f9750ef039 feat: add endpoint for schedule 2024-05-29 03:46:16 +03:00
17961ccefc feat: add email endpoint 2024-05-29 03:45:02 +03:00
5d308f1a24 feat: add request for cache 2024-05-29 03:44:39 +03:00
f5deeec6c9 feat: add endpoint for logging 2024-05-29 03:44:24 +03:00
29c9c10a53 feat: add endpoint for cache 2024-05-29 03:43:08 +03:00
c02240077f fix: add missing ref 2024-05-29 03:42:39 +03:00
2d67a565ca docs: add xml doc for request 2024-05-29 03:41:54 +03:00
ba8ccf8b7e feat: add set database endpoints 2024-05-29 03:38:21 +03:00
e7ed69169c feat: add setter database 2024-05-29 03:37:04 +03:00
eefb049e0e feat: add cache for save intermediate settings 2024-05-29 03:35:52 +03:00
07d7fec24f feat: add localhost for generate token 2024-05-29 03:30:26 +03:00
22793c7882 feat: add localhost attribute 2024-05-29 03:30:00 +03:00
9bf9eabad7 fix: add full path to settings 2024-05-28 07:20:21 +03:00
966ab9bdda feat: add generate and check token 2024-05-28 07:19:40 +03:00
481839159c feat: add middleware for custom exception 2024-05-28 07:16:15 +03:00
59785f600f feat: add argument exception for controllers 2024-05-28 07:15:13 +03:00
fb6e119a34 feat: add token for setup controllers 2024-05-28 07:14:17 +03:00
35eb1eab39 feat: implement validation of settings 2024-05-28 07:12:58 +03:00
08f13108d8 feat: add validator for settings 2024-05-28 07:10:32 +03:00
ae0b9daefa refactor: change the api path 2024-05-28 07:09:40 +03:00
3f30b98cf9 feat: add middleware for ignore maintenance 2024-05-28 07:04:07 +03:00
af284e945f feat: add attribute maintenance ignore for controllers 2024-05-28 07:02:35 +03:00
b62ddc9015 fix: delete ';' from property 2024-05-28 07:01:58 +03:00
d1a806545d feat: add maintenance mode 2024-05-28 07:01:23 +03:00
7b779463bb feat: add converter from Dto.PairPeriodTime toEnpoint.PairPeriodTime and vice versa 2024-05-28 06:59:28 +03:00
baedc667b7 feat: add PairPeriod for ApiDto 2024-05-28 06:57:54 +03:00
0e9bb04b96 feat: add setup token 2024-05-28 06:56:25 +03:00
36a78a8284 fix: add using Configuration.General 2024-05-28 06:53:52 +03:00
7a1281692e fix: it is correct to delete comments 2024-05-28 06:51:40 +03:00
fb736a1c34 feat: use path wrapper 2024-05-28 06:49:40 +03:00
d7299a1afd feat: add wrapper for Path.Combine with default path 2024-05-28 06:48:23 +03:00
817f9d2308 feat: add default path 2024-05-28 06:47:25 +03:00
99ecc4af5c fix: get connection string from configuration 2024-05-28 06:45:31 +03:00
202098b723 refactor: move the functionality to create a persistence database on Enpoint 2024-05-28 06:43:24 +03:00
266e66a35c feat: expand the configuration functionality 2024-05-28 06:38:24 +03:00
41689bca7f feat: add attribute for required configuration 2024-05-28 06:36:18 +03:00
c06ed8b479 refactor: move files 2024-05-28 06:30:42 +03:00
dfdc2ec109 feat: add interface for check configured settings 2024-05-28 06:28:24 +03:00
6716ee9cf0 Add specific weeks of disciplines () from feat/improved-sugroup into release/v1.0.0
Reviewed-on: 
2024-05-19 13:57:09 +03:00
0d3461b769 feat: add specific weeks 2024-05-19 13:53:25 +03:00
22579038d3 fix: change BIT to BOOLEAN 2024-05-19 13:52:10 +03:00
d8f2f51cd7 fix: error CS1061 2024-05-19 12:44:35 +03:00
ca18804a33 fix: error CS1061 2024-05-19 12:30:17 +03:00
97d2fae79e feat: add specific week to persistence 2024-05-19 11:52:56 +03:00
628faf7e68 feat: add interface for specific week 2024-05-19 11:52:08 +03:00
05dadff455 feat: add specific week 2024-05-19 11:49:03 +03:00
312 changed files with 13824 additions and 1600 deletions
.editorconfig.env
.gitea/workflows
.github/workflows
.gitignore
ApiDto
Application
Backend.slnDockerfile
Endpoint
Backend.http
Common
Configuration
Controllers
Endpoint.csprojProgram.cs
Properties
Sync
wwwroot
css
Persistence
README.md
Security
SqlData
Application
Application.csproj
Common
Cqrs
Campus
Discipline
Faculty
Group
LectureHall
Professor
Schedule
TypeOfOccupation
DependencyInjection.cs
Interfaces
Domain
Migrations
Persistence

278
.editorconfig Normal file

@ -0,0 +1,278 @@
# Удалите строку ниже, если вы хотите наследовать параметры .editorconfig из каталогов, расположенных выше в иерархии
root = true
# Файлы C#
[*.cs]
#### Основные параметры EditorConfig ####
# Отступы и интервалы
indent_size = 4
indent_style = space
tab_width = 4
# Предпочтения для новых строк
end_of_line = unset
insert_final_newline = false
#### Действия кода .NET ####
# Члены типа
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Поиск символов
dotnet_search_reference_assemblies = true
#### Рекомендации по написанию кода .NET ####
# Упорядочение Using
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# Предпочтения для this. и Me.
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Параметры использования ключевых слов языка и типов BCL
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Предпочтения для скобок
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Предпочтения модификатора
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Выражения уровень предпочтения
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Предпочтения для полей
dotnet_style_readonly_field = true
# Настройки параметров
dotnet_code_quality_unused_parameters = non_public
# Параметры подавления
dotnet_remove_unnecessary_suppression_exclusions = none
# Предпочтения для новых строк
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = false
#### Рекомендации по написанию кода C# ####
# Предпочтения var
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
# Члены, заданные выражениями
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = true:silent
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_operators = true:silent
csharp_style_expression_bodied_properties = true:silent
# Настройки соответствия шаблонов
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Настройки проверки на null
csharp_style_conditional_delegate_call = true:suggestion
# Предпочтения модификатора
csharp_prefer_static_anonymous_function = true:suggestion
csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
# Предпочтения для блоков кода
csharp_prefer_braces = when_multiline:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_namespace_declarations = file_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_top_level_statements = false:silent
# Выражения уровень предпочтения
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# предпочтения для директивы using
csharp_using_directive_placement = outside_namespace:silent
# Предпочтения для новых строк
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:silent
csharp_style_allow_embedded_statements_on_same_line_experimental = false:silent
#### Правила форматирования C# ####
# Предпочтения для новых строк
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Предпочтения для отступов
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Предпочтения для интервалов
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Предпочтения переноса
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Стили именования ####
# Правила именования
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Спецификации символов
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Стили именования
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
[*.{cs,vb}]
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = unset
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_code_quality_unused_parameters = non_public:suggestion
dotnet_style_predefined_type_for_member_access = true:silent
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
dotnet_style_allow_statement_immediately_after_block_experimental = false:silent
dotnet_style_readonly_field = true:suggestion
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent

163
.env Normal file

@ -0,0 +1,163 @@
# 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)
# If the specified path ends with "/api", the system will avoid duplicating "api" in the final URL.
# This allows flexible API structuring, especially when running behind a reverse proxy or in containerized environments.
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
### OAuth2
#### GOOGLE
# The client ID for Google OAuth
# string
# This is the client ID provided by Google when you register your application for OAuth.
# It's necessary for enabling Google login functionality.
GOOGLE_CLIENT_ID=
# The client secret for Google OAuth
# string
# This is the client secret provided by Google, used alongside the client ID to authenticate your application.
# Make sure to keep it confidential.
GOOGLE_CLIENT_SECRET=
#### Yandex
# The client ID for Yandex OAuth
# string
# This is the client ID provided by Yandex when you register your application for OAuth.
# It's required for enabling Yandex login functionality.
YANDEX_CLIENT_ID=
# The client secret for Yandex OAuth
# string
# This is the client secret provided by Yandex, used alongside the client ID to authenticate your application.
# Keep it confidential to ensure the security of your app.
YANDEX_CLIENT_SECRET=
#### MailRu
# The client ID for MailRu OAuth
# string
# This is the client ID provided by MailRu (Mail.ru Group) when you register your application for OAuth.
# It's necessary for enabling MailRu login functionality.
MAILRU_CLIENT_ID=
# The client secret for MailRu OAuth
# string
# This is the client secret provided by MailRu, used alongside the client ID to authenticate your application.
# Keep it confidential to ensure the security of your app.
MAILRU_CLIENT_SECRET=

@ -1,26 +0,0 @@
name: .NET Test Pipeline
on:
pull_request:
branches: [master, 'release/*']
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up .NET Core
uses: actions/setup-dotnet@v3
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

30
.github/workflows/code-analyze.yaml vendored Normal file

@ -0,0 +1,30 @@
name: .NET Test Pipeline
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checking out
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: SonarScanner for .NET 8 with pull request decoration support
uses: highbyte/sonarscan-dotnet@v2.3.3
with:
sonarProjectKey: $(echo "${{ github.repository }}" | cut -d'/' -f2)
sonarProjectName: $(echo "${{ github.repository }}" | cut -d'/' -f2)
sonarHostname: ${{ secrets.SONAR_HOST_URL }}
dotnetPreBuildCmd: dotnet nuget add source --name="Winsomnia" --username ${{ secrets.NUGET_USERNAME }} --password ${{ secrets.NUGET_PASSWORD }} --store-password-in-clear-text ${{ secrets.NUGET_ADDRESS }} && dotnet format --verify-no-changes --diagnostics -v diag --severity warn
dotnetTestArguments: --logger trx --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
sonarBeginArguments: /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" -d:sonar.cs.vstest.reportsPaths="**/TestResults/*.trx"
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

97
.github/workflows/release-version.yml vendored Normal file

@ -0,0 +1,97 @@
name: Build and Deploy Docker Container
on:
push:
branches:
[master]
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 }} --build-arg NUGET_ADDRESS=${{ secrets.NUGET_ADDRESS }} -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 }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
YANDEX_CLIENT_ID: ${{ secrets.YANDEX_CLIENT_ID }}
YANDEX_CLIENT_SECRET: ${{ secrets.YANDEX_CLIENT_SECRET }}
MAILRU_CLIENT_ID: ${{ secrets.MAILRU_CLIENT_ID }}
MAILRU_CLIENT_SECRET: ${{ secrets.MAILRU_CLIENT_SECRET }}
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 \
-e GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID \
-e GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET \
-e YANDEX_CLIENT_ID=$YANDEX_CLIENT_ID \
-e YANDEX_CLIENT_SECRET=$YANDEX_CLIENT_SECRET \
-e MAILRU_CLIENT_ID=$MAILRU_CLIENT_ID \
-e MAILRU_CLIENT_SECRET=$MAILRU_CLIENT_SECRET \
$DOCKER_IMAGE
"
- name: Remove all keys from ssh-agent
run: ssh-add -D

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

@ -5,9 +5,9 @@
<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.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion> <AssemblyVersion>1.0.3.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion> <FileVersion>1.0.3.0</FileVersion>
<AssemblyName>Mirea.Api.Dto</AssemblyName> <AssemblyName>Mirea.Api.Dto</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace> <RootNamespace>$(AssemblyName)</RootNamespace>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
@ -20,4 +20,23 @@
</None> </None>
</ItemGroup> </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> </Project>

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

@ -0,0 +1,17 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Specifies the types of caching mechanisms available.
/// </summary>
public enum CacheType
{
/// <summary>
/// Memcached caching type.
/// </summary>
Memcached,
/// <summary>
/// Redis caching type.
/// </summary>
Redis
}

@ -0,0 +1,24 @@
using System;
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Represents a date or date range to skip during cron update scheduling.
/// </summary>
public class CronUpdateSkip
{
/// <summary>
/// Gets or sets the start date of the skip range.
/// </summary>
public DateOnly? Start { get; set; }
/// <summary>
/// Gets or sets the end date of the skip range.
/// </summary>
public DateOnly? End { get; set; }
/// <summary>
/// Gets or sets a specific date to skip.
/// </summary>
public DateOnly? Date { get; set; }
}

@ -0,0 +1,22 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Specifies the types of databases supported.
/// </summary>
public enum DatabaseType
{
/// <summary>
/// MySQL database type.
/// </summary>
Mysql,
/// <summary>
/// SQLite database type.
/// </summary>
Sqlite,
/// <summary>
/// PostgreSQL database type.
/// </summary>
PostgresSql
}

@ -0,0 +1,17 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Defines the actions that can be performed with an OAuth token.
/// </summary>
public enum OAuthAction
{
/// <summary>
/// The action to log in the user using the provided OAuth token.
/// </summary>
Login,
/// <summary>
/// The action to bind an OAuth provider to the user's account.
/// </summary>
Bind
}

@ -0,0 +1,22 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Represents different OAuth providers for authentication.
/// </summary>
public enum OAuthProvider
{
/// <summary>
/// OAuth provider for Google.
/// </summary>
Google,
/// <summary>
/// OAuth provider for Yandex.
/// </summary>
Yandex,
/// <summary>
/// OAuth provider for Mail.ru.
/// </summary>
MailRu
}

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

@ -0,0 +1,32 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Represents the password policy settings for user authentication.
/// </summary>
public class PasswordPolicy
{
/// <summary>
/// Gets or sets the minimum length required for a password.
/// </summary>
public int MinimumLength { get; set; }
/// <summary>
/// Gets or sets a value indicating whether at least one letter is required in the password.
/// </summary>
public bool RequireLetter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the password must contain both lowercase and uppercase letters.
/// </summary>
public bool RequireLettersDifferentCase { get; set; }
/// <summary>
/// Gets or sets a value indicating whether at least one digit is required in the password.
/// </summary>
public bool RequireDigit { get; set; }
/// <summary>
/// Gets or sets a value indicating whether at least one special character is required in the password.
/// </summary>
public bool RequireSpecialCharacter { get; set; }
}

@ -0,0 +1,17 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Represents the steps required after a login attempt.
/// </summary>
public enum TwoFactorAuthentication
{
/// <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,
}

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

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

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

@ -0,0 +1,38 @@
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; }
/// <summary>
/// Gets or sets the API key for integrating with Seq, a log aggregation service.
/// If provided, logs will be sent to a Seq server using this API key.
/// </summary>
public string? ApiKeySeq { get; set; }
/// <summary>
/// Gets or sets the server URL for the Seq logging service.
/// This property specifies the Seq server endpoint to which logs will be sent.
/// If <see cref="ApiKeySeq"/> is provided, logs will be sent to this server.
/// </summary>
public string? ApiServerSeq { 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; }
}

@ -0,0 +1,30 @@
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>
[Required]
[EmailAddress]
public required string Email { get; set; }
/// <summary>
/// Gets or sets the username of the user.
/// </summary>
[Required]
[MinLength(2)]
public required string Username { get; set; }
/// <summary>
/// Gets or sets the password of the user.
/// </summary>
[Required]
[MinLength(2)]
public required string Password { get; set; }
}

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

@ -8,30 +8,30 @@ public class ScheduleRequest
/// <summary> /// <summary>
/// Gets or sets an array of group IDs. /// Gets or sets an array of group IDs.
/// </summary> /// </summary>
/// <remarks>This array can contain null values.</remarks>
public int[]? Groups { get; set; } = null; public int[]? Groups { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to retrieve schedules for even weeks. /// Gets or sets a value indicating whether to retrieve schedules for even weeks.
/// </summary> /// </summary>
/// <remarks>This property can contain null.</remarks>
public bool? IsEven { get; set; } = null; public bool? IsEven { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets an array of discipline IDs. /// Gets or sets an array of discipline IDs.
/// </summary> /// </summary>
/// <remarks>This array can contain null values.</remarks>
public int[]? Disciplines { get; set; } = null; public int[]? Disciplines { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets an array of professor IDs. /// Gets or sets an array of professor IDs.
/// </summary> /// </summary>
/// <remarks>This array can contain null values.</remarks>
public int[]? Professors { get; set; } = null; public int[]? Professors { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets an array of lecture hall IDs. /// Gets or sets an array of lecture hall IDs.
/// </summary> /// </summary>
/// <remarks>This array can contain null values.</remarks>
public int[]? LectureHalls { get; set; } = null; public int[]? LectureHalls { get; set; } = null;
/// <summary>
/// Gets or sets an array of lesson type IDs.
/// </summary>
public int[]? LessonType { get; set; } = null;
} }

@ -0,0 +1,19 @@
using Mirea.Api.Dto.Common;
namespace Mirea.Api.Dto.Requests;
/// <summary>
/// Represents a request for verifying two-factor authentication.
/// </summary>
public class TwoFactorAuthRequest
{
/// <summary>
/// Gets or sets the two-factor authentication code provided by the user.
/// </summary>
public required string Code { get; set; }
/// <summary>
/// Gets or sets the type of the two-factor authentication method used (e.g., TOTP, Email).
/// </summary>
public TwoFactorAuthentication Method { get; set; }
}

@ -0,0 +1,27 @@
using Mirea.Api.Dto.Common;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents the response containing information about available OAuth providers.
/// </summary>
public class AvailableOAuthProvidersResponse
{
/// <summary>
/// Gets or sets the name of the OAuth provider.
/// </summary>
[Required]
public required string ProviderName { get; set; }
/// <summary>
/// Gets or sets the enum value representing the OAuth provider.
/// </summary>
public OAuthProvider Provider { get; set; }
/// <summary>
/// Gets or sets the redirect URL for the OAuth provider's authorization process.
/// </summary>
[Required]
public required string Redirect { get; set; }
}

@ -0,0 +1,29 @@
using Mirea.Api.Dto.Common;
namespace Mirea.Api.Dto.Responses.Configuration;
/// <summary>
/// Represents a response containing cache configuration details.
/// </summary>
public class CacheResponse
{
/// <summary>
/// Gets or sets the type of cache database.
/// </summary>
public CacheType Type { get; set; }
/// <summary>
/// Gets or sets the server address.
/// </summary>
public string? Server { get; set; }
/// <summary>
/// Gets or sets the port number.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets or sets the password.
/// </summary>
public string? Password { get; set; }
}

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses.Configuration;
/// <summary>
/// Represents the response containing the cron update schedule and the next scheduled task dates.
/// </summary>
public class CronUpdateScheduleResponse
{
/// <summary>
/// Gets or sets the cron expression representing the update schedule.
/// </summary>
[Required]
public required string Cron { get; set; }
/// <summary>
/// Gets or sets the list of next scheduled task dates based on the cron expression.
/// </summary>
[Required]
public required List<DateTime> NextStart { get; set; }
}

@ -0,0 +1,49 @@
using Mirea.Api.Dto.Common;
namespace Mirea.Api.Dto.Responses.Configuration;
/// <summary>
/// Represents a response containing database configuration details.
/// </summary>
public class DatabaseResponse
{
/// <summary>
/// Gets or sets the type of database.
/// </summary>
public DatabaseType Type { get; set; }
/// <summary>
/// Gets or sets the server address.
/// </summary>
public string? Server { get; set; }
/// <summary>
/// Gets or sets the port number.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets or sets the database name.
/// </summary>
public string? Database { get; set; }
/// <summary>
/// Gets or sets the username.
/// </summary>
public string? User { get; set; }
/// <summary>
/// Gets or sets a value indicating whether SSL is enabled.
/// </summary>
public bool Ssl { get; set; }
/// <summary>
/// Gets or sets the password.
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Gets or sets the path to database. Only for Sqlite
/// </summary>
public string? PathToDatabase { get; set; }
}

@ -1,22 +0,0 @@
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; }
}

@ -1,36 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents detailed information about a faculty.
/// </summary>
public class FacultyDetailsResponse
{
/// <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; }
/// <summary>
/// Gets or sets the unique identifier of the campus to which the faculty belongs (optional).
/// </summary>
public int? CampusId { get; set; }
/// <summary>
/// Gets or sets the name of the campus to which the faculty belongs (optional).
/// </summary>
public string? CampusName { get; set; }
/// <summary>
/// Gets or sets the code name of the campus to which the faculty belongs (optional).
/// </summary>
public string? CampusCode { get; set; }
}

@ -3,7 +3,7 @@
namespace Mirea.Api.Dto.Responses; namespace Mirea.Api.Dto.Responses;
/// <summary> /// <summary>
/// Represents basic information about a faculty. /// Represents information about a faculty.
/// </summary> /// </summary>
public class FacultyResponse public class FacultyResponse
{ {
@ -18,9 +18,4 @@ public class FacultyResponse
/// </summary> /// </summary>
[Required] [Required]
public required string Name { get; set; } public required string Name { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the campus to which the faculty belongs (optional).
/// </summary>
public int? CampusId { get; set; }
} }

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents information about a lesson type.
/// </summary>
public class LessonTypeResponse
{
/// <summary>
/// Gets or sets the unique identifier of the lesson type.
/// </summary>
[Required]
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the lesson type.
/// </summary>
[Required]
public required string Name { get; set; }
}

@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses.Schedule;
/// <summary>
/// Represents information about a specific schedule entry for a professor.
/// </summary>
public class DisciplineScheduleInfo
{
/// <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 type of occupation for the schedule entry.
/// </summary>
[Required]
public required string TypeOfOccupation { get; set; }
/// <summary>
/// Gets or sets the names of the group for the schedule entry.
/// </summary>
[Required]
public required string Group { get; set; }
/// <summary>
/// Gets or sets the IDs 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; }
}
/// <summary>
/// Represents a response containing schedule information for a professor.
/// </summary>
public class DisciplineScheduleResponse
{
/// <summary>
/// Gets or sets the name of the discipline.
/// </summary>
[Required]
public required string Discipline { get; set; }
/// <summary>
/// Gets or sets the ID of the discipline.
/// </summary>
[Required]
public required int DisciplineId { get; set; }
/// <summary>
/// Gets or sets the schedules for the professor.
/// </summary>
[Required]
public required IEnumerable<DisciplineScheduleInfo> Schedules { get; set; }
}

@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses.Schedule;
/// <summary>
/// Represents information about a specific schedule entry for a group.
/// </summary>
public class GroupScheduleInfo
{
/// <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 the type of occupation for the schedule entry.
/// </summary>
[Required]
public required string TypeOfOccupation { 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; }
}
/// <summary>
/// Represents a response containing schedule information for a group.
/// </summary>
public class GroupScheduleResponse
{
/// <summary>
/// Gets or sets the name of the group.
/// </summary>
[Required]
public required string Group { get; set; }
/// <summary>
/// Gets or sets the ID of the group.
/// </summary>
[Required]
public required int GroupId { get; set; }
/// <summary>
/// Gets or sets the schedules for the group.
/// </summary>
[Required]
public required IEnumerable<GroupScheduleInfo> Schedules { get; set; }
}

@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses.Schedule;
/// <summary>
/// Represents information about a specific schedule entry for a lecture hall.
/// </summary>
public class LectureHallScheduleInfo
{
/// <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 the type of occupation for the schedule entry.
/// </summary>
[Required]
public required string TypeOfOccupation { get; set; }
/// <summary>
/// Gets or sets the names of the group for the schedule entry.
/// </summary>
[Required]
public required string Group { get; set; }
/// <summary>
/// Gets or sets the IDs of the group for the schedule entry.
/// </summary>
[Required]
public required int GroupId { 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 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 links to online meetings for the schedule entry.
/// </summary>
public required IEnumerable<string?> LinkToMeet { get; set; }
}
/// <summary>
/// Represents a response containing schedule information for a lecture hall.
/// </summary>
public class LectureHallScheduleResponse
{
/// <summary>
/// Gets or sets the names of the lecture halls.
/// </summary>
public required string LectureHalls { get; set; }
/// <summary>
/// Gets or sets the IDs of the lecture halls.
/// </summary>
public required int LectureHallsId { get; set; }
/// <summary>
/// Gets or sets the schedules for the lecture hall.
/// </summary>
[Required]
public required IEnumerable<LectureHallScheduleInfo> Schedules { get; set; }
}

@ -1,108 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses.Schedule;
/// <summary>
/// Represents information about a specific schedule entry for a professor.
/// </summary>
public class ProfessorScheduleInfo
{
/// <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 the type of occupation for the schedule entry.
/// </summary>
[Required]
public required string TypeOfOccupation { get; set; }
/// <summary>
/// Gets or sets the names of the group for the schedule entry.
/// </summary>
[Required]
public required string Group { get; set; }
/// <summary>
/// Gets or sets the IDs 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 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; }
}
/// <summary>
/// Represents a response containing schedule information for a professor.
/// </summary>
public class ProfessorScheduleResponse
{
/// <summary>
/// Gets or sets the name of the professor.
/// </summary>
[Required]
public required string Professor { get; set; }
/// <summary>
/// Gets or sets the ID of the professor.
/// </summary>
[Required]
public required int ProfessorId { get; set; }
/// <summary>
/// Gets or sets the schedules for the professor.
/// </summary>
[Required]
public required IEnumerable<ProfessorScheduleInfo> Schedules { get; set; }
}

@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses.Schedule; namespace Mirea.Api.Dto.Responses;
/// <summary> /// <summary>
/// Represents a response object containing schedule information. /// Represents a response object containing schedule information.
@ -39,11 +39,34 @@ public class ScheduleResponse
[Required] [Required]
public required int DisciplineId { get; set; } 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> /// <summary>
/// Gets or sets the type of occupation for the schedule entry. /// Gets or sets the type of occupation for the schedule entry.
/// </summary> /// </summary>
[Required] [Required]
public required string TypeOfOccupation { get; set; } public required IEnumerable<string> TypeOfOccupations { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the group for the schedule entry. /// Gets or sets the name of the group for the schedule entry.

@ -0,0 +1,17 @@
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents the response containing the TOTP (Time-Based One-Time Password) key details.
/// </summary>
public class TotpKeyResponse
{
/// <summary>
/// Gets or sets the secret key used for TOTP generation.
/// </summary>
public required string Secret { get; set; }
/// <summary>
/// Gets or sets the image (QR code) representing the TOTP key.
/// </summary>
public required string Image { get; set; }
}

@ -0,0 +1,35 @@
using Mirea.Api.Dto.Common;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents a response containing user information.
/// </summary>
public class UserResponse
{
/// <summary>
/// Gets or sets the email address of the user.
/// </summary>
[Required]
public required string Email { get; set; }
/// <summary>
/// Gets or sets the username of the user.
/// </summary>
[Required]
public required string Username { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user has two-factor authentication enabled.
/// </summary>
[Required]
public bool TwoFactorAuthenticatorEnabled { get; set; }
/// <summary>
/// Gets or sets a collection of OAuth providers used by the user.
/// </summary>
[Required]
public required IEnumerable<OAuthProvider> UsedOAuthProviders { get; set; }
}

@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<Company>Winsomnia</Company>
<Version>1.0.0-a0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyName>Mirea.Api.DataAccess.Application</AssemblyName>
<RootNamespace>$(AssemblyName)</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>

@ -1,28 +0,0 @@
using AutoMapper;
using System;
using System.Linq;
using System.Reflection;
namespace Mirea.Api.DataAccess.Application.Common.Mappings;
public class AssemblyMappingProfile : Profile
{
public AssemblyMappingProfile(Assembly assembly) =>
ApplyMappingsFromAssembly(assembly);
private void ApplyMappingsFromAssembly(Assembly assembly)
{
var types = assembly.GetExportedTypes()
.Where(type => type.GetInterfaces()
.Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IMapWith<>)))
.ToList();
foreach (var type in types)
{
var instance = Activator.CreateInstance(type);
var methodInfo = type.GetMethod("Mapping");
methodInfo?.Invoke(instance, new[] { this });
}
}
}

@ -1,9 +0,0 @@
using AutoMapper;
namespace Mirea.Api.DataAccess.Application.Common.Mappings;
public interface IMapWith<T>
{
void Mapping(Profile profile) =>
profile.CreateMap(typeof(T), GetType());
}

@ -1,32 +0,0 @@
namespace Mirea.Api.DataAccess.Application.Cqrs.Faculty.Queries.GetFacultyDetails;
/// <summary>
/// Represents faculties.
/// </summary>
public class FacultyInfoVm
{
/// <summary>
/// The unique identifier for the faculty.
/// </summary>
public int Id { get; set; }
/// <summary>
/// The name of the faculty.
/// </summary>
public required string Name { get; set; }
/// <summary>
/// ID indicating the faculty's affiliation to the campus.
/// </summary>
public int? CampusId { get; set; }
/// <summary>
/// Campus name indicating the faculty's affiliation to the campus.
/// </summary>
public string? CampusName { get; set; }
/// <summary>
/// Campus code indicating the faculty's affiliation to the campus.
/// </summary>
public string? CampusCode { get; set; }
}

@ -1,8 +0,0 @@
using MediatR;
namespace Mirea.Api.DataAccess.Application.Cqrs.Faculty.Queries.GetFacultyDetails;
public class GetFacultyInfoQuery : IRequest<FacultyInfoVm>
{
public required int Id { get; set; }
}

@ -1,27 +0,0 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Mirea.Api.DataAccess.Application.Common.Exceptions;
using Mirea.Api.DataAccess.Application.Interfaces.DbContexts.Schedule;
using System.Threading;
using System.Threading.Tasks;
namespace Mirea.Api.DataAccess.Application.Cqrs.Faculty.Queries.GetFacultyDetails;
public class GetFacultyInfoQueryHandler(IFacultyDbContext dbContext) : IRequestHandler<GetFacultyInfoQuery, FacultyInfoVm>
{
public async Task<FacultyInfoVm> Handle(GetFacultyInfoQuery request, CancellationToken cancellationToken)
{
var faculty = await dbContext.Faculties
.Include(f => f.Campus)
.FirstOrDefaultAsync(f => f.Id == request.Id, cancellationToken) ?? throw new NotFoundException(typeof(Domain.Schedule.Faculty), request.Id);
return new FacultyInfoVm()
{
Id = faculty.Id,
Name = faculty.Name,
CampusId = faculty.CampusId,
CampusName = faculty.Campus?.FullName,
CampusCode = faculty.Campus?.CodeName
};
}
}

@ -1,8 +0,0 @@
using MediatR;
namespace Mirea.Api.DataAccess.Application.Cqrs.Professor.Queries.GetProfessorDetails;
public class GetProfessorInfoQuery : IRequest<ProfessorInfoVm>
{
public required int Id { get; set; }
}

@ -3,28 +3,49 @@ 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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
.editorconfig = .editorconfig
.env = .env
.gitattributes = .gitattributes .gitattributes = .gitattributes
.gitignore = .gitignore .gitignore = .gitignore
.github\workflows\code-analyze.yaml = .github\workflows\code-analyze.yaml
Dockerfile = Dockerfile Dockerfile = Dockerfile
LICENSE.txt = LICENSE.txt LICENSE.txt = LICENSE.txt
README.md = README.md README.md = README.md
.github\workflows\release-version.yml = .github\workflows\release-version.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\Application.csproj", "{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}" 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 ProjectSection(ProjectDependencies) = postProject
{C27FB5CD-6A70-4FB2-847A-847B34806902} = {C27FB5CD-6A70-4FB2-847A-847B34806902} {48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "Persistence\Persistence.csproj", "{4C1E558F-633F-438E-AC3A-61CDDED917C5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MysqlMigrations", "SqlData\Migrations\MysqlMigrations\MysqlMigrations.csproj", "{5861915B-9574-4D5D-872F-D54A09651697}"
ProjectSection(ProjectDependencies) = postProject ProjectSection(ProjectDependencies) = postProject
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4} = {E7F0A4D4-B032-4BB9-9526-1AF688F341A4} {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
@ -33,26 +54,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
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Release|Any CPU.ActiveCfg = Release|Any CPU {0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Release|Any CPU.Build.0 = Release|Any CPU {0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = Release|Any CPU
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Release|Any CPU.Build.0 = 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

@ -1,25 +1,32 @@
#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
LABEL company="Winsomnia"
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base LABEL maintainer.name="Wesser" maintainer.email="support@winsomnia.net"
USER app
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 ARG NUGET_ADDRESS
ENV NUGET_USERNAME=$NUGET_USERNAME
ENV NUGET_PASSWORD=$NUGET_PASSWORD
ENV NUGET_ADDRESS=$NUGET_ADDRESS
RUN dotnet nuget add source --name="Winsomnia" --username ${NUGET_USERNAME} --store-password-in-clear-text --password ${NUGET_PASSWORD} ${NUGET_ADDRESS}
RUN dotnet restore ./Backend.sln
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
###

@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Mirea.Api.Dto.Responses;
using System; using System;
namespace Mirea.Api.Endpoint.Common.Attributes; namespace Mirea.Api.Endpoint.Common.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status400BadRequest); public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status400BadRequest);

@ -0,0 +1,26 @@
using System;
namespace Mirea.Api.Endpoint.Common.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, 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;
}
}

@ -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 | AttributeTargets.Method, Inherited = false)]
public class MaintenanceModeIgnoreAttribute : Attribute;

@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Mirea.Api.Dto.Responses;
using System; using System;
namespace Mirea.Api.Endpoint.Common.Attributes; namespace Mirea.Api.Endpoint.Common.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status404NotFound); public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status404NotFound);

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

@ -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 var 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 @@
using System;
namespace Mirea.Api.Endpoint.Common.Exceptions;
public class ServerUnavailableException(string message, bool addRetryAfter) : Exception(message)
{
public bool AddRetryAfter { get; } = addRetryAfter;
}

@ -0,0 +1,7 @@
namespace Mirea.Api.Endpoint.Common.Interfaces;
public interface IMaintenanceModeNotConfigureService
{
bool IsMaintenanceMode { get; }
void DisableMaintenanceMode();
}

@ -0,0 +1,8 @@
namespace Mirea.Api.Endpoint.Common.Interfaces;
public interface IMaintenanceModeService
{
bool IsMaintenanceMode { get; }
void EnableMaintenanceMode();
void DisableMaintenanceMode();
}

@ -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,27 @@
using Mirea.Api.Dto.Responses;
using Mirea.Api.Security.Common.Domain;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mirea.Api.Endpoint.Common.MapperDto;
public static class AvailableProvidersConverter
{
public static Dto.Common.OAuthProvider ConvertToDto(this OAuthProvider provider) =>
provider switch
{
OAuthProvider.Google => Dto.Common.OAuthProvider.Google,
OAuthProvider.Yandex => Dto.Common.OAuthProvider.Yandex,
OAuthProvider.MailRu => Dto.Common.OAuthProvider.MailRu,
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null)
};
public static List<AvailableOAuthProvidersResponse> ConvertToDto(this IEnumerable<(OAuthProvider Provider, Uri Redirect)> data) =>
data.Select(x => new AvailableOAuthProvidersResponse()
{
ProviderName = Enum.GetName(x.Provider)!,
Provider = x.Provider.ConvertToDto(),
Redirect = x.Redirect.ToString()
}).ToList();
}

@ -0,0 +1,19 @@
using Mirea.Api.Endpoint.Common.Services;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using System.Collections.Generic;
using System.Linq;
namespace Mirea.Api.Endpoint.Common.MapperDto;
public static class CronUpdateSkipConverter
{
public static List<Dto.Common.CronUpdateSkip> ConvertToDto(this IEnumerable<ScheduleSettings.CronUpdateSkip> pairPeriod) =>
pairPeriod.Select(x => new Dto.Common.CronUpdateSkip()
{
Start = x.Start,
End = x.End,
Date = x.Date
}).ToList();
public static List<ScheduleSettings.CronUpdateSkip> ConvertFromDto(this IEnumerable<Dto.Common.CronUpdateSkip> pairPeriod) =>
pairPeriod.Select(x => x.Get()).ToList();
}

@ -0,0 +1,14 @@
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using System.Collections.Generic;
using System.Linq;
namespace Mirea.Api.Endpoint.Common.MapperDto;
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));
}

@ -0,0 +1,23 @@
using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy;
namespace Mirea.Api.Endpoint.Common.MapperDto;
public static class PasswordPolicyConverter
{
public static Security.Common.Model.PasswordPolicy ConvertFromDto(this PasswordPolicy policy) =>
new(policy.MinimumLength,
policy.RequireLetter,
policy.RequireLettersDifferentCase,
policy.RequireDigit,
policy.RequireSpecialCharacter);
public static PasswordPolicy ConvertToDto(this Security.Common.Model.PasswordPolicy policy) =>
new()
{
MinimumLength = policy.MinimumLength,
RequireLetter = policy.RequireLetter,
RequireDigit = policy.RequireDigit,
RequireSpecialCharacter = policy.RequireSpecialCharacter,
RequireLettersDifferentCase = policy.RequireLettersDifferentCase
};
}

@ -0,0 +1,23 @@
using Mirea.Api.Dto.Common;
using System;
namespace Mirea.Api.Endpoint.Common.MapperDto;
public static class TwoFactorAuthenticationConverter
{
public static TwoFactorAuthentication ConvertToDto(this Security.Common.Model.TwoFactorAuthenticator authenticator) =>
authenticator switch
{
Security.Common.Model.TwoFactorAuthenticator.None => TwoFactorAuthentication.None,
Security.Common.Model.TwoFactorAuthenticator.Totp => TwoFactorAuthentication.TotpRequired,
_ => throw new ArgumentOutOfRangeException(nameof(authenticator), authenticator, null)
};
public static Security.Common.Model.TwoFactorAuthenticator ConvertFromDto(this TwoFactorAuthentication authentication) =>
authentication switch
{
TwoFactorAuthentication.None => Security.Common.Model.TwoFactorAuthenticator.None,
TwoFactorAuthentication.TotpRequired => Security.Common.Model.TwoFactorAuthenticator.Totp,
_ => throw new ArgumentOutOfRangeException(nameof(authentication), authentication, null)
};
}

@ -0,0 +1,20 @@
using Mirea.Api.Endpoint.Configuration.Model;
using Mirea.Api.Security.Common.Model;
namespace Mirea.Api.Endpoint.Common.MapperDto;
public static class UserConverter
{
public static User ConvertToSecurity(this Admin data) =>
new()
{
Id = 1.ToString(),
Email = data.Email,
Username = data.Username,
PasswordHash = data.PasswordHash,
Salt = data.Salt,
SecondFactorToken = data.Secret,
TwoFactorAuthenticator = data.TwoFactorAuthenticator,
OAuthProviders = data.OAuthProviders
};
}

@ -0,0 +1,78 @@
using Cronos;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mirea.Api.Endpoint.Common.Services;
public static class CronUpdateSkipService
{
public static ScheduleSettings.CronUpdateSkip Get(this Dto.Common.CronUpdateSkip date)
{
if (date.Date.HasValue)
return new ScheduleSettings.CronUpdateSkip(date.Date.Value);
if (date is { Start: not null, End: not null })
return new ScheduleSettings.CronUpdateSkip(date.Start.Value, date.End.Value);
throw new ArgumentException("It is impossible to create a structure because it has incorrect values.");
}
public static List<ScheduleSettings.CronUpdateSkip> FilterDateEntry(this List<ScheduleSettings.CronUpdateSkip> data, DateOnly? currentDate = null)
{
currentDate ??= DateOnly.FromDateTime(DateTime.Now);
return data.OrderBy(x => x.End ?? x.Date)
.Where(x => x.Date == currentDate || (x.Start <= currentDate && x.End >= currentDate))
.ToList();
}
public static List<ScheduleSettings.CronUpdateSkip> FilterDateEntry(this List<ScheduleSettings.CronUpdateSkip> data, DateTime? currentDate = null) =>
data.FilterDateEntry(DateOnly.FromDateTime(currentDate ?? DateTime.Now));
public static List<ScheduleSettings.CronUpdateSkip> Filter(this List<ScheduleSettings.CronUpdateSkip> data, DateOnly? currentDate = null)
{
currentDate ??= DateOnly.FromDateTime(DateTime.Now);
return data.Where(x => x.Date >= currentDate || x.End >= currentDate)
.OrderBy(x => x.End ?? x.Date)
.ToList();
}
public static List<DateTimeOffset> GetNextTask(this List<ScheduleSettings.CronUpdateSkip> data,
CronExpression expression, int depth = 1, DateOnly? currentDate = null)
{
if (depth <= 0)
return [];
DateTimeOffset nextRunTime = (currentDate?.ToDateTime(TimeOnly.MinValue) ?? DateTime.Now).ToUniversalTime();
List<DateTimeOffset> result = [];
do
{
var lastSkippedEntry = data.FilterDateEntry(nextRunTime.DateTime).LastOrDefault();
if (lastSkippedEntry is { Start: not null, End: not null })
nextRunTime = lastSkippedEntry.End.Value.ToDateTime(TimeOnly.MinValue).AddDays(1);
else if (lastSkippedEntry.Date.HasValue)
nextRunTime = lastSkippedEntry.Date.Value.ToDateTime(TimeOnly.MinValue).AddDays(1);
var nextOccurrence = expression.GetNextOccurrence(nextRunTime.AddMinutes(-1), TimeZoneInfo.Local);
if (!nextOccurrence.HasValue)
return result;
if (data.FilterDateEntry(nextOccurrence.Value.DateTime).Count != 0)
{
nextRunTime = nextOccurrence.Value.AddDays(1);
continue;
}
result.Add(nextOccurrence.Value.ToLocalTime());
nextRunTime = nextOccurrence.Value.AddMinutes(1);
} while (result.Count < depth);
return result;
}
}

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

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

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

@ -0,0 +1,10 @@
using System;
namespace Mirea.Api.Endpoint.Common.Services;
public static class ScheduleSyncManager
{
public static event Action? OnForceSyncRequested;
public static void RequestForceSync() =>
OnForceSyncRequested?.Invoke();
}

@ -0,0 +1,64 @@
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);
}

@ -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.NameIdentifier, 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;
}
}

@ -0,0 +1,63 @@
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 _));
}

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Linq;
namespace Mirea.Api.Endpoint.Common.Services;
public static class UrlHelper
{
public static string GetCurrentScheme(this HttpContext context) =>
context.Request.Headers["X-Forwarded-Proto"].FirstOrDefault() ?? context.Request.Scheme;
public static string GetCurrentDomain(this HttpContext context) =>
context.Request.Headers["X-Forwarded-Host"].FirstOrDefault() ?? context.Request.Host.Host;
public static int? GetCurrentPort(this HttpContext context) =>
string.IsNullOrEmpty(context.Request.Headers["X-Forwarded-Port"].FirstOrDefault()) ? context.Request.Host.Port :
int.Parse(context.Request.Headers["X-Forwarded-Port"].First()!);
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);
if (parts[^1].Equals("api", StringComparison.CurrentCultureIgnoreCase))
parts = parts.Take(parts.Length - 1).ToArray();
return CreateSubPath(string.Join("/", parts));
}
}
public static string GetSubPathSwagger => CreateSubPath(Environment.GetEnvironmentVariable("SWAGGER_SUB_PATH"));
public static string GetApiUrl(this HttpContext context, string apiPath = "")
{
var scheme = GetCurrentScheme(context);
var domain = GetCurrentDomain(context).TrimEnd('/').Replace("localhost", "127.0.0.1");
var port = GetCurrentPort(context);
var portString = port.HasValue && port != 80 && port != 443 ? $":{port}" : string.Empty;
return $"{scheme}://{domain}{portString}{GetSubPathWithoutFirstApiName}{apiPath.Trim('/')}";
}
}

@ -0,0 +1,149 @@
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.Configuration.Model.GeneralSettings;
using Mirea.Api.Endpoint.Sync;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks;
public class ScheduleSyncService : IHostedService, IDisposable
{
private Timer? _timer;
private string _cronUpdate;
private List<ScheduleSettings.CronUpdateSkip> _cronUpdateSkip;
private readonly ILogger<ScheduleSyncService> _logger;
private CancellationTokenSource _cancellationTokenSource = new();
private readonly IServiceProvider _serviceProvider;
private readonly IDisposable? _onChangeUpdateCron;
public ScheduleSyncService(IOptionsMonitor<GeneralConfig> generalConfigMonitor, ILogger<ScheduleSyncService> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
_cronUpdate = generalConfigMonitor.CurrentValue.ScheduleSettings!.CronUpdateSchedule;
_cronUpdateSkip = generalConfigMonitor.CurrentValue.ScheduleSettings!.CronUpdateSkipDateList;
ScheduleSyncManager.OnForceSyncRequested += OnForceSyncRequested;
_onChangeUpdateCron = generalConfigMonitor.OnChange((config) =>
{
var updated = false;
if (config.ScheduleSettings?.CronUpdateSchedule != null && _cronUpdate != config.ScheduleSettings.CronUpdateSchedule)
{
_cronUpdate = config.ScheduleSettings.CronUpdateSchedule;
updated = true;
}
if (config.ScheduleSettings?.CronUpdateSkipDateList != null && !config.ScheduleSettings.CronUpdateSkipDateList.SequenceEqual(_cronUpdateSkip))
{
_cronUpdateSkip = config.ScheduleSettings.CronUpdateSkipDateList
.OrderBy(x => x.End ?? x.Date)
.ToList();
updated = true;
}
if (updated)
OnUpdateIntervalRequested();
});
}
private void OnForceSyncRequested()
{
_logger.LogInformation("It was requested to synchronize the data immediately.");
StopAsync(CancellationToken.None).ContinueWith(_ =>
{
_cancellationTokenSource = new CancellationTokenSource();
ExecuteTask(null);
});
}
private void OnUpdateIntervalRequested()
{
_logger.LogInformation("It was requested to update the time interval immediately.");
StopAsync(CancellationToken.None).ContinueWith(_ =>
{
StartAsync(CancellationToken.None);
});
}
private void ScheduleNextRun()
{
if (string.IsNullOrEmpty(_cronUpdate))
{
_logger.LogWarning("Cron expression is not set. The scheduled task will not run.");
return;
}
var expression = CronExpression.Parse(_cronUpdate);
var nextRunTime = _cronUpdateSkip.GetNextTask(expression).FirstOrDefault();
if (nextRunTime == default)
{
_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.ToString("G"));
var delay = (nextRunTime - 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, delay > int.MaxValue ? int.MaxValue : (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(CancellationToken.None).GetAwaiter().GetResult();
_timer?.Dispose();
ScheduleSyncManager.OnForceSyncRequested -= OnForceSyncRequested;
_onChangeUpdateCron?.Dispose();
_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,94 @@
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Mirea.Api.DataAccess.Application.Common.Exceptions;
using Mirea.Api.Endpoint.Common.Exceptions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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 traceId = Activity.Current?.Id ?? context.TraceIdentifier;
var problemDetails = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
Title = "An unexpected error occurred.",
Status = StatusCodes.Status500InternalServerError,
Detail = "Please provide this traceId to the administrator for further investigation.",
Extensions = new Dictionary<string, object?>()
{
{ "traceId", traceId }
}
};
switch (exception)
{
case ValidationException validationException:
problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1";
problemDetails.Title = "Validation errors occurred.";
problemDetails.Extensions = new Dictionary<string, object?>
{
{ "errors", validationException.Errors.Select(e => e.ErrorMessage).ToArray() },
{ "traceId", traceId }
};
break;
case NotFoundException:
problemDetails.Status = StatusCodes.Status404NotFound;
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.4";
problemDetails.Title = "Resource not found.";
break;
case ControllerArgumentException:
problemDetails.Status = StatusCodes.Status400BadRequest;
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1";
problemDetails.Title = "Invalid arguments provided.";
problemDetails.Detail = exception.Message;
break;
case SecurityException:
problemDetails.Status = StatusCodes.Status401Unauthorized;
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.2";
problemDetails.Title = "Unauthorized access.";
problemDetails.Detail = exception.Message;
break;
case ServerUnavailableException unavailableException:
problemDetails.Status = StatusCodes.Status503ServiceUnavailable;
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.6.4";
problemDetails.Title = "Server unavailable.";
problemDetails.Detail = unavailableException.Message;
if (unavailableException.AddRetryAfter)
context.Response.Headers.RetryAfter = "600";
break;
}
if (problemDetails.Status == StatusCodes.Status500InternalServerError)
logger.LogError(exception, "Internal server error when processing the request");
context.Response.ContentType = "application/json";
context.Response.StatusCode = problemDetails.Status.Value;
return context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
}
}

@ -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,34 @@
using Microsoft.AspNetCore.Http;
using Mirea.Api.Endpoint.Common.Attributes;
using Mirea.Api.Endpoint.Common.Exceptions;
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";
if (maintenanceModeService.IsMaintenanceMode)
throw new ServerUnavailableException("The service is currently undergoing maintenance. Please try again later.", true);
throw new ServerUnavailableException(
"The service is currently not configured. Go to the setup page if you are an administrator or try again later.", false);
}
}
}

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

@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mirea.Api.Dto.Common;
using Mirea.Api.Endpoint.Configuration.Model;
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 != CacheType.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);
var 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();
}
}

@ -0,0 +1,67 @@
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)
};
});
}
}

@ -0,0 +1,102 @@
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.Context;
using Serilog.Events;
using Serilog.Filters;
using Serilog.Formatting.Compact;
using System;
using System.Diagnostics;
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);
}
if (generalConfig != null && !string.IsNullOrEmpty(generalConfig.ApiServerSeq) &&
Uri.TryCreate(generalConfig.ApiServerSeq, UriKind.Absolute, out var _))
configuration.WriteTo.Seq(generalConfig.ApiServerSeq, apiKey: generalConfig.ApiKeySeq);
configuration
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Authorization", 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.Use(async (context, next) =>
{
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
using (LogContext.PushProperty("TraceId", traceId))
using (LogContext.PushProperty("UserAgent", context.Request.Headers.UserAgent.ToString()))
using (LogContext.PushProperty("RemoteIPAddress", context.Connection.RemoteIpAddress?.ToString()))
{
await next();
}
}).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() ?? string.Empty);
};
});
}
}

@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mirea.Api.Dto.Common;
using Mirea.Api.Endpoint.Common.Services.Security;
using Mirea.Api.Endpoint.Configuration.Model;
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 == CacheType.Redis)
services.AddSingleton<ICacheService, DistributedCacheService>();
else
services.AddSingleton<ICacheService, MemoryCacheService>();
return services;
}
}

@ -0,0 +1,77 @@
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.OperationFilter<TagSchemeFilter>();
options.SchemaFilter<ExampleFilter>();
options.OperationFilter<DefaultValues>();
options.OperationFilter<ActionResultSchemaFilter>();
options.SchemaFilter<EnumSchemaFilter>();
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('/');
}
});
}
}

@ -1,24 +0,0 @@
using System;
using System.IO;
namespace Mirea.Api.Endpoint.Configuration;
internal static class EnvironmentManager
{
public static void LoadEnvironment(string filePath)
{
if (!File.Exists(filePath)) return;
foreach (var line in File.ReadAllLines(filePath))
{
var parts = line.Split(
'=',
StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
continue;
Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1][..(parts[1].Contains('#') ? parts[1].IndexOf('#') : parts[1].Length)].Trim());
}
}
}

@ -0,0 +1,5 @@
namespace Mirea.Api.Endpoint.Configuration;
public interface ISaveSettings
{
void SaveSetting();
}

@ -0,0 +1,41 @@
using Mirea.Api.Endpoint.Common.Services;
using Mirea.Api.Security.Common.Domain;
using Mirea.Api.Security.Common.Model;
using System.Collections.Generic;
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";
private string _username = string.Empty;
private string _email = string.Empty;
[JsonIgnore]
public static string FilePath => PathBuilder.Combine(FileName);
public required string Username
{
get => _username;
set => _username = value.Trim();
}
public required string Email
{
get => _email;
set => _email = value.Trim();
}
public required string PasswordHash { get; set; }
public required string Salt { get; set; }
public TwoFactorAuthenticator TwoFactorAuthenticator { get; set; } = TwoFactorAuthenticator.None;
public string? Secret { get; set; }
public Dictionary<OAuthProvider, OAuthUser>? OAuthProviders { get; set; }
public void SaveSetting()
{
File.WriteAllText(FilePath, JsonSerializer.Serialize(this));
}
}

@ -0,0 +1,36 @@
using Mirea.Api.Endpoint.Common.Services;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using Mirea.Api.Security.Common.Model;
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 PasswordPolicy PasswordPolicy { get; set; } = new();
public string? SecretForwardToken { get; set; }
public void SaveSetting()
{
File.WriteAllText(
FilePath,
JsonSerializer.Serialize(this, new JsonSerializerOptions
{
WriteIndented = true
})
);
}
}

@ -0,0 +1,18 @@
using Mirea.Api.Dto.Common;
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 CacheType TypeDatabase { get; set; }
public string? ConnectionString { get; set; }
public bool IsConfigured()
{
return TypeDatabase == CacheType.Memcached ||
!string.IsNullOrEmpty(ConnectionString);
}
}

@ -0,0 +1,28 @@
using Mirea.Api.DataAccess.Persistence.Common;
using Mirea.Api.Dto.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 DatabaseType TypeDatabase { get; set; }
public required string ConnectionStringSql { get; set; }
[JsonIgnore]
public DatabaseProvider DatabaseProvider =>
TypeDatabase switch
{
DatabaseType.PostgresSql => DatabaseProvider.Postgresql,
DatabaseType.Mysql => DatabaseProvider.Mysql,
DatabaseType.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;
}
}

@ -0,0 +1,21 @@
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 string? ApiKeySeq { get; set; }
public string? ApiServerSeq { get; set; }
public bool IsConfigured()
{
return !EnableLogToFile ||
!string.IsNullOrEmpty(LogFilePath) &&
!string.IsNullOrEmpty(LogFileName);
}
}

@ -0,0 +1,69 @@
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 record struct CronUpdateSkip
{
public DateOnly? Start { get; set; }
public DateOnly? End { get; set; }
public DateOnly? Date { get; set; }
public CronUpdateSkip(DateOnly d1, DateOnly d2)
{
if (d1 > d2)
{
Start = d2;
End = d1;
}
else
{
Start = d1;
End = d2;
}
}
public CronUpdateSkip(DateOnly d1) => Date = d1;
}
public required string CronUpdateSchedule { get; set; }
public DateOnly StartTerm { get; set; }
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
public List<CronUpdateSkip> CronUpdateSkipDateList { get; set; } = [];
public bool IsConfigured()
{
return !string.IsNullOrEmpty(CronUpdateSchedule) &&
StartTerm != default &&
PairPeriod.Count != 0 &&
PairPeriod.Any();
}
}

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